DekGenius.com
[ Team LiB ] Previous Section Next Section

3.2 A Day in the Life

Although I know that FrameMaker is scriptable, I have no idea how to script it. I haven't the slightest notion how to talk to FrameMaker, using AppleScript, about the illustrations in my manuscript. So the first thing I need to do is to try to find this out.

3.2.1 Caught in the Web of Words

My starting place, as with any new AppleScript programming task, is the dictionary of the application I'm going to be talking to. The dictionary is where an application lists the nouns and verbs I can use in speaking to it with AppleScript. To see FrameMaker's dictionary, I start up Apple's Script Editor, open the Library window, add FrameMaker to the library, and double-click its icon in the Library window. The dictionary opens, as shown in Figure 3-1.

Figure 3-1. FrameMaker dictionary
figs/as_0301.gif

This is a massive and, to the untrained eye (or even to the trained eye), largely incomprehensible document. What are we looking for here? Basically, I'd like to know whether FrameMaker gives me a way to talk about illustrations. To find out, I open each of the headings on the left, and under each heading I open the Classes subheading. A class is basically a noun, the name of a type of thing in the world of the application we're talking to. So what I'm trying to find out is what things FrameMaker knows about, so that I can guess which of those things is likely to be most useful for the problem I'm facing. In particular, I'd like to find a class that stands a chance of being what FrameMaker thinks my illustrations are.

The fact is, however, that I don't see anything that looks promising. The trouble is that I don't really understand what an illustration is, in FrameMaker's terms. I know that to add an illustration to a FrameMaker document, using the template my publisher has set up, I begin by inserting a table. And sure enough, there is a table class in the FrameMaker dictionary. But then, to make the reference to an illustration file, I choose the Import File menu item, and it is not at all clear to me what kind of entity I generate as a result.

At this point an idea strikes me. Perhaps I should start with an existing illustration and see if I can find a way to ask FrameMaker, "What's this?" In fact, very near the start of FrameMaker's dictionary—and you can see this in Figure 3-1—there's a listing called selection. This suggests that perhaps if I select an illustration manually in FrameMaker and then use AppleScript to ask FrameMaker for its selection, I will learn what sort of thing an illustration is.

The word selection is listed as a property of the application class. A property is a feature of a class that you can refer to with the word of followed by an instance of the class. For example, if you have a reference to a paragraph and the paragraph class has a font property, you can refer to the "font of" this paragraph. However, the application class is special; it's the ultimate reference, and you don't need any "of" to talk about its properties when you're already talking to that application.

To talk to an application in AppleScript, you embed your code in a tell block, like this:

tell application "FrameMaker 7.0"
        ...
end tell

Whatever I say inside that block will be addressed to the named application, which in this case is FrameMaker. The way you ask for the value of a thing in AppleScript is with the verb get. So, in FrameMaker, I manually select an illustration; then, in Script Editor, I make a new script window and enter this code:

tell application "FrameMaker 7.0"
        get selection
end tell

I run that code, and the result comes back in the lower part of the script window, in the Result tab (I've reformatted the result here to emphasize its structure):

inset 1
        of anchored frame 43 
                of document "extra:applescriptBook:ch02places.fm" 
                        of application "FrameMaker 7.0"

Wow! All of a sudden I'm getting someplace. I now have a chain of ofs showing the classes needed to refer to an illustration. So it turns out there's a class called inset, and that an inset belongs to another class called anchored frame. I certainly would never have thought of any of that on my own; even with the help of the dictionary I would never have realized that these were the classes I needed. Now that I know, of course, I see that these classes are indeed listed in the FrameMaker dictionary.

In particular, looking at the dictionary listing for the inset class, I see that it has a property inset file, which is described as follows:

inset file alias -- The file where the graphic originated

This could be just what I'm after—the link between an illustration in FrameMaker and the illustration file on disk. To find out, I'll ask for the inset file property of the same illustration I just selected a moment ago. So I make a new script and enter some new code and run it. Everything here is based on the reply I just received from AppleScript a moment ago; however, for convenience and clarity I've reversed the order of everything, and I'm using nested tell blocks instead of the word of. (Tell blocks and the word of are almost the same thing, except that they work in the opposite order.)

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell anchored frame 43
                        get inset file of inset 1
                end tell
        end tell
end tell

And here's the answer that comes back:

"extra:applescriptBook:figures:ch02:fileMaker1.eps"

Perfect! This is sensational. I've started with a way of referring to an illustration inside FrameMaker, and I've ended up with the pathname of the corresponding illustration file on disk—the very file I'm going to want to rename. Clearly I'm on the right track. I set this script aside and move on to the next part of the problem.

3.2.2 One for All and All for One

I now have some idea of how I'm going to refer to an illustration in FrameMaker and how I'm going to mediate between that reference and the pathname of the file on disk that the illustration comes from. So, having solved this piece of the puzzle for one illustration, I move on to the problem of generalizing. I'm going to want to do this for every illustration in the document. How, using AppleScript, am I going to talk about every illustration?

In the previous code, I specified a particular illustration by talking about "anchored frame 43." This sort of reference is not a property; it specifies an element. An element is very like a property, except that with a property there is just one of a thing; with an element, there can be many things of a certain class, and you have to say which one you want. So if the class that represents my illustrations is the anchored frame class, perhaps the way I'm going to solve the problem is by cycling through the anchored frame elements of my document—that is, by talking about "anchored frame 1," then "anchored frame 2," and so on. I'm a little surprised, however, by how the numbers are working here. I don't have 43 illustrations in this document, so why am I talking about "anchored frame 43"? However, I press on regardless; we'll cross that bridge when we come to it.

Let's see if I can list the inset files for all the illustrations in the document. To do so, I'll start by gathering up a list of all the anchored frame elements; if FrameMaker will let me, I should be able to do that using the word every. Let's try it:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                get every anchored frame
        end tell
end tell

Here's the response:

{anchored frame 1 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0", 
 anchored frame 2 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0", 
 anchored frame 3 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0",
 ... 
}

I've left out most of it, but you get the idea. This seems to be working nicely so far. What I've gotten back is a list—that's what the curly braces around the response mean—and each item of this list, separated by commas, is a reference to one of the anchored frames in the document. So I should be able to run through this list and ask each anchored frame for the inset file property of its "inset 1" element, just as I did with anchored frame 43 earlier.

The way you run through a list in AppleScript is with a repeat block. I'll start by making a variable allFrames to store the list in—in AppleScript, you define the value of a variable using the verb set—and then I'll see if I can run through it:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                set allFrames to get every anchored frame
                repeat with oneFrame in allFrames
                end repeat
        end tell
end tell

That code runs, which is good. First I've made a variable allFrames to hold the list; then I've made another variable oneFrame to represent each item of that list as I run through it. But the code doesn't do anything, because I haven't said yet what I want to do with oneFrame; there is no code inside the repeat block.

What I'll do now is create yet another variable, allPaths, to hold my file paths. I'll start this variable as an empty list, which in AppleScript is symbolized by empty curly braces; every time I get a file path, I'll append it to the list, which you do in AppleScript with the verb set end of. So here's my code:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                set allPaths to {}
                set allFrames to get every anchored frame
                repeat with oneFrame in allFrames
                        set end of allPaths to inset file of inset 1 of oneFrame
                end repeat
        end tell
end tell

I run this code, and . . . it doesn't work! I get an error message:

FrameMaker 7.0 got an error: Can't get inset file of inset 1 of anchored frame 1 
        of document "extra:applescriptBook:ch02places.fm".

I don't really know what this error message means. That sort of thing happens a lot when you're working with AppleScript; stuff goes wrong, but you don't get a very helpful error message explaining why. However, I do see that we didn't get far in the list; right at the start, with anchored frame 1, we had a problem. Now, we know that this is going to work for anchored frame 43, so maybe the problem is related to the mystery of the numbering of the anchored frames. Maybe I've got two kinds of anchored frame: those that represent illustrations and those that don't, which apparently is the same thing as saying those that have an inset file and those that don't.

Since I believe this code should work when I get up to anchored frame 43, I'd like to ignore the problem with anchored frame 1 and any other anchored frames that may not be relevant here. There's an easy way to do this: I'll wrap the code in a try block. A try block is AppleScript's form of error-handling. I expect I'll still get an error, but now the code won't stop; it will shrug off the error and keep going. In this way I hope to cycle far enough through the list of anchored frames that I get to the ones where I don't get an error. Here's the code now:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                set allPaths to {}
                set allFrames to get every anchored frame
                repeat with oneFrame in allFrames
                        try
                                set end of allPaths to inset file of inset 1 of oneFrame
                        end try
                end repeat
        end tell
end tell

I run that code, and there's no error. However, I'm not getting much of a result, either; here's what I get:

"extra:applescriptBook:figures:ch02:RB.eps"

That's the pathname of an illustration file, all right, but it's not what I was expecting; I wanted a list of all the pathnames of all the illustration files. Oh, wait, I see what I did wrong. I constructed the list, as the variable allPaths, but I forgot to ask for that list as the final result of the script. The result that you see in the Result pane of the Script Editor after you run a script is the value of the last command that was executed. So the way to display, as your result, the value of a variable you're interested in is to say the name of that variable as the last executable line of your code. Let's try again, like this:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                set allPaths to {}
                set allFrames to get every anchored frame
                repeat with oneFrame in allFrames
                        try
                                set end of allPaths to inset file of inset 1 of oneFrame
                        end try
                end repeat
        end tell
end tell
allPaths

And here's the result:

 {"", "", "", "", "", "", 
 "extra:applescriptBook:figures:ch02:fileMaker1.eps",
 "extra:applescriptBook:figures:ch02:fileMaker2.eps",
 "extra:applescriptBook:figures:ch02:fileMaker3.eps",
 "extra:applescriptBook:figures:ch02:ib.eps",
 "extra:applescriptBook:figures:ch02:ib2.eps",
 "extra:applescriptBook:figures:ch02:scriptEditor3.eps",
 "extra:applescriptBook:figures:ch02:scriptEditorDict.eps",
 "extra:applescriptBook:figures:ch02:scriptDebugger.eps",
 "extra:applescriptBook:figures:ch02:scriptDebuggerDict.eps",
 "extra:applescriptBook:figures:ch02:radio.eps",
 "extra:applescriptBook:figures:ch02:radio2.eps",
 "extra:applescriptBook:figures:ch02:word3.eps",
 "extra:applescriptBook:figures:ch02:word4.eps",
 "extra:applescriptBook:figures:ch02:ib3.eps",
 "extra:applescriptBook:figures:ch02:RB.eps"}

Well, that's pretty good. I have no idea what those first six items are, the ones that just show up as empty strings (symbolized by empty pairs of quotation marks). But in my final code I guess I could just ignore the empty strings, so that's not really a problem. And then we've got 15 pathnames, which is exactly right because the chapter has 15 illustrations.

But there's a problem. A really big problem. The pathnames are in the wrong order.

Remember, our entire purpose is to rename these files in accordance with the order in which they appear in the document. But this is not the order in which they appear in the document. I don't know what order it is, but I do know that the first illustration in the document is scriptEditor3.eps. This is a disaster. Our efforts so far have probably not been a total waste, but there's no denying that we're completely stuck. The "every anchored frame" strategy is a failure.

3.2.3 Seek and Ye Shall Find

At this point I'm exhausted and frustrated, so I do something else for a while and brainstorm subconsciously about the problem to see if I can come up with a new angle. Instead of gathering up all anchored frame references as FrameMaker understands them, we want to run forward through the document itself looking for anchored frames, just as a user would. Hmm . . . as a user would . . . .

This gives me an idea. How would I, as a user, run through the illustrations in a FrameMaker document? I'd use the Find dialog. Perhaps FrameMaker lets me do the same thing with AppleScript. Yes, by golly; looking in the dictionary, I discover there's a find command. Here's the dictionary entry:

find: Find text, objects, or properties in a Frame document.
        find text/text having paragraph tag/text having character tag/marker/
                marker having type/marker containing text/variable/variable having name/
                anchored frame/table/table having tag/footnote/xref/xref having format/
                unresolved xref/autohyphen -- The type of object or property to be found.
                [with value string] -- The value of the text, tag, type, format, or name being found.
                [with properties a list of list] -- The properties of the text to be found.
                         Most text properties will work here. Finding both value and properties
                         is unlikely to work, and some combinations of properties don't work
                         well together.
                in reference -- The document in which to find.
                [using use case/whole word/use wildcards/backwards/no wrap/
                        find next] -- The options to be applied to the find.
        Result: reference -- to the object that was found.

The words "anchored frame" in the first paragraph leap right off the screen at me. I can find an anchored frame! I create a new script window in Script Editor, and try it.

tell application "FrameMaker 7.0"
        find anchored frame
end tell

No, when I run that it generates an error. What's gone wrong? Oh, I see: I've left out the in parameter. I have to tell FrameMaker what document to look in.

tell application "FrameMaker 7.0"
        find anchored frame in document "extra:applescriptBook:ch02places.fm"
end tell

It works! The first illustration is selected, and the result is a reference to it, just as the dictionary promises:

anchored frame 48 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0"

So now it begins to look like I can use find repeatedly to get a succession of references to the anchored frames in the document in the order in which they actually appear. To test this idea, I'll just make an artificial loop by using the repeat command without worrying for now about how many times I would have to loop in real life:

tell application "FrameMaker 7.0"
        set allPaths to {}
        repeat 5 times
                set oneFrame to find anchored frame ¬
                        in document "extra:applescriptBook:ch02places.fm"
                set end of allPaths to inset file of inset 1 of oneFrame
        end repeat
end tell
allPaths

Here's the result:

 {"extra:applescriptBook:figures:ch02:scriptEditor3.eps",
 "extra:applescriptBook:figures:ch02:scriptEditor3.eps",
 "extra:applescriptBook:figures:ch02:scriptEditor3.eps",
 "extra:applescriptBook:figures:ch02:scriptEditor3.eps",
 "extra:applescriptBook:figures:ch02:scriptEditor3.eps"}

Oops. We're not moving forward through the document; we're just finding the same illustration repeatedly. In FrameMaker itself, hitting the Find button over and over keeps finding the next match, which is what we want; in AppleScript, though, it appears that giving the find command over and over keeps finding the same match. But wait; the find command has a using parameter where I can specify find next. Let's try that:

tell application "FrameMaker 7.0"
        set allPaths to {}
        repeat 5 times
                set oneFrame to find anchored frame ¬
                        in document "extra:applescriptBook:ch02places.fm" using find next
                set end of allPaths to inset file of inset 1 of oneFrame
        end repeat
end tell
allPaths

Darn it; this generates the same result. I guess "find next" simply means to find forwards as opposed to backwards. The trouble is I'm not finding forwards. It appears that once I've selected something, finding again just finds that same thing again. All right, then, maybe if I can just somehow move the selection point forward a little after finding an illustration, I'll be able to find the next illustration instead of the current one. So now I have to figure out how to move the selection point forward. I start by selecting some text, and then comes a long round of experimentation, of which I'll spare you the details, at the end of which I come up with this:

tell application "FrameMaker 7.0"
        select insertion point after selection
end tell

This works just fine when the selection is some text. Unfortunately, as I soon discover, when the selection is an illustration, I get an error message. This is so frustrating! Just when I thought I had the problem solved, I'm completely blocked again, simply because I don't know how to move the selection off an illustration.

3.2.4 Turning the Tables

At this point I remember that every illustration is embedded in a table. According to FrameMaker's dictionary, the find command has an option to find a table. Perhaps this will work better if I start by dealing with tables instead of anchored frames. So I try this:

tell application "FrameMaker 7.0"
        find table in document "extra:applescriptBook:ch02places.fm"
        select insertion point after selection
end tell

Gee, there's no error. Could it be that this is actually working? To find out, I'll try to cycle through several tables, collecting references to them to see if I'm finding different ones:

tell application "FrameMaker 7.0"
        set allTables to {}
        repeat 5 times
                set oneTable to find table ¬
                        in document "extra:applescriptBook:ch02places.fm"
                set end of allTables to oneTable
                select insertion point after selection
        end repeat
end tell
allTables

Here's the result:

{table 52 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0",
 table 53 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0",
 table 51 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0",
 table 54 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0",
 table 55 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0"}

That's great. The numbers are once again mystifying; I have no idea why FrameMaker thinks there are at least 55 tables in this document, and of course it is numbering them in a different order than they appear in the document, just as it did with anchored frames. But the important thing is that those are five different tables. That means I really can cycle through the tables of the document this way.

Now I need to prove to myself that having found a table I can get to the anchored frame—the illustration—inside it. This could be tricky, but surely there's a way. Examining the table class listing in the dictionary, I see that a table has cell elements. Well, for a table representing an illustration, that should be simple enough; there's only one cell. Let's see:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell table 55
                        get cell 1
                end tell
        end tell
end tell

Yes, that runs without error. Now, what's inside a cell? Looking in the cell class listing in the dictionary, I see that it has various possible elements, including paragraph, word, and text. Let's try paragraph:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell table 55
                        tell cell 1
                                get paragraph 1
                        end tell
                end tell
        end tell
end tell

Yes, that too runs without error. But can I get from this paragraph to the anchored frame, the actual illustration? There's only one way to find out—try it:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell table 55
                        tell cell 1
                                tell paragraph 1
                                        get anchored frame 1
                                end tell
                        end tell
                end tell
        end tell
end tell

Here's the result:

anchored frame 52 of document "extra:applescriptBook:ch02places.fm" 
        of application "FrameMaker 7.0"

Son of a gun, it worked. Starting with a reference to a table, I've found a way to refer to the anchored frame inside it. But in that case—dare I say it?—the problem is essentially solved. I know that in principle I can cycle through all the tables in a document, in the order in which they appear. I know that in principle I can get from a reference to a table to a reference to an anchored frame. I know that, given an anchored frame, I can obtain the pathname for the file on disk that is the source of the illustration. So I should be able to put these abilities all together and get the pathnames for the illustration files, in the order in which the illustrations appear in the document.

Let's try it. I'll start things off at the top of the document by selecting the first paragraph. Then I'll cycle through the tables. As I come to each table, I'll get the anchored frame, and from there I'll get the pathname to its source file and append it to a list. I'll continue my policy of cycling some arbitrary number of times, because I don't want to worry yet about the real question of how many times to do it; I just want to prove to myself that I can do it.

The first thing is to learn how to select the first paragraph. This turns out to be somewhat tricky. I try this, but it doesn't work:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                select paragraph 1
        end tell
end tell

By once again using my trick of selecting the first paragraph manually and then asking FrameMaker for its selection, so as to learn how it thinks of a paragraph, I finally come up with this:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                select paragraph 1 of text flow 1
        end tell
end tell

Now I'm ready to put it all together:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                set allPaths to {}
                select paragraph 1 of text flow 1
                repeat 5 times
                        set oneTable to find table in it
                        set end of allPaths to inset file of inset 1 ¬
                                of anchored frame 1 of paragraph 1 of cell 1 of oneTable
                        select insertion point after selection
                end repeat
        end tell
end tell
allPaths

A change you'll notice here is the use of the word it. In AppleScript, that's how you refer to a thing when you're already inside a tell block addressing that thing. This is needed because the find command requires a reference to a document, but all the other commands are already being addressed to that document.

Here's the result:

{"extra:applescriptBook:figures:ch02:scriptEditor3.eps",
 "extra:applescriptBook:figures:ch02:scriptEditorDict.eps",
 "extra:applescriptBook:figures:ch02:scriptDebugger.eps",
 "extra:applescriptBook:figures:ch02:scriptDebuggerDict.eps",
 "extra:applescriptBook:figures:ch02:radio.eps"}

That's the right answer: those are the pathnames to the first five illustrations in the document, in the order in which they appear. For the first time since starting to work on the problem, I now believe I'm going to be able to solve it.

3.2.5 Refiner's Fire

Now let's make a few refinements. First, it occurs to me that I'm doing something rather stupid here; I'm finding every table. That's going be troublesome, because some tables are illustrations but some are just ordinary tables. I want to find illustration tables only. I know that in my FrameMaker template these are tables whose tag (or style name) is "Figure". The find command, according to FrameMaker's dictionary, lets me find a table having tag. So that's the way I should find my tables. After a short struggle to understand the syntax of this command, I come up with the following new version of my script:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                set allPaths to {}
                select paragraph 1 of text flow 1
                repeat 5 times
                        set oneTable to find table having tag with value "Figure" in it
                        set end of allPaths to inset file of inset 1 ¬
                                of anchored frame 1 of paragraph 1 of cell 1 of oneTable
                        select insertion point after selection
                end repeat
        end tell
end tell
allPaths

The result is just the same, so I haven't wrecked the successes I've already had (known in the programming business as a "regression"), and I believe I've eliminated some possible false positives from the find.

Next, let's worry about how to know how many times to loop. By changing "5 times" to "20 times", which is more times than the number of illustrations in the document, and then running the script again, I discover that when I get to the end of the document the search wraps around and starts from the top once more. I try to fix this by adding the option using no wrap to the find, but it doesn't help. Therefore I'd like to know beforehand exactly how many times to loop.

Now, I know from the dictionary that a table has a table tag property. AppleScript has a construct (called a "boolean test specifier") that allows me to specify particular objects of a class in terms of the value of one of that class's properties; not every scriptable application implements this construct when you'd like it to, but the only way to find out whether FrameMaker does in this case is to try it, so I do. After some stumbling about, I realize that a document has a text flow element that I have to refer to before I can refer to a table, and I come up with this:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell text flow 1
                        get tables whose table tag is "Figure"
                end tell
        end tell
end tell

That works. But I don't really want this entire list; I just want to know how many items it contains. In AppleScript, the size of a list can be obtained with the count command:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell text flow 1
                        count (get tables whose table tag is "Figure")
                end tell
        end tell
end tell

The result is 15. That's correct. So I should be able to use this approach before starting my loop in order to know just how many times to loop. Here's my new version of the script:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell text flow 1
                        set howMany to count (get tables whose table tag is "Figure")
                end tell
                set allPaths to {}
                select paragraph 1 of text flow 1
                repeat howMany times
                        set oneTable to find table having tag with value "Figure" in it
                        set end of allPaths to inset file of inset 1 ¬
                                of anchored frame 1 of paragraph 1 of cell 1 of oneTable
                        select insertion point after selection
                end repeat
        end tell
end tell
allPaths

And here's the result; it's absolutely perfect, the correct names of the correct illustrations in the correct order:

{"extra:applescriptBook:figures:ch02:scriptEditor3.eps",
 "extra:applescriptBook:figures:ch02:scriptEditorDict.eps",
 "extra:applescriptBook:figures:ch02:scriptDebugger.eps",
 "extra:applescriptBook:figures:ch02:scriptDebuggerDict.eps",
 "extra:applescriptBook:figures:ch02:radio.eps",
 "extra:applescriptBook:figures:ch02:radio2.eps",
 "extra:applescriptBook:figures:ch02:word3.eps",
 "extra:applescriptBook:figures:ch02:word4.eps",
 "extra:applescriptBook:figures:ch02:fileMaker1.eps",
 "extra:applescriptBook:figures:ch02:fileMaker2.eps",
 "extra:applescriptBook:figures:ch02:fileMaker3.eps",
 "extra:applescriptBook:figures:ch02:ib.eps",
 "extra:applescriptBook:figures:ch02:ib2.eps",
 "extra:applescriptBook:figures:ch02:ib3.eps",
 "extra:applescriptBook:figures:ch02:RB.eps"}

3.2.6 Naming of Parts

Let's now turn our attention to the business of deriving the new name of each illustration. This will involve the chapter number. How can we learn this number?

It happens that in the FrameMaker template I'm using, every chapter document has exactly one paragraph whose tag (paragraph style) is "ChapterLabel," and that the text of this paragraph is the chapter number. So if FrameMaker gives me a way to refer to this paragraph, I should be home free. The dictionary tells me that the paragraph class has a paragraph tag property. Using the same sort of boolean test specifier construct I used a moment ago to find only those tables with a particular table tag, I try to find just those paragraphs that have this particular paragraph tag, expecting there to be just one:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell text flow 1
                        get paragraphs whose paragraph tag is "ChapterLabel"
                end tell
        end tell
end tell

This doesn't give me an error, but the result is not quite what I expected:

{""}

I guess the problem is that the paragraph itself is empty; the chapter number is generated automatically through FrameMaker's autonumbering feature, and doesn't really count as its text. The dictionary lists a couple of paragraph properties that look promising here:

autoNum string string -- The automatic numbering format string.
paragraph number string -- The formatted string representation of the paragraph number.

The second one looks like what I'm after, so I try it:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell text flow 1
                        get paragraph number of paragraphs ¬
                                whose paragraph tag is "ChapterLabel"
                end tell
        end tell
end tell

The result is this:

{"Chapter 2"}

That's the right answer! It's a list because I asked for all such paragraphs, but it's a list of just one item because there is only one such paragraph, and the string "Chapter 2" provides the chapter number for this chapter. Now, of course, I need to extract just the "2" from this string, but that's easy, because AppleScript understands the concept of a word:

tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell text flow 1
                        set chapNum to (get paragraph number of paragraphs ¬
                                whose paragraph tag is "ChapterLabel")
                end tell
        end tell
end tell
set chapNum to word -1 of item 1 of chapNum
chapNum

The element item 1 extracts the single string "Chapter 2" from the list, and the element word -1 extracts the last word of that. The result is "2", which is perfect.

I want this number formatted to have two digits. Now, this is a piece of functionality I'm going to need more than once, so I write a little handler (a subroutine). The idea of a handler is that repeated or encapsulated functionality can be given a name and moved off to another location; this makes your code cleaner. This handler's job is to accept a string and pad it with zero at the front until it is two characters long. Having written the handler, I add some code to call the handler and test it, twice, because I want to make sure it works for a string that is either one or two characters long.

on pad(s)
        repeat while length of s < 2
                set s to ("0" & s)
        end repeat
        return s
end pad
log pad("2")
log pad("22")

The pad handler makes use of concatenation (via the ampersand operator) to assemble the desired string. Those last two lines do two things:

  • The pad command calls the pad handler that appears earlier in the code.

  • The log command puts the result of the pad command into the Event Log History window. The Event Log History window must be opened manually before running the script.

Logging like this is a good approach when you want to test more than one thing in a single running of a script. The result looks good:

        (*02*)
        (*22*)

Those parentheses and asterisks are comment delimiters; I don't quite understand why the log window uses them, but it doesn't matter.

Another problem is that the new name of each illustration is going to be based partly on its old name. I need to break up the illustration file's pathname into its components, and I need to break up the last component, the actual name of the file, into the name itself and the file-type suffix, because the only part of the pathname I want to change is the name itself.

The typical AppleScript way to break up a string into fields based on some delimiter is to set a special variable called text item delimiters to that delimiter and then ask for the string's text items. So I'll do that twice, once with colon as the delimiter and again with period as the delimiter. Once again, I'll make this a handler, just because it makes the script so much neater. I'll have the handler return both results, the list of pathname components together with the list of filename components, combined as a single list. That way, when I call this handler, I will have all the pieces and can reassemble them the way I want:

on bust(s)
        set text item delimiters to ":"
        set pathParts to text items of s
        set text item delimiters to "."
        set nameParts to text items of last item of pathParts
        return {pathParts, nameParts}
end bust
bust("disk:folder:folder:file.suffix")

The result shows that the right thing is happening:

{{"disk", "folder", "folder", "file.suffix"}, {"file", "suffix"}}

Now I'm ready to practice renaming an illustration file. I'll write a handler that takes two numbers and the current pathname of the illustration file and generates the new pathname for that file:

on pad(s)
        repeat while length of s < 2
                set s to ("0" & s)
        end repeat
        return s
end pad
on bust(s)
        set text item delimiters to ":"
        set pathParts to text items of s
        set text item delimiters to "."
        set nameParts to text items of last item of pathParts
        return {pathParts, nameParts}
end bust
on rename(n1, n2, oldPath)
        set bothLists to bust(oldPath)
        set extension to last item of item 2 of bothLists
        set pathPart to items 1 thru -2 of item 1 of bothLists
        set newFileName to "as_" & pad(n1) & pad(n2)
        set newFileName to newFileName & "." & extension
        set text item delimiters to ":"
        return (pathPart as string) & ":" & newFileName
end rename
rename("2", "3", "disk:folder:folder:oldName.eps")

The expression as string when applied to a list, as in the very last line of the rename handler, assembles the items of the list, together with the text item delimiters between each pair of items, into a single string. And here's the result:

"disk:folder:folder:as_0203.eps"

Got it right the first time! It's hard to believe, but I am now ready for a practice run using an actual FrameMaker document.

3.2.7 Practice Makes Perfect

If I've learned one thing about programming over the years it's to practice before doing anything drastic. I'm going to run through the document and pretend to change the illustration names. Instead of really changing them, I'll log a note telling myself what I would have changed the name to if this had been the real thing. So, putting it all together, here's my practice script:

on pad(s)
        repeat while length of s < 2
                set s to ("0" & s)
        end repeat
        return s
end pad
on bust(s)
        set text item delimiters to ":"
        set pathParts to text items of s
        set text item delimiters to "."
        set nameParts to text items of last item of pathParts
        return {pathParts, nameParts}
end bust
on rename(n1, n2, oldPath)
        set bothLists to bust(oldPath)
        set extension to last item of item 2 of bothLists
        set pathPart to items 1 thru -2 of item 1 of bothLists
        set newFileName to "as_" & pad(n1) & pad(n2)
        set newFileName to newFileName & "." & extension
        set text item delimiters to ":"
        return (pathPart as string) & ":" & newFileName
end rename
tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch02places.fm"
                tell text flow 1
                        set howMany to count (get tables whose table tag is "Figure")
                        set chapNum to (get paragraph number of paragraphs ¬
                                whose paragraph tag is "ChapterLabel")
                end tell
                set chapNum to word -1 of item 1 of chapNum
                set allPaths to {}
                select paragraph 1 of text flow 1
                set counter to 1
                repeat howMany times
                        set oneTable to find table having tag with value "Figure" in it
                        set thisFile to inset file of inset 1 ¬
                                of anchored frame 1 of paragraph 1 of cell 1 of oneTable
                        set newName to ¬
                                my rename(chapNum, (counter as string), thisFile)
                        log "I found " & thisFile
                        log "I'm thinking of changing it to " & newName
                        select insertion point after selection
                        set counter to counter + 1
                end repeat
        end tell
end tell

Observe that I have put in the variable counter, which starts at 1 and is incremented in every loop; this is how I know how many times I've done the find, and therefore tells me the number of the current illustration. I must admit that it took me a couple of tries to get this script to run. When I first tried to run it, I got an error at the point where I call the rename handler. This was because I had forgotten to put the magic word my before it; this word tells AppleScript that even though I'm talking to FrameMaker I want to call a handler in my own script. And then, after I made that change and ran the script again, I got another error: it appeared that the rename handler was called but was now choking. The reason was that the variable counter was a number, and the rename handler was passing this on to the pad handler, which was expecting a string. The phrase counter as string converts the number to a string for purposes of passing it to the rename handler.

Here are the relevant entries of the resulting log:

        (*I found extra:applescriptBook:figures:ch02:scriptEditor3.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0201.eps*)
        (*I found extra:applescriptBook:figures:ch02:scriptEditorDict.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0202.eps*)
        (*I found extra:applescriptBook:figures:ch02:scriptDebugger.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0203.eps*)
        (*I found extra:applescriptBook:figures:ch02:scriptDebuggerDict.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0204.eps*)
        (*I found extra:applescriptBook:figures:ch02:radio.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0205.eps*)
        (*I found extra:applescriptBook:figures:ch02:radio2.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0206.eps*)
        (*I found extra:applescriptBook:figures:ch02:word3.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0207.eps*)
        (*I found extra:applescriptBook:figures:ch02:word4.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0208.eps*)
        (*I found extra:applescriptBook:figures:ch02:fileMaker1.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0209.eps*)
        (*I found extra:applescriptBook:figures:ch02:fileMaker2.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0210.eps*)
        (*I found extra:applescriptBook:figures:ch02:fileMaker3.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0211.eps*)
        (*I found extra:applescriptBook:figures:ch02:ib.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0212.eps*)
        (*I found extra:applescriptBook:figures:ch02:ib2.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0213.eps*)
        (*I found extra:applescriptBook:figures:ch02:ib3.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0214.eps*)
        (*I found extra:applescriptBook:figures:ch02:RB.eps*)
        (*I'm thinking of changing it to 
                extra:applescriptBook:figures:ch02:as_0215.eps*)

That looks as good as I could wish.

3.2.8 Finder's Keepers

I must not change only the file reference of each illustration within FrameMaker; I must change also the name of the actual illustration file. This involves speaking to the Finder. As a test, I create a file and run a little code to make sure I know how to tell the Finder how to change the name of a file.

tell application "Finder"
        set name of file "xxx:Users:mattneub:Desktop:testing.txt" to "itWorked"
end tell

This works, and now we are ready to run our original script for real. Before doing so, we make sure we have backup copies of everything involved, in case something goes wrong. Even with backups, it's a scary business making such possibly disastrous changes, both in a FrameMaker document and on disk, so I start by running the script against a document that has just one illustration—in fact, I run it against this very chapter, Chapter 3.

To make the script work for real, I change it in two places. First, at the start I add another little handler to extract the final component from a pathname, so that I can obtain the new name that the Finder is to give to the illustration file:

on justName(s)
        set text item delimiters to ":"
        return last text item of s
end justName

Second, I replace the two "log" lines from the previous version with this:

set newShortName to my justName(newName)
tell application "Finder" ¬
        to set name of file thisFile to newShortName
set inset file of inset 1 ¬
        of anchored frame 1 of paragraph 1 of cell 1 of oneTable ¬
        to newName

The first two lines are simply a rewrite of the Finder file-renaming code we just tested a moment ago, with the values coming from variables in the script instead of being hardcoded as literal strings. The second line actually changes the name of the illustration file on disk. Observe that we can talk to the Finder even inside code where we are already talking to FrameMaker. The last line is the only one that makes an actual change in the FrameMaker document—the crucial change, the one we came here to make, altering the illustration's file reference to match the new pathname of the illustration file. I run the script against Chapter 3 and it works: the illustration file's name is changed, and the illustration's file reference in the FrameMaker document is changed to match.

3.2.9 I've Got a Little List

Recall that one of our purposes is to generate the figure list requested by the illustration department, as shown in Table 3-1. I already know the chapter number, the illustration number, and the illustration file's name. The only missing piece of information is the illustration's caption. The FrameMaker dictionary shows that a table has a title property that looks like what I want. A quick test against a specific table shows that it is:

tell application "FrameMaker 7.0"
        set theTitle to (get title of table 46 of document ¬
                "extra:applescriptBook:ch03handson.fm")
end tell

This works, but because of the way the template is constructed, it includes an unwanted return character at the start of the result. To eliminate this, I use an AppleScript expression that extracts all but the first character of a string:

tell application "FrameMaker 7.0"
        set theTitle to text from character 2 to -1 of ¬
                (get title of table 46 of document ¬
                "extra:applescriptBook:ch03handson.fm")
end tell

That works; the result is this:

"FrameMaker dictionary"

That is indeed the caption of the first illustration of this chapter. AppleScript can write to a file, so all I need now is a handler that appends to a file, nicely formatted, a line containing the information for the illustration currently being processed. Here, then, is the final version of the script, including this handler and a call to it:

on pad(s)
        repeat while length of s < 2
                set s to ("0" & s)
        end repeat
        return s
end pad
on bust(s)
        set text item delimiters to ":"
        set pathParts to text items of s
        set text item delimiters to "."
        set nameParts to text items of last item of pathParts
        return {pathParts, nameParts}
end bust
on rename(n1, n2, oldPath)
        set bothLists to bust(oldPath)
        set extension to last item of item 2 of bothLists
        set pathPart to items 1 thru -2 of item 1 of bothLists
        set newFileName to "as_" & pad(n1) & pad(n2)
        set newFileName to newFileName & "." & extension
        set text item delimiters to ":"
        return (pathPart as string) & ":" & newFileName
end rename
on justName(s)
        set text item delimiters to ":"
        return last text item of s
end justName
on writeInfo(n1, n2, theName, theTitle)
        set s to return & n1 & "-" & n2 & tab & theName & tab & theTitle & return
        set f to open for access file "xxx:Users:mattneub:figs" with write permission
        write s to f starting at (get eof of f)
        close access f
end writeInfo
tell application "FrameMaker 7.0"
        tell document "extra:applescriptBook:ch03handson.fm"
                tell text flow 1
                        set howMany to count (get tables whose table tag is "Figure")
                        set chapNum to (get paragraph number of paragraphs ¬
                                whose paragraph tag is "ChapterLabel")
                end tell
                set chapNum to word -1 of item 1 of chapNum
                set allPaths to {}
                select paragraph 1 of text flow 1
                set counter to 1
                repeat howMany times
                        set oneTable to find table having tag with value "Figure" in it
                        set thisFile to inset file of inset 1 ¬
                                of anchored frame 1 of paragraph 1 of cell 1 of oneTable
                        set newName to my rename(chapNum, (counter as string), thisFile)
                        set newShortName to my justName(newName)
                        tell application "Finder" to set name of file thisFile to newShortName
                        set inset file of inset 1 ¬
                                of anchored frame 1 of paragraph 1 of cell 1 of oneTable ¬
                                to newName
                        set theTitle to text from character 2 to -1 of (get title of oneTable)
                        my writeInfo(chapNum, (counter as string), newShortName, theTitle)
                        select insertion point after selection
                        set counter to counter + 1
                end repeat
        end tell
end tell

There is just one thing I don't like about that script, namely this line:

        tell document "extra:applescriptBook:ch03handson.fm"

That line hardcodes the pathname of the document file. This works, but it means that I have to change the script manually for each file I process. That's not so terrible, since only about eight chapters of this book have any illustrations at all, but it would be nice not to have to do it at all, if only because it seems a possible source of error. Nevertheless, I think we can save this matter for some future round of refinements, and for now at least, consider the problem solved.

    [ Team LiB ] Previous Section Next Section