Wednesday, August 31, 2005
Need the Name of a File?
Sometimes, when you're writing scripts, you need a quick and dirty way to get the name of a file in your file system. A way I have used (particulary in CS days when ESTK wasn't available) was to run a script like this:
The second statement displays the file-system specific name of the file in a text box in the prompt dialog allowing you to copy it and thus paste it into your script.
This approach doesn't work so well on a Windows machine because of the backslash character serving the double purpose of separating folder names in a file path and serving as an escape character in strings. When you copy the file path out of the prompt, you get single backslashes (on Windows) which need to be converted to double backslashes in order that the string be interpreted properly by the JavaScript interpreter.
Works fine on a Mac, though.
myFile = File.openDialog("Pick a file");The first of these statements throws up a dialog that lets you choose a file from the filing system -- notice that that's all it does; the file is not opened or disturbed in any way.
prompt("File name is:",myFile.fsName);
The second statement displays the file-system specific name of the file in a text box in the prompt dialog allowing you to copy it and thus paste it into your script.
Warning
This approach doesn't work so well on a Windows machine because of the backslash character serving the double purpose of separating folder names in a file path and serving as an escape character in strings. When you copy the file path out of the prompt, you get single backslashes (on Windows) which need to be converted to double backslashes in order that the string be interpreted properly by the JavaScript interpreter.
Works fine on a Mac, though.
Script of the Day -- Divide Story into Two
The task of splitting a story into two parts is complicated by the possibility that a table is sitting at the point where the user wants to split the story. I recall when I first wrote this script (a year or so ago for InDesign CS) feeling rather proud of working out how to deal with that.
If ever there was an object lesson in the need to carefully document what a script is doing, here is one. I'm staring at this code I created and I don't see how it knits together the second story. I'm going to have to step through it to see it in action!
OK, I see what happens. The script starts at the back end of the original story, replacing each frame with an empty duplicate and linking those empty duplicates together. This makes the first story overset (or increases the amount of overset text, depending on its original state). Eventually, the script reaches the frame that is destined to be the first frame of the second story. This time, instead of emptying the duplicate, the original frame is emptied of text and then deleted. The duplicate it threaded to the start of the chain of empty text frames and so its text now flows through them.
This works even if the start of the frame is in the middle of a table. But, in that case, there is some extra work to do because the whole table is still part of the first story (albeit everything that is now in the second story is overset). So, the script calculates how much of the table is actually visible and deletes the rest.
It should be noted that if you have special settings for table headers and footers, now that the table is split in two, the settings will apply to each table, and so the two stories might not look exactly the same as before the split.
In truth, all this effort relating to tables is highly unlikely to be put into practice by anybody. Most people wanting to use this script do not want to split in the middle of a table, but for those that do, it is possible. Here's the code of the script:
To prevent confusion with the SplitStory.jsx released with InDesign CS2 by Adobe, I'm calling my script DivideStory.jsx.
If ever there was an object lesson in the need to carefully document what a script is doing, here is one. I'm staring at this code I created and I don't see how it knits together the second story. I'm going to have to step through it to see it in action!
OK, I see what happens. The script starts at the back end of the original story, replacing each frame with an empty duplicate and linking those empty duplicates together. This makes the first story overset (or increases the amount of overset text, depending on its original state). Eventually, the script reaches the frame that is destined to be the first frame of the second story. This time, instead of emptying the duplicate, the original frame is emptied of text and then deleted. The duplicate it threaded to the start of the chain of empty text frames and so its text now flows through them.
This works even if the start of the frame is in the middle of a table. But, in that case, there is some extra work to do because the whole table is still part of the first story (albeit everything that is now in the second story is overset). So, the script calculates how much of the table is actually visible and deletes the rest.
It should be noted that if you have special settings for table headers and footers, now that the table is split in two, the settings will apply to each table, and so the two stories might not look exactly the same as before the split.
In truth, all this effort relating to tables is highly unlikely to be put into practice by anybody. Most people wanting to use this script do not want to split in the middle of a table, but for those that do, it is possible. Here's the code of the script:
//DESCRIPTION: Splits story at the selected text frame.
// The selected frame becomes the first of the new story.
// Note that the behavior when an overset last frame is selected
// is different from that of the break-out text frame script.
// This script moves the overset text to the second story while
// breaking out the last frame leaves the overset text attached to the first story.
if ((app.documents.length != 0) && (app.selection.length != 0)) {
var myFrame = app.selection[0];
if (myFrame.constructor.name != "TextFrame") {
errorExit('Please select a text frame');
}
var myStory = myFrame.parentStory;
var mySplit = myFrame.textFrameIndex;
var myTot = myStory.textFrames.length;
// Because of the possibility of tables, we must always work from the back
var myStart = myTot - 1;
var myEnd = mySplit;
// Nothing to do if user has selected first frame.
if (myEnd != 0) {
if (myStart > myEnd) {
var myPrevFrame = splitMe(myStory.textFrames[myStart]);
myStart--;
for (var i = myStart; i> myEnd; i--) {
var myNewFrame = splitMe(myStory.textFrames[i]);
myPrevFrame.previousTextFrame = myNewFrame;
myPrevFrame = myNewFrame;
}
}
// Now we deal with the last frame
myFrame = myStory.textFrames[myEnd]
try {
myIndex = myFrame.characters[0].index;
stEnd = myStory.length - 1;
myText = myStory.texts[0].characters.itemByRange(myIndex,stEnd);
} catch (e) { } // Ignore; happens if last character is a table or frames are empty.
myNewFrame = myFrame.duplicate();
try{myText.remove();}catch(e){} //ignore empty frame
myFrame.remove();
try{myPrevFrame.previousTextFrame = myNewFrame;}catch(e){} //fails if one frame only
//Finally, if, and only if, the split is mid-table, myStory is now overset
if (myStory.textFrames[-1].overflows) {
myTable = myStory.characters[-1].tables[0];
myNewTable = myNewFrame.parentStory.characters[0].tables[0];
myRowCount = myNewTable.rows.length;
myTable.rows.itemByRange(0 - myRowCount,-1).remove();
}
}
} else {
errorExit();
}
// +++++++ Functions Start Here +++++++++++++++++++++++
function splitMe(myFrame) {
myDupeFrame = myFrame.duplicate();
while(myDupeFrame.contents.length > 0) {
myDupeFrame.texts[0].remove();
}
myFrame.remove();
return myDupeFrame;
}
function errorExit(message) {
if (arguments.length > 0) {
if (app.version != 3) { beep() } // CS2 includes beep() function.
alert(message);
}
exit(); // CS exits with a beep; CS2 exits silently.
}
// +++++++ Script Ends Here ++++++++++++++++++++++++++
To prevent confusion with the SplitStory.jsx released with InDesign CS2 by Adobe, I'm calling my script DivideStory.jsx.
Tuesday, August 30, 2005
Script of the Day -- Break Out Text Frame
One of the things that PageMaker users yearn for is the ease of splitting stories into their frames or into two (or more) stories. With InDesign treating the story as a separate entity from the frames that contain it, it's a little more complicated to do this kind of thing in InDesign.
On the other hand, I have grown to prefer InDesign's approach because when working interactively with a story in a document, it is great to be able to do such things as add a text frame on the pasteboard to a story so I can have access to the next text (without turning the page) as I work on a right-hand page. Life is full of these kinds of swings-and-roundabouts options and generally speaking, I have ended up preferring InDesign's approach to things -- although there are a small number of exceptions: perhaps I should list them one of these days.
This particular issue (splitting a story) lends itself to some fairly easy scripting solutions. By the way, one of the sample scripts that ships with InDesign CS2 is actually called SplitStory.jsx, but it does something different from my SplitStory.jsx (which I'll write about tomorrow). The bundled script breaks a story into as many stories as there are text frames in the story.
First, a script to break out an individual text frame (For those not familiar with PageMaker, if you selected a frame (or text block) in PageMaker, cut it and then pasted back in place, the effect would be the same as what this script does. Try the same thing in InDesign, and you'll end up with the text in two places: in the new frame and also at the start of the next frame of the original story.):
On the other hand, I have grown to prefer InDesign's approach because when working interactively with a story in a document, it is great to be able to do such things as add a text frame on the pasteboard to a story so I can have access to the next text (without turning the page) as I work on a right-hand page. Life is full of these kinds of swings-and-roundabouts options and generally speaking, I have ended up preferring InDesign's approach to things -- although there are a small number of exceptions: perhaps I should list them one of these days.
This particular issue (splitting a story) lends itself to some fairly easy scripting solutions. By the way, one of the sample scripts that ships with InDesign CS2 is actually called SplitStory.jsx, but it does something different from my SplitStory.jsx (which I'll write about tomorrow). The bundled script breaks a story into as many stories as there are text frames in the story.
First, a script to break out an individual text frame (For those not familiar with PageMaker, if you selected a frame (or text block) in PageMaker, cut it and then pasted back in place, the effect would be the same as what this script does. Try the same thing in InDesign, and you'll end up with the text in two places: in the new frame and also at the start of the next frame of the original story.):
//DESCRIPTION: Breaks out selected text frame from a storyNotice that I don't bother to check if there's actually a selection. Perhaps, given the popularity of this script and how widely spread it has become, I should add that check.
if (app.selection[0].constructor.name == "TextFrame"){
myFrame = app.selection[0];Since we know a text frame is selected, we create a reference to it in the variable named myFrame.
myText = myFrame.texts[0];Because duplicating a frame also duplicates its contents, we need this reference to the text in the original frame so that we can delete it later.
myDupeFrame = myFrame.duplicate();This creates an exact copy of myFrame. I wonder why I bothered to create a reference to it (myDupeFrame) given that I never use it. However, this new frame is exactly what we wanted to extract from the original story. So now all we have to do is clean up the original story by deleting the text in the frame and then deleting the frame:
myText.remove();Well, what do you know! This little script that I've used so many times has a bug! What if the frame is empty? OK, here's a new and improved version that both checks to make sure there's a selection in an opened document and that deals with the possibility that the selected frame is empty:
myFrame.remove();
}
//DESCRIPTION: Breaks out selected text frame from a story
if ((app.documents.length != 0) && (app.selection.length != 0)) {
if (app.selection[0].constructor.name == "TextFrame"){
myFrame = app.selection[0];
myText = myFrame.texts[0];
myDupeFrame = myFrame.duplicate();
try{
myText.remove();
} catch (e) {} // An error indicates the frame was already empty
myFrame.remove();
}
} else {
errorExit();
}
// +++++++ Functions Start Here +++++++++++++++++++++++
function errorExit(message) {
if (arguments.length > 0) {
if (app.version != 3) { beep() } // CS2 includes beep() function.
alert(message);
}
exit(); // CS exits with a beep; CS2 exits silently.
}
Monday, August 29, 2005
Another Script -- Select Rest of Column
I'm processing the results of this morning's script and it turns out that I need to be able to extend a selection in a table from the current cell to the last populated cell in the same column. This script does just that (although be warned that I have completely glossed over the issue of merged cells which will almost certainly cause this script to malfunction if it encounters any):
//DESCRIPTION: Select to last used cell in columnNo time for much discussion right now. Notice the use of itemByRange to address the sequence of cells in the column of interest. Notice also that cell names take the form c:r where "c" stands for the column number and "r" for the row number.
var myCell = app.selection[0];
var myTable = myCell.parent;
if (myTable.constructor.name == "Cell") {
// Text is selected in a cell, so:
myCell = myCell.parent;
myTable = myTable.parent;
}
if (myTable.constructor.name != "Table") {
errorExit("Please select a cell and try again.");
}
var myName = myCell.name;
var myRow = myName.split(":")[1];
var myCol = myName.split(":")[0];
// Find row reference of last populated cell in column
var myLim = myTable.rows.length;
theLast = -1;
for (var j=myLim - 1; j >= 0; j--) {
if (myTable.cells.item(String(myCol) + ":" + String(j))) {
theLast = j;
break;
}
}
app.select(myTable.columns[Number(myCol)].cells.itemByRange(Number(myRow),theLast));
function errorExit(message) {
if (app.version != 3) { beep() } // CS2 includes beep() function.
if (arguments.length > 0) {
alert(message);
}
exit(); // CS exits with a beep; CS2 exits silently.
}
Script of the Day -- List Document Styles
Just a quickie this morning because I'm up to my eyes in deadlines. However, even today, I find myself banging out a quick script to address an immediate problem:
Because my workflow has at its base the much-frowned-upon technique of basing each document in a series the previous member of the series, I don't have a central template for my current book project. I also tend to "clean up" the documents when I finish them -- something I mentioned yesterday. Well, this cleaning up process can cause valuable styles to disappear if a particular chapter doesn't happen to use it. So I need a quick reference of the styles used by each chapter. [Of course, I could just open each chapter and look in its styles palettes, but opening each takes a while when your documents are as complex as the ones I'm producing and anyway, what I really need is to get the lists all into one place.]
So, as a starting point, I created this quickie script to export all the style names of a document to a text file in the same folder as the document. For a document named "MyDocument.indd", the script produces a text file named "MyDocumentStyles.txt":
By opening the file for write access (that's what the "w" argument means), we overwrite anything that might already be in the file. If we wanted to append to existing data, we would open it for editing using an "e" argument, while "r" opens for reading.
There is a possible point of confusion at this stage (and indeed, I might be confused myself). When writing to text files, the "\n" construction produces a new line while when working with InDesign text it would produce a forced line break.
As a test, I ran the script against the current document I'm working on and it indeed produced a text file named CulArtsChapter10Styles that looks like this (I've snipped out a lot of the detail):
By way of a postscript: how much better the output would have been had it identified the document? Geez, how easy it is to overlook simple stuff. Obviously, that first write statement should be
Because my workflow has at its base the much-frowned-upon technique of basing each document in a series the previous member of the series, I don't have a central template for my current book project. I also tend to "clean up" the documents when I finish them -- something I mentioned yesterday. Well, this cleaning up process can cause valuable styles to disappear if a particular chapter doesn't happen to use it. So I need a quick reference of the styles used by each chapter. [Of course, I could just open each chapter and look in its styles palettes, but opening each takes a while when your documents are as complex as the ones I'm producing and anyway, what I really need is to get the lists all into one place.]
So, as a starting point, I created this quickie script to export all the style names of a document to a text file in the same folder as the document. For a document named "MyDocument.indd", the script produces a text file named "MyDocumentStyles.txt":
//DESCRIPTION: Export Style Names to Text FileThe first part of this script shows how to manipulate folder and file names. We then create a file object (at the point we do that, the file itself might or might not exist -- when we open it, if it exists, we open it; if it doesn't exist, we get a new file created for us).
myDoc = app.activeDocument;
var myFldrName = myDoc.filePath;
var myTextName = myDoc.name.split(".indd").join("Styles.txt");
var myFile = File(myFldrName + "/" + myTextName);
myFile.open("w");
myFile.write("Paragraph Styles " + myDoc.paragraphStyles.length + "\n");
myFile.write(myDoc.paragraphStyles.everyItem().name.join("\n"));
myFile.write("\n\nCharacter Styles " + myDoc.characterStyles.length + "\n");;
myFile.write(myDoc.characterStyles.everyItem().name.join("\n"));
myFile.write("\n\nObject Styles " + myDoc.objectStyles.length + "\n") ;
myFile.write(myDoc.objectStyles.everyItem().name.join("\n"));
myFile.close();
By opening the file for write access (that's what the "w" argument means), we overwrite anything that might already be in the file. If we wanted to append to existing data, we would open it for editing using an "e" argument, while "r" opens for reading.
There is a possible point of confusion at this stage (and indeed, I might be confused myself). When writing to text files, the "\n" construction produces a new line while when working with InDesign text it would produce a forced line break.
As a test, I ran the script against the current document I'm working on and it indeed produced a text file named CulArtsChapter10Styles that looks like this (I've snipped out a lot of the detail):
Paragraph Styles 139So, there you have it. A quick script that should save me a lot of hunting around through completed documents. One thing you'll notice about these "quick" scripts is that they don't put a lot of stock in error checking. If I try to run this script with no document open or with an untitled document, I'll get a run time error. So be it.
[No Paragraph Style]
[Basic Paragraph]
AssessNL
BasicSkillBL
BasicSkillHd
[snipped 132 style names]
~Footer-Recto
~Footer-Verso
Character Styles 46
[None]
Activity_Cont
BasicSkillLeadoff
Bold
[snipped 40 style names]
~FooterFolio
~FooterTriangleDownshift
Object Styles 12
[None]
[Normal Graphics Frame]
[Normal Text Frame]
[Normal Grid]
FeatheredRoundedImage
OutlinedRndCrnrImage
FrameImageInGallery
Note
ReviewAssessmentFrame
MathHolder
ProcedureHolder
SpicesGalleryImages
By way of a postscript: how much better the output would have been had it identified the document? Geez, how easy it is to overlook simple stuff. Obviously, that first write statement should be
myFile.write(myDoc.name + "\nParagraph Styles " + myDoc.paragraphStyles.length + "\n");
Sunday, August 28, 2005
Script of the Day -- Style in Use?
I was wondering which script to present today (I have quite a backlog), when the need for a new script dropped out of the clear blue sky. I'm doing some administrative clean up of some documents that are nearing readiness for printing. I like to do four things as part of my preflight:
While I suppose I could write a script for much of this activity, the UI serves me very well for most of it, and there are judgment calls to be made here that would be hard to encapsulate into a script. I worked through the first six chapters of a book (small "b" -- I rarely, if ever, make use of InDesign's Book feature), but the seventh has thrown up a new challenge: one of the paragraph styles is apparently "in use." It is called "DTP" and is used by my client to give me instructions. By the end of setting one of these chapters, that style should no longer be in use.
Turning to Find/Change to see where it is being used, I am dismayed to discover that no text actually uses the style. So, what does "in use" mean in this situation? It means, that the style is either used for Based-on or Next Style in one of the styles that actually is in use. Next Style is unlikely, but Based-on is possible if I needed to create a new style for some special element in this chapter and I accidentally did so while some text in the DTP style was selected.
This actually sounds like a very logical behavior, but it flies in the face of 20 years of using PageMaker and InDesign and has been driving me nuts. The saving grace is that if you switch the style in question to be based-on No Paragraph Style then you get the old behavior.
Sad to say, there's a bug in the CS2 original release (build 421) scripting system whereby if by script you change the based-on style of a paragraph style to No Paragraph Style, you do not get the exceptional behavior that the UI provides. This makes it a bit harder (to say the least) to use a script to change the based-on of a paragraph style without changing the specification of the style.
There are two problems with this: a script can't interrogate the palette and because a use of this style can't be found, I can't click in a paragraph to select it that way. I suppose I could have the user type a paragraph using the style in a gash text frame, but that is surely very clumsy. For our immediate needs, we know the name of the style, so we can just build it directly into the script. Let's do that for now (a better solution would be to use a drop-down menu in a user dialog and a feature request to the scripting team would be to allow for scripts to interrogate palettes to find out what is selected). So, let's get started on the script.
Notice also that I have included a note to myself to remind me at some future date to make the script more flexible.
The "item" method provides a way of creating a reference to a particular item in a collection. In this case, we know that every paragraph style has a unique name so we do not have to worry about myStyle returning more than one element (as it might for other kinds of named objects).
Armed with this reference, all we need to do is walk through all the paragraph styles looking for any use of myStyle as either based-on or next style. Thinks: we don't care about next style because changing that does no harm to the document. Well that makes it easier.
In this second part of the script (the "meat" of the script) we first set local variable "myStyles" to reference a collection of all the paragraph styles in the document. Then, we set another local variable to the length of this collection to be used to control how many times we execute the upcoming loop -- this speeds the script up a tad because this is a fixed value that is not changed by the loop, so we use it to control the loop rather than calculate the value each time around the loop.
Now, all we need to do is compare the basedOn property of each style with myStyle. If they're the same, then for now, all we're going to do is alert the user that the style has been found and exit so that the user can deal with the situation. Perhaps later, I'll extend this script to handle the change in based-on automatically, but for now, I've solved my problem (subject to testing the script, which I am about to do right now).
What an informative error message that is! Tells me exactly what's wrong and which line of my script made the error.
Of course, the solution is easy. Since we know that No Paragraph Style can't have a based-on style, it surely isn't of interest to us, so we can simply ignore it by starting our loop at 1 instead of zero:
I can't help thinking that it would have taken me less time to check each style manually than write this blog entry, but it would not have been nearly as much fun.
Footnote: This script is of such special, specific purpose that I will not be posting it for download.
- Clean up Paragraph Styles
Clean up Character Styles
Clean up Swatches
Check Document Fonts
While I suppose I could write a script for much of this activity, the UI serves me very well for most of it, and there are judgment calls to be made here that would be hard to encapsulate into a script. I worked through the first six chapters of a book (small "b" -- I rarely, if ever, make use of InDesign's Book feature), but the seventh has thrown up a new challenge: one of the paragraph styles is apparently "in use." It is called "DTP" and is used by my client to give me instructions. By the end of setting one of these chapters, that style should no longer be in use.
Turning to Find/Change to see where it is being used, I am dismayed to discover that no text actually uses the style. So, what does "in use" mean in this situation? It means, that the style is either used for Based-on or Next Style in one of the styles that actually is in use. Next Style is unlikely, but Based-on is possible if I needed to create a new style for some special element in this chapter and I accidentally did so while some text in the DTP style was selected.
Feature Change Raises Head
There has been a major change in the behavior of Based-on in InDesign CS2. Whereas before, if you changed the nominated Based-on paragraph style in a style, that other style was not changed in any way -- just the description changed to reflect the new base -- now the style changes. It sounds quite logical: if a characteristic of the style in question was identical to the previous based-on style's corresponding then changing the based-on for the style will cause that characteristic to change to that of the new based-on style.This actually sounds like a very logical behavior, but it flies in the face of 20 years of using PageMaker and InDesign and has been driving me nuts. The saving grace is that if you switch the style in question to be based-on No Paragraph Style then you get the old behavior.
Sad to say, there's a bug in the CS2 original release (build 421) scripting system whereby if by script you change the based-on style of a paragraph style to No Paragraph Style, you do not get the exceptional behavior that the UI provides. This makes it a bit harder (to say the least) to use a script to change the based-on of a paragraph style without changing the specification of the style.
Back to the Script
Let's start out with a "quick and dirty" script. The first issue with this script is: which paragraph style should the script look for? The easiest way to communicate this, you would think, would be to select the style in the palette and have the script look there.There are two problems with this: a script can't interrogate the palette and because a use of this style can't be found, I can't click in a paragraph to select it that way. I suppose I could have the user type a paragraph using the style in a gash text frame, but that is surely very clumsy. For our immediate needs, we know the name of the style, so we can just build it directly into the script. Let's do that for now (a better solution would be to use a drop-down menu in a user dialog and a feature request to the scripting team would be to allow for scripts to interrogate palettes to find out what is selected). So, let's get started on the script.
I've adopted a convention in my scripts that myDoc is always a global variable set to the active document. It saves a lot of messing around passing an extra parameter to functions. Generally speaking it is better to mark variables as local (by labeling them with "var" as I've done for "myStyle"
//DESCRIPTION: Locate any paragraph Styles that use a particular style as based-on
// Later: add a drop-down. For now, we'll hard-wire the style
myDoc = app.activeDocument; // Note: global variable
var myStyle = myDoc.paragraphStyles.item("DTP");
Notice also that I have included a note to myself to remind me at some future date to make the script more flexible.
The "item" method provides a way of creating a reference to a particular item in a collection. In this case, we know that every paragraph style has a unique name so we do not have to worry about myStyle returning more than one element (as it might for other kinds of named objects).
Armed with this reference, all we need to do is walk through all the paragraph styles looking for any use of myStyle as either based-on or next style. Thinks: we don't care about next style because changing that does no harm to the document. Well that makes it easier.
var myStyles = myDoc.paragraphStyles;Combine the two snippets above, and that's the whole script.
var mySlen = myStyles.length;
for (var j=0; mySlen > j; j++) {
if (myStyles[j].basedOn == myStyle) {
alert("Paragraph style " + myStyles[j].name + " uses DTP for based-on");
exit();
}
}
In this second part of the script (the "meat" of the script) we first set local variable "myStyles" to reference a collection of all the paragraph styles in the document. Then, we set another local variable to the length of this collection to be used to control how many times we execute the upcoming loop -- this speeds the script up a tad because this is a fixed value that is not changed by the loop, so we use it to control the loop rather than calculate the value each time around the loop.
Now, all we need to do is compare the basedOn property of each style with myStyle. If they're the same, then for now, all we're going to do is alert the user that the style has been found and exit so that the user can deal with the situation. Perhaps later, I'll extend this script to handle the change in based-on automatically, but for now, I've solved my problem (subject to testing the script, which I am about to do right now).
Debugging the Script
Well, what do you know, the script has an error! JavaScript counts from zero, but in InDesign's object model the first paragraph style is special. It is always the "No Paragraph Style" style -- so special that it isn't even displayed in the InDesign CS2 paragraph palette, although you can see it in the Paragraph Style drop-down in the Find/Change panel. One thing that you can't do with that first style is access its basedOn property because it doesn't have one -- it is the ultimate base of all paragraph styles. So attempting to run the script produces this error:What an informative error message that is! Tells me exactly what's wrong and which line of my script made the error.
Of course, the solution is easy. Since we know that No Paragraph Style can't have a based-on style, it surely isn't of interest to us, so we can simply ignore it by starting our loop at 1 instead of zero:
for (var j=0; mySlen > j; j++) {And, whoopee! It worked. I have a style that was indeed added for this document called ChartAssignment that uses DTP as its based on.
I can't help thinking that it would have taken me less time to check each style manually than write this blog entry, but it would not have been nearly as much fun.
Footnote: This script is of such special, specific purpose that I will not be posting it for download.
Saturday, August 27, 2005
Script of the Day -- Smart Title Case
I posted a version of this script on the Adobe U2U forum just the other day. This is an improved version that will use text files for the two lists (words that stay lowercase and words with internal capitals) rather than requiring you to edit the script itself. But I get ahead of myself. Let me start by explaining what I mean by "Smart" Title Case.
In the InDesign user interface, you can select some text and then choose Title Case from the Change Case submenu of the Type menu. If you do this, InDesign simply gives each selected word an initial cap, forcing the rest of the word to lowercase. So, if you type:
And, as is so often the case, I no sooner publish a script and I realize there's a problem with it. What if the first word in a title is a word like "iTunes"? You surely don't want it to be represented by "ITunes" and yet that's what this script will do. I need to come up with a mechanism for for dealing with this (although you could just choose to not select the first character and the script will then work in this situation).
It is also worth pointing out that the technique employed here will cause any internal character styling to be lost, unless the character styling is applied by a nested style. In all my use of this script over the past two or more years, this has never mattered.
In the InDesign user interface, you can select some text and then choose Title Case from the Change Case submenu of the Type menu. If you do this, InDesign simply gives each selected word an initial cap, forcing the rest of the word to lowercase. So, if you type:
- this is an indesign test
- This Is An Indesign Test
- This is an InDesign Test
//DESCRIPTION: Converts selected text to title case smartly
// Customize this script by either editing these arrays:
var ignoreWords = ["a","an","and","the","to","with","in","on",
"as","of","or","at","is","into","by","from","for"];
var intCaps = ["PineRidge","InDesign","NJ","UMC"];
// or by creating text files named ignoreWords.txt
// and intCaps.txt in the script's folder
ignoreWords = getIgnoreFile(ignoreWords);
intCaps = getIntCaps(intCaps);
try {
myText = app.selection[0].texts[0].contents;
} catch(e) {
exit();
}
theWords = myText.toLowerCase().split(" ");
//First word must have a cap, but might have an internal cap
myNewText = "";
for (var j = 0; theWords.length > j; j++) {
k = isIn(intCaps,theWords[j])
if (k > -1) {
myNewText = myNewText + intCaps[k] + " ";
continue;
} else {
if ((isIn(ignoreWords,theWords[j]) > -1) && (j != 0)) {
myNewText = myNewText + theWords[j] + " ";
} else {
myNewText = myNewText + InitCap(theWords[j]) + " ";
}
}
}
app.selection[0].texts[0].contents = myNewText.substring(0,myNewText.length - 1);
// +++++++ Functions Start Here +++++++++++++++++++++++
function getIgnoreFile(theWords) {
var myFile = File(File(getScriptPath()).parent.fsName + "/ignoreWords.txt");
if (!myFile.exists) { return theWords }
// File exists, so use it instead
myFile.open("r");
var importedWords = myFile.read();
myFile.close();
return importedWords.split("\n"); // Could filter these, but what's the point?
}
function getIntCaps(theWords) {
var myFile = File(File(getScriptPath()).parent.fsName + "/intCaps.txt");
if (!myFile.exists) { return theWords }
// File exists, so use it instead
myFile.open("r");
var importedWords = myFile.read();
myFile.close();
return importedWords.split("\n"); // Could filter these, but what's the point?
}
function getScriptPath() {
// This function returns the path to the active script, even when running ESTK
try {
return app.activeScript;
} catch(e) {
return e.fileName;
}
}
function isIn(aList,aWord) {
for (var i = 0; aList.length > i; i++) {
if (aList[i].toLowerCase() == aWord) {
return i;
}
}
return -1;
}
function InitCap(aWord) {
if (aWord.length == 1) {
return (aWord.toUpperCase());
}
return (aWord.substr(0,1).toUpperCase() + aWord.substring(1,aWord.length))
}
And, as is so often the case, I no sooner publish a script and I realize there's a problem with it. What if the first word in a title is a word like "iTunes"? You surely don't want it to be represented by "ITunes" and yet that's what this script will do. I need to come up with a mechanism for for dealing with this (although you could just choose to not select the first character and the script will then work in this situation).
It is also worth pointing out that the technique employed here will cause any internal character styling to be lost, unless the character styling is applied by a nested style. In all my use of this script over the past two or more years, this has never mattered.
Everything's an Object
One of the powers of JavaScript is that just about anything you work with is either an object or a property of an object. What’s more, some properties can themselves be objects too, so you end up with a hierarchy of objects. To take a simple example, consider the File System. Here, the objects are the files and folders of your file system.
Once I realized this, it opened my eyes to a new world of possibilities. For example, it allowed me to create a new method for my strings that mimics the AppleScript construct is in. In AppleScript (which I used to script InDesign 1.5 and 2.0 before JavaScript became available), a very useful feature was the ability to ask if a string was in an array of strings using something like:
Having grasped this, I created the following definition of a new method for strings in my script:
It wasn't long before I realized that this method could be generalized to work with any object by simply changing the declaration to:
Well, even though it's Saturday morning, I have work I must attend to, so that's it for now.
By The Way: Objects vs. Object Model
- It takes some getting used to, separating the concept of an object model from the actual objects you work with. An object model defines how the objects of a "universe" relate to each other and what their properties are. An object is a particular object within the specific instance of its object model that you happen to be working with.
For example, the InDesign object model says that a property of the application object is that it has a collection of documents. But, if you are not running InDesign, then you don't have an actual application object for your scripts to operate upon. Or, if you have no documents open, then the collection of documents for your InDesign at this moment is empty.
Once I realized this, it opened my eyes to a new world of possibilities. For example, it allowed me to create a new method for my strings that mimics the AppleScript construct is in. In AppleScript (which I used to script InDesign 1.5 and 2.0 before JavaScript became available), a very useful feature was the ability to ask if a string was in an array of strings using something like:
- If myString is in myString Array then ...
Having grasped this, I created the following definition of a new method for strings in my script:
String.prototype.isInArray = function(myArray){Armed with this, I could write statements like:
for (var i=0; myArray.length > i; i++) {
if(myArray[i] == this){
return true;
}
}
return false;
}
If (myStyleName.isInArray(myDoc.paragraphStyles.everyItem()) {This code snippet tests to see if the document referenced by the variable I named "myDoc" includes a paragraph style with the name contained in the variable I named "myStyleName".
It wasn't long before I realized that this method could be generalized to work with any object by simply changing the declaration to:
Object.prototype.isInArray = function(myArray){While it is rare that I use this method with anything other than a string, the possibility is always there.
Well, even though it's Saturday morning, I have work I must attend to, so that's it for now.
Friday, August 26, 2005
Master of the Universes
Perhaps I've been watching too much Stargate, but it occurred to me that one way of explaining what scripting is all about is to think of your script as being master of three parallel universes:
These objects behave in just the ways you would expect based on your experiences dealing with them in your hands on use of them through the user interface. The difference for the scripter is that your script (I'm going to say "you" from here one) can deal with these elements directly rather than through the intermediary of the user interface.
That'll do for now. It's time for me to go watch some more Stargate!
- The JavaScript Universe
The InDesign Universe
The File System Universe
These objects behave in just the ways you would expect based on your experiences dealing with them in your hands on use of them through the user interface. The difference for the scripter is that your script (I'm going to say "you" from here one) can deal with these elements directly rather than through the intermediary of the user interface.
Scripts are Not Macros
Some "scripting" languages are little more than user interface commands strung together. But this is not the case with scripts that drive InDesign. Indeed, there are cases where the behavior of the user interface can confuse a scripter:- In the UI, you address application preferences and defaults only when no documents are open. In a script, you can address them all the time. Indeed, if you want to address the preferences and defaults of a document then you must explicitly do so. A script does not have an implicit relationship with the active document. If that's what you want to work with, then you must say so.
Also in the UI, some attributes are automatically linked together. Have you noticed how when you change a stroke weight to zero the swatch automatically changes to None? Well that happens because the UI is a program and it does that to save you trouble. When you script this function, you are the programmer and so you must deal with both settings.
That'll do for now. It's time for me to go watch some more Stargate!
Script of the Day -- Add Item to Group
I'm doing some corrections for a client. An element he wanted changing is a map where I had labeled North and South Carolina with callouts that sat over the states. The client asked me to move the names into the ocean (easy) and add pointers. How does one add pointers to an existing group? For that matter, how do you add anything to an existing group?
In the User Interface, the problem is close to insurmountable without taking the group apart, creating your new elements and regrouping. But for a script, all you have to do is add an element to the collection of like elements in the group (even if that collection is originally empty).
So, I composed this script:
In the User Interface, the problem is close to insurmountable without taking the group apart, creating your new elements and regrouping. But for a script, all you have to do is add an element to the collection of like elements in the group (even if that collection is originally empty).
So, I composed this script:
//DESCRIPTION: Add Graphic Line to Parent Group
if ((app.documents.length != 0) && (app.selection.length != 0)) {
// Check that selected item is member of a group (start simple)
mySel = app.selection[0];
myGroup = mySel.parent;
if (myGroup.constructor.name != "Group") {
errorExit("Selection is not a member of a group");
}
myGMbounds = mySel.geometricBounds;
myStroke = myGroup.graphicLines.add({geometricBounds:myGMbounds});
} else {
errorExit();
}
// +++++++ Functions Start Here +++++++++++++++++++++++
function errorExit(message) {
if (arguments.length > 0) {
if (app.version != 3) { beep() } // CS2 includes beep() function.
alert(message);
}
exit(); // CS exits with a beep; CS2 exits silently.
}
// +++++++ Script Ends Here ++++++++++++++++++++++++++