1/ Characters, Text & Styles
A basic InDesign-to-HTML Encoder
Breakable Lines and Overset Text
Having Fun with anyItem()
UnBase Styles

2/ PageItems
Anchors and Bounding Boxes
On Changing the ContentType of a PageItem
CS6-CC Bug: Unreachable FormField Properties

3/ Event Management
Understanding stopPropagation()
Dealing with Once-Called Event Installers
ScriptUI: Window.update() and Dynamic Adjustment

4/ On the Javascript Side
Local Variables, Scopes and Closures
Adding Hidden Keys to an Array Object
Those RegExp that Freeze InDesign

5/ BONUS TRACKS
ScanRefs: Keeping Track of Memory References
New Version of ProgressBar.jsx
InGoogle


1/ Characters, Text & Styles

A basic InDesign-to-HTML Encoder

In any script that deals with character encoding we should remember that InDesign characters may
    (a) have SpecialCharacters values
    (b) lead to U+FFFD (REPLACEMENT CHARACTER) when no Unicode value is associated
    (c) lead to UTF-16 surrogate pairs based on two bytes in JavaScript strings (for high Unicode values)

Non-exhaustive list, I'm afraid!

Having these rules in mind here is my first step in implementing an HTML encoder:

//=======
// HELPERS  
//=======
 
var findGrep = function(/*Document*/doc, /*str*/argFind,
               /*str?*/pStyle, /*str?*/cStyle)  
//------------------------------------------------  
{  
    var t;  
 
    app.findGrepPreferences = NothingEnum.nothing;  
 
    (t=app.findGrepPreferences).findWhat = argFind;  
    pStyle && t.appliedParagraphStyle = pStyle;  
    cStyle && t.appliedCharacterStyle = cStyle;  
 
    return doc.findGrep();  
};  
 
// ---  
// Remember that JavaScript uses UTF-16 code units for strings,  
// so the argument 's' might have 2 chars (surrogate pairs) for the
// supplementary multilingual plane. Here you have to take a
// decision related to the HTML side (UTF-8 vs. UTF-16 encoding?)  
// ---  
// Also, InDesign uses U+FFFD when some replacement is done, that is,  
// when no Unicode code point is associated to the 'character'  
// ---  
 
var htmlEncode = function F(/*str*/ s)  
//------------------------------------------------  
{  
    // Cache  
    // ---  
    F.PREFIX_SZ || (  
        (F.PREFIX_SZ = "ZZ".toSource().replace(/ZZ.+$/,'').length),  
        (F.SUFFIX_RE = /["']\)+$/),  
        (F.BSLASHU_RE = /\\u/g)  
        );  
 
    // Translate forms like '(new String("\uABCD"))'  
    // into '&#xABCD' -- Multiple chars are supported too  
    // ---  
    return s.toSource().substr(F.PREFIX_SZ).  
        replace(F.BSLASHU_RE,'&#x').  
        replace(F.SUFFIX_RE,';');  
};  
 
 
//================================================  
// MAIN ROUTINE  
// This will encode any character from U+00A0  
//================================================  
 
const TARGET = "[^\x00-\x9F]";  
var doc = app.activeDocument,  
    chars = findGrep(doc, TARGET),  
    i = chars.length,  
    c, s;  
while( i-- )  
    {  
    s = (c=chars[i].texts[0]).contents;  
    c.contents = htmlEncode(s);  
    } 
 

• Original discussion: forums.adobe.com/thread/1674335

Breakable Lines and Overset Text

azimmon wrote: “I would like to know that when TextFrame.overflows is true, whether it was because there was too much text or because a line could not be broken.”

First above all, there are many possible reasons why TextFrame.overflows is true in InDesign—not to mention baseline grid constraints, keep options, wrapping, and so many more.

Going back to the original question, it's worth noting that a-line-that-cannot-be-broken is, surprisingly, a meaningless concept, for the line in question does not actually exist! As long as the overset text is not framed in a visual container, you're just talking about a virtual line and therefore you have no clue (such as location, width, height…) for solving the problem.

So one should rephrase the question in more specific terms, or at least with additional conditions: Given a specific text frame that overflows, would it still overflow if…

In the particular case pictured in the topic, the implicit problem was to determine whether the frame width, in itself, explains why the hypothetical line does not appear. Which leads to an indirect question: all things being equal, what if the frame height was increased to make as much room as needed for, say, the whole story? Would it still overflows? If the answer is YES, then one can strongly suspect that the width is involved in the issue, that is, the text “cannot be broken” the way one expects it to.

Practically, we could just check how things evolve if we temporarily multiply the frame height by 2:

const CS_INNER = +CoordinateSpaces.innerCoordinates,
      AP_TOP_LEFT = +AnchorPoint.topLeftAnchor,
      RM_MULT = +ResizeMethods.multiplyingCurrentDimensionsBy;
 
var tf = app.selection[0], // assumed a TextFrame is selected
    msg = "No issue.";     // default message  
 
if( tf.overflows )  
    {  
    tf.resize(CS_INNER, AP_TOP_LEFT, RM_MULT, [1,2]);  
    msg = "The selected frame overflows because of:\r" +  
       (tf.overflows ? "Unbreakable line." : "Some other reason.");
    tf.resize(CS_INNER, AP_TOP_LEFT, RM_MULT, [1,.5]);  
    }  
 
alert( msg );
 

• Original discussion: forums.adobe.com/thread/1563624

Having Fun with anyItem()

Nannini wrote: “Is it possible to have a script that makes a text randomly have caps and lower caps? I would love to create a kind of effect so it would look like manuscript — but it cannot be only the same letter in caps or lower caps — I wOuld NeEd oF SOmeTHing LikE This.”

Detailed answers have been brought forward in this thread, based on either GREP tricks and Math.random().

In addition, we suggest a solution which is shorter than the question:

app.selection[0].texts[0].words.everyItem().characters.
    anyItem().capitalization = Capitalization.ALL_CAPS;
 

• Original discussion: forums.adobe.com/thread/1557880

UnBase Styles

cchimi wrote: “I have a function that removes the basedOn style from a paragraph or character style that is passed to it. I thought it was working, but it came to my attention that character styles were losing their formatting when their basedOn style was removed (actually, set to '[No Character Style]'). Setting the basedOn style to the '[None]' style has the same effect. (…) My solution has been to create a new character style, copy all of the first style's properties (other than basedOn and properties) over to it, remove the original style (replacing it with the new style) and then rename the new style to match the old. As far as I can tell that works, but it seems like overkill. Does anyone have a better suggestion or hint?”

Easily checking whether a style property is inherited is an open question to me. But regarding the rewriting-from-scratch routine, here is a simpler approach:

function unBaseStyle(/*ParagraphStyle|CharacterStyle*/sty)  
{  
    var o = sty.properties;  
    if( !o.hasOwnProperty('basedOn') || 0 <= o.name.indexOf('[') )
      return;  
 
    o.basedOn = "$ID/None";  
    sty.properties = o;  
} 
 

• Original discussion: forums.adobe.com/thread/1685566


2/ PageItems

Anchors and Bounding Boxes

Here are some insights on InDesign's bounding boxes.

1. Although bounding boxes are not coordinate spaces in the strict sense, they provide additional coordinate systems (always related to coordinate spaces). A little known fact is that a given page item—say an Oval instance—has distinct bounding boxes depending on the coordinate space you consider along its hierarchy. Study the figure below. At a minimum, one can distinguish the inner space bounding box of the object (blue rectangle), and its spread-space-related bounding box (magenta).

Distinct bounding boxes for the same object.

So for the same object, there are as many distinct bounding boxes as coordinate spaces involved in the hierarchy: inner space, parent space(s)… spread (and page) space, and finally pasteboard space. Each bounding box reflects the orientation—in fact, the transformation state—of the coordinate space we consider. Two bounding boxes do not coincide as soon as the related coordinate spaces are connected through an affine map that contains some non trivial rotation and/or shearing parameter.

2. Another curious fact is, while InDesign's GUI shows the inner space bounding box of the selection (for a single object), the geometricBounds property always returns the coordinates of the pasteboard-related bounding box (which in most cases is equivalent to the spread-related bounding box). In addition, obj.geometricBounds returns the [top, left, bottom, right] values in a special coordinate system, the Ruler space system, which I will not explore here.

To keep things simple, I just consider the basic situation depicted above—an oval rotated in its parent spread—and will handle coordinates in the pasteboard space.

3. The PageItem.resolve() method has three parameters: location, inSpace, and consideringRulerUnits (optional). The most important one, location, has almost ten different forms (all poorly documented). Basically, location specify a point anywhere in any space. You can define as well a bounding box related location, a ruler space related location, or a coordinate space location. Here we only consider the first option. The full syntax of a bounding box location is as follows:

 
[ MyAnchorPoint, MyBoundingBoxLimits, MyCoordinateSpace ]
 

which means: “considering the bounding box relative to this coordinate space (3rd param), considering whether outer strokes are included or not in that box (2nd param), take this anchor point (1st param).”

Fortunately we have a shortcut: MyAnchorPoint alone is equivalent to
[MyAnchorPoint, BoundingBoxLimits.GEOMETRIC_PATH_BOUNDS, CoordinateSpaces.INNER_COORDINATES].

Therefore, in the above figure:

• the location of the blue point is (fully encoded):
[AnchorPoint.TOP_LEFT_ANCHOR, BoundingBoxLimits.GEOMETRIC_PATH_BOUNDS, CoordinateSpaces.INNER_COORDINATES]

• the location of the magenta point is:
[AnchorPoint.TOP_LEFT_ANCHOR, BoundingBoxLimits.GEOMETRIC_PATH_BOUNDS, CoordinateSpaces.SPREAD_COORDINATES]

Finally, given a location, the inSpace parameter tells resolve() in which coordinate space that location must be expressed.

Here is a simple code to check these rules:

// Assuming a PageItem is selected  
// ---  
var obj = app.selection[0];  
 
// Boring constants  
// ---  
const AP_TOP_LEFT = +AnchorPoint.TOP_LEFT_ANCHOR,  
      BB_GEOMETRIC = +BoundingBoxLimits.GEOMETRIC_PATH_BOUNDS,  
      CS_SPREAD = +CoordinateSpaces.SPREAD_COORDINATES,  
      CS_INNER = +CoordinateSpaces.INNER_COORDINATES;  
 
// Locations in the 'bounds space' format (full syntax)  
// ---  
var innerTopLeft = [AP_TOP_LEFT, BB_GEOMETRIC, CS_INNER],
    spreadTopLeft = [AP_TOP_LEFT, BB_GEOMETRIC, CS_SPREAD];
 
// Invoke resolve for both locations and  
// return (x,y) results in spread coordinates  
// ---  
var xyInner = obj.resolve(innerTopLeft, CS_SPREAD)[0],  
    xySpread = obj.resolve(spreadTopLeft, CS_SPREAD)[0];  
 
alert([  
    "INNER SPACE corner location in spread coordinates: "+xyInner,  
    "SPREAD SPACE corner location in spread coordinates: "+xySpread  
    ].join('\r\r')  
    ); 
 
 

• Original discussion: forums.adobe.com/thread/1661859

On Changing the ContentType of a PageItem

When you change the contentType property of an unassigned page item into ContentType.TEXT_TYPE, the object implicitly becomes a TextFrame. This conversion may yield an unresolved specifier, as shown below:

// Assuming spec will point out to some empty shape (Rectangle, etc)
var spec = app.documents[0].pages[0].allPageItems[0];
 
spec.contentType = ContentType.TEXT_TYPE; 
spec.contents = 'TEST';
// => ERROR! Object doesn't support the property or method 'contents'
 

Assigning the long path app.documents[0].pages[0].allPageItems[0] to a variable, spec, is the cause of the problem.

Indeed spec becomes somehow obsolete—i.e. unresolved—as soon as we change the contentType property, because this command converts the underlying object from a Rectangle (or equivalent) into a TextFrame. But once the mutation is done, spec still refers to the rectangle path—e.g. /document[@id=1]//rectangle[@id=342]—as shown using spec.toSpecifier(). Thus, the property spec.contents is not supported. Syntactically, spec is still a Rectangle.

The issue does not occur if the code reuses the explicit path app.documents[0].pages[0].allPageItems[0], which forces the system to resolve the object again:

app.documents[0].pages[0].allPageItems[0]
   .contentType = ContentType.TEXT_TYPE; 
 
app.documents[0].pages[0].allPageItems[0]
   .contents = 'TEST'; 
 

In the first line app.documents[0].pages[0].allPageItems[0] points out to a Rectangle (or any equivalent unassigned SplineItem);
In the secund line app.documents[0].pages[0].allPageItems[0] now points out to a TextFrame.

However, reusing a complex path is not as elegant as fixing the specifier in a generic way. The usual method to repair unresolved specifiers is getElements(). In my original code, a clean solution would be:

var spec = app.documents[0].pages[0].allPageItems[0]; 
spec.contentType = ContentType.TEXT_TYPE; 
 
// spec.contents = 'TEST';               // => ERROR! 
spec.getElements()[0].contents = 'TEST'; // => OK
 

• Original discussion: forums.adobe.com/thread/1645245

CS6-CC Bug: Unreachable FormField Properties

ID CS6 to CC does not properly provide access to FormField properties as soon as there is a conflict between the default Buttons and Forms panel settings and what is done through scripting. This serious bug is detailed here by Uwe Laubender.

Note. — See comment #1 below for additional details from Uwe.

As a very first test one can check that using myObject = myTextBox.properties in a snippet won't generate errors anymore, since all failing properties (fontSize, multiline, etc.) are not considered available at all in the TextBox instance.

Interestingly, all these properties become fully functional (read and write access) when explicitly set at creation time:

var TB = {  
    description: 'foobar',       // default: ''  
    fontSize: 16,                // default: 0  
    hiddenUntilTriggered: true,  // default: false  
    multiline: true,             // default: false  
    name: 'myTextBox',           // default: auto  
    password: true,              // default: false  
    readOnly: true,              // default: false  
    required: true,              // default: false  
    scrollable: true,            // default: false  
    };  
 
var doc = app.activeDocument,  
    tb = doc.textBoxes.add(TB),  
    k,  
    a = [];  
 
for( k in TB )  
    {  
    a[a.length] = k + ': ' + tb[k]; // no issue  
    }  
 
alert( a.join('\r') );
 

However, if the user changes any of these via the Buttons and Forms panel, there is good chance that the “multiple values” error comes back when the script attempts to read the modified property. For instance, if the user manually changes the TextBox description to any non-empty string, I don't see any workaround to safely access that custom value. There are also problems with defaults in various circumstances.

So my assumption: there is a sync issue between GUI settings and their reflection in the scripting DOM—especially when no behaviors and/or states are defined for the control.

• Original discussion: forums.adobe.com/thread/1566300


3/ Event Management

Understanding stopPropagation()

As suggested by Robert Kyle let's consider the following snippet:

var beforeCloseListenerTest = app.addEventListener("beforeClose",
    beforeCloseListenerTest);
 
function beforeCloseListenerTest(beforeCloseEvent)
{
    if( beforeCloseEvent.target.constructor === LayoutWindow )
    {
      // first beforeClose event
      // ---
      alert("LayoutWindow is closing. Propagation will be stopped.");
      beforeCloseEvent.stopPropagation();
    }
    else
    {
      alert(beforeCloseEvent.target.constructor.name + " is closing.");
    }
}
 

Robert wrote: “When this event listener is running and a document is closed the alert is displayed saying that the Layout Window is closing. As expected. But the second alert also displays when the Document closes. My brain is stuck on the idea that stopPropagation() should prevent that.”

But Event.stopPropagation() does not operate the way one could expect it to. In fact, there are very few cases where stopping the propagation of an event is relevant in the InDesign DOM event field, because most of the time we only manage an event from a single listener and through a single event handler. That is, our code usually manages a single propagation step and there is no further stage to be stopped at all.

What is confusing in the above code is that two distinct events are actually considered, each having its own propagation process. The first event that occurs—say ev1—is a beforeClose on the LayoutWindow target, then a distinct beforeClose event occurs—ev2—on the Document target.

Stopping event propagation makes sense only if some event has multiple registered listeners and/or handlers that may manage the event instance during its BUBBLING phase, that is, from the AT_TARGET step to the very last listener in the hierarchy.

Let's consider ev1 (that is, the layout window's beforeClose event). It occurs at the LayoutWindow target, then it ‘bubbles’ to the document level (since LayoutWindow.parent is a document) and finally to the Application level. Hence there are three propagation steps for that event.

As for ev2 (that is, the document's beforeClose event), it occurs separately. Its target is the document being closed, then it bubbles to the app, so there are two propagation steps for that event.

To highlight this, let's create a generic event handler (evHandler) then attach it to any possible listener for the beforeClose event:

#targetengine 'test01'
 
function evHandler(ev)
{  
    alert([  
        'evHandler being called.',  
        'target: ' + ev.target.constructor.name,  
        'listener: ' + ev.currentTarget.constructor.name,  
        'phase:  ' + ev.eventPhase  
        ].join('\r'));  
}  
 
// assuming that a document is open  
// just before you execute the script  
// ---  
app.activeWindow.addEventListener('beforeClose', evHandler);  
app.activeDocument.addEventListener('beforeClose', evHandler);  
app.addEventListener('beforeClose', evHandler);
 

On closing the document's window, you will observe that evHandler is called 5 times, that is:

• Three times for the window's beforeClose event (ev1) which traverses LayoutWindow, Document and Application, in that order;

• Two times for the document's beforeClose event (ev2) which traverses Document and Application, in that order.

Now if you restart InDesign, set up a similar context and append the line
ev.stopPropagation();
to evHandler before you run the script, then you observe that the callback is now called twice, that is, once for each event. Only the AT_TARGET phase is reported, because both the window and the document event propagation is stopped as soon as the event has been managed (at the target stage). That's the actual meaning of stopPropagation(). This method only prevents a specific event from being handled by higher listeners during the bubbling mechanism. More on this topic can be found here: quirksmode.org/js/events_order.html

But as said above, there is usually no point in handling events from volatile listeners such as Document or Window instances when dealing with InDesign DOM events. Most scripts set listeners and handlers at the Application level, since this is a stable entity that can listen to any event (whatever its target).

• Original discussion: forums.adobe.com/thread/1551372

Dealing with Once-Called Event Installers

There are many situations where event-driven scripts need to check whether some event stuff is installed, then to prevent the script from reloading, re-invoking or duplicating what is already here. A radical approach is to remove everything everywhere at the startup script level in order to make sure that the room is clean before processing. But this is a very bad idea. No scripter should ever release a startup script that contains code like app.eventListeners.everyItem().remove()—except for clinical purpose!

We need rather to face a generic problem: How to make sure that some routine or action will be performed once (in particular in a session-persistent context)?

There are various ways to reach this goal in ExtendScript. Here are my favorites:

1. At the engine level, you can prevent the entire code from being re-executed, using the following pattern:

#targetengine 'setupOnce'
 
var UID;
$[UID='_'+$.engineName] || ($[UID]=function()
{
   // your script goes here
})();
 

2. In a more complex script or library, you can ‘decorate’ a function so that it does not accidentally re-run:

#targetengine 'functionOnce'
 
var fOnce;
fOnce || (fOnce = function F()
{
   if( !F.ONCE ) return;
 
   // your function code goes here
   // ...
 
   delete F.ONCE;
}).ONCE=1;
 
 
// calling the func somewhere
// ---
fOnce(); // executed
 
// calling the func somewhere else
// ---
fOnce(); // noop
 

Depending on your requirements, one or other of these strategies should help you prevent event listeners from being registered multiple times.

ADDITIONAL NOTE: Functions vs. Listeners Live Cycle

Removing an event listener is something like removing a link between an event and a function (the event handler). Nothing more. Technically, best is probably to backup listener's id somewhere—using either session-persistent data structure, app.insertLabel() or whatever—then to call app.eventListeners.itemByID(id).remove().

Now let's try to get the whole picture. Say we have a jsx file that contains some code under a #targetengine directive. This directive creates a persistent scope—or might we say a context—where any defined variable, function, or entity, will persist during the InDesign session. That is, once the script has been executed, all those entities will keep their state or value. In particular, some functions are then available as event handlers, meaning they can be triggered from the external world (i.e. InDesign DOM events, or ScriptUI user events if a palette is active).

Such script as a program is only running when the jsx is executed. This stage might be regarded as the installation of the context. Then, the code contained in this script is not running anymore as long as ID (or ScriptUI) events do not occur. But, thanks to session persistence, functions and data are available on-demand in their very last state. Now if some event occurs which is attached to some event handler defined in the code, then the corresponding function is executed. At this specific time, some part of the code will therefore be running, depending on how the called function interacts with other stuff, data, subroutines, available in the scope.

In short, the whole script installs data and functions to be driven later by incoming events.

Question: “Can the script (itself) be removed, so that the code can no longer react to anything?”

To my knowledge the answer is no. The script can't be removed in the sense of cleaning out the session-persistent engine that owns the code—except of course if you quit/restart InDesign.

But you can inhibit event handler(s) by removing the related listener(s) as said above:
• InDesign DOM: myEventListener.remove()
• ScriptUI: myWidget.removeEventListener(
/*str*/ eventName,
/*fct*/ eventHandler,
/*bool*/ capturePhaseAsDeclared )

• Original discussion: forums.adobe.com/thread/1551727

ScriptUI: Window.update() and Dynamic Adjustment

Can Window.update() help us keeping a text widget center-justified during dynamic changes in a ScriptUI container? Here are some details regarding this mysterious method:

ScriptUI Window.update()

“Allows a script to run a long operation (such as copying a large file) and update UI elements to show the status of the operation. Normally, drawing updates to UI elements occur during idle periods, when the application is not doing anything and the OS event queue is being processed, but during a long scripted operation, the normal event loop is not running. Use this method to perform the necessary synchronous drawing updates, and also process certain mouse and keyboard events in order to allow a user to cancel the current operation (by clicking a Cancel button, for instance).

During the update() operation, the application is put into a modal state, so that it does not handle any events that would activate a different window, or give focus to a control outside the window being updated. The modal state allows drawing events for controls in other windows to occur (as is the case during a modal show() operation), so that the script does not prevent the update of other parts of the application's UI while in the operation loop.”

Anyway, I strongly doubt we could reach any satisfactory enhancement using either w.update() and/or w.layout.layout(…).

But here is a promising fact: reassigning StaticText.location seems much faster than any other approach. So one can manually center the message—keeping the StaticText left-justified—by simply repositioning the control with respect to the width of the text. That can be done on the fly using StaticText.graphics.measureString(newText). Give a look at the new implementation of my ProgressBar snippet to see how it works.

My tests provide good results (so far) on various ID versions from CS4 to CC.

• Original discussion: forums.adobe.com/thread/1631865


4/ On the Javascript Side

Local Variables, Scopes and Closures

Given two independent function f1 and f2, is it possible for f2 to have access to some locale variable created in f1's body?

No, it is not. A local variable only lives in the scope of the function (body) where it is declared. In that sense any local variable should be considered private (in OOP terms.)

A basic workaround is based on setting a global scope for the variable you want to share throughout the functions, but then it is visible from every place of your code. Technically, the variable is declared outside of the function bodies, which leads to “polluting the global namespace.”

Another solution can be implemented using closure mechanism. You can create a local scope (in an anonymous function which is executed once) then return your main function and additional helpers from that scope. All inner functions have then access to the local variables that have been declared in the scope. For example, you can create a function f1 and attach to it a getter function as follows:

var f1 = (function(/*anonymous*/__) 
{ 
   // Local variable to be nested in the closure 
   // --- 
   var v1; 
 
   (__ = function(/*optional init value*/x) 
   { 
      // Here is your *actual* function 
      // do what you want with v1 
      // --- 
      v1 = 'undefined'!=typeof x ? x : Math.random(); 
   } 
   ).getVar = function() 
   { 
      // Here is your getter 
      // --- 
      return v1; 
   }; 
 
   return __; 
})(); 
 
 
var f2 = function() 
{ 
   alert( f1.getVar() ); 
}; 
 
 
// Process 
// --- 
f1('init'); 
f2(); // => 'init' 
 
f1(); 
f2(); // => some random number 
 
f1(); 
f2(); // => another random number 
// etc 
 

This way v1 remains almost private but the outer code can read it through f1.getVar().

However this might be a complicate approach for scripts that don't require high security level. Another option, really easy to set up, is to use the fact that a function is an object. Instead of declaring a local variable, one can just handle v1 as a (public) member of f1, as follows:

var f1 = function F(/*optional init*/x) 
{ 
   // Do something with F.var 
   // --- 
   F.v1 = 'undefined'!=typeof x ? x : Math.random(); 
}; 
 
 
var f2 = function() 
{ 
   alert( f1.v1 ); 
} 
 
 
f1('init'); 
f2(); // => 'init' 
 
f1(); 
f2(); // => some random number 
 
f1(); 
f2(); // => another random number 
 

• Original discussion: forums.adobe.com/thread/1543304

Adding Hidden Keys to an Array Object

One of my favorite tricks is to add hidden keys into an Array object. These keys remain transparent to ScriptUI—in case you use this array as a data provider for a ListBox—but they can still be used to save ordering data for additional sort or display features.

Here is a basic example:

var names = ["Alan Turing","Steve Jobs","Bill Gates","John Mac Doe"];
 
(function(a,i,k,p) 
{ 
   i = a.length; 
   while( i-- ) 
   { 
      p = (k=a[i]).indexOf(' '); 
      0 <= p && (k=k.substr(1+p)+', '+k.substr(0,p)); 
      a['_'+a[i]] = k; 
   }
 
   a.sort(function(x,y){return x=a['_'+x],y=a['_'+y],(-(x<y)||(x>y))});
})(names); 
 
// Here your code
 

The above function performs two tasks:

• It creates hidden keys in the form "_<ItemString>" associated to <Name>,<Surname> values.

• It reorders array items with respect to those hidden key-value pairs.

This way you can also handle the associated <Name>,<Surname> sequence of any item using the "_" (underscore) prefix.
E.g.: names["_John Mac Doe"] => "Mac Doe, John"

• Original discussion: forums.adobe.com/thread/1595578

Those RegExp that Freeze InDesign

In the field of JS RegExp objects, greedy quantifiers like +, *, or {m,n} cause issues in InDesign—in particular, in CS6 and later—when mixed with optional sub-patterns. For instance, a very simple way to create an infinite loop in CS6-CC is:

alert( /(aA?|bB?)+$/.test("bx") );
// CS4: FALSE ; CS6-CC: FREEEEZE!
 

or even:

alert( /(a|bB?)+$/.test("bx") );
// CS4: FALSE ; CS6-CC: FREEEEZE!
 

Those explosive quantifiers are not properly addressed due to various backtracking bugs which Adobe devs don't seem to be aware of. Another fact is that ExtendScript CS6-CC doesn't interpret the scheme /((A|B)|C)/ the right way:

alert( /^((a|b)|c)+$/.test("ac") ); // CC: FALSE! 
alert( /^((a|b)|c)+$/.test("ca") ); // CC: TRUE 
 
// Interestingly: 
alert( /^(c|(a|b))+$/.test("ac") ); // CC: TRUE 
alert( /^(c|(a|b))+$/.test("ca") ); // CC: TRUE 
 

But, in fact, /((A|B)|C)/ is equivalent to /(A|B|C)/ isn't it? So there is no reason in principle to use these additional capturing parentheses.

• Original discussion: forums.adobe.com/thread/1639463


5/ BONUS TRACKS

ScanRefs: Keeping Track of Memory References

How to keep track of objects and methods created in a long script? How to check whether some reference (in JS terms) still exists in ExtendScript's memory. A few months ago I wrote a tool that collects and displays those references based on $.list(). This might help to visualize deep reference relationships in your code.

You just need to include the script at the beginning of your code and call $.scanRefs() whenever you need it.

ScanRefs.jsx is now available on GitHub:

github.com/indiscripts/extendscript/blob/master/devtools/ScanRefs.jsx

• See also: forums.adobe.com/thread/1587488

New Version of ProgressBar.jsx

A new version of my ProgressBar component (ScriptUI) has been released on GitHub:

github.com/indiscripts/extendscript/blob/master/scriptui/ProgressBar.jsx

It now keeps the message centered and provides various improvements.

InGoogle

Based on Martin Fischer's googleSelection.jsx script (Oct. 2010) InGoogle invokes Google from InDesign considering the active text selection.

github.com/indiscripts/extendscript/blob/master/InGoogle.jsx

• My GitHub main page: github.com/indiscripts