Ride for Roswell 2016

Rick and Jason have reached their fundraising goals. If you would like to purchase any of the scripts, please go to our online store.


It’s time for the Annual Ride for Roswell bike event to raise money for cancer research at the Roswell Cancer Institute in Buffalo. My son Jason and I are riding the 102 mile route again this year. This year, I have decided to make some of my ExtendScript scripts available in exchange for a donation to the ride. And, you can donate any amount you want for each script! ExtendScript is built into FrameMaker 10 and higher. Here is a brief description of each script:

TableCleanerES: This is the new and improved version of my TableCleaner plugin. Some of the batch commands can now be performed on all of the files in a book! Click here for details.

PathChanger: This script allows you to manage paths for graphics, text insets, external cross-references, and book components with an Excel spreadsheet. You need this script when you rename or move referenced files. Click here for details.

FindChangeFormatsBatch: This script allows you to Find/Change hundreds of FrameMaker formats in a document or book with a single command. Formats are specified in a simple FrameMaker table. Click here for details.

PageLabelerES: The ExtendScript version of a long-time favorite. Transfers your FrameMaker book’s numbering to your PDF file. Click here to download the documentation.

ImportFormatsSpecialES: This script allows you more granular control of the document properties that you import from a template. It also allows you to import User Variables without System Variables and vise versa so you can import one type of variable format without clobbering the others. Click here to download the documentation.

Thank you for your generosity!

Solving Path Problems with PathChanger

Let’s face it, the FrameMaker documents that we work with rarely stand alone. They often have graphics imported by reference, text insets, and external cross-references, all pointing to files outside of the FrameMaker document. FrameMaker books point to book components that can be located just about anywhere. And when files get moved or renamed, we can end up with a combination of missing graphics, unresolved text insets, unresolved cross-references, or books with missing components.

A FrameMaker document or book stores paths internally for each of these items. PathChanger is a series of ExtendScript scripts that makes it easy to change these paths for a FrameMaker document or book. It has a command for extracting and writing these paths to a simple .csv file. This file can be opened with Excel where you can easily see and edit and change the paths. Once the paths are updated, another command applies them back to the FrameMaker document or book, quickly resolving the missing or unresolved objects. There are additional commands for writing and updating books and their paths to each book component.

Here is how it works for imported graphics, text insets, and external cross-references:

  1. Open the document or book that has paths that need to be updated.
  2. Choose File > Utilities > Write Paths to File. The script will write the path information to a paths.csv file in the same folder as the document or book.
  3. Open the paths.csv file with Excel and edit the Path column in any rows that you want to update. Do not change any of the information in the other columns. You can delete any rows from the csv file that you don’t want to update.
  4. Save the edited Excel file. The file must by saved as a csv file, not as a native xls or xlsx file.
  5. Choose File > Utilities > Update Paths.
  6. Choose the updated csv file that you saved in step 4. PathChanger will open the files listed in the csv file and update the paths.

The process is similar for updating book component paths:

  1. Open the book that has book component paths that need to be updated.
  2. Choose File > Utilities > Write Book Component Paths to File. The script will write the book component paths information to a book_components_paths.csv file and save it in the same folder as the book.
  3. Open the book_components_paths.csv file with Excel and edit the Path column in any rows that you want to update. Do not change any of the information in the other columns. You can delete any rows from the csv file that you don’t want to update.
  4. Save the edited Excel file. The file must by saved as a csv file, not as a native xls or xlsx file.
  5. Choose File > Utilities > Update Book Component Paths.
  6. Choose the updated csv file that you saved in step 4. PathChanger will update the book component paths in the book.

Download the documentation for more details on this powerful new program. Purchase PathChanger today for only $79 at our online store.

FindChangeFormatsBatch

FindChangeFormatsBatch is an ExtendScript script for FrameMaker 10 and higher. It allows you to Find/Change hundreds of FrameMaker formats in a document or book with a single command. It is controlled by a simple FrameMaker table that you fill in with your find/change formats. You can change the following format types: paragraph, table, character, condition, master page, marker type, cross-reference, and user variable. You can also use FindChangeFormatsBatch to delete unwanted formats from your document or book. FindChangeFormatsBatch is available now for $79 US. For more information, view the documentation. To purchase it, go to our online store.

Introducing TableCleanerES

FrameMaker and Word have always had an awkward relationship to each other. Technical writers are often required to integrate Word content into their FrameMaker publications. They soon learn that there is not always a one-to-one correspondence between the programs’ features. TableCleaner was designed to help users format FrameMaker tables that were imported from Word. The original TableCleaner, released in 1999, focused on two tasks: removing custom ruling and shading from tables and converting body rows to heading rows.

Some tables originating in Word have custom ruling and shading applied to them and that prevents ruling and shading changes applied from the Table Designer from appearing. So TableCleaner gives you a quick way to remove custom ruling and shading from all of the tables in a document. (NOTE: Screenshots are from the latest version of TableCleaner.)

removeDialog

The second issue is heading rows; tables imported from Word do not have true FrameMaker header rows that repeat at the top of each new column and page. Manually converting the first row to a heading row is a multi-step process, so TableCleaner gives you a batch command for converting the first row in all of the tables in the document.

convertDialog

Over time, I added more goodies to TableCleaner that are useful regardless of where the tables originated from. One popular feature is the ability to resize multiple tables with a single command. This includes an option to resize “by example”; you resize a single table to your liking and then apply this sizing to a bunch of other tables with a single command.

resizeDialog

TableCleaner was originally written as a FrameMaker plugin, but I have recently rewritten it in ExtendScript. ExtendScript is Adobe’s implementation of JavaScript and it is built into FrameMaker 10 and higher. There are two significant new features: batch commands can now be applied to all of the documents in a book. And the interface can now be localized through a simple XML file. TableCleanerES is available now for only $39 US. Check out the full documentation, or purchase it now at my online store.

Adobe Webinar: FrameMaker ExtendScript Examples

Update

Thanks to the generosity of many, Jason and I have reached our fundraising goals for the Ride! As a result, I am withdrawing the offer for the scripts. Thank you for your help!

If you attended the webinar on April 29, 2015, you saw some realistic examples of using ExtendScript to automate FrameMaker. The scripts are well-commented and ready for production work. If you are learning ExtendScript, you can use many of the functions and techniques in your own scripts.

Here is a PDF that describes each script.

Thank you for your generous support of a great cause!

Update

For those that received the scripts by donating, or saw them during the webinar, I am interested in your questions and feedback. Please add a comment to this post, and I will start a new post to discuss your questions and comments.

FrameMaker 12 ExtendScript: The Object Model

In a recent Adobe webinar, I used a chm file for FrameMaker 12 ExtendScript’s object model. Here is a link to the web site. Scroll to the bottom and you will see the link. After you download the chm, right+click on it and choose Properties. Click the Unblock button and you will be able to use the chm file.

The chm file has the same content as the ExtendScript Object Model Viewer, but in my opinion, is much easier to use. If you prefer HTML, there is a link to an HTML version as well.

Automatically Saving to MIF

It is not unusual to have “mixed” FrameMaker workflows where members of a team are using different versions of FrameMaker. Documents saved in a lower version can be opened with higher versions, but you can’t open documents saved with a higher version with a lower version of FrameMaker. There are two solutions: First, you can use the higher version to “save down” to the lower version. However, you can only save down to the next lowest version, for example, from FrameMaker 12 to FrameMaker 11. Second, you can save the document to MIF (Maker Interchange Format) in which case you can open it with any lower version of FrameMaker.

The main sticking point with saving to MIF is that you have to remember to do it. But with ExtendScript, you can automate this. You can create a script that will automatically save the document to MIF whenever you choose File > Save or press Control+S. When you are ready to pass the MIF file onto another team member that may be using a lower FrameMaker version, you know that it will always reflect the latest saved changes.

Here is how to set up the script below:

  1. Copy the code below and paste it into an empty text file.
  2. Save the text file to a convenient location with the name “SaveAsMif_Event.jsx”.
  3. Quit FrameMaker if it is running.
  4. Copy the file to C:\Users\<UserName>\AppData\Roaming\Adobe\FrameMaker\<Version>\startup, where <UserName> is your Windows login name and <Version> is the FrameMaker version that you are using. If the startup folder does not exist, create it.
  5. Start FrameMaker and test the script. Open a document, make some changes to it, and save it. You should see a corresponding MIF file in the same folder as the document.

If you have any questions, problems, or comments, please post them in the comments section. Here is a link to a video that walks through the creation of the script.

Notification (Constants.FA_Note_PostSaveDoc, true);

function Notify (note, object, sparam, iparam) {

    switch (note) {
        case Constants.FA_Note_PostSaveDoc :
        saveAsMif (object);
        break;
    }
}

function saveAsMif (doc) {
	
	// Get required parameters for the save function.
    var params = GetSaveDefaultParams();
    var returnParamsp = new PropVals();
    
    // Replace the .fm extension with .mif.
    var saveName = doc.Name.replace (/\.[^\.\\]+$/,".mif");
    
    // Get the FileType save parameter and set it to MIF.
    var i = GetPropIndex(params, Constants.FS_FileType);
    params[i].propVal.ival = Constants.FV_SaveFmtInterchange;

    // Save the document as MIF.
    doc.Save(saveName, params, returnParamsp);
}

FrameMaker ExtendScript Training

Adobe is hosting a pair of ExtendScript webinars, taught by Rick Quatro (me). Here is the press release and a link to the registration page. I hope to see you there!

Write your own FrameMaker ExtendScript, Part 1

Wed Jan 14 at 10 AM

Registration link: http://adobe.ly/1DOIa9x

In this two part series, FrameMaker maven Rick Quatro of Carmen Publishing will be walking you through the process of writing your own Extendscripts, and will include files you can download during the session. Rick’s previous session gave an excellent overview of Regular Expressions in FrameMaker. Join this two-part deep dive and discover how you can automate many functions w/in FrameMaker via your own, custom scripts.

Removing Conditions From Text and Table Rows – Part 4

Before continuing our discussion of removing conditions from text, let’s take a look at table rows. Working with conditional table rows is easier that working with conditionalized text, so we can get this code out of the way. To set the stage, add a table to a document and apply two conditions, Condition1 and Condition2 to one of the rows. Select the conditionalized row and run this code:

#target framemaker

var doc = app.ActiveDoc;
var row = doc.SelectedTbl.TopRowSelection;

alert (row.InCond);

Condition Formats applied to the row

This is similar to what we did when working with a text range in Part 1, but more direct. Since we have a function for removing conditions from text, we can use this as a starting point for a table rows function.

function removeConditionsFromText (textRange, removeConditions, doc) {
    
    var prop, condFmts, i;
    
    // Get a list of the conditions applied to the beginning of the text range.
    prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
    condFmts = prop.propVal.osval;
    if (condFmts.length === 0) {
        return; // No conditions are applied to the text range; exit the function.
    }

    // Loop through the condition format names that are applied to the text.
    for (i = 0; i < condFmts.length; i += 1) {
        // See if the format is in the remove list.
        if (removeConditions.indexOf (condFmts[i].Name) > -1) {
            // Remove it from the list.
            Array.prototype.splice.call (condFmts, i, 1);
        }
    }

    if (condFmts.length !== prop.propVal.osval.length) {
        // Conditions were removed; apply the updated list back to the text range.
        prop.propVal.osval = condFmts;
        doc.SetTextPropVal (textRange, prop);
    }
}

Let’s rework it together line-by-line: on line 1, change the name to removeConditionsFromRow and change the first argument from textRange to row.

function removeConditionsFromText (textRange, removeConditions, doc) {

We won’t need a prop variable so remove it from line 3:

function removeConditionsFromRow (row, removeConditions, doc) {
    
    var condFmts, i;

Lines 5-10 can be shortened to this:

function removeConditionsFromRow (row, removeConditions, doc) {
    
    var condFmts, i;
    
    // Get a list of the conditions applied to the row.
    condFmts = row.InCond;
    if (condFmts.length === 0) {
        return; // No conditions are applied to the row; exit the function.
    }

Lines 12-19 from the original function can be used as is, except for a minor change to the first comment:

function removeConditionsFromRow (row, removeConditions, doc) {
    
    var condFmts, i;
    
    // Get a list of the conditions applied to the row.
    condFmts = row.InCond;
    if (condFmts.length === 0) {
        return; // No conditions are applied to the row; exit the function.
    }

    // Loop through the condition format names that are applied to the row.
    for (i = 0; i < condFmts.length; i += 1) {
        // See if the format is in the remove list.
        if (removeConditions.indexOf (condFmts[i].Name) > -1) {
            // Remove it from the list.
            Array.prototype.splice.call (condFmts, i, 1);
        }
    }

    if (condFmts.length !== prop.propVal.osval.length) {
        // Conditions were removed; apply the updated list back to the text range.
        prop.propVal.osval = condFmts;
        doc.SetTextPropVal (textRange, prop);
    }
}

Finally, lines 21-25 of the original function can be changed to work with a row:

function removeConditionsFromRow (row, removeConditions, doc) {
    
    var condFmts, i;
    
    // Get a list of the conditions applied to the row.
    condFmts = row.InCond;
    if (condFmts.length === 0) {
        return; // No conditions are applied to the row; exit the function.
    }

    // Loop through the condition format names that are applied to the row.
    for (i = 0; i < condFmts.length; i += 1) {
        // See if the format is in the remove list.
        if (removeConditions.indexOf (condFmts[i].Name) > -1) {
            // Remove it from the list.
            Array.prototype.splice.call (condFmts, i, 1);
        }
    }

    if (condFmts.length !== row.InCond.length) {
        // Conditions were removed; apply the updated list back to the row.
        row.InCond = condFmts;
    }
}

Now we can test the finished function. Add the following code before the removeConditionsFromRow function and add the augmentObjects function to the end of the script. Select the table row that has Condition1 and Condition2 applied to it and run the complete script. You should see the Condition2 format removed from the selected row.

#target framemaker

// Call a function to add functions to built-in objects.
augmentObjects ();

var doc = app.ActiveDoc;

// List of conditions to remove.
var removeConditions = ["Condition2"];

// Remove the condition(s) from the selected row.
var row = doc.SelectedTbl.TopRowSelection;
removeConditionsFromRow (row, removeConditions, doc);

In an upcoming post, we will return to working removing conditions from text.

Removing Conditions From Text and Table Rows – Part 3

Part 1 and Part 2 of this series show the beginning of the typical script development process that I use. I like to isolate scripting tasks to the smallest units possible. For this task, we worked on a single selection of conditionalized text in the active document. When we switched to tables, we selected a single row with conditions applied. This approach allows us to get the basic functionality working without worrying about the overhead that the larger script requires. Once the basic functionality is working, we can expand it to work on an entire document or book.

Let’s start by figuring out how to specify which Condition Formats to remove from the text and table rows in a document. To keep things simple, we can use an array of Condition Format names:

#target framemaker

var doc = app.ActiveDoc;

// Make an array of Condition Format names to remove.
var removeConditions = ["Condition2"];

Even if we add an interface to the script later (like a dialog box), we can still use an array to store the condition names. We need to figure out a way to compare the contents of the array with the conditions that are applied to a given range of text. To set this up, make sure you have a document open with two Condition Formats, Condition1 and Condition2, applied to a paragraph. Select some of the text in the paragraph. We can add our array to some of the code we previously developed:

#target framemaker

var doc = app.ActiveDoc;

// An array of condition formats to remove from the text and rows.
var removeConditions = ["Condition2"];

var textRange = doc.TextSelection;
var prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
var condFmts = prop.propVal.osval;

// Display the condition format names that are applied to the text.
for (var i = 0; i < condFmts.length; i += 1) {
    alert (condFmts[i].Name);
}

The script will display each condition format that is applied to the selected text. We need a way to test each name and see if it is in the removeConditions array. Any conditions that are in the removeConditions array need to be removed from the condFmts list. In some implementations, a JavaScript array has an indexOf method so you can test and see if the array contains a particular member. If the member exists, indexOf returns the array index; otherwise, it returns -1. We could do this:

#target framemaker

var doc = app.ActiveDoc;

var removeConditions = ["Condition2"];

var textRange = doc.TextSelection;
var prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
var condFmts = prop.propVal.osval;

// Loop through the condition format names that are applied to the text.
for (var i = 0; i < condFmts.length; i += 1) {
    // See if the format is in the remove list.
    if (removeConditions.indexOf (condFmts[i].Name) > -1) {
        // Remove it from the list.
        Array.prototype.splice.call (condFmts, i, 1);
    }
}

// Apply the updated list back to the text range.
prop.propVal.osval = condFmts;
doc.SetTextPropVal (textRange, prop);

The indexOf method is being used in the if statement on line 12. There is one problem with this code: The current Adobe ExtendScript implementation does not have an indexOf method on arrays. We are going to call (line 4 in the following code) a function (lines 27-45) that will augment the built-in Array object and add an indexOf method to it. Object augmentation is beyond the scope of this lesson, but google it if you are interested in how it works. Here is the entire code:

#target framemaker

// Call a function to add functions to built-in objects.
augmentObjects ();

var doc = app.ActiveDoc;

var removeConditions = ["Condition2"];

var textRange = doc.TextSelection;
var prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
var condFmts = prop.propVal.osval;

// Loop through the condition format names that are applied to the text.
for (var i = 0; i < condFmts.length; i += 1) {
    // See if the format is in the remove list.
    if (removeConditions.indexOf (condFmts[i].Name) > -1) {
        // Remove it from the list.
        Array.prototype.splice.call (condFmts, i, 1);
    }
}

// Apply the updated list back to the text range.
prop.propVal.osval = condFmts;
doc.SetTextPropVal (textRange, prop);

function augmentObjects () {
    
    // Add an indexOf method to the Array object.
    if (!Array.prototype.indexOf) {
        Array.prototype.indexOf = function (obj, fromIndex) {
            if (fromIndex == null) {
                fromIndex = 0;
            } else if (fromIndex < 0) {
                fromIndex = Math.max(0, this.length + fromIndex);
            }
            for (var i = fromIndex; i < this.length; i++) {
                if (this[i] === obj) {
                    return i;
                }
            }
            return -1;
        };
    }
}

OK, we have the basic functionality working, so let's encapsulate it into a function that we easily drop into a bigger script. We want the function to take a text range, a list of conditions to remove, and a document object. Here is one way we can do this:

#target framemaker

// Call a function to add functions to built-in objects.
augmentObjects ();

var doc = app.ActiveDoc;

var removeConditions = ["Condition2"];
var textRange = doc.TextSelection;

// Remove the condition(s) from the selected text.
removeConditionsFromText (textRange, removeConditions, doc);

function removeConditionsFromText (textRange, removeConditions, doc) {
    
    var prop, condFmts, i;
    
    // Get a list of the conditions applied to the beginning of the text range.
    prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
    condFmts = prop.propVal.osval;

    // Loop through the condition format names that are applied to the text.
    for (i = 0; i < condFmts.length; i += 1) {
        // See if the format is in the remove list.
        if (removeConditions.indexOf (condFmts[i].Name) > -1) {
            // Remove it from the list.
            Array.prototype.splice.call (condFmts, i, 1);
        }
    }

    // Apply the updated list back to the text range.
    prop.propVal.osval = condFmts;
    doc.SetTextPropVal (textRange, prop);
}

// augmentObjects function not shown but required!

Some important points:

  • The new function is on lines 14-34.
  • We followed best practices and declare all variables at the top of the function (line 16).
  • Because the function uses the indexOf method, we have to include the augmentObjects function in any script that uses our new function. In addition, we have to remember to call augmentObjects at the top of our script (like we do on line 4).
  • IMPORTANT: The script gets the condition format properties at the beginning of the text range. It removes the appropriate conditions from that list and then applies those properties to the entire text range. For this reason, we want to pass in text ranges that only have distinct condition formats applied to them. If you are not sure why, post a comment below.

Before moving on, there is one issue we should address. One line 32, we apply the updated list of conditions back to the propVal object; then line 33 applies the property list to the text range. But what if this list was empty to start with, or hasn't been changed by the loop on lines 23-29? There is no sense in going further, so let's add a couple of tests to the function. We can add this below line 20:

if (condFmts.length === 0) {
    return; // No conditions are applied to the text range; exit the function.
}

If there are no conditions applied to the text, then there are none that can be removed; so we simply exit the function. If there are conditions applied, they may be conditions that aren't in our removeConditions array. So we can test to make sure that conditions were actually removed before updating the text (this code would replace lines 31-33):

if (condFmts.length !== prop.propVal.osval.length) {
    // Conditions were removed; apply the updated list back to the text range.
    prop.propVal.osval = condFmts;
    doc.SetTextPropVal (textRange, prop);
}

Here is the finished function:

function removeConditionsFromText (textRange, removeConditions, doc) {
    
    var prop, condFmts, i;
    
    // Get a list of the conditions applied to the beginning of the text range.
    prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
    condFmts = prop.propVal.osval;
    if (condFmts.length === 0) {
        return; // No conditions are applied to the text range; exit the function.
    }

    // Loop through the condition format names that are applied to the text.
    for (i = 0; i < condFmts.length; i += 1) {
        // See if the format is in the remove list.
        if (removeConditions.indexOf (condFmts[i].Name) > -1) {
            // Remove it from the list.
            Array.prototype.splice.call (condFmts, i, 1);
        }
    }

    if (condFmts.length !== prop.propVal.osval.length) {
        // Conditions were removed; apply the updated list back to the text range.
        prop.propVal.osval = condFmts;
        doc.SetTextPropVal (textRange, prop);
    }
}

In a future post, we will show how to loop through the text in a paragraph and isolate ranges of text that have distinct sets of conditions applied to them. These will get passed to our new removeConditionsFromText function to remove the specified conditions from paragraphs in a document. We will also develop a function that will remove conditions from table rows.