Removing Conditions From Text and Table Rows – Part 2

In the previous post, we gained a basic understanding of how to remove a CondFmt (Condition Format) from a text range. Let’s see it in action. To set this up, make sure you have a sample document with two conditions, Condition1 and Condition2, and have both conditions applied to a paragraph in the document. Select some of the text in the paragraph and run this:

#target framemaker

var doc = app.ActiveDoc;

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

// Remove the second condtion format and update the osval list.
Array.prototype.splice.call (condFmts, 1, 1);
prop.propVal.osval = condFmts;

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

To keep the example simple, we are making a couple of assumptions: First, we are removing Condition2 from the text and second, that Condition2 is the second CondFmt in the osval list. We will use code to eliminate these assumptions later.

On line 7, we assign the osval list to a variable (condFmts). We need a way to remove members from this condFmts list. If condFmts was a true JavaScript array, we could use its splice method to remove the desired member. But it is actually an array-like list of CondFmt objects that doesn’t contain a splice method. However, JavaScript allows us to “borrow” the splice method from the built-in Array object and use it on our list; this is what we are doing on line 10.

On line 11, we assign the modified list back to prop.propVal.osval and on line 14 we apply the updated property to the selected text. When you run this, you should see the Condition2 format removed from the selected text.

If we wanted to remove all formats from the text, we could use this shortcut:

#target framemaker

var doc = app.ActiveDoc;

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

// Remove all conditions from the text.
prop.propVal.osval.length = 0;

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

Line 9 simply sets the length of the osval list to 0 (zero), which removes all of the members.

Conditionalized table rows are handled a little bit differently than text. To set this up, add a table to your document and apply Condition1 and Condition2 to one of the rows and leave the row selected. Run this code:

#target framemaker

var doc = app.ActiveDoc;

// Get the selected row object.
var tbl = doc.SelectedTbl;
var row = tbl.TopRowSelection;

// Display the list of Condition Formats applied to the row.
alert (row.InCond);

As you can see, the condition list is easier to get to with a table row than it is with text. Let’s splice the second member from the list and apply it back to the table row:

#target framemaker

var doc = app.ActiveDoc;

// Get the selected row object.
var tbl = doc.SelectedTbl;
var row = tbl.TopRowSelection;

// Remove the second condition from the list and update the row.
var inCond = row.InCond;
Array.prototype.splice.call (inCond, 1, 1);
row.InCond = inCond;

As with text, we can use a shortcut to remove all conditions from the row:

#target framemaker

var doc = app.ActiveDoc;

// Get the selected row object.
var tbl = doc.SelectedTbl;
var row = tbl.TopRowSelection;

// Remove all conditions from the list and update the row.
var inCond = row.InCond;
inCond.length = 0;
row.InCond = inCond;

In the next post, we will generalize this code into functions so that you can remove conditions from all of the text and table rows in a document.

Removing Conditions From Text and Table Rows

It is easy to delete Condition Formats from a FrameMaker document in the interface. If you try to delete a Condition Format that is use, you will be prompted on what to do to the conditionalized text:

DeleteConditionTag

With ExtendScript, things aren’t so easy. If you delete a Condition Format programmatically, the conditionalized content is always deleted along with the format. There is no “option” on the Delete function to make the text unconditional. If you want to keep the text, you will have to remove the Condition Format from any text (and table rows) where it is applied.

A couple of preliminary things: First of all, you should show all of the conditions at the beginning of your code. You can store the current ShowAll setting so you can restore it later. Second, if you do want to delete the text and rows with the Condition Format, just delete the Condition Format (CondFmt):

#target framemaker

var doc = app.ActiveDoc;

// Store the current ShowAll setting and show all conditions.
var showAll = doc.ShowAll;
doc.ShowAll = 1;

// Get the CondFmt object and delete it (and any text and rows) from the document.
var condFmt = doc.GetNamedCondFmt ("Comment");
if (condFmt.ObjectValid () === 1) {
    condFmt.Delete ();
}

// Restore the document's ShowAll setting.
doc.ShowAll = showAll;

To see how to remove Condition Formats from text, make a sample document and add two Condition Formats to it, Condition1 and Condition2. Apply both conditions to a single paragraph. Select part of the paragraph and run this code:

#target framemaker

var doc = app.ActiveDoc;

var textRange = doc.TextSelection;
var prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
alert (prop);

This will show us a PropVal object that has information about the Condition Formats (if any) that are applied to the text. Run the following code to see the properties of the PropVal object.

#target framemaker

var doc = app.ActiveDoc;

var textRange = doc.TextSelection;
var prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
// Use the ExtendScript reflection interface to see the properties.
alert (prop.reflect.properties);

The propIndent value is the kind of property it is (FP_InCond), while the propVal properties has specific information about the Condition Formats that are applied. Let’s poke further down and look at the propVal properties with this code:

#target framemaker

var doc = app.ActiveDoc;

var textRange = doc.TextSelection;
var prop = doc.GetTextPropVal (textRange.beg, Constants.FP_InCond);
// Use the ExtendScript reflection interface to see the properties.
alert (prop.propVal.reflect.properties);

As you can see, the propVal property has a lot of child properties. For Condition Formats, we are interested in the osval property. Make sure your conditional text is still selected and run this code:

#target framemaker

var doc = app.ActiveDoc;

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

You should see a dialog box like this:

osval

The osval property is an array-like list of the CondFmt objects that are applied to the text. If there is no CondFmts applied, this osval list will be empty. Because this list is similar to an array, we can loop through it to look at the individual CondFmt objects.

#target framemaker

var doc = app.ActiveDoc;

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

var condFmt;
for (var i = 0; i < prop.propVal.osval.length; i += 1) {
    condFmt = prop.propVal.osval[i];
    alert (condFmt.Name);
}

When you run this code, you should see the name of each CondFmt displayed in a dialog box. You can probably see what we need to do here. Assuming that we want to remove Condition2 from the text, we need to figure out how to remove the Condition2 CondFmt object from the osval list. Once we do that, we can take the modified PropVal and apply it back to the text, which will remove the unwanted Condition Format. We will explore that in Part 2.

Run a Script in Response to a Command

Here is a question from the Adobe FrameMaker ExtendScript forum: “I wonder if there is there a way to make [a] script event-driven, so that it will be triggered after importing formats from another file?” This post describes how to do this with an ExtendScript script.

First, some terminology: When something happens in FrameMaker, it is called an event. You want your script to listen for one or more events to occur. In ExtendScript, you want to tell FrameMaker to notify to you when one or more events has occurred.

You can set this up in your script with a couple of functions. You first have to tell the script which events you want it to listen for.

function setupNotifications () {

  // Set up a notification for after a book is updated.
  Notification (Constants.FA_Note_PostGenerate, true);
}

Here, we are telling FrameMaker to notify us after a book is updated. This is sometimes referred to as registering the event. You can register as many events as you want in this function.

NOTE: There are no specific notifications when importing formats, but we will show you how to handle this shortly.

You will call this function at the beginning of your script.

setupNotifications ();

Now you need a function that will listen for the notifications (or events) that you specified. FrameMaker ExtendScript has a built-in function for this:

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

  // Test for the event type.
  switch (note) {
    case Constants.FA_Note_PostGenerate :
    // Do something here (respond to the event).
    break;
  }
}

The Notify function will receive four arguments:

  • note: This is the event that was triggered. We usually use a JavaScript switch statement to test for the event, because we may want to test for more than one event.
  • object: This is the relevant object that was in play when the event occurred. In the above example, this will be the Book object of the book that was updated.
  • sparam: This will be a relevant string value or null, depending on the event.
  • iparam: This will be a relevant integer value or null, depending on the event.

Once you receive the correct notification in the Notify function, you can call the appropriate code in your script.

Let’s go back to the forum question. FrameMaker ExtendScript has a bunch of built-in notifications, many of them in a “Pre” and “Post” format. For example, FA_Note_PreGenerate is triggered just before a book is updated and FA_Note_PostGenerate happens just after a book is updated. However, there are no specific notifications that occur before or after importing formats. To handle this, we can use the generic FA_Note_PostFunction notification, which gets triggered after nearly every FrameMaker event. When this notification calls the Notify function, the iparam value will be the unique number of the function. To find the number for a particular command, run the following code, then run the desired command in FrameMaker.

#target framemaker

setupNotifications ();

function setupNotifications () {
    
    // Set the notification for after a book is updated.
    Notification(Constants.FA_Note_PostFunction , true);

}

function Notify (note, object, sparam, iparam) {
    
    // Handle the generate book event.
    switch (note) {
        case Constants.FA_Note_PostFunction :
        // Display the command number.
        alert (iparam);
        break;
    }
}

After running the script and importing formats, you should see the number 790 displayed in the alert dialog box. You probably saw a lot of other numbers if you performed other FrameMaker operations before importing formats! Now that you have the number you need, uninstall the script above or quit and restart FrameMaker.

Now we can go back and edit the Notify function that we added to our script. We will test for the Import Formats command by using the command number.

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

  // Test for the event type.
  switch (note) {
    case Constants.FA_Note_PostFunction :
      if (iparam === 790) { // Formats were imported.
        // Do something here.
      }
    break;
  }
}

There are a couple of things to note about this particular example. If you cancel the Import Formats dialog box, the event is still triggered and the Notify event is called. If you go ahead with the Import Formats command, the Notify event is actually called twice. At this point, I am not sure how to work around this. In any case, always test Notify event carefully to ensure that your code gets called exactly when it is needed.

Naming Variables and Functions in Scripts

A friend recently asked me about how I name variables in my FrameScript scripts. I used to use a “v” prefix on all of my variables names. For example, for a paragraph I would use:

Set vDoc = ActiveDoc;
Set vPgf = vDoc.MainFlowInDoc.FirstPgfInFlow;

The “v” prefix would simply indicate that I was setting a variable. This is the convention I used in my 2003 FrameScript book FrameScript: A Crash Course. Later, I decided to make the prefixes reflect the data type that the variable represents. I replaced the “v” with another letter indicating the data type. For example,

Set oDoc = ActiveDoc;
Set oPgf = oDoc.MainFlowInDoc.FirstPgfInFlow;

The “o” prefix means “object” since both lines set variables for FrameMaker objects. I use “s” for strings, “i” for integers, “r” for real numbers, “m” for metric values, etc. These prefixes help me see at a glance what kind of data type the variable represents. This is important when I am looking at a script that I may not have worked on in awhile. Prefixes like this are also helpful when you are reusing functions. For example, here is a function that applies a named paragraph format to a paragraph.

Function ApplyPgfFmt oPgf sName oDoc
//
Local oPgfFmt(0);

Get Object Type(PgfFmt) Name(sName) DocObject(oDoc) NewVar(oPgfFmt);
If oPgfFmt
  Set oPgf.Properties = oPgfFmt.Properties;
Else
  Set oPgf.Name = sName;
EndIf
//
EndFunc //-------------------------------------------

Even if I don’t know exactly how this function works, I can see at a glance that it takes three parameters: a paragraph object (oPgf), a string indicating the paragraph format name (sName), and a document object (oDoc). The prefixes help me to quickly see the data types of each parameter.

As far as the variable name itself, I try to use the object name that the variable represents. In the ApplyPgfFmt function, I use the Get Object command to get a PgfFmt object. So, I use oPgfFmt as the variable name. Using this convention lets me quickly see that oPgfFmt represents a PgfFmt object. The sName parameter gets matched up with Name in the Get Object command and so on. I find that this method helped me memorize the FrameMaker object model because I have closely associated my variable names with the built-in FrameMaker object names.

When naming functions, the common sense approach is to use some kind of a verb form that describes what the function does. When you look at the function name and its parameters, it should be evident what the function does without looking at the actual function code.

With ExtendScript, I do things a little bit different. I don’t use the prefixes, I simply use the object name but with a lowercase first letter. Here is the example I showed earlier:

var doc = ActiveDoc;
var pgf = doc.MainFlowInDoc.FirstTextFrameInFlow.FirstPgf;

ExtendScript (JavaScript) is a case sensitive language, so pgf is different from Pgf. For function names (and longer variable names) I use the “camel case” convention. I don’t use the datatype prefixes, mainly because this is not a normal convention for JavaScript programmers. I suppose it is a bit vain, but I don’t want my ExtendScript code to look too out-of-the-ordinary. Here is the ExtendScript version of the ApplyPgfFmt function:

function applyPgfFmt(pgf,name,doc) {

  var pgfFmt = 0, props = 0;

  pgfFmt = doc.GetNamedPgfFmt(name);
  if (pgfFmt.ObjectValid()) {
    props = pgfFmt.GetProps();
    pgf.SetProps(props);
  }
  else {
    pgf.Name = name;
  }
}

Beginning Scripting Webinar – Functions – January 22, 2013

Thank you very much to all who attended the webinar. Download or view the webinar slides. FrameScript users download this PDF.

You can download the ExtendScript sample scripts here.

For more information on the Unstructured FrameMaker 11 book, go to http://www.scriptorium.com/books/unstructured-framemaker-11/.

For the webinar replay in wmv format, click here.

Beginning FrameMaker Scripting Webinar – January 15, 2013

Thank you very much to all who attended the webinar. Download or view the webinar slides.

Download the webinar code in both ExtendScript and FrameScript versions. Download the sample FrameMaker document that we used during the webinar.

Download the FrameMaker 10 Object Reference chm file. The zip archive includes a screenshot that shows you how to disable the security warning.

For more information on the Unstructured FrameMaker 11 book, go to http://www.scriptorium.com/books/unstructured-framemaker-11/.

For the webinar replay in wmv format, click here.

Using a regular expression to convert an image name to a path

Mike and I started an interesting discussion about using ExtendScript to import images from a base file name that the user enters. One of the key tasks is to figure out how to take a base file name that uses their syntax and derive a full path from it. Based on Mike’s examples, I came up with the following ExtendScript function:

alert (getImagePath ("MSP4321"));

function getImagePath (imageName) {

  var regex = /([A-Z]{3})((\d)\d)(\d\d)/;
  var path = "\\\\server\\Graphics\\$1\\$1$3000-$3999\\$1$200-$299\\$1$2$4.pdf";

  if (imageName.search(regex) !== -1) {
    return imageName.replace (regex, path);
  } else {
    return null;
  }
}

This function illustrates the power of regular expressions. The basic regular expression is this:

Find 3 uppercase letters: [A-Z]{3}

followed by four digits: \d\d\d\d

The parentheses “capture” parts of the match and put them into special variables in the form of $#, where # is a number from 1 to 9. To figure out what number goes with what set of parentheses, you start counting from the left. So, in our regular expression

([A-Z]{3}) will be $1. In our example, this will be MSP.

((\d)\d) will be $2. In our example, this will be 43.

(\d) nested in $2 will be $3. In our example, this will be 4.

and (\d\d) will be $4. In our example, this will be 21.

We can use these variables to build up our path. Notice the $# variables in the replacement string (color-coded for clarity):

"\\\\server\\Graphics\\$1\\$1$3000-$3999\\$1$200-$299\\$1$2$4.pdf"

This will become

"\\\\server\\Graphics\\MSP\\MSP4000-4999\\MSP4300-4399\\MSP4321.pdf"

The backslash in JavaScript is an escape character, so to get the literal backslash character, we have to use 2 where we want 1.

Copy the function into the ExtendScript Toolkit and try a few filenames that use this format, and you will get the correct path every time. Regular expressions are a deep topic, but as you can see, they are very powerful for parsing tasks like this.

-Rick

FrameMaker 10 ExtendScript: The Object Model

Thank you to all that attended this webinar. I will post the link to the recording as soon as it is available. In the meantime, here is a link to the FrameMaker Object Model chm file and webinar slides:

http://www.rickquatro.com/resources/FM10_ObjectReference.zip

Here is a link to the FrameMaker 10 FDK:

http://www.adobe.com/devnet/framemaker.html

After you install it, the documentation will be in the docs folder.

Arnis asked about dumping a list of properties for an object. In FrameScript it is easy:

Set oDoc = ActiveDoc;
// Write the properties to the Console.
Write Console oDoc.Properties;

With ExtendScript, you can use a function; in this case, I am writing to the JavaScript Console instead of the FrameMaker Console:

var doc = app.ActiveDoc;
writePropsToConsole (doc);

function writePropsToConsole (object) {

  for (var name in object) {
    // Write the properties to the JavaScript Console.
    $.writeln (name + ": " + object[name]);
  }
}

Please let me know if you have suggestions for future webinars. Thanks again for all your support.

-Rick

ExtendScript Webinars: What Do You Want to Learn?

I just finished my second FrameMaker 10 ExtendScript webinar, which I enjoyed immensely. The first one was done in conjunction with Adobe, the second was sponsored by Carmen Publishing Inc. I would like to do more ExtendScript webinars and want to know what topics people are interested in. Please leave comments with suggestions for ExtendScript topics. Thank you very much.

-Rick

In Memory of Bruce Foster

Bruce Foster, creator of many fine FrameMaker plugins, passed away suddenly on Saturday, June 11, 2011. Although Bruce had been diagnosed with multiple myeloma in September 2010, his death still came earlier than was expected. Bruce’s plugins for FrameMaker were BookInfo, ImpGraph, MifSave, MifToFM, RtfSave, and his most famous, Archive.

Bruce’s website is now offline and, as far as I know, his plugins are no longer available  for sale.

Adobe Engineering has just released a free ExtendScript script for archiving FrameMaker 10 books:

http://blogs.adobe.com/techcomm/2011/07/create-a-book-packager-using-extendscript.html

I haven’t tested the script yet, so I can’t necessarily recommend it, but it is certainly worth looking at.

– Rick