Thursday, April 5, 2012

Working with Structured Documents

When working with a structured document, you can choose to navigate the document using the tree of elements. Or, if it is more convenient, you can work with the document as if it does not have structure. That is, you can work with paragraphs in the main flow exactly as you would in unstructured FrameMaker.

Is there a downside to this approach? The answer is, it depends. If you are cutting and pasting or otherwise adding content, working outside the element structure may lead you to make changes that leave the structure invalid. But if you are simply inspecting the document or you are working with content that is not part of the element structure (e.g.. marker content), feel free to do whatever is easiest.


Selecting an Entire Element

Selecting elements is tricky business. You will find helpful information in the FDK Programmer's Guide. I happen to have the one for version 6 on hand. In that version, you should refer to pages 366-7.

The key idea is to create an ElementRange that refers to the appropriate selection and then set the document's element selection to be that range. The following function sets the specified document's element selection to be the element passed in.

Element ranges have a beginning and an end. They consist of parent, child, and offset information. 

The parent element is the containing element for both the beginning and end of the range. The starting child is the element itself. The ending child is the element's next sibling element. As the whole element is being selected, both offsets are zero.

//Selects the specified element in the specified document
function setElementSelection(doc, element) {
        var eRange = new ElementRange;
       
        eRange.beg.parent = element.ParentElement;
        eRange.beg.child = element;
        eRange.beg.offset = 0;
        eRange.end.parent = element.ParentElement;
        eRange.end.child = element.NextSiblingElement;
        eRange.end.offset = 0;
        doc.ElementSelection = eRange; 
}

Wednesday, March 14, 2012

Opening Files Off Screen

If you are processing a large number of files without user interaction, consider opening those files off screen. This allow your script to work faster (as there is no need to update the display) and to do its work without screen fireworks.

Use Open() and set the open parameter Constants.FS_MakeVisible to False to open a file off screen. Files opened this way can be modified in exactly the same way as files that are on screen.

If you work with files in this way, keep in mind is that your script must take total responsibility for managing off screen files. When a script is done with an off screen file, it must save (if changes are to be retained) and close the file. Failing to do so leaves the file locked and open but inaccessible to an end user.

There may be a case where you want to open a file off screen, work with that file, and then make it visible to users. To do so, set the document property, FS_MakeVisible to True when you are ready to reveal the file to users. 


Tuesday, March 13, 2012

Opening Book Files when there are Issues

If you use SimpleOpen() to open book files non-interactively, files with missing graphics, missing fonts, or other issues will not open. Use Open() with an appropriate set of parameters to solve this problem.

The following function sets up open preferences that allow files of the following types to open without user interaction:
  • Files which reference missing files.
  • Files that are in an old FM version.
  • Files with missing font issues.
  • Files that are locked.
function getOpenPrefs() {
    var params, i;
  
    params = GetOpenDefaultParams();
   
    i = GetPropIndex(params, Constants.FS_RefFileNotFound);
    params[i].propVal.ival =
Constants.FV_AllowAllRefFilesUnFindable;       
    i = GetPropIndex(params, Constants.FS_FileIsOldVersion);
    params[i].propVal.ival = Constants.FV_DoOK;
    i = GetPropIndex(params, Constants.FS_FontChangedMetric);
    params[i].propVal.ival = Constants.FV_DoOK;  
    i = GetPropIndex(params, Constants.FS_FontNotFoundInCatalog);
    params[i].propVal.ival = Constants.FV_DoOK;   
    i = GetPropIndex(params, Constants.FS_FontNotFoundInDoc);
    params[i].propVal.ival = Constants.FV_DoOK;   
    i = GetPropIndex(params, Constants.FS_LockCantBeReset);
    params[i].propVal.ival = Constants.FV_DoOK;       
    i = GetPropIndex(params, Constants.FS_FileIsInUse);
    params[i].propVal.ival = Constants.FV_OpenViewOnly;     
    return (params);
}
 
The function to open the book using these parameters is shown here:

function openBookFiles(book) {
    var doc, component, compName;
    var openParams, openReturnParams;
   
    openParams = getOpenPrefs ();
    openReturnParams =  new PropVals();

    component =book.FirstComponentInBook;
    while(component.ObjectValid() ){    
        compName = component.Name;
        doc = Open(compName, openParams, openReturnParams);
        component =  component.NextComponentInBook;
    }   
}

Monday, March 12, 2012

Saving FM Binary in Old Version

If you have ever needed to revert FrameMaker files to an older version, the following script does so automatically. It works on the active document but you could convert it to work on a book or a directory of files.

The key to the script is the Save() method which allows you to set the file type as desired. I have chosen FrameMaker 9 (FV_SaveFmtBinary90) but you can  go as far back as FrameMaker 6 (FV_SaveFmtBinary60).

I chose not to change the file name. I end the script by closing the FM 10 version of the file without saving it. The FM9 version is already on disk.

var doc,  name, saveParams, i;

doc = app.ActiveDoc;
name = doc.Name;

saveParams = GetSaveDefaultParams();
returnParams = new PropVals();
i = GetPropIndex(saveParams, Constants.FS_FileType);
saveParams[i].propVal.ival =Constants.FV_SaveFmtBinary90;
doc.Save(name, saveParams, returnParams);
doc.Close(Constants.FF_CLOSE_MODIFIED);

After running the script, I opened the test file just to convince myself this really works.


Sunday, March 11, 2012

Using SimpleOpen() without User Interaction

SimpleOpen() can be called interactively or without user interaction. This distinction makes a difference if you are dealing with files that have issues.

If you are calling SimpleOpen() interactively, the user decides what to do when there are missing fonts, locked files, or other problematic situations.

If you call SimpleOpen() without user interaction FrameMaker defaults to very conservative behavior.
I created a simple a test to demonstrate this behavior. I cobbled together a set of files, each with a distinct issue, and put those files into a FrameMaker book. Each of my four files has one of the following issues:
  • It is locked.
  • It contains missing graphics.
  • It uses unavailable fonts.
  • It is in an older FrameMaker version.
Opening these files from the user interface brings up a dialog that allow the user to proceed or abort the open. Opening these files using the following code produces no user interaction and only one of the files opens: the locked file is opened in view only mode.

doc = SimpleOpen(compName, false);

Wednesday, February 22, 2012

Opening a File with User Interaction

The easiest way to open a file is with SimpleOpen(). This is the analog to SimpleSave() discussed here.

If you do the open interactively, you do not need to know the file's location. You simply let the user locate the proper file.

The following script passes an empty string for the fileName parameter. FrameMaker assumes the file is the last one opened.

var doc;
doc = SimpleOpen("", true);


To open a file without user interaction, you need to pass it is full pathname and false for the second parameter. 

Monday, February 20, 2012

Working with Book Components

You might be tempted to think of FrameMaker books as containing documents. In fact, they contain book components which reference the files that when opened become FrameMaker documents.

To work with all of the documents in a book, you must traverse the list of book components and open each component in turn.

The list of book components is an ordered list. You can start with the FirstComponentInBook. Having found that component, you can use its NextComponentInBook property to find the next book component.

It is also possible to traverse a books components from last to first. In this case you start with LastComponentInBook and use the component's PrevComponentInBook property to move up the list.

The following code snippet shows how you to traverse a book's components from first to last. 

component =book.FirstComponentInBook;
while(component.ObjectValid() ){
    doSomething();
    component =  component.NextComponentInBook;
}

Working with the Active book

The ActiveBook is an exact analog of the ActiveDoc. It refers to the book that has the user focus.

There can be an active book or an active document but not both at the same time. It is also possible that there is neither an active book nor an active document. 

When working with either an active book or an active document, you should check for the possibility that what you are seeking does not exist.

var doc, book;

book  = app.ActiveBook;
if (book.ObjectValid()) {
    Alert("Active book found", Constants.FF_ALERT_CONTINUE_NOTE);
} else {
    doc = app.ActiveDoc;
    if (doc.ObjectValid()) {
        Alert("Active document found", Constants.FF_ALERT_CONTINUE_NOTE);
    } else {
         Alert("No active document or book found", Constants.FF_ALERT_CONTINUE_NOTE);
    } 
}


Saturday, February 4, 2012

Converting Font Style Names to Font Angle Index Values

If your script queries a text property such as font angle, the value you get back is a number and not a name. For example, the following code snippet asks what is the font angle at the text location (tLoc) specified. That information is returned as an integer.

textProp =  doc.GetTextPropVal(tLoc, Constants.FP_FontAngle);
angleIndex = textProp.propVal.ival; //integer value

The integer value angleIndex is an index into an array containing the possible font angle values available in the current FrameMaker session. Use the app (session) property FontAngleNames to get this array of strings.

angleNames = app.FontAngleNames;

Here is that data structure as viewed in the ExtendScript DataBrowser:

The following script converts an angle name to its corresponding index in the angleNames array.

function findAngleIndex(angleName) {
    var angleNames, index;
    angleNames = app.FontAngleNames;
    for (index = 1; index < angleNames.len; index += 1) {
        if (angleNames[index] === angleName) {
            break;
        }
    }
    if (index === angleNames.len) {
        index = null;
    }
    return (index);
}

var index;

index = findAngleIndex("Italic");
Alert(index, Constants.FF_ALERT_CONTINUE_NOTE);


The output is as shown here:

Monday, January 30, 2012

Text Item and Line Ends

Understanding exactly what text items are produced when you call GetText() is critical to writing correct scripts. I have been looking at when you get a FTI_CharPropsChange notification upon calling GetText(). In particular, I am interested in what happens at line ends.

Each of the examples below show what happens when you run a simple script that prints a set of requested text items including changes in character properties and line ends to the Console. Each file tested has one "word" consisting of the digits from 0 through 9. In each case three of the digits have been italicized using the Format>Style menu.

I was expecting to see, among other things, an indicator of where any changes in the italic properties of the text begin and end. The problem I have found occurs when the italic text is at the end of a line. In such cases, I do not get the second FTI_CharPropsChange indicator I am expecting. This behavior deviates from what I knew to be the case in the FDK in earlier versions of FrameMaker. I have not yet tried this in the FM 10 FDK.

var doc, mainflow, tItems;

doc = app.ActiveDoc;
mainflow = doc.MainFlowInDoc;
tItems = mainflow.GetText(Constants.FTI_CharPropsChange | Constants.FTI_TextObjId | Constants.FTI_LineEnd);
PrintTextItems(tItems);


Here is a case that goes as expected, yielding a change indicator before and after the italicized text.



Now look at what happens when the italic text is at the end of the line. Here I get only the initial indicator. In my view, this is acceptable as this this a special end of flow case. There is no non-italic character after the italic "9".






If a carriage return is added after the italic "9", I expect but do not get the second character properties change indicator. Note that end of paragraph symbols are selectable and this one is not italic.




Finally, lets use a soft return as our final character. Once again, there is only the beginning indicator.


I bring all this up not to be picky but because this change/bug complicates working with GetText(). If have I have missed something, please let me know.

Sunday, January 29, 2012

Customizing a Save Operation

The following script uses the Save() method to alter the format in which a file is saved. Using this method requires more code than SimpleSave() but offers a whole range of customization options. It takes three pages in the Scripting Guide to document the possibilities. Whatever changes you want to make, however, can be accomplished by following the model shown here:
  • Use GetSaveDefaultParams() to get a list of parameters whose default settings can be altered.
  • Use  new PropVals() to create an empty parameter list to be used to return information about the Save() operation.
  • Use GetPropIndex() to locate one or more of these properties of interest.
  • Reset that properties value in the parameter list.
  • Call Save() passing in the save as name, and the two parameter lists. 
The critical function is shown here:

function saveAsPdf(doc) {
    var saveParams, name, i, baseName, status, fullName;
    name = doc.Name;
    baseName = dropSuffix(name);//drops everything from last period
    fullName = baseName + ".pdf";
    saveParams = GetSaveDefaultParams();
    returnParams = new PropVals(); 
    i = GetPropIndex(saveParams, Constants.FS_FileType);
    saveParams[i].propVal.ival =Constants.FV_SaveFmtPdf;
    doc.Save(fullName, saveParams, returnParams);
    i = GetPropIndex(returnParams, Constants.FS_SaveNativeError);
    status = returnParams[i].propVal.ival;
    if (status === Constants.FE_Success) {
        return (true);
    } else {
        return (false);
    }
}

Friday, January 27, 2012

Creating a Marker (Correction to previous post)

When you create a marker you  need to specify the marker type identifier. If you are using an built-in type, use the name found in the English user interface.


Use GetNamedObject() to get this identifier. Use the appropriate marker type name. (This is a document and not a session property because of the fact that new types can be added to a given document.)

function createMarker(doc, pgf, offset, type, text) {
    var tLoc, marker, markerType;
    tLoc = new TextLoc(pgf, offset);
    marker = doc.NewAnchoredObject(Constants.FO_Marker, tLoc);
    markerType = doc.GetNamedObject(Constants.FO_MarkerType, type);
    marker.MarkerTypeId = markerType;
    marker.MarkerText = text;
    return 1;
}


You only need to  set the marker type properties for a built-in type if you want to change them. For example, add the following line of code to change the type name as displayed in the user interface.

markerType.Name = "My glossary";


Thursday, January 26, 2012

Saving a document without user interaction

The following script makes a trivial change to that presented in my previous post to save the active document with no user interaction.


var doc, name;

doc = app.ActiveDoc;
name = doc.Name;
doc.SimpleSave(name, false); //no user involvement

Sunday, January 22, 2012

Saving a document with user interaction

If your script makes changes to a document, you will want to save that file. This post, as will the next several posts, discuss the various ways you can do so.

From a scripting point of view, there are two possible ways to save a file. The first is deemed "simple" and the second is more complex to call and customizable. (In the FDK world, this option was deemed 'script-able.') The different between the two lies in the degree of configuration offered.

Simple operations use default settings but do offer one big choice. They allow you to choose whether you want to do the save operation with or without user interaction.

If you are doing a batch operation, you are not going to want user interaction. If, however, you are creating a command a user might call that needs user input, allowing a user to make choices regarding a save in the exact same way as he or she might had they invoked the command form the menu can be useful.

The following script invokes the save dialog using a script.

var doc, name;

doc = app.ActiveDoc;
name = doc.Name;
doc.SimpleSave(name, true); //true signifies user directed


Here is what happens when I saved a file using this script. It is the equivalent o the user choosing File>Save.

Wednesday, January 11, 2012

Rearranging the Tree of Elements

You can use the Cut, Copy, Paste methods to move selected elements around in the tree. There are, however, some special cases where you can take advantage of built-in tree manipulation functions.
  • Use the document method PromoteElement() to make the selected element the sibling of its former parent.
  • Use the document method DemoteElement() to make the selected element the child of its preceding sibling.

The following simple script promotes the selected element. You could easily modify it to demote an element.

var doc;

doc = app.ActiveDoc;
doc.PromoteElement();
if (FA_errno !== 0) {
    Alert(FA_errno + '');
}

The script checks for an error code:
  • Constants.FE_WrongProduct (-60) signifies that the product interface is not set to Structured FrameMaker .
  • Constants.FE_BadDocId (-2) indicates a problem with the document identifier.
  • Constants.FE_BadSelectionForOperation (-59) indicates that the action requested could not be taken on the selected element. For example, you cannot promote the root element or its children.

Here is the before and after that results from promoting the p element.
Before element promotion
After element promotion
This promote operation happens to make the structure tree invalid but the operation itself is sound.

Attempting to promote the root element produces the following error code message Constants.FE_BadSelectionForOperation (-59) as displayed by the script.


Sunday, January 8, 2012

Text Nodes

Text nodes appear in the element tree when a container element has multiple children some of which are containers for text and some of which are just a text string.

Consider the case of an element, p, that allows text (<TEXT>) and other elements as described in the EDD:



I have made a small modification to oil.xml to illustrate this point. The word "every" is italicized by wrapping it in the element i. Before this change, the p element had no children. Now it has three. The first and the last of these are text nodes as illustrated below.



The function getElementName() uses the element property ElementDef to find the definition object and then the name property to get the name string.

function getElementName(elem) {
    var elemDef, name = null;

    elemDef = elem.ElementDef;
    if (elemDef.ObjectValid()) {
        name = elemDef.Name;
    }
    return name;
}

If an element definition is NULL, that element is a text node. Armed with this knowledge, the displayAttrs() function used in a previous post can be improved.

//writes element name and list of attributes to the console
function  displayAttrs(elem) {
    var i, name, aName, attrs;
   
    name = getElementName(elem);
    if (name != null) {
        Console(name);
        attrs = elem.GetAttributes();
        for (i = 0; i < attrs.len; i++) {
            aName = attrs[i].name;
            Console( "     " + aName);      
        }
    } else {
        Console("Selected element is a text node");
     }
}

Here a text node is selected:

The output when displayAttrs() is called on the selected text node is shown here:


Element Navigation Schematic

The following illustration shows the possible  element properties for use in navigating the element tree in a structured document. All properties, with the exception of HighestLevelElement are those of the element shown in black.


Getting Attribute Names

Once you have an element's object, you can examine its attributes. This script lists the attributes of the currently selected element in the FrameMaker console.

The script uses the GetAttributes() method and then iterates over the attribute list obtained, each time getting the name and writing that value to the FrameMaker Console. (The extra space prior to the name is used solely for readability purposes.)

// determines associated element definition name
function getElementName(elem) {
    var elemDef, name = null;

    elemDef = elem.ElementDef;
    if (elemDef.ObjectValid()) {
        name = elemDef.Name;
    }
    return name;
}

//writes element name and list of attributes to the console
function  displayAttrs(elem) {
    var i, name, aName, attrs;
   
    name = getElementName(elem);
    Console(name);
    attrs = elem.GetAttributes();
    for (i = 0; i < attrs.len; i++) {
        aName = attrs[i].name;
        Console( "     " + aName);      
    }
}
var doc, elem, eLoc, eRange;

doc = app.ActiveDoc;
eRange = doc.ElementSelection;
eLoc = eRange.beg;
elem = eLoc.child;
if (elem.ObjectValid()) {
    displayAttrs(elem)
} else {
    Alert("No element was selected.",
       
Constants.FF_ALERT_CONTINUE_NOTE); 
}

Consider the test case where the oil.xml document's title element is selected.

The script produces the following output:


Monday, January 2, 2012

Getting an Element's Text

Once you know how to work with text, working with an element's text is straight-forward. The following function calls the element GetText() method asking for strings. It then iterates over any text items found and concatenates the values found into a single string.

The getElementText() function is shown below. An example of its use will be provided in my next post.

function getElementText(elem) {
    var  tItems, i, text = "";

    tItems = elem.GetText(Constants.FTI_String);
    for (i = 0; i < tItems.len; i += 1) {
        switch (tItems[i].dataType) {
        case Constants.FTI_String:
            text =  text + tItems[i].sdata;
            break;
        }
    }
    return text;
}

Sunday, January 1, 2012

Collapsing Elements in the Structure View

Once you can access an element's object, you can affect how it is displayed in the structure view.

By updating the walkTree() function with a single line of code, your script can collapse, as is the case here, or uncollapse elements in the tree.

function walkTree(elem, count) {
    var child, name;

    name = getElementName(elem);
    elem.ElementIsCollapsed = true; // collapse element
    Console(count + ":  " + name);
    child = elem.FirstChildElement; //get first child if any
    while (child.ObjectValid()) {
        count = count + 1; //upate element count
        count = walkTree(child, count); //traverse subtree with child root
        child = child.NextSiblingElement; //get the siblings
    }
    return count;
 }

Just be sure to redisplay the document when you are done to make your changes visible to the user.

doc.Redisplay();

Before the script was run, the structure view appears as follows:

After the running the script, it appears as shown here:



Just to verify that all elements in the tree were collapsed, I manually uncollapsed the root element to reveal that the script works as expected.