On ‘everyItem( )’ – Part 2
July 19, 2010 | Tips | en
In the previous episode we learned that every scripting DOM object is in fact an object specifier, which acts like a path in the InDesign object tree. Collection's methods only allow us to build object specifiers. Invoking a property or a method provided by a specifier causes InDesign to send a “verb-first command” to the underlying receiver(s). We will now consider how this process impacts your code, what is returned into your script and how to deal with some side effects.
DISCLAIMER (September 22, 2023). — I was very unpleasantly surprised to find that almost all of the links mentioned in my original article were now obsolete, including resources as fundamental as those originally published by Apple about AppleScript. I therefore decided — since no one seems to care about saving old sources of information — to host on this server the mentioned material, i.e. the PDFs to which I refer for educational purposes. This undoubtedly constitutes a “copyright violation” in the eyes of a lawyer and I am fully aware of it, but I then respectfully request the rights holders to contact me and provide a RELIABLE link to refer my readers to. Thank you for your attention.
Specifier Validation
As previously stated, the system attempts to resolve a specifier each time a command is sent to InDesign through this specifier. But what exactly does that mean? Instinctively we all realized that a specifier has a recursive structure. Indeed we could describe any specifier by the following pattern:
<SPECIFIER> := <CONTAINER> <SELECTOR>
,
where <CONTAINER>
is itself a reliable specifier and <SELECTOR>
a symbolic link to reliable sub-element(s).
The <CONTAINER>
can be app
(the root specifier) or any expression that wraps a specifier, provided that the container is not empty.
The <SELECTOR>
can be:
• Nothing (since any container is a specifier!)
• Any available property, array element, or method call, which results in a specifier. E.g.:
.parent
,
.activeDocument
,
.parentStory
,
.allPageItems[0]
,
.rectangles.add()
,
etc.
• Any available collection selector. E.g.:
.stories[0]
,
.documents.anyItem()
,
.characters.itemByRange(0,5)
,
.pages.item('2')
,
.rectangles.everyItem()
,
etc.
By detecting actual InDesign object(s) behind the <CONTAINER>
, the system dynamically checks for the validity of the container. It throws a runtime error if the container is empty. Anyway, the complete <SPECIFIER>
remains unresolved at this stage: the system does not need to identify the actual receiver(s) as long as we don't send a command through the specifier. In other words, the specifier behaves as a pure symbolic link, as demonstrated by the following script:
//------------------------------------------- // InDesign CS4/CS5 // Test this script with NO OPENED DOCUMENT ! //------------------------------------------- // create 2 docs and 1 oval in doc2 var doc1 = app.documents.add(), doc2 = app.documents.add(), ov2 = doc2.ovals.add(); // create 3 groups in doc2 doc2.groups.add([ov2.duplicate(),ov2.duplicate()]); doc2.groups.add([ov2.duplicate(),ov2.duplicate()]); doc2.groups.add([ov2.duplicate(),ov2.duplicate()]); // doc1 is still empty // doc2 has 3 groups (but no rectangle within) // spec is OK because one of the documents contains 3 groups var spec = app.documents.everyItem().groups[2] // CONTAINER .rectangles.firstItem(); // SELECTOR // but spec is invalid // because there is no rectangle anywhere: alert( spec.isValid ); // FALSE // we can now remove all groups // without causing any error! doc2.groups.everyItem().remove(); // of course spec is still invalid: alert( spec.isValid ); // FALSE // then we create 3 rectangle groups in doc1: var rec1 = doc1.rectangles.add(); doc1.groups.add([rec1.duplicate(),rec1.duplicate()]); doc1.groups.add([rec1.duplicate(),rec1.duplicate()]); doc1.groups.add([rec1.duplicate(),rec1.duplicate()]); // spec is now valid: alert( spec.isValid ); // TRUE
What is marvelous in the above code? There is absolutely no connection between the reliability of the specifier at the moment it is declared and the receiver to which it finally points out. When the spec
variable is set, the system only needs to find a groups[2]
somewhere. There are 3 groups in doc2
, so the <CONTAINER>
part of the specifier is temporarily checked as nonempty and no error is thrown. The specifier is invalid anyway, because the <SELECTOR>
points out to nothing (there is no rectangle in the third group of doc2
). Then the groups of doc2
are removed by doc2.groups.everyItem().remove()
! At this stage no document contains any group, so the specifier really means nothing. And yet we can rely on its isValid
property without error. Finally the script creates 3 groups in doc1
—not doc2
!— such as doc1.groups[2]
contains a rectangle. Then spec.isValid
is true! This shows that spec
is fully reassessed from its symbolic path: /document/group[2]/rectangle[@location=first]
.
Note. — Contrary to popular belief, it is not necessary that each document hosts 3 groups to set app.documents.everyItem().groups[2]
as a container. It suffices that at least one document satisfies this requirement.
From what I have empirically understood, the isValid
property of a specifier is TRUE
if:
(1) The <CONTAINER>
is nonempty (of course!);
AND
(2) The <SELECTOR>
selects at least one actual receiver,
OR ends by everyItem()
.
The everyItem
exception is proved by the following code:
//------------------------------------------- // InDesign CS4/CS5 // Test this script with NO OPENED DOCUMENT ! //------------------------------------------- var spec1 = app.documents.itemByRange(0,-1); var spec2 = app.documents.everyItem(); alert( spec1.isValid ); // FALSE alert( spec2.isValid ); // TRUE
Since no document is available, both spec1
and spec2
points out to nothing. However, while spec1
(based on itemByRange
) is not valid, spec2
(based on everyItem
) is valid. This slight difference, in fact, reflects a pure AppleScript rule: “every item
is equivalent to items 1 through -1
, except that every
returns an empty list if there are no elements, instead of an error.” (Apple's Technical Note TN2106, “Scripting Interface Guidelines,” section Object Specifiers) This peculiarity may lead to wrong inferences in a script, so keep in mind that spec.isValid
does not always mean that spec
has a receiver.
Note 1. — isValid
is a very special property in that it does not delegate the command to the underlying receiver(s). Like constructor
or other fundamental properties of any JS object, isValid
remains attached to the specifier itself and cannot result in an Array
when you get it from a collective specifier. (See below: “Collective Mode.”) However, retrieving this property causes the resolution of the specifier in case of validity. (See below: “Resolving a Specifier.”)
Note 2. — Before InDesign CS4 there was no direct way to check for the validity of a specifier without error management. The usual workaround was to hit an arbitrary property —like mySpec.id
— within a try...catch
block. ID CS4 introduced the common isValid
property to fix this problem.
Resolving a Specifier
As demonstrated in our first example, a valid specifier can be seen as a pending query whose actual receiver(s) are not identified until we send a command to InDesign. By performing the specifier's query, the system translates any generic path, like /document[0]/story
(corresponding to app.documents[0].stories.everyItem()
), into a set of absolute paths pointing out to the receiver(s), e.g. /document[@name="Test"]//story[@id=229]
. Each resulting specifier is resolved in that it provides a direct access to an UI element identified by its id or name (the name
property is used to identify a Document
object in CS4 and previous versions). When the original specifier describes multiple receivers, it is resolved to an Array
of absolute paths.
A basic way to get resolved specifier(s) from a generic specifier, or path, is to invoke the function resolve(/*string*/path)
offered by the JavaScript layer:
//------------------------------------------- // InDesign CS4/CS5 // Sample uses of the 'resolve' function //------------------------------------------- // Resolving a specifier: //============= var spec = app.documents[0].groups.firstItem().rectangles[1]; var resolvedSpec1 = resolve(spec.toSpecifier()); // Here 'resolve' returns a single absolute specifier: alert( resolvedSpec1.toSpecifier() ); /* E.g.: /document[@name="Test.indd"]//rectangle[@id=253] */ // Resolving a path: //============= var path = '/document[0]/group/rectangle'; var resolvedSpec2 = resolve(path); // Here 'resolve' returns an Array of absolute specifiers: var i = resolvedSpec2.length; while( i-- ) resolvedSpec2[i] = resolvedSpec2[i].toSpecifier(); alert( resolvedSpec2.join('\r') ); /* E.g.: /document[@name="Test.indd"]//rectangle[@id=254] /document[@name="Test.indd"]//rectangle[@id=253] /document[@name="Test.indd"]//rectangle[@id=252] */
The function resolve(...)
takes as argument a String
—a specifier's path— and returns either a single object specifier, or an Array
of object specifiers if the query hits several receivers.
Note that InDesign cannot actually resolve every UI object to an absolute path. For example, the Character
objects have no individual id
property and are only described by zero-based indexes. Therefore any specifier that handles Text
objects (Character
, Word
, Paragraph
, etc.) is resolved to a range of character indexes within a known container:
//-------------------------------------- // InDesign CS4/CS5 // 'Resolving' a Text specifier //-------------------------------------- var spec = app.documents[0].stories.everyItem().words[1]; var resolvedSpec = resolve(spec.toSpecifier()); var i = resolvedSpec.length; while( i-- ) resolvedSpec[i] = resolvedSpec[i].toSpecifier(); alert( resolvedSpec.join('\r') ); // see screenshot below
The previous example shows that a resolved specifier doesn't necessary store an invariant entity that you could trust blindly. Indeed, in some cases, it is preferable to work with a generic (dynamic) specifier that you don't resolve too early. Consider myTextFrame.words.firstItem()
. The inner path of this unresolved specifier is a context-sensitive link to the first word of myTextFrame
. Suppose your script hits the text by adding a new word at the beginning of the story. The generic specifier remains semantically valid while a resolved specifier only provides a snapshot at a given time. This snapshot will no longer be relevant after the modification:
//------------------------------------------- // InDesign CS4/CS5 // 'Resolved' vs. 'Dynamic' Specifier //------------------------------------------- var tf = app.activeDocument.textFrames[0], firstWord = tf.words.firstItem(); // dynamic spec. // Sets the frame contents (1st word: 'aaaaa') tf.contents = "aaaaa bbb cc"; // Resolves the specifier... too early! var resolvedWord = resolve(firstWord.toSpecifier()); // The contents is then modified: tf.insertionPoints[0].contents = "zz "; // The 1st word is then 'zz' alert( 'Frame contents: ' + tf.contents + '\r' + '1st word (resolved): ' + resolvedWord.contents + '\r' + '1st word (dynamic): ' + firstWord.contents ); /* Displays: Frame contents: zz aaaaa bbb cc 1st word (resolved): aaaaa 1st word (dynamic): zz */
The above snippet demonstrates that the resolved specifier is not reliable at the moment the script is querying the word contents. There is another hidden issue: despite the fact that resolvedWord.contents
contains the string "aaaaa"
(which is the contents of the original first word), we could check that resolvedWord.texts[0].contents
is not "aaaaa"
. . . but "zz aa"
. Why? Because while the contents
property has been retrieved and stored in the object specifier at the exact moment when it was being resolved, the internal path has been updated to reflect the corresponding character range. Thus, resolvedWord
is pointing out to the range [0,4] within the characters collection. So when you resolve resolvedWord.texts[0].contents
, you send a new command that returns the first five characters of the frame, which now are "zz aaa"
. In other words, the contents
property of the resolved specifier is out of sync with its inner path.
Keep in mind that the visible path of a specifier —spec.toSpecifier()
— is never updated in the original object, whatever the way it is resolved. The resolve
function is a safe approach as it creates a new specifier, or an array of specifiers, so you can always distinguish the original dynamic specifier with the resolved one(s) and the original object is not affected. But in general, script developers do not use resolve
, they directly act from/on the specifier by invoking its properties or methods. Then the specifier is mutely and automatically resolved by the system, which can lead to confusing side effects:
//------------------------------------------- // InDesign CS4/CS5 // Hidden resolution of a specifier // Run this script on an empty document //------------------------------------------- var doc = app.documents[0], spec1 = doc.rectangles.everyItem(), spec2 = doc.rectangles.everyItem(); app.documents[0].rectangles.add(); // 1st rectangle app.documents[0].rectangles.add(); // 2nd rectangle spec1.isValid; // innocent access to a property... app.documents[0].rectangles.add(); // 3rd rectangle // Checks that spec1 and spec2 have the same path: alert( spec1.toSpecifier() == spec2.toSpecifier() ); // true // Displays the number of receivers: alert( spec1.id.length ); // 2 (spec1 was already resolved) alert( spec2.id.length ); // 3 (spec2 is NOW resolved)
In the above example, you could replace spec1.isValid
by any other access to the specifier's interface: spec1.label
, spec1.move(...)
, etc. Any command which involves receivers causes an automatic resolution of the specifier. So spec1
will not see the third rectangle: spec1
is already resolved!
A common way to explicitly resolve a specifier and push the resolved data into an Array
is to use spec.getElements()
. This method performs two operations:
(1) It causes an automatic resolution of spec
;
(2) It creates and returns an Array
that contains each spec
's receiver converted into a resolved specifier.
Moreover, getElements()
is much more reliable than any other command because it actually resolves or updates the specifier from its original path:
//------------------------------------------- // InDesign CS4/CS5 // getElements() used as an updater // Run this script on an empty document //------------------------------------------- var doc = app.documents[0], spec = doc.rectangles.everyItem(); app.documents[0].rectangles.add(); // 1st rectangle app.documents[0].rectangles.add(); // 2nd rectangle spec.getElements(); // resolves spec app.documents[0].rectangles.add(); // 3rd rectangle alert( spec.id.length ); // 2 spec.getElements(); // resolves spec again! alert( spec.id.length ); // 3
Of course it is stupid to ask spec.id.length
to count the receivers, since spec.getElements().length
would much better provide the reliable value! The interesting aspect of the above code is that spec.getElements()
updates the specifier even if it has been already resolved, while other properties and methods cannot do so. Think getElements()
as a method which rebuilds and resolves any specifier from its original path. In this respect, spec.getElements()
is similar to resolve(spec.toSpecifier())
, except that getElements()
changes the inner state of the object.
Note. — Contrary to resolve(spec.toSpecifier())
, spec.getElements()
always returns an Array
whatever the number of receivers. This array contains one element if the specifier points out to a single receiver, and it can even be empty if no receiver is found.
The ‘Collective Mode’
According to the Adobe scripting help, the pattern ...collection.everyItem()
should return an Array
of objects having the underlying type. We now know that this is absolutely wrong. An expression like app.documents.everyItem()
is a Document
specifier, not an Array
of Document
objects. That said, we have already experienced that querying any property on a collective specifier does not return a single property, it returns a JavaScript Array
of properties. For example, app.documents.everyItem().name
brings an array of strings, app.documents.everyItem().modified
brings an array of booleans, app.documents.everyItem().properties
brings an array of objects, etc. It is the same with specifiers based on itemByRange
.
Note. — The expressions “collective mode” and “collective specifier” are mine and in no way normative. In other articles, the same concept may be referred to with the adjective “plural” (e.g. “plural specifier”, or “plural class”, in AppleScript documentation).
This magic behavior reflects the fact that a resolved specifier updates its inner state and fits its own interface to work in collective mode if necessary. In this mode, the specifier acts as a delegate that represents all the receivers in a single JavaScript object. Any single property is returned as an array (one item for each receiver), although you can set this property to a simple value (which is then uniformly applied to every receiver). Here are some examples:
//------------------------------------------- // InDesign CS4/CS5 // Using properties in 'collective mode' //------------------------------------------- var doc = app.documents[0], spec1 = doc.rectangles.everyItem(), spec2 = doc.stories.itemByRange(0,2); // Assign 'myLabel' to every rectangle's label: spec1.label = 'myLabel'; // Return an ARRAY of strings // [ 'myLabel', 'myLabel'... ] alert( spec1.label ); // Assign the same bounds to every rectangle: spec1.geometricBounds = [0,0,50,50]; // Return an ARRAY of arrays // [ [0,0,50,50], [0,0,50,50]... ] alert( spec1.geometricBounds ); // Assign the same contents to the // three first stories of the document: spec2.contents = 'Some text'; // Return an ARRAY of strings // [ 'Some text', 'Some text', 'Some text' ] alert( spec2.contents ); // Ends the collected stories by an exclamation mark spec2.insertionPoints.lastItem().contents = '!';
Well! What about methods? They behave exactly the same way. By calling a method on a collective specifier, you send an ‘all-in-one command’ which performs the same operation on every receiver. If the method is supposed to return a value, then you get an Array
of corresponding values. E.g.:
//------------------------------------------- // InDesign CS4/CS5 // Using duplicate() in 'collective mode' //------------------------------------------- var doc = app.documents[0], spec = doc.rectangles.everyItem(); // Duplicates every rectangle var dups = spec.duplicate(undefined,[10,10]); alert( dups ); // Array of duplicates
Note that spec
is a collective Rectangle
specifier while dups
is just an Array
of resolved specifiers (the duplicates), so you cannot use dups
as a new collective specifier. As far as I know, the Scripting DOM does not offer the ability to regenerate a collective specifier from an array. If you need to perform other operations on dups
elements, sorry, you must loop into the array.
An enlightening test is to prototype a custom method within an object and to study its behavior in collective mode. You might think that such a method is invoked N times (one call on each receiver). Nothing of the kind! The method is called once because the this
object refers to the (resolved) collective specifier. This confirms the fact that a DOM object is nothing but a specifier, and if necessary a collective one:
//------------------------------------------- // InDesign CS4/CS5 // Custom method called in collective mode // // (Before testing, create several rectangles // in your document) //------------------------------------------- Rectangle.prototype.customDuplicate = function() { // In the context, // 'this' is a collective specifier: alert( this.toSpecifier() ); /* Displays sth like: (/document[@name="Test.indd"]//rectangle[@id=207], /document[@name="Test.indd"]//rectangle[@id=206], /document[@name="Test.indd"]//rectangle[@id=205]) */ // ...so r is an Array! var r = this.duplicate(undefined,[10,10]); alert( r.length ); return r; } var doc = app.documents[0], spec = doc.rectangles.everyItem(); var dups = spec.customDuplicate();
The code above is pretty convincing! First, it corroborates the fact that spec
—i.e. doc.rectangles.everyItem()
— is actually treated as a simple Rectangle
object: spec.customDuplicate()
works perfectly. Secondly, we note that the method is called only once even though spec
has several recipients. Finally, in that context, we discover that the this
object is encoded as a collective specifier —check out the path string— whose each item is already resolved:
(/document[@name="Test.indd"]//rectangle[@id=207], /document[@name="Test.indd"]//rectangle[@id=206]...)
.
Tips and tricks
Now that we understand better how DOM objects work under the hood, let's study some useful snippets:
• Nested everyItem()
. — The collection.everyItem()...
syntax can easily aggregate a number of objects in one chained expression. E.g.:
var pPoints = app. documents.everyItem(). rectangles.everyItem(). paths.everyItem(). pathPoints[0]; // funny effect! pPoints.anchor = [0,0];
• Using everyItem().properties
. — Like any other, the properties
property of an object works like a charm in ‘collective mode.’ So you can set a bundle of uniform properties on multiple objects in one step:
var tableSpec = app.activeDocument. stories.everyItem(). tables.everyItem(); tableSpec.properties = { topBorderStrokeColor:'Black', topBorderStrokeTint: 100, topBorderStrokeWeight: '2pt', bottomBorderStrokeColor:'Black', bottomBorderStrokeTint:100, bottomBorderStrokeWeight: '2pt', leftBorderStrokeColor: 'Black', leftBorderStrokeTint: 50, leftBorderStrokeWeight: '4pt', rightBorderStrokeColor: 'Black', rightBorderStrokeTint: 50, rightBorderStrokeWeight: '4pt', // etc. };
• Using spec.getElements()[0]
. — This pattern brings you the first underlying object (as a resolved specifier) behind a generic specifier. It's especially useful when you know that a specifier is only temporarily valid while its receiver will survive. For example, suppose you want to clone the last rectangle of the first page and to put this clone in a new page inserted before the first page:
var pages = app.activeDocument.pages, rec = pages[0].rectangles.lastItem(); // because pages[0] will change, // we need to resolve 'rec': rec = rec.getElements()[0]; // insert a new page and // create a clone of rec: pages.add(LocationOptions.atBeginning). rectangles.add(rec.properties);
Note that technically it is not necessary to reset rec
to rec.getElements()[0]
as long as rec
is resolved before the creation of the new page. So we could simply send a command like rec.isValid
:
var pages = app.activeDocument.pages, rec = pages[0].rectangles.lastItem(); if( rec.isValid ) // this resolves 'rec' too! { pages.add(LocationOptions.atBeginning). rectangles.add(rec.properties); }
• Grouped items backup. — A practical use of getElements()
is to backup the items of a Group
before ungrouping them:
var gp = app.activeDocument.groups[0], items = gp.pageItems.everyItem().getElements(); gp.ungroup(); // 'gp' is no more valid... // ...but 'items' are safe: alert( items[0].constructor.name );
• Creating a collective specifier to manage multiple Spread
objects. — This trick only works with a set of page items that belong to the same spread or page. The goal is to manually create a true specifier to handle those objects collectively. Initially you have an Array
of page items. The trick is to group these objects temporarily and to resolve group.pageItems.everyItem()
:
var page = app.activeDocument.pages[0], // an arbitrary array of page items: myItems = page.rectangles.everyItem().getElements(). concat(page.ovals.everyItem().getElements()). concat(page.textFrames.everyItem().getElements()); // Groups the items ('myItems' is still an Array) var tempGroup = page.groups.add(myItems); // Converts 'myItems' into a collective specifier: myItems = tempGroup.pageItems.everyItem(); // Resolves myItems myItems.isValid; // Remove the group tempGroup.ungroup(); // NOW 'myItems' is a nice collective specifier: myItems.fillColor = 'Black'; // etc.
• Creating your own collection filter. — Contrary to AppleScript, JavaScript does not allow to filter the elements of a collection by using something like the whose
keyword. You cannot create smart specifiers in a form like everyItemWhose(/*filters*/)
, and I don't think it will ever be possible to extend the specification mechanism within a collection. Instead, we can create generic collection methods to encapsulate a filter on everyItem()
, provided that we admit to return a resolved object or an Array
of resolved objects. Harbs illustrated this point in the Scripting Forum by emulating an itemByLabel
function. Here is a more abstract way to create a collection filter:
//-------------------------------------- // Add a generic 'findItems' method // callable from any collection //-------------------------------------- Object.prototype.findItems = function(/*obj*/props) { if( !('everyItem' in this) ) { throw new Error("Error: " + this + " is not a collection."); } var ret = this.everyItem().getElements(), i = ret.length, e, p; while( i-- && e=ret[i] ) { for( p in props ) { if( (p in e) && e[p]===props[p] ) continue; ret.splice(i,1); } } return ret; }; //-------------------------------------- // Sample use //-------------------------------------- // find all text frames having an // empty label AND overflows==true var tfs = app.activeDocument. textFrames.findItems( { label: '', overflows:true, }); alert(tfs.length);
Feel free to put below your own everyItem()
tips...
Comments
Much food for thought here -- thank you.
Peter
Wow!
That's a very impressive analysis of a very complex subject!
I don't think I've ever seen these concepts explained so clearly.
@ Peter & Harbs,
Thank you for your feedback, guys ;-)
@+
Marc
You sir, are a bonafide InDesign JavaScript ninja!
Thanks for explaining how the official documentation for everyitem() is wrong, I had thought so for a while, but I could never quite understand why it seemed wrong to me. Now I know why, but to be honest, ignorance was bliss, because specifiers and receivers don't make it easy to understand.
Thanks again for writing this article!
Thanks a lot for this great article!
You've explained the depths of InDesign scripting very comprehensible. Gave me a lot of new perspectives.
I can see already this is an excellent article, and I just skimmed it.
I will be digesting this at work. Slowly, the scales fall from my eyes.