1/ Text and Typography
Speeding Up Text Processing Using Character Ranges
Surgical Modification of a Paragraph Style
Find/Change Interacting with the Selection
Shuffle the Selected Words

2/ Graphics and Geometry
Placing Small Circles on Curve Path Points
Converting a “Quasi Rectangle” into a Rectangle
The Danger of Changing Bounds Before Resolving
Dealing with Overridden Margin Preferences

3/ Miscellaneous Notes
Note on the count() Method Attached to a Collection
RegExp: Combining Negative and Positive Lookahead
Quick Note on ExtendScript Triple Quote Syntax
Shortcut for Flattening a Two-Dimensional Array


1/ Text and Typography

Speeding Up Text Processing Using Character Ranges

Let's suppose the following:

(a) We need to apply some constant operation to a collection of characters (by 'constant operation,' I mean, any task that can be addressed from within a function),

(b) This operation regards a specific character property (e.g, horizontalScale), but is not as trivial as assigning a constant value.

For example, you want to reduce or increase the attribute by the same amount, or apply to it some constant multiplier, etc.

How can we optimize such process when the targets are sequential characters having, unfortunately, non-uniform settings? In the worst case all attributes would be distinct, and then each character would require an individual treatment. But in practice attributes are not randomly distributed, we likely encounter ranges of identical values. (At least, that's what we shall assume.)

As the key idea is to reduce DOM hits as much as possible, a good algorithm should try to handle the largest character ranges it can seek. Then it may operate range-by-range rather than character-by-character.

Here is a general approach:

 
// SAMPLE TASK
// (Increasing horizontalScale by 0.75.)
// ---
const KEY_PROP = 'horizontalScale';
const CHANGE_VALUE = function(x){ return x + 0.75 };
 
 
(function(/*fct*/f,/*str*/k,  t,a,z,dz,s,x)
// ---------------------------
// Range-optimized change of a character property.
// (Targets all paragraphs containing the selection.)
{
    const __ = $.global.localize;
 
    // Selection check.
    // ---
    if( !(t=app.selection||[]).length ) return;
    if( !((t=t[0]).hasOwnProperty('paragraphs')) ) return;
    if( !(t=t.paragraphs).count() ) return;
 
    // Retrieve all values in an array.
    // ---
    a = t.everyItem().characters.everyItem()[k];
    if( !(z=a.length) ) return;
 
    // First paragraph.
    // ---
    t = t[0];
 
    // Range specifier pattern.
    // ---
    s = __(
        "(%1[%%1] to %1[%%2])",
        t.parentStory.toSpecifier() + "/insertion-point"
    );
 
    // Quick Loop
    // ---
    app.scriptPreferences.enableRedraw = false;
    for(
        a.unshift(-9999), dz=t.index, x=a[z] ;
        z-- ;
        x===(t=a[z]) ||
           (
             (resolve(__(s,dz+z,dz+a.length-1))[k]=f(x)),
             (a.length=1+z),
             (x=t)
           )
    );
    app.scriptPreferences.enableRedraw = true;
 
})(CHANGE_VALUE, KEY_PROP);
 
 

The code may seem a bit cryptic, but it relies on a simple idea: as long as you can capture characters that share the same attribute regarding the target property, keep them together and only apply the function to the whole range once required, by sending a single DOM command to the underlying set of insertion points.

• Original discussion: forums.adobe.com/message/9467763#9467763

Surgical Modification of a Paragraph Style

Say you have a huge Story that needs to be locally re-styled with no side-effect on the original paragraph style(s) in use. For example, you need to change the leading property of each paragraph having myOrigStyle applied—in that story—but you cannot modify myOrigStyle itself because it is used in other parts of your document.

As tested by laindustria, the code

for( var i=0 ; i < myStory.paragraphs.length ; i++ )
{
    if( myStory.paragraphs[i].appliedParagraphStyle.name == "myOrigStyle" )
    {
        myStory.paragraphs[i].leading = myLeading;
    }
}
 

is “extremely slow.”

Various solutions can be implemented (see the original thread for an overview.)

An interesting question behind this is, how to declare a variant of a style in a document, and apply it to a specific story? My approach is to create a temporary ghost document just to load the target story alone, then to rewrite the properties of myOrigStyle, including a new name. It is then possible to reinject the result into the original document without interfering with existing styles.

Note. — However, default settings and/or [Basic Paragraph] settings and/or style hierarchy are not so easy to manage from the temporary document, so when the text goes back some style override flags may appear within the story.

This experimental code, which is very fast, could surely be improved as to provide a perfect job:

// SETTINGS
// ---
const TARGET_STYLE = 'MyOrigStyle';
const NEW_STYLE_PROPS = {
    name: TARGET_STYLE + '_Fixed',
    leading: 12,
    // etc.
    };
 
// Provide your story here:
// ---
var myStory = app.selection[0].parentStory;
 
// Only applies style fix to myStory.
// ---
const LO_BEG = +LocationOptions.AT_BEGINNING;
var ip = myStory.insertionPoints[0],
    tDoc = app.documents.add(false),
    tSto = myStory.move(LO_BEG, tDoc.textFrames.add().parentStory);
 
tDoc.paragraphStyles.itemByName(TARGET_STYLE)
    .properties = NEW_STYLE_PROPS;
tSto.duplicate(LocationOptions.AT_BEGINNING, ip);
tDoc.close(SaveOptions.NO);
 

• Original discussion: forums.adobe.com/message/9293997#9293997

Find/Change Interacting with the Selection

Suppose you initialize a reference to the selected text — say, var selText = app.selection[0]; — and then run a find/change process over that selText reference. What does it become during the changes? Does it keep the meaning of “currently selected text”? No, it probably does not.

As jb_alvarado has experienced, calling change... methods usually alters the selection. So the selText variable — based on the original selection — may become invalid as soon as its internal indices no longer match the current text range. For example, in a loop that will invoke Text.changeGrep() multiple times, the very first iteration is likely sufficient to modify the selection and make selText invalid. (That is, selText.isValid==false.)

The above issue seems to magically disappear when you explicitly reuse app.selection[0] rather than selText. That's because app.selection is a command that forces InDesign to recalculate the selection each time the interpreter meets it, so it takes into account changes introduced by the latest find/change.

However this is a terribly time-consuming approach. In the case we were studying, the goal was to process every paragraph associated to the original (text) selection. So these paragraphs are already known at the beginning of the find/change operation. Rather than looping through an unstable selection reference, better is to call a single changeGrep() command based on a plural Paragraph, namely, selText.paragraphs.everyItem().

function myChangeProcess(/*Text*/tx)
{
    app.changeGrepPreferences = NothingEnum.nothing;
    app.findGrepPreferences = NothingEnum.nothing;
    app.findGrepPreferences.findWhat = "\\[(.+)\\]";
    app.changeGrepPreferences.changeTo = "$1";
    numberList = tx.changeGrep();
}
 
// Assuming some text selected.
// ---
var selText = app.selection[0];
 
// One-step processing.
// ---
myChangeProcess( selText.paragraphs.everyItem() );
 

Note 1. — The key idea is, since changeGrep is supported by the Paragraph object, you may call it from a plural paragraph as well.

Note 2. — There are cases where this trick doesn't perfectly work, depending on the regular expression in use. Find/change is not 100% safe when working on plural text specifiers.

• Original discussion: forums.adobe.com/message/9664807#9664807

Shuffle the Selected Words

We already proposed a ShuffleSelectedParagraphs routine in this series. What if we need to shuffle words rather than entire paragraphs?

Keeping the same logics:

function shuffleSelectedWords()
{
    const mRAND = Math.random;
 
    var s = app.properties.selection && app.selection[0],
        ws = s && s.hasOwnProperty('words') && s.words,
        n = ws && ws.length;
 
    if( n < 2 ) throw Error("Please select at least two words.");
 
    var sto = ws[0].parentStory,
        ip0 = ws[0].insertionPoints.firstItem().index,
        ip1 = ws[n-1].insertionPoints.lastItem().index;
 
    if( s=s.hasOwnProperty('appliedFont') ) app.selection = [];
 
    ws = ws.everyItem().contents;
    for( var t ; n ; (t=ws.splice(~~(n--*mRAND()),1)[0]), ws.push(t) );
 
    ws = ws.join(' ');
    sto.insertionPoints.itemByRange(ip0,ip1).contents = ws;
 
    if( !s ) return;
    sto.insertionPoints[ip0].select();
    sto.insertionPoints[ip0+ws.length].select(SelectionOptions.ADD_TO);
}
 
try{ shuffleSelectedWords() }catch(_){ alert(_.message) }
 

• Original discussion: forums.adobe.com/message/9670225#9670225

• See also: “What Exactly is a Word?”


2/ Graphics and Geometry

Placing Small Circles on Curve Path Points

henum wrote: “I am (…) looking for a script to add small circles on a curve with several nodes on it.”

So we basically want an automatic process for decorating a spline object as follows:

How to automatically generate circles on path points?

Suggested code:

// Settings:
// - Radius of the circles,
// - Additional properties (color, etc.)
// ====================================
const RADIUS = '1mm';
 
const CIRCLE_PROPS = {
    fillColor:       'Black',
    strokeColor:     'None',
    // Etc... but don't change the line below.
    geometricBounds: ['-'+RADIUS,'-'+RADIUS,'+'+RADIUS,'+'+RADIUS],
    };
 
const parentSpread = function F(/*DOM*/o,  p)
//--------------------------------------
// Get the parent spread of this DOM object, or NULL.
{
    p = o && o.parent;
    if( (!p) || (p instanceof Document) ) return null;
    return ( (p instanceof Spread) || (p instanceof MasterSpread) ) ?
        p :
        F(p);
};
 
(function(/*?DOM[]*/a,  o,t,u)
//--------------------------------------
// Place circles on the selected curve.
{
    // Check.
    // ---
    if( !a || 1 != a.length )
       { alert( "Please select one object!" ); return; }
    if( !('paths' in (a=a[0])) )
       { alert( "Please select a spline item!" ); return; }
    if( !(o=parentSpread(a)) )
       { alert( "No parent spread found!" ); return; }
 
    // Create the template circle at [0,0] (ruler space.)
    // ---
    o = o.ovals.add(CIRCLE_PROPS);
 
    // Flat array of all pathpoints coordinates.
    // a[i] :: [x,y] | [[lx,ly],[x,y],[rx,ry]]
    // ---
    a = [].concat.apply([],a.paths.everyItem().entirePath);
 
    // Duplicate the template at each point.
    // ---
    for( ;
        t=a.pop() ;
        o.duplicate(u, 'number' == typeof t[0] ? t : t[1] )
    );
 
    // Remove the template.
    // ---
    o.remove();
 
})(app.properties.selection);
 

• Original discussion: forums.adobe.com/message/9218732#9218732

Converting a “Quasi Rectangle” into a Rectangle

Preamble on InDesign Rectangles

In InDesign, a regular rectangle is a closed path that satisfies—among others—the following properties:
1. Its first path point (index 0) is in TOP-LEFT position.
2. Its path points are counterclockwise oriented, so the last one (index 3) is TOP-RIGHT.

Note. — Position and orientation here expressed in the ruler perspective and assuming no angular transformation is involved.

But InDesign's regular rectangle is one thing, while rectangular shapes you may produce defining your own path points (or via entirePath) are another thing. Given the path points of a regular polygon, you can both reverse their order (reversed path) or apply any cycle to the sequence. You then get a custom shape that perfectly fits the original although no longer in sync with InDesign's default object.

Given a rectangle which may result from either a native tool or a computed sequence of coordinates, you can make no assumption about the actual location of a point just considering its index in the path. Such relationship is purely conventional in the default case, and not reliable at all in the general case.

As an exercise, suppose you have to identify the TOP-LEFT corner of a polygon that happens to be a rectangle along the ruler axes. Here is a very simple and agnostic approach: just find the point having the minimal X and the minimal Y coordinate (in the ruler system.) This can be done in looking for the PathPoint index where X+Y meets a minimum:

function topLeftIndex(/*xy[]*/a,  m,i,t,r)
// -------------------------------------
// Get the index of the TOP-LEFT path point, `a`
// being the entire path of coords (in ruler space.)
// REM: This code still works if the shape is
// flipped or undergoes a ±90° rotation.
{  
    for(
       m=1/0, i=a.length ;
       i-- ;
       m > (t=a[i][0]+a[i][1]) && (m=t, r=i)
    );
    return r;
}
 
var ep = myRectangle.paths[0].entirePath,
    iTopLeft = topLeftIndex(ep);
 
// etc.
 

Application: Fixing a “Quasi Rectangle”

Now let's go back to the original question asked by Liphou in the scripting forum. Let's call a quasi rectangle a shape like this:

An altered rectangle having extra points we want to detect.

Black points are OK (as they form a pure rectangle,) but the shape contains three parasitic points (in red) that the user wants to identify and remove. This undesired arrow may be anywhere relative to the rectangular area, inside or outside of it, and on any of the four edge (top, left, bottom, right.)

So our goal is as follows: Given a quasi rectangle, how to isolate the extra points and remove them? In terms of path point sequence, the problem is to determine from which index the rectangle is altered. In the figure below, this is to find the point A:

We can find A from a basic assumption on vectors.

As you can see, the equality AB = DC (in vector terms) looks like a very relevant condition for our purpose. Once A is determined (this is an index i in the path), we just have to remove points i-1, i-2, i-3 (modulo 7). More generally, if the quasi rectangle is a polygon based on 4+x points (where x represents the number of extra points), we'll need to remove items i-1, i-2i-x (modulo 4+x).

Which leads to the following algorithm:

function removeArrow(/*Polygon*/poly,  pp,ep,n,i,p,q,a,m,t,r)
{
    const mABS = Math.abs;
 
    ep = (pp=poly.paths[0].pathPoints).everyItem().anchor;
    n = ep.length;
 
    if( n <= 4 )
    {
       throw "Invalid polygon. Should have at least 5 points."
    }
 
    // Vector sequence (avoid later recalculations.)
    // ---
    for( a=[], i=-1 ; ++i < n ; )
    {
        p = ep[i];
        q = ep[(1+i)%n];
        a[i] = [ q[0]-p[0], q[1]-p[1] ];
    }
 
    // Find p[i], q[i+2]  s.t |p + q| minimal.
    // ---
    for( m=1/0, i=-1 ; ++i < n ; )
    {
        p = a[i];
        q = a[(2+i)%n];
        t = mABS(p[0]+q[0])+mABS(p[1]+q[1]);
        t < m && ( m=t, r=i );
    }
 
    // Remove points r-1, r-2... (modulo n.)
    // ---
    for( t=n-4 ; t-- ; pp[r=-1+(r||n)].remove(), --n );
}
 
var poly = app.selection[0];
removeArrow(poly);
 

• Original discussion: forums.adobe.com/message/9648722#9648722

The Danger of Changing Bounds Before Resolving

Simon pointed an interesting issue that we can put this way: let myFrame be a specifier —e.g. myFrame = doc.pages[0].textFrames[1]— which you didn't access before now. Technically, myFrame is not resolved yet, in that it just refers to an abstract path. The underlying InDesign component (if any) hasn't been invoked. Now suppose you change the geometric bounds of another object in a way that makes it suddenly belong to the page doc.pages[0]. What will myFrame become? Can we still use it as a trustable reference?

No, we cannot! Let's take an example to illustrate the point:

Initial setup.

We have a single-page document, three text frames stacked as shown in the Layers panel, HEAD and FOOT being already positioned in the page while EXTRA stands outside of it, in the spread area. So the following declarations faithfully reflect the initial configuration:

var doc = app.activeDocument,
    pageFrames = doc.pages[0].textFrames,
    // ---
    head = pageFrames[0],
    foot = pageFrames[1],
    // ---  
    extra = doc.textFrames.itemByName('EXTRA');
 
// ...
 

Now suppose we reposition EXTRA before selecting foot:

// ...
 
extra.geometricBounds = [100,-100,150,20];
 
foot.select(); // really?
 

Did you guess which block is actually selected?

And the winner is:

The —unexpected!— result of foot.select()

Why does foot.select() cause EXTRA to be finally selected?!? Because EXTRA now belongs to the page (due to geometricBounds reset), and the foot specifier hasn't been resolved before our move. Syntactically, foot still refers to pageFrames[1] (as originally declared) which now points out to EXTRA (while FOOT has now index 2, with respect to layer ordering.)

Specifiers are resolved only when you hit them through a DOM command, such as select(). This can be shown using this fix:

// ...
foot.select(); // => FOOT is selected and now *resolved*.
 
extra.geometricBounds = [100,-100,150,20];  
 
foot.select(); // => FOOT remains selected.
 

The fact of resolving foot before moving extra into the page magically changes the outcome:

Expected outcome, thanks to specifier resolution.

• Original discussion: forums.adobe.com/message/9345248#9345248

• See also: “On everyItem() — Part 1 (about InDesign 'specifiers')

Dealing with Overridden Margin Preferences

BRVDL wrote: “In a script I'm making, I'm trying to adjust all margins on all pages and masterSpreads. This adjustment is a formula based on its current marginPreferences so the outcome differs for every page. The problem is that when I loop through all pages and adjust their marginPreferences, their margins get detached from their respective masterSpreads. (…) I need to loop through the pages that have marginPreferences already overridden by the user and adjust those margins while leaving the other pages alone. However, I can't find a way to check if a page has its marginPreferences overridden.”

Very interesting question indeed! I won't pretend to provide a full answer. As far as I know, the Scripting DOM does not directly tell us whether myPage's margin preferences actually override its master page settings. That's a missing tool in the box, I'm afraid. So we likely have to implement our own method.

Here is my reasoning. First, if a page P and its master page M do not share the same margin preferences, then we can instantly conclude that P has its preferences overridden. Otherwise, we can't make the decision from just inspecting the attributes—they match!—so we have to check whether they are actually linked. To do so, I suggest we temporarily change a parameter, say columnGutter, at the master level and test if P's gutter undergoes the same change. (Of course we have to restore the original parameter before returning.)

  1. From this, we need a way to compare margin preferences, that is, to decide whether the relevant parameters match. The toSource() method is not a option, because it both adds irrelevant data for our purpose, and does not always reveal the relevant ones. So I've designed a custom marginPrefTrace() method that can be injected in Page.prototype. It returns a string that somehow reflects the margin prefs and can be compared against another trace.

  2. In addition —although this appears to be an ancillary issue— we need to get access to the master page of a page. I'm not sure this issue has been already discussed in this forum, but there is no masterPage property, only masterSpread (i.e appliedMaster.) So the problem is to find the corresponding index of P in the collection P.masterSpread.pages. SDK developers have a tool for that (IMasterPage::GetMasterSpreadPageIndex), and “the calculation depends on the index position (0, 1, 2, ...) of the page within its spread and the document set-up” (InDesign CC Programing Guide, Vol 1.) But a generic algorithm hasn't be made available to scripters. Below is an experimental one, Page.prototype.getMasterPage(), which I've not tested in every circumstance and should then be considered a starting point only.

  3. Putting all this together, I finally suggest a Page.prototype.overriddenMargins() method —returning either true, false, or undefined— which implements the approach discussed above.

Page.prototype.marginPrefTrace = function F(  o,k,t,a)
// -------------------------------------
// Return a string that reflects the margin
// preferences of this page.
{
    o = F.Q||(F.Q={
        bottom:           'toString',
        left:             'toString',
        right:            'toString',
        top:              'toString',
        columnGutter:     'toString',
        // ---
        columnCount:      'valueOf',
        customColumns:    'valueOf',
        columnDirection:  'valueOf',
        // ---
        columnsPositions: 'toSource',
        });
 
    t = this.marginPreferences.properties;
    a = [];
    for( k in o )
        o.hasOwnProperty(k) &&
        a.push(t[k][o[k]]());
 
    return a.join(',');
};
 
Page.prototype.getMasterPage = function(  m,n,s)
// -------------------------------------
// Return the master page of this page,
// or NULL if no master applied.
{
    m = this.appliedMaster;
    if( !m || !m.isValid ) return null;
 
    n = (m=m.pages).length;
 
    return m[
        2 < n || +PageSideOptions.SINGLE_SIDED==(s=+this.side) ?
        ( this.index % n ) :
        +(s==+PageSideOptions.RIGHT_HAND)
        ];
};
 
Page.prototype.overriddenMargins = function(  r,m,t,v)
// -------------------------------------
// Tell whether this page has overridden
// margin prefs relative to its master page.
// => TRUE | FALSE | undefined
{
    if( !(m=this.getMasterPage()) )
        return;
 
    // 1. Simple case (distinct traces.)
    // ---
    if( m.marginPrefTrace() != this.marginPrefTrace() )
        return true;
 
    // 2. Difficult case (matching traces.)
    // ---
    v = (t=m.marginPreferences).columnGutter;
    t.columnGutter = 1+v;  // change
    r = m.marginPrefTrace() != this.marginPrefTrace();
    t.columnGutter = v;    // restore
 
    return r;
};
 
 
// =====================================
// Test
// =====================================
 
var doc = app.properties.activeDocument,
    pages = doc && doc.pages.everyItem().getElements(),
    r = [],
    t;
 
for(
     i=pages.length ;
     i-- ;
     r[i] = (t=pages.pop()).name + ': ' + t.overriddenMargins()
   );
 
if( r.length ) alert( r.join('\r') );
 

• Original discussion: forums.adobe.com/message/9239715#9239715


3/ Miscellaneous Notes

Note on the count() Method Attached to a Collection

InDesign implements the concept of collectionPageItems, Graphics, Spreads, and many more— and we can access a collection from various specifiers, including from a plural syntax, as in the following assignment:
myItems = doc.masterSpreads.everyItem().pageItems;

Keep in mind that myItems is an object (typeof myItems == "object") whose actual class is PageItems. In other words, (myItems instanceof PageItems) == true. And we know that PageItems is a collection, not an array. That kind of object exposes a length property which normally returns the number of underlying elements. Thus, myCollection.length sounds equivalent to myCollection.count().

What is interesting here? We formed a PageItems (single) object from an everyItem() command, which is known to make up a plural specifier. Usually, adding a property after a plural specifier creates an array. So one could have expected ...everyItem().pageItems to be an “array of PageItems collections,” which it is not. Furthermore ...everyItem().pageItems is not a specifier either! We might call it an “incomplete specifier” in the sense that it just opens a branch in the path and still waits for something next to finalize an actual specifier.

Note. — A way to prove that myItems (above) is not a specifier —apart from the fact that it is a collection!— is checking the unavailability of myItems.toSpecifier(). This would cause a runtime error.

But there is something more intriguing: myItems.toSource() results in the string 'resolve("/document[@id=1]/master-spread/page-item")', which exactly describes the path that myItems.everyItem().toSpecifier() would have shown. Also, myItems.count() returns the correct number of items, the one we would get using the complete syntax. So everything happens as if myItems (as a PageItems collection) already integrates the effect of doc.masterSpreads.everyItem() used upstream.

Then Dirk Becker adds to this discussion a crucial remark:

“While myItems claims to be an [object PageItems], it actually targets a multitude of collections (…). To me this is a distinctive type by its behaviour. This works along the same principle as when you access one property via the everyItem() of a collection — that also yields an array of the property values, one per individually targeted object.”

So, there is a sneaky mistake in the assumption that myCollection.length is equivalent to myCollection.count(). More precisely, it is not true that xyz.collection.count()===xyz.collection.length for every possible xyz specifier.

Indeed, given
xyz a plural specifier in the ...everyItem() form
and
collection a property (typically, pageItems) owned by xyz,

we discovered —thanks to Dirk and Uwe Laubender— that xyz.collection.count() is an Array of numbers [z0, z1, ..., zk], while xyz.collection.length is a single number N, such that N = Σ zk.

In this specific discussion we were talking about the entity doc.masterSpreads.everyItem().pageItems, whose type is PageItems. Applying .length to it returns the total number of underlying page items, while applying .count() returns an array that still discriminates the count of page items for each master spread.

In summary:

var doc = app.activeDocument,  
    entity = doc.masterSpreads.everyItem().pageItems;
 
alert( entity.toSource() );
// => resolve("/document[@id=1]/master-spread/page-item")
 
alert( entity.count() );
// => [z0,z1...]
 
alert( entity.length );
// => Σ(zk)  
 

• Original discussion (incidental notes): forums.adobe.com/message/9345792#9345792

RegExp: Combining Negative and Positive Lookahead

Being in the JavaScript Regular Expression space and considering strings of the form AA+BB+CC<DD+EE+FF>GG+HH+II<JJ+KK>LL+MM, we want to capture all the + signs outside of the <...> tags.

The regex /.\+(?![^<>]*>)/g (based on a single negative lookahead) would probably do the job, but in my original attempt I suggested something more complicated, combining negative and positive lookahead:

/\+(?![^<>]*>)(?=[^><+]*(?:[\+<]|$))/g

In fact, the positive part (to the right) only reinforces the assertion already done in the negative part (to the left), but it is still interesting to study how this regex works. Let's dissect it:

Combination of a negative and a positive lookahead.

First, the regex looks after a plus sign \+, then it needs to satisfy two lookahead assertions to validate that match. Keep in mind that an assertion is not supposed to consume further characters in the string (that is, the inner index of the RegExp engine doesn't move during theses validation steps), but the way assertions are designed deeply impacts how the whole regex works.

• In red, the NEGATIVE LOOKAHEAD (NL) assertion, (?!pattern), means that, from the current point, the embedded pattern MUST FAIL. In other words, if that pattern is found, then the condition is not satisfied and the plus sign under consideration won't be captured as a match (whatever the other assertion, which won't be tested at all.) Otherwise, the condition is satisfied and the other assertion is tested.

• In green, the POSITIVE LOOKAHEAD (PL) assertion, (?=pattern), means that, from the same current point (since the index has not moved), the embedded pattern MUST SUCCEED. If that pattern is found, all is fine and the plus sign under consideration is definitely a match. Otherwise, it is ignored.

So those two assertions work as a logical AND: (NL pattern must be KO) AND (PL pattern must be OK.)

The negative pattern prevents any plus sign nested in a <...> tag from being validated. This is done by testing the pattern [^<>]*>, which means “non-markup sign (zero or more times) then a closing mark.” This pattern can only succeed from within a tag as it needs to find a form "XXX>" where X is neither a "<" nor a ">".

Why should we need a positive pattern? We shouldn't, in fact! The previous condition is sufficient to assert that the plus sign under consideration is external from any tag. However, as I had no hint about extra-conditions or constraints the input string may undergo, I found it safer to positively define the pattern in which the plus sign is expected. So the PL looks after the pattern [^><+]*(?:[+<]|$), which is a complicate syntax for just saying "XXXY", where X is neither ">" nor "<" nor "+", and Y is either "+", or "<", or the end of the string ($). This explicitly describes any suffix string that must follow the plus sign under consideration.

The above animation shows how the entire regex dynamically operates:

Showing the regex in action.

• Original discussion: forums.adobe.com/message/9209877#9209877

Quick Note on ExtendScript Triple Quote Syntax

The triple quote syntax (ExtendScript specific, not available in regular JavaScript) allows to declare literal strings using either the form """xxx""" (based on triple double quotes) or '''xxx''' (based on triple single quotes.)

1. It supports all escape sequences available in JS:
\" \' \\ \n \r \b \t \v \f \xHH \uHHHH.

2. But those escape sequences aren't required — except \\ — if the character is entered as is. This way you can insert quotes and newlines without breaking the syntax. Hence you save readability with literals where characters would require to be escaped in normal JS declaration.

From 1 and 2 you can observe the following results:

// ---
// In the following comments,
// <TQ> represents a triple quote.
// ---
 
var s1 = """"test"""";   // <TQ>"test"<TQ>
 
var s2 = """\"test\""""; // <TQ>\"test\"<TQ>
 
// Escape was not needed:
// ---
alert( s1===s2 );        // => true
 
// Entering newlines:  
// ---
var s3 = """  
"test"  
""";  
 
alert( s3===s1 );        // => false  
 
// Indeed:
// ---
alert( s3.toSource() );  // (new String("\n\"test\"\n"))
 

• Original discussion (incidental notes): forums.adobe.com/message/8490942#8490942

Shortcut for Flattening a Two-Dimensional Array

Let a be either an Array of things or an Array of Arrays of things. Then a simple flattening trick is:

// One-line flattening routine:
// ---
a = [].concat.apply([],a);
 

OK, but why does this work?

1. The method Array.prototype.concat supports as many arguments as desired, and each of these arguments can be either an Array or a simple item to be added to the result. That's the very key point. (The normalized algorithm of concat is described in the specification ECMA-262:440.)

2. Then, given an array X and a set of arguments arg0, arg1…, argN whose each may indifferently be an array or a single item, the syntax X.concat(arg0, arg1..., argN) would return the full concatenation X(+)arg0(+)arg1(+)…(+)argN. And this always works whatever the array-ness of any arg.

3. Note also that in the above expression the caller object, X, can be the empty array []. Then the result would just be the concatenation arg0(+)arg1(+)…(+)argN.

4. Now the problem can be put as follows: let a be the array [arg0, arg1..., argN] which we don't know whether its elements are either arrays or simple elements. What would be great would be to write out the expression “[].concat(arg0, arg1…, argN)” since that would exactly return what we're looking after. But this expression is not literally available, because we only have a as an identifier, not as a developed set of arguments. So an additional trick is needed:

5. Given any function F and any array a, we can call F as a method from a context (obj) and supply A's elements as formal arguments, using the syntax F.apply(obj, a). In easier words, the apply scheme used on F emulates the expression “obj.F(arg0, arg1..., argN)” as if a's elements were taken as arguments. For deeper details on this point, see ECMA-262:352.

6. Thus, using the empty array [] as the calling context (obj) and Array.prototype.concat as the called function (F) we can now see what happens with Array.prototype.concat.apply([], a). It mimicks the literal expression seen in point 4 and therefore solves our problem as pictured in point 3.

7. Final refinement, the reference to the function Array.prototype.concat can be abbreviated [].concat, this is just a lazy shortcut pointing out to the prototyped method.

Hence the expression: [].concat.apply([], a)

• Original discussion: forums.adobe.com/message/9258588#9258588


GitHub page: github.com/indiscripts

Twitter stream: twitter.com/indiscripts

YouTube: youtube.com/user/IndiscriptsTV