Skip navigation
Currently Being Moderated

Using ExtendScript to automate a task

Aug 19, 2013 3:04 PM

Tags: #robohelp #framemaker #extendscript #automate



First off, I like to say that I do not have any experience with ExtendScript and looking for some code to get me started. Any help is most welcome.


Here's my current FM book's layout.


For each page, I have 3 main elements in this order from top to bottom.

     - An imported GRAPHICS inside an anchored frame.

     - Below the anchored frame is the figure CAPTION (e.g. Figure 3. Title of the graphics here).

     - Following the caption is some NOTES (description of the graphics)


Goal: Insert/Paste the CAPTION of the next page onto the bottom of the current page after the NOTES section.


Why: I'm converting from FM to RoboHelp. RH Requires a topic title for each page. Our current template does not have any heading text so I need to copy and paste the figure CAPTION to use as the heading text.


How: At the end of each NOTES section of the current page, I like to copy/paste the CAPTION of the next page, with a specific paragraph tag (e.g. h2x or something similar).


The layout after this process will be:


     - CAPTION (of current page)

     - NOTES

     - CAPTION (of next page)


Result: At the end of each notes section, I will have the CAPTION of the next page that I can use to tell RH to create that as a separate topic.


Let me know if I need to provide any more details, but the main idea is that I need to have some script to automate this process as it is very tedious to copy/paste for each page everytime I need to convert a FM book.


Thanks, and appreciate any help I can get.

- Henry

  • Currently Being Moderated
    Aug 20, 2013 4:12 AM   in reply to Henry_Q

    Hi Henry,


    This is a perfect job for ExtendScript, but I think you may have a tough time getting some sample code here, at least as your request stands currently. Your requirements involve some tricky operations and hands-on knowledge of your files. Folks here are happy to help with small snippets and advice for targeted issues, but you are starting from zero and I don't find it likely that somebody will write up this script for free.


    That said, I don't want to discourage you because the extensibility of FM is one of its great strengths. With the right skills, you or someone could pull this off. I will acknowledge, though, that the initial learning curve is steep. I had many years of heavy FDK experience before ES came around and I am still stumbling through it myself.


    In the meantime, you might think about the general architecture... perhaps you could simplify and thus reduce the overhead. Are you looking to do this as a one-time operation just before a conversion; that is, you don't want the copied CAPTIONs to hang around after that? Or do you want them to stay permanently and just have the script refresh itself? If they could stay permanently, would cross-references work? That is, will RoboHelp recognize the text within a cross-reference to generate a heading? I'm asking because I'm not that familiar with RH and I don't know.


    Just some ideas. There may be others.


    Mark as:
  • Currently Being Moderated
    Aug 21, 2013 4:44 AM   in reply to Henry_Q

    Hi Henry,


    One of the challenges here is that your original idea requires a text operation, where you need to go one place, get the text, then paste it somewhere else. While one might think it should be otherwise, text operations like this are some of the most difficult tasks to figure out with a script. It seems deceptively simple when you are manually using your eyes, a keyboard, and a mouse. However, when it comes to programmatically navigating a document where you can't "see" the whole thing in front of you, the story is much different. I presume your documents are unstructured, which doubles the complexity.


    Again, I don't mean to say it can't be done... it surely can and it is just the kind of thing that the scripting engine is provided for. It's just that a newcomer would likely struggle with it and a guru would likely charge for it, given the advanced nature of the task. I would encourage you to keep pursuing about it, though, because once you get a hold of skills like this, your efficiency and marketability will ascend quickly.




    Mark as:
  • Currently Being Moderated
    Aug 22, 2013 6:10 AM   in reply to Henry_Q

    Hi Henry,


    I think you are exactly right about that. Is there anything in there to positively identify the destination location? Like, is there some unique paragraph format that applies a page break or something? Or, is there some unique sequence of formats? If there is something unique there, this task may not be quite as complicated as I originally imagined.


    Your persistence with this thread has sparked my curiosity. If you could send me a sample file at, I might be able to take a quick look.



    Mark as:
  • Currently Being Moderated
    Aug 22, 2013 10:36 AM   in reply to Henry_Q

    Well, like I said, I'm thinking that it may be less complex than I originally thought. If you can send me a sample file and you are willing to wait a few days, I'll work up some kind of sample code for you. I definitely need a sample file.

    Mark as:
  • Currently Being Moderated
    Aug 22, 2013 11:10 AM   in reply to Henry_Q

    While I can't help with the inevitable politics, if you simply move the fig title above the graphic, you can use that as your RH topic title.

    Mark as:
  • Currently Being Moderated
    Aug 30, 2013 12:07 PM   in reply to Henry_Q

    Lucky for Henry, I became intrigued by this task and had a little time to mess around with a sample script. I'm posting it here for the benefit of the community. Basically, it:


    - Goes through the book or document and finds all instances of a "z_anchor" pgf followed by a "fig" pgf followed by a "v_num" pgf followed by a "z_notes" pgf. This test is to make sure we are at a place to do the text operation.


    - Retrieves the text from the fig pgf


    - Inserts a new pgf above the "z_anchor" pgf, gives it a specified pgf format and adds the text retrieved in the previous step.


    It also has a function to clean out all old insertions, by simply deleting all pgfs with that format. In this way, it will only work correctly if the template is using a unique pgf tag for the auto-inserted Robohelp titles.


    Hopefully it can be of some use to someone. At the very least, I hope it might inspire others to explore the capabilities of ExtendScript, because while it is tough to figure out, it is a marvelous thing. I have historically been an FDK buff, but I'm really starting to lean in the ES direction for tasks like this. Once you get a hold of it (and despite the clunky ESTK editor), it is much easier than the FDK.





    //Define some tags that appear in the template.

    //We do this here for convenience, to make them

    //easier to change in the future, if necessary.

    var roboHelpFormatTag = "RoboHelpTitle";

    var anchoredFrameTag = "z_anchor";

    var figCaptionTagPrefix = "fig";

    var figNumTag = "v_num";

    var notesHeadingTag = "z_notes";


    //Get the active book or document object

    var file = app.ActiveDoc;


        file = app.ActiveBook;


    //If there is no active file, abort.



        alert("No active book or document. Cannot continue.");


    //Otherwise, let's get started

    else InitiateScript(file);


    //Everything after this point is a set of functions that drive the

    //main features of the script.


    //This initation function is the front

    //door that  handles overall file iteration,

    //reporting, etc. It also allows us

    //to confirm with the user at the beginning.

    function InitiateScript(file)


        //Make sure we really want to do this.

        if(!confirm("Run the RoboHelp title script " +

            "on the following file?\n\n" + file.Name))



        //we'll keep counters of what we do.

        var doc;

        var doInsertions = false;

        var totalCleanups = 0;

        var totalInsertions = 0;

        var totalFilesProcessed = 0;


        //If we are processing a book, let's get the first open chapter.

        //Otherwise, we'll process directly on the document object

        //we retrieved earlier.

        //GetNextOpenBookChapter is a custom function defined later.

        if( == "Book")

            doc = GetNextOpenBookChapter(file, null);

        else doc = file;


        //Confirmation. We allow the option to just remove old

        //titles without new insertions.

        if(confirm("Would you like to do the insertions or " +

            "just the cleanup phase? Click Yes to do both."))

                doInsertions = true;


        //Iterate through our documents for processing.

        while(doc != null && doc.ObjectValid())




            //Clear out the titles currently in the doc

            totalCleanups += ClearOutRoboHelpTitles(doc); 


            //...and, do the insertions, if requested       


                totalInsertions += InsertRoboHelpTitles(doc);


            //If we are processing a book, get the next open

            //chapter, otherwise null out the object to end the loop.

            if( == "Book")

                doc = GetNextOpenBookChapter(file, doc);

            else doc = null;



        //Report what we did.

        alert("Process complete.\n\n" + totalFilesProcessed +

            " file(s) processed.\n" + totalCleanups +

            " previous title(s) deleted.\n" + totalInsertions +

            " new title(s) inserted.");



    //This is the function that cleans out the

    //old titles from a single document.

    function ClearOutRoboHelpTitles(doc)


        var deletedPgfs = 0;

        var nextPgf;

        var pgf;


        //Get the first paragraph in the main flow

        //to start the iteration.

        pgf = doc.MainFlowInDoc.FirstTextFrameInFlow.FirstPgf;


        //Iterate over all paragraphs in the main flow.



            //Before we do anything, we need to

            //get the next paragraph object, so we

            //don't get lost if we end up deleting this one.

            nextPgf = pgf.NextPgfInFlow;


            //If the paragraph name (ie, the last assigned

            //format tag) is the title tag, let's delete.

            if(pgf.Name == roboHelpFormatTag)






            //Reassign our main loop variable

            //and continue iteration.

            pgf = nextPgf;



        //Return the total count of deleted pgfs.

        return deletedPgfs;



    //This is the function that inserts new titles

    //for a single document.

    function InsertRoboHelpTitles(doc)


        var totalInsertions = 0;

        var pgf;


        //Get the first paragraph in the main flow and begin iteration.

        pgf = doc .MainFlowInDoc.FirstTextFrameInFlow.FirstPgf;



            //Check to see if we are at a place where a title requires

            //insertion. We'll use a custom function for this, defined


            if(CheckIfInsertionIsRequired(doc, pgf))


                //If we get here, an insertion is required. Let's use this custom

                //function to do it.

                InsertPgf(doc, roboHelpFormatTag,







            //get the next paragraph to continue the loop. 

            pgf = pgf.NextPgfInFlow;       



        return totalInsertions;      



    //This is the function that checks whether

    //we are at an anchor paragraph tag, for which

    //a title insertion is required before it.

    //It basically checks a sequence of

    //tags, which if they all match, we

    //assume that it is a place for insertion.

    function CheckIfInsertionIsRequired(doc, pgf)


        var returnVal = false;


        //Check if we are at the right anchored frame tag

        if(pgf.Name == anchoredFrameTag)


            //If so, see if the next paragraph is a figure caption

            pgf = pgf.NextPgfInFlow;

            if(pgf.ObjectValid() && pgf.Name.indexOf(figCaptionTagPrefix) == 0)


                //If so, see if the next paragraph is a figure number

                pgf = pgf.NextPgfInFlow;

                if(pgf.ObjectValid() && pgf.Name == figNumTag)


                    //If so, see if the next paragraph is a notes heading

                    pgf = pgf.NextPgfInFlow;

                    if(pgf.ObjectValid() && pgf.Name == notesHeadingTag)


                        //if so, we have a winner!

                        returnVal = true;

                    } //end notes heading tag if

                } //end fig num tag if

            } //end fig caption tag if

        } //end anchored frame tag if


        return returnVal;



    //A general purpose paragraph inserter. It will insert

    //the new paragraph after the reference paragraph,

    //using the format and text provided.

    function InsertPgf(doc, formatName, referencePgf, newText)


        var newPgf;

        var everythingWorkedRight = false;


        //Insert the new paragraph.   


            newPgf = doc.NewSeriesObject (Constants.FO_Pgf, referencePgf);


            newPgf = doc.NewSeriesObject (Constants.FO_Pgf, 0);


        //If it inserted OK, set the text and the format;



            //Here is how to add text

            var tr = new TextRange();

            tr.beg.obj = tr.end.obj = newPgf;

            tr.beg.offset = 0;

            doc.AddText(tr.beg, newText);


            //Reset the text range offset to the end of the

            //newly-inserted text to prepare for formatting.

            tr.end.offset = Constants.FV_OBJ_END_OFFSET;


            //Get the format tag object, then apply if it exists in the template

            var formatObj =

                doc.GetNamedObject(Constants.FO_PgfFmt, formatName);



                //This is how you apply a paragraph format, by

                //copying the properties from the format object

                //to the paragraph object.

                var props = formatObj.GetProps();



                everythingWorkedRight = true;




        return everythingWorkedRight;



    //A simple function to retrieve the text of

    //a paragraph.

    function GetPgfText(pgf)


        var text = "";


        if(!pgf.ObjectValid()) return text;


        var ti = pgf.GetText(Constants.FTI_String);


        for(var i = 0; i < ti.length; i++)

            text += ti[i].sdata;


        return text;



    //A function to allow stepping through a book, chapter

    //by chapter. Send it the document you are currently at

    //and it will send back the next one that is open. It will

    //not open any closed files.

    //Send null for currentDoc to get the first open chapter.

    function GetNextOpenBookChapter(book, currentDoc)


        var comp;

        var returnDoc = null;

        var foundReference = false;


        //We will loop through all chapters (components).

        //However, we don't really want to consider any

        //until we've found the one we are currently at,

        //which is the doc sent here. So, the loop will operate

        //on this flag to know when to start considering

        //components. In the case where we want the first

        //open chapter, we can automatically assume that

        //any component is a valid candidate.

        if(currentDoc == null) foundReference = true;


        //get the first component in the book.

        comp = book.FirstComponentInBook;


        //Iterate through the components.

        while(comp != null && comp.ObjectValid())


            //If we find that we have reached the reference point;

            //that is, the current document, we can reset

            //this flag to start considering components

            //for return.

            if(!foundReference && comp.Name == currentDoc.Name)

                foundReference = true;


            //Otherwise, if we are already considering components,

            //let's see if there is an open document for this one.

            else if(foundReference)

                returnDoc = DocIsOpen(comp.Name);


            //If we got ourselves the next chapter, we are done. Null

            //out the loop variable to end iteration.

            if(returnDoc != null)

                comp = null;


            //otherwise, get the next component and keep going.

            else comp = comp.NextBookComponentInDFSOrder;



         //Send back whatever we got, if anything.

         return returnDoc;



    //A simple function to find an open document

    //based on its fully-qualified path.

    function DocIsOpen(path)


        var tempDoc;

        var returnDoc = null;


        //If our path is empty, no sense continuing.

        if(path == "") return returnDoc;


        //get the first open document in the session.

        tempDoc = app.FirstOpenDoc;


        //Loop through all open documents

        //until we find the one we want or we just

        //run out.



            if(tempDoc.Name == path)


                //if we found it, set our return

                //variable and break out of the loop.

                returnDoc = tempDoc;



            tempDoc = tempDoc.NextOpenDocInSession;



        //send back whatever we got, if anything.

        return returnDoc;

    Mark as:

More Like This

  • Retrieving data ...

Bookmarked By (0)

Answers + Points = Status

  • 10 points awarded for Correct Answers
  • 5 points awarded for Helpful Answers
  • 10,000+ points
  • 1,001-10,000 points
  • 501-1,000 points
  • 5-500 points