image
   

FrameScript Tutorials

 
 

Loops and Linked Lists

In the previous lesson, we introduced a simple FrameScript loop to process all of the paragraph formats in a document. In this lesson, we will introduce a variation of the loop, and the important concept of “linked lists” of FrameMaker objects.

Introduction to loops

The general syntax for the Loop ForEach command is

Loop ForEach(Object) In(Object) LoopVar(Variable)
...(perform tasks here)...
EndLoop

There are times, however, when this kind of loop will not work. For instance, you cannot use Loop ForEach to loop through all of the graphics in the active document; see the Scriptwriter’s Reference for a list of valid parameters. In addition, some commands will not work inside the loop. Here is an example. Create a blank, portrait document, choose FrameScript > Script Window, and type the following in the Script Window:

Loop ForEach(PgfFmt) In(ActiveDoc) LoopVar(vPgfFmt)
  Delete Object(vPgfFmt);
EndLoop

Open the document’s Paragraph Catalog, and click the Run button. We would expect the script to delete all of the paragraph formats in the document, but it doesn’t. Each time you click Run, the script deletes one format and stops. To understand why this happens, let’s introduce a different kind of loop and examine how loops work in general.

First, here is a loop from the last lesson that we know works.

Loop ForEach(PgfFmt) In(ActiveDoc) LoopVar(vPgfFmt)
  Display vPgfFmt.Name;
EndLoop

Now, we will introduce another loop that performs the same task.

Set vPgfFmt = ActiveDoc.FirstPgfFmtInDoc;
Loop While(vPgfFmt)
  Display vPgfFmt.Name;
  Set vPgfFmt = vPgfFmt.NextPgfFmtInDoc;
EndLoop

Here is a summary of what the second loop does.

  1. Sets the vPgfFmt variable equal to the document’s first paragraph format.
  2. Begins a loop that will continue as long as vPgfFmt is “true.” In other words, the loop will continue as long as vPgfFmt “exists.” If there are no paragraph formats in the document, the script will never enter the loop, because vPgfFmt would be “false” (does not exist).
  3. Once inside the loop, the script will display the current paragraph format’s name.
  4. The second line inside the loop is the key. The value of vPgfFmt is changed to the next paragraph format in the document. If it exists, the script stays in the loop, if not, the script exits the loop.

While the syntax of the loops is different, it is important to understand that the two loops operate the same. The first loop is simply a shorthand version of the second.

Linked lists of FrameMaker objects

The basic purpose of a loop is to perform a task for each member of a list of objects. You go from one member of the list to the next, performing the task on each member, until you reach the bottom of the list. FrameMaker maintains separate “linked lists” of each object type, such as paragraph formats, paragraphs, graphics, and tables. Each object represents a member of its list. Understanding linked lists is the key to writing scripts! This may seem like an overstatement, but you need to be able to access and traverse these lists (move from member to member) in order to manipulate them with scripts.

To illustrate linked lists, go to the Scriptwriter’s Reference and look up Document Properties. This list contains Document properties, but it also contains links to other lists of properties. To access the other lists, the Document object gives us a “link” to the first member of each list. In our script, we use dot notation to help us reach the first member in the list of paragraph formats.

Set vPgfFmt = ActiveDoc.FirstPgfFmtInDoc;

What if we want to reach the second paragraph format in the document? Can we use Set vPgfFmt = ActiveDoc.SecondPgfFmtInDoc; or Set vPgfFmt = ActiveDoc.NextPgfFmtInDoc;? Neither of these will work, because the Document object only gives us access to the first member of the list. The Document object says, if effect, “I’ll get you to the first member of the list, but after that, you are on your own.”

The responsibility of moving from member-to-member in a list falls on each of the list members. Each PgfFmt object has a NextPgfFmtInDoc property. Each Pgf object has a PrevPgfInDoc property and a NextPgfInDoc property. The Document object “passes the baton” to the first member of the list, and the first member passes it the next, etc. This what our script is doing inside the loop.

Set vPgfFmt = vPgfFmt.NextPgfFmtInDoc;

When the script gets to the last member of the list, its NextPgfFmtInDoc property returns zero because the NextPgfFmtInDoc object does not exist. This causes the vPgfFmt variable to be “false” and the loop exits.

Our first loop uses the same method to access the document’s paragraph formats, but the “baton passing” is hidden. Let’s use the second form of the loop (and our knowledge of linked lists) to solve the problem of the script stopping before it deletes all the formats. Here is the revised form of the script to delete all of the paragraph formats in the document.

Set vPgfFmt = ActiveDoc.FirstPgfFmtInDoc;
Loop While(vPgfFmt)
  Delete Object(vPgfFmt);
  Set vPgfFmt = vPgfFmt.NextPgfFmtInDoc;
EndLoop

Copy this script into your Script Edit Window and click Run. Like before, it only deletes one format, and this time, it gives an error. Can you figure out why it only deletes one format? If we use the runner with the baton metaphor, the answer is simple. The line

Delete Object(vPgfFmt);

deletes the vPgfFmt object before it has a chance to “pass the baton” to the next paragraph format in the document. To put it bluntly, we killed the runner before he could pass the baton! The next line in the script gives an error because the vPgfFmt object no longer exists.

A successful delete loop

Here is the solution:

Set vPgfFmt = ActiveDoc.FirstPgfFmtInDoc;
Loop While(vPgfFmt)
  Set vNextPgfFmt = vPgfFmt.NextPgfFmtInDoc;
  Delete Object(vPgfFmt);
  Set vPgfFmt = vNextPgfFmt;
EndLoop

What we are doing is setting another variable to the next paragraph format with the line

Set vNextPgfFmt = vPgfFmt.NextPgfFmtInDoc;

We are passing the baton to the next paragraph format before deleting the existing one.

Deleting unused paragraph formats

If you don’t quite understand this lesson yet, don’t be too concerned. Keep working with the mechanics of the scripts, and the concepts will become clearer. As a reward for your endurance, I will finish (and, hopefully, reinforce) the lesson with a very useful script. This script will delete all of the paragraph formats from the catalog that are not in use in the document. This is a good way to clean up a document before importing formats from a template. The script will also introduce another loop and some new concepts, such as error checking and conditional statements.

Determine the tasks

Before beginning the script, it is best to figure out how we are going to attack the problem. At this point, we want to determine the “logic” of the script without worrying about the syntax. I like to verbalize the overall solution in plain English, and then make a list of the individual tasks. Here is the list form of my solution.

  1. Make a list of all of the paragraph formats in the catalog.
  2. For each paragraph in document, determine what paragraph format it uses.
  3. If the paragraph’s format is in the list, remove it from the list.
  4. After testing all of the paragraphs, see if there are any paragraph formats left in the list.
  5. If there are, delete them from the document’s catalog.

Test for an active document

Before starting on the task list, you need to do some preliminary testing with the script. Since the script is working with paragraph formats in a document, it requires that a document be opened and active. The first part of the script will make sure that a document is active; if not, the script will exit. You should be careful to anticipate and test for different possible conditions when writing production scripts.

// Delete all unused paragraph formats in the document.
// First, test for an active document.
If ActiveDoc = 0
  MsgBox 'There is no active document.    ';
  LeaveSub; // Exit the script.
Else
  Set vDoc = ActiveDoc;
EndIf

We are using an If/Else/EndIf statement to do the test. If there is no active document, the Session property ActiveDoc will return 0, and the script will display a message to the user and exit. If there is an active document, the Else part of the statement will execute and we set a variable for the active document. Now we can start on the task list.

Make a list of paragraph formats in the document.

We are actually making a list of the names of the paragraph formats in the document. The Name property is a string, so it is easiest to store them in a string list variable.

// Make a string list of paragraph formats to delete.
New StringList NewVar(vPgfFormatsToDelete);

The next step is to add the paragraph format names to the string list.

// Add the paragraph format names to the string list.
Set vPgfFmt = vDoc.FirstPgfFmtInDoc;
Loop While(vPgfFmt)
  Add Member(vPgfFmt.Name) To(vPgfFormatsToDelete);
  Set vPgfFmt = vPgfFmt.NextPgfFmtInDoc;
EndLoop

At this point, you can try the script up to this point. Add the following line to the end of the script and run it.

Display vPgfFormatsToDelete;
image

The names and order of the names will vary for your document, but you can see if the script is working. When you are finished testing, delete the Display line from the end of the script.

In FrameScript 2.1 and later, there is a shortcut to making the string list without the loop. It uses a new, built-in list variable that contains the names of the formats.

// Make a string list of paragraph formats to delete.
Set vPgfFormatsToDelete = vDoc.DocPgfFmtNameList;

Test each paragraph

Now that we have a list of paragraph formats, we need to test each paragraph in the document to see if it is using one of the paragraph formats in the list. We want to remove paragraph names that are in use from the list. When we are finished, any names remaining in the list represent paragraph formats that are not in use and can be deleted.

// Loop through the paragraphs in the document.
Loop ForEach(Pgf) In(vDoc) LoopVar(vPgf);
  // See if the paragraph's format is in the list.
  Find Member(vPgf.Name) InList(vPgfFormatsToDelete)
    ReturnStatus(vFound);
  If vFound = 1
    // If it is, remove it from the list.
    Remove Member(vPgf.Name) From(vPgfFormatsToDelete);
  EndIf
EndLoop

We are using a simple loop through all of the paragraphs in the document. For each paragraph in the document, we test to see if the Pgf.Name value is in the string list of paragraph format names. If it is, we remove it from the list.

There is one thing we can do to improve the performance of the loop. If all of the members are removed from the string list, the loop will continue testing any remaining paragraphs in the document. For many documents, this won’t matter much, but on large documents it could slow things down. We can add a test to see if the string list is empty, and if it is, leave the loop.

// Loop through the paragraphs in the document.
Loop ForEach(Pgf) In(vDoc) LoopVar(vPgf);
  // See if the paragraph's format is in the list.
  Find Member(vPgf.Name) InList(vPgfFormatsToDelete)
    ReturnStatus(vFound);
  If vFound = 1
    // If it is, remove it from the list.
    Remove Member(vPgf.Name) From(vPgfFormatsToDelete);
    // See if there any members, left in the list.
    If vPgfFormatsToDelete.Count = 0
      LeaveLoop; // If the list is empty, exit the loop.
    EndIf
  EndIf
EndLoop

You can test the script at this point by displaying the string list. Add the following line at the end of the script and test the script.

Display vPgfFormatsToDelete;

image

In my example, you can see that there are 12 unused paragraph formats in the document. The original list had 20 formats, so there are 8 formats in use in my document. If the string list has 0 members, then all of the paragraph formats are in use in the document.

Delete the unused formats

If there are unused format names in the list, the final step is to loop through the list and delete the formats.

// Loop through the string list and delete each unused format.
Loop While(i <= vPgfFormatsToDelete.Count)
  LoopVar(i) Init(1) Incr(1)
  Get Member Number(i) From(vPgfFormatsToDelete) 
    NewVar(vPgfFormat);
  Get Object Type(PgfFmt) Name(vPgfFormat) DocObject(vDoc) 
    NewVar(vPgfFmt);
  Delete Object(vPgfFmt);
EndLoop