1/ Dealing with PageItems and/or Text
    Remove Pasteboard Items (InDesign CS5+)
    Release all Anchored Objects at once
    Shuffle the Selected Paragraphs
    Identify Rotated Spreads
    Duplicate Items into a Different Document

2/ Miscellaneous
    Generic Menu Loader
    ScriptUI Collapsible Containers
    Convert an Image File to a Base64 String
    Use the JsZip library in ExtendScript
    List of Available Languages (and related secrets)
    Extending the Interface of DOM objects?

3/ Bugs, Issues, Workarounds
    Beware of String.search(RegExp)
    ESTK Data Browser may Fail in Displaying “Associative Arrays”
    Create a Tiny Page-Sized Document
    Invalid Constructor of Converted Polygons (CS5+)
    About Directly Affecting Paragraph Contents


1/ Dealing with PageItems and/or Text

Note. — People who are new to scripting may be confused by Text entities (Paragraph, Word, Line, and so on). A common mistake is to mix up Text (as an object) with the Text.contents property. Many beginners miss the fact that the Text interface wraps formatting properties and useful manipulation methods, such as duplicate() and move(). A number of scripts rely on a wrong approach although they sound to work properly in simple contexts. For this reason, some discussions I mention further may promote a bad snippet as a “Correct Answer” and ignore cleaner suggestions. Keep aware of this when you read the original discussion.

Remove Pasteboard Items (InDesign CS5+)

In InDesign CS5+, any top-level page item belongs to a Spread even if it is located on a page, so old implementations of find-page routines don't work anymore. Fortunately the DOM now offers a parentPage property which returns null if the object has no parent page (=does not hit any page). This can be used to make a script remove all pasteboard items.

Suggested code:

// =================
// Remove Pasteboard Items / InDesign CS5+
// =================
 
function removePasteboardItems(/*?Document*/doc)
//--------------------------------------
{
    if( !doc ) return;
 
    var items = doc.pageItems.everyItem().getElements(),
        t = null;
 
    while( t=items.pop() ) t.parentPage || removeItem(t);
 
    t = items = null;
}
 
function removeItem(/*PageItem*/item)
//--------------------------------------
{
    try {
        item.locked = false;
        item.remove();
        }
    catch(_){}
}
 
removePasteboardItems(app.documents.length && app.activeDocument);
 

• Original discussion: http://forums.adobe.com/message/3868351#3868351
• Older implementation for InDesign CS3/CS4: “Clean up your Pasteboard!”

Release all Anchored Objects at once

“I have a file for 4 pages which is containing more than 150 images to be released from anchored [objects]. Those images should be placed in the same place. Only need to release…”

Suggested code:

var a = app.activeDocument.allPageItems,
    t;
 
while( t = a.pop() )
    {
    t.isValid &&
    t.hasOwnProperty('anchoredObjectSettings') &&
    (t.parent instanceof Character) &&
    (t=t.anchoredObjectSettings).isValid &&
    t.releaseAnchoredObject();
    }
 

• Original discussion: http://forums.adobe.com/message/4062574#4062574

Shuffle the Selected Paragraphs

It may be fun to shuffle the selected paragraphs in a random order.

Suggested code:

var mRAND = Math.random,
    LOCS = [LocationOptions.AFTER, LocationOptions.BEFORE];
 
function shuffleSelectedParagraphs()
//--------------------------------------
// The user must select the paragraphs before running the script
{
    var s = app.selection && app.selection[0],
        ps = s && s.hasOwnProperty('paragraphs') && s.paragraphs,
        n = ps && ps.length,
        i = n, j, k, d;
 
    if( n < 2 )
        throw Error("Please select at least two paragraphs.");
 
    var sto = ps[0].parentStory,
        ip0 = ps[0].insertionPoints.firstItem().index,
        ip1 = ps[n-1].insertionPoints.lastItem().index,
        EOF = ps[n-1].characters.lastItem().contents != '\r';
 
    if( s = s.hasOwnProperty('appliedFont') ) app.selection = [];
 
    if( EOF ) ps[n-1].insertionPoints.lastItem().contents = '\r';
 
    while( i-- )
        {
        do{ j = ~~(n*mRAND()) }while( i==j )
        k = ~~(2*mRAND());
        if( 2*k-1 != j-i ) ps[i].move(LOCS[k],ps[j]);
        }
 
    if( EOF ) sto.characters.lastItem().contents = '';
 
    if( !s ) return;
    sto.insertionPoints[ip0].select();
    sto.insertionPoints[ip1].select(SelectionOptions.ADD_TO);
}
 
try{ shuffleSelectedParagraphs() }catch(_){alert(_.message);}
 

• Original discussion: http://forums.adobe.com/message/3834317#3834317

Identify Rotated Spreads

Five months ago I posted in the forum a ridiculously tiny function which checks whether a spread view is rotated and returns the corresponding rotation angle. Although my code has not been rated—other solutions were posted first—I think using the spread coordinate space is the cleanest and the safest approach.

Suggested code:

function getSpreadRotation(/*Page|Spread|MasterSpread*/ps)
//--------------------------------------
//   0 => NORMAL , -90 => CW, +90 => CCW, 180 => REVERSED
{
    return ps.
      transformValuesOf(CoordinateSpaces.pasteboardCoordinates)[0].
      counterclockwiseRotationAngle;
}
 
 
// Test
// ---
var mySpread = app.activeDocument.spreads[0];
alert( getSpreadRotation(mySpread) );
 

• Original discussion: http://forums.adobe.com/message/3831188#3831188

• See also: “Dealing with Rotated Spread Views in a Script”, which explains the trick but provides a different routine.

Duplicate Items into a Different Document

If you want to duplicate a collection of page items from a document (or layer) to another one, you don't need to group them first. Simply use PageItems.everyItem().duplicate(...).

Suggested code:

var sourceLayer = app.documents[0].layers.itemByName("Layer1");
var destLayer = app.documents[1].layers[0];
 
sourceLayer.pageItems.everyItem().duplicate(destLayer);
 

• Original discussion: http://forums.adobe.com/message/4050457#4050457

2/ Miscellaneous

Generic Menu Loader

Generic Menu Loader for InDesign CS4/CS5+.

I have recently posted in the forum a complete Menu Loader which—to my humble opinion—can help scripters to easily manage custom menus and menu actions based on a set of scripts. The code supports submenus (one level) and menu separators. You just need to provide the menu captions, the related script file paths, and optionally the submenu names, all in a single object (FEATURES). E.g.:

FEATURES = [
   { caption: "My Menu Item 1", fileName: 'File-One.jsx', subName: "" },
   { separator: true, subName: "" },
   { caption: "My Menu Item 2", fileName: 'File-Two.jsx', subName: "My Sub Menu" },
   { separator: true, subName: "My Sub Menu" },
   { caption: "My Menu Item 3", fileName: 'File-Three.jsx', subName: "My Sub Menu" }
]
 

The Menu Loader also checks whether the underlying scripts exist. Of course it can be used as a startup script. Here is the whole code:

// Custom Menu Loader for InDesign CS4/CS5+
// (#targetengine is not required)
 
(function()
// -------------------------------------
// Install and/or update the menu/submenu and connect
// the corresponding menu actions if script files are available
{
    // Settings and constants
    // ---
    var MENU_NAME = "My Test Menu",
        FEATURES = [
            { caption: "My Menu Item 1", fileName: "File-One.jsx", subName: "" },
            { separator: true, subName: "" },
            { caption: "My Menu Item 2", fileName: "File-Two.jsx", subName: "My Sub Menu" },
            { separator: true, subName: "My Sub Menu" },
            { caption: "My Menu Item 3", fileName: "File-Three.jsx", subName: "My Sub Menu" }
            ],
        LO_END = LocationOptions.atEnd,
        INDESIGN_ROOT_MENU = app.menus.item( '$ID/Main' ),
        FEATURE_LOCATION_PATH = (function()
            {
            var f;
            try{ f=app.activeScript; }
            catch(_){ f=File(_.fileName); }
            return f.parent.parent + '/';
            })();
 
    // (Re)set the actions
    // Note: checks also whether script files are available
    // ---
    var t, f,
        i = FEATURES.length;
    while( i-- )
        {
        t = FEATURES[i];
        if( t.separator ) continue;
 
        if( (f=File(FEATURE_LOCATION_PATH + t.fileName)).exists )
            {
            // The script file exists => create the corresponding action
            // and directly attach the event listener to the file
            // (no need to use app.doScript(...) here)
            // ---
            (t.action = app.scriptMenuActions.add( t.caption )).
                addEventListener('onInvoke', f);
            }
        else
            {
            // The script file does not exist => remove that feature
            // ---
            FEATURES.splice(i,1);
            }
        }
 
    // ---
    // Create/reset the custom menu container *if necessary*
    // Note:  menus/submenus are application-persistent
    // ---
    var mnu = INDESIGN_ROOT_MENU.submenus.itemByName( MENU_NAME );
    if( !mnu.isValid )
        {
        // Our custom menu hasn't been created yet
        // ---
        if( !FEATURES.length ) return;
        mnu = INDESIGN_ROOT_MENU.submenus.add(
            MENU_NAME,
            LocationOptions.after,
            INDESIGN_ROOT_MENU.submenus.item( '$ID/&Window' )
            );
        }
    else
        {
        // Our custom menu already exists, but we must clear
        // any sub element in order to rebuild a fresh structure
        // ---
        mnu.menuElements.everyItem().remove();
 
        // If FEATURES is empty, remove the menu itself
        // ---
        if( !FEATURES.length ){ mnu.remove(); return; }
        }
 
    // ---
    // Now, let's fill mnu with respect to FEATURES' order
    // (Possible submenus are specified in .subName and created on the fly)
    // ---
    var s,
        n = FEATURES.length,
        subs = {},
        sub = null;
    for( i=0 ; i < n ; ++i )
        {
        t = FEATURES[i];
 
        // Target the desired submenu
        // ---
        sub = (s=t.subName) ?
            ( subs[s] || (subs[s]=mnu.submenus.add( s, LO_END )) ) :
            mnu;
 
        // Connect the related action OR create a separator
        // ---
        if( t.separator )
            sub.menuSeparators.add( LO_END);
        else
            sub.menuItems.add( t.action, LO_END );
        }
})();
 

• Original discussion: http://forums.adobe.com/message/4089126#4089126
• See also: “How to Create your Own InDesign Menus”

ScriptUI Collapsible Containers

Want to make your UI window able to collapse/hide widgets? Let's try a new approach using the maximumSize property and the layout manager.

Suggested code:

var NULL_SIZE = [0,0],
    MAX_SIZE = [1000,1000];
 
var u,
    w = new Window('dialog', 'test'),
    p = w.add('panel'),
    s1 = p.add('statictext', u, "This is a static text"),
    // collapsible group
    g = p.add('group'),
    e = g.add('edittext', u, "Edit your text..."),
    r = g.add('checkbox', u, "Blablablabla"),
    // ---
    b = w.add('button', u, 'Toggle');
 
g.orientation = 'column';
 
// Initial state : hidden
// ---
g.visible = false;
g.maximumSize = NULL_SIZE;
 
// Toggles container's visibility
// ---
b.onClick = function()
{
    g.maximumSize = (g.visible ^=1) ? MAX_SIZE : NULL_SIZE;
    w.layout.layout(true);
};
 
w.show();
 

• Original discussion: http://forums.adobe.com/message/3708111#3708111

Convert an Image File to a Base64 String

Although ExtendScript does not offer a direct way to encode/decode Base64 data, you can easily implement your own function(s). This allows you to store an image (or any other file) as a Base64 string which you will more easily transport in your script.

Suggested code:

var base64Encode = function(/*str*/s)
//--------------------------------------
{
    var ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'+
        'abcdefghijklmnopqrstuvwxyz0123456789+/=';
 
    var n = s.length,
        a = [], z = 0, c = 0,
        b, b0, b1, b2;
 
    while( c < n )
        {
        b0 = s.charCodeAt(c++);
        b1 = s.charCodeAt(c++);
        b2 = s.charCodeAt(c++);
 
        var b = (b0 << 16) + ((b1 || 0) << 8) + (b2 || 0);
 
        a[z++] = ALPHA.charAt((b & (63 << 18)) >> 18);
        a[z++] = ALPHA.charAt((b & (63 << 12)) >> 12);
        a[z++] = ALPHA.charAt(isNaN(b1) ? 64 : ((b & (63 << 6)) >> 6));
        a[z++] = ALPHA.charAt(isNaN(b2) ? 64 : (b & 63));
        }
 
    s = a.join('');
    a.length = 0;
    a = null;
    return s;
};
 
var fileToBase64 = function(/*File|str*/f)
//--------------------------------------
{
    var s = null;
 
    if( f && (f = new File(f)) && (f.encoding='BINARY') && f.open('r') )
        {
        s = f.read();
        f.close();
        }
 
    return s && base64Encode(s);
};
 
 
// Client code
// ---
var b64 = fileToBase64("my/path/to/image.jpg");
 

• Original discussion: http://forums.adobe.com/message/4050443#4050443

Use the JsZip library in ExtendScript

How to create a pure ZIP-package from an InDesign script? The JavaScript JsZip library is not very fast when wrapped in ExtendScript, but it works provided a slight fix to the JSZip.prototype.addExistingFile method.

Suggested code:

#include 'jszip/jszip.js'
 
JSZip.prototype.addExistingFile = function(/*File*/f, newName)
//--------------------------------------
{
var contents = false;
 
if( f.constructor == File && f.exists && (f.encoding = 'BINARY') && f.open('r') )
    {
    contents = f.read();
    f.close();
    return this.add(
        newName||f.name,
        contents,
        {binary: true, date: f.modified}
        );
    }
throw new Error("Unable to open the file "+f);
}
 
 
// Sample Client Code
// ---
 
/*
var folder = app.activeScript.parent;
var zip = new JSZip();
zip.add("Hello.txt", "Hello World\n");
 
zip.folder("images").
     addExistingFile(File(folder+"/test.jpg"));
 
var content = zip.generate(true); // asBytes
 
var f = File(folder+'/test.zip');
f.encoding = 'BINARY';
if( f.open('w') )
     {
     f.write(content);
     f.close();
     }
*/
 

• Original discussion: http://forums.adobe.com/message/3750810#3750810

List of Available Languages (and related secrets)

Dealing with document's Languages and application's LanguagesWithVendors collections is a complicated topic, because when we set myText.appliedLanguage with a direct String, a localized language name is expected—and myText.appliedLanguage.name returns that localized language name—whereas the languagesWithVendors.itemByName() and languages.itemByName() methods seem to expect the English key of the language. Nevertheless this is not exactly what it happens. For example, the following code will work:

// THIS WILL WORK:
myText.appliedLanguage = app.languagesWithVendors.
    itemByName("German: Swiss");
 

but the following one will not:

// THIS WON'T WORK:
myText.appliedLanguage = app.languagesWithVendors.
    itemByName("German: Swiss 2006 Reform");
 

Now, if we try:

myText.appliedLanguage = app.languagesWithVendors.
    itemByName("de_CH_2006");
 

we get the expected result. How to explain this? Under the hood, the itemByName() method uses an internal translated key string. It mutely appends the $ID/ prefix to the supplied argument, so the above statement is equivalent to:

myText.appliedLanguage = "$ID/de_CH_2006";
 

which by the way also works!

Hence, the whole problem is to retrieve the secret key strings used by InDesign to manage language names. Here is the trick:

var a = app.languagesWithVendors.everyItem().name,
    i = a.length;
while( i-- ) a[i] = a[i] + " => " + app.findKeyStrings(a[i]).join('  OR  ');
alert( a.sort().join('\r') );
 
/* RESULT:
Bulgarian => $ID/Bulgarian  OR  $ID/IDX_Bulgarian
Catalan => $ID/Catalan
Croatian => $ID/Croatian  OR  $ID/IDX_Croatian
Czech => $ID/Czech  OR  $ID/IDX_Czech
Danish => $ID/Danish
Dutch: 2005 Reform => $ID/nl_NL_2005
Dutch: Old Rules => $ID/Dutch
English: Canadian => $ID/English: Canadian
English: UK => $ID/English: UK
English: USA => $ID/English: USA
English: USA Legal => $ID/English: USA Legal
English: USA Medical => $ID/English: USA Medical
Estonian => $ID/IDX_Estonian  OR  $ID/Estonian
Finnish => $ID/Finnish
French => $ID/French
French: Canadian => $ID/French: Canadian
German: 1996 Reform => $ID/German: Reformed
German: 2006 Reform => $ID/de_DE_2006
German: Old Rules => $ID/German: Traditional
German: Swiss 2006 Reform => $ID/de_CH_2006
German: Swiss => $ID/German: Swiss
Greek => $ID/kWRIndexGroup_GreekAlphabet  OR  $ID/Greek  OR  $ID/Greek Mode
Hungarian => $ID/Hungarian  OR  $ID/IDX_Hungarian
Italian => $ID/Italian
Latvian => $ID/IDX_Latvian  OR  $ID/Latvian
Lithuanian => $ID/IDX_Lithuanian  OR  $ID/Lithuanian
Norwegian: Bokmål => $ID/Norwegian: Bokmal
Norwegian: Nynorsk => $ID/Norwegian: Nynorsk
Polish => $ID/Polish  OR  $ID/IDX_Polish
Portuguese => $ID/Portuguese
Portuguese: Brazilian => $ID/Portuguese: Brazilian
Romanian => $ID/IDX_Romanian  OR  $ID/Romanian
Russian => $ID/Russian  OR  $ID/IDX_Russian
Slovak => $ID/Slovak  OR  $ID/IDX_Slovak
Slovenian => $ID/Slovenian  OR  $ID/IDX_Slovenian
Spanish => $ID/IDX_Spanish  OR  $ID/Spanish: Castilian  OR  $ID/Spanish
Swedish => $ID/Swedish
Turkish => $ID/IDX_Turkish  OR  $ID/Turkish
Ukrainian => $ID/IDX_Ukrainian  OR  $ID/Ukrainian
[No Language] => $ID/[No Language]
*/
 

• Original discussion: http://forums.adobe.com/message/4073509#4073509

Extending the Interface of DOM objects?

In the Scripting DOM, InDesign proxy objects are just syntactic specifiers that encapsulate commands, they don't actually offer read-and-write access to the final InDesign components. For example, what we call an 'instance' of the Cell object just represents a command, which may or may not point out to one, or several, actual InDesign cell(s). So when we extend the prototype of such object, we simply decorate the interface of that command, but this has no impact on actual document cells:

Cell.prototype.myProperty = 1;
Cell.prototype.myMethod = function(){ alert('foo'); };
 
// individual specifier:
alert( myTable.cells[0].myProperty );    // => 1
 
// collective specifier:
alert( myTable.cells.everyItem().myProperty );    // => 1!
 
// individual specifier:
myTable.cells[0].myMethod();    // => 'foo'
 
// collective specifier:
myTable.cells.everyItem().myMethod();    // => 'foo' (once!)
 

In the example above, myProperty and myMethod() do not extend cell's interface in terms of component's interface. This syntactically works as long as you don't attempt to write data within the real cell component (which you cannot extend):

myTable.cells[0].myProperty = 2;
// => ERROR: myProperty is not supported!
 

That said, ExtendScript provide a way to store data in most of the components through the label property.

• Original discussion: http://forums.adobe.com/message/3835649#3835649
• Similar discussion: “How to save/retrive custom properties to a PageItem”
• See also: “On everyItem()”

3/ Bugs, Issues, Workarounds

Beware of String.search(RegExp)

According to ECMA-262, String.prototype.search(RegExp) shouldn't change the lastIndex property of the regexp. But ExtendScript does not apply this rule, so we have a very unexpected behavior as shown in the the following example:

var reg = /bar/;
alert( 'foobar'.search(reg) );    // => 3
alert( 'foobar'.search(reg) );    // => -1 !
 

The second search(...) fails because reg.lastIndex has been modified during the first call. This bug (?) may be really harmful If you're used to 'precompile' your regex for optimization purpose:

var reg = /bar/;
var myString;
 
while( . . . )
    {
    . . .
 
    myString.search(reg);    // will only work the first time!
 
    . . .
    }
 

You can fix this issue by inserting reg.lastIndex = 0; before each call to the search() method.

• Original discussion: http://forums.adobe.com/message/3719879#3719879

ESTK Data Browser may Fail in Displaying “Associative Arrays”

The ExtendScript ToolKit data browser has a problem in displaying object properties whose name starts with a (recurring) digit. It seems that the bug only occurs with digits greater than 1.

Compare:

var obj = {
    "0a": null,
    "00b": null,
    "1a": null,
    "11b": null
    };
 
// The data browser properly displays all obj properties
 

and:

var obj = {
    "2a": null,
    "22b": null,
    "5a": null,
    "55b": null
    };
 
// The data browser only displays obj['5a'] and obj['55b']!
 

Note. — This is not an internal ExtendScript bug. In all cases the object is properly set, the for...in loop works fine and obj.__count__ returns the correct number of enumerable properties.

• Original discussion: http://forums.adobe.com/message/3745176#3745176

Create a Tiny Page-Sized Document

In InDesign CS4/CS5+, the minimum document page size is 1pt × 1pt. However, setting such tiny values through scripting can fail because we have to inhibit the default margin preferences. The problem is that the Document.marginPreferences property does not work during document construction—I think this is a bug. So the following code fails:

// DOES NOT WORK -- BUG?
app.documents.add(true, undefined,
    {
    marginPreferences: {
        top:0, left:0, bottom:0, right:0,
        columnGutter:0, columnCount:1
        },
    documentPreferences: {
        pageWidth: '1pt', pageHeight: '1pt'
        }
    });
 

A workaround is to temporarily change the Application margin preferences:

// Backup the default app margin prefs
// ---
var bkp = app.marginPreferences.properties;
 
// Change the margin prefs to allow small document size
// ---
app.marginPreferences.properties = {
     top:0, left:0, bottom:0, right:0,
     columnGutter:0, columnCount:1
     };
 
// Create a 'minimal' document
var doc = app.documents.add(true, undefined,
     {documentPreferences: {pageWidth:'1pt', pageHeight:'1pt'}}
     );
 
// Restore the default app margin prefs
// ---
app.marginPreferences.properties = bkp;
 

• Original discussion: http://forums.adobe.com/message/3868599#3868599

Invalid Constructor of Converted Polygons (CS5+)

Sometimes a rectangle converted into an oval is considered a Rectangle, sometimes it is considered an Oval! This bug, originally reported by Keith Gilbert, seems specific to InDesign CS5/CS5.5. We can reproduce it in a systematic way using the following code:

var doc = app.activeDocument,
    obj = doc.rectangles.add(),
    objId = obj.id;
 
obj.convertShape(ConvertShapeOptions.CONVERT_TO_OVAL);
 
alert([
    "Rectangles: " +
        doc.rectangles.length,
    "Ovals: " +
        doc.ovals.length,
    "Current object: " +
        doc.pageItems.itemByID(objId).getElements()[0].constructor.name
    ].join('\r'));
 
// CS4 =>
//   Rectangles: 0
//   Ovals: 1
//   Current object: Oval
 
// CS5+ =>
//   Rectangles: 1
//   Ovals: 0
//   Current object: Rectangle
 

I am not sure if it is a good idea to search for a workaround, but here is the hack I suggest:

function isOval(/*PageItem*/o)
//--------------------------------------
// Returns TRUE if o is 'actually' an oval
// despite wrong cast -- Hack for ID CS5+
{
    var t, r;
 
    if( o instanceof Oval ) return true;
 
    if( (t=o.paths).length > 1 ) return false;
 
    t = ''+t[0].entirePath;
 
    o.convertShape(ConvertShapeOptions.CONVERT_TO_OVAL);
    r = t==(''+o.paths[0].entirePath);
 
    t = o.toSpecifier();
    resolve(t.substr(0,t.indexOf('//'))).undo();
 
    return r;
}
 
// Sample code
// ---
alert( isOval(app.selection[0]) );
 

• Original discussion: http://forums.adobe.com/message/4074747#4074747

About Directly Affecting Paragraph Contents

Considering only the “plain text” level, the statement:

myParagraph.contents = "XXX" + myParagraph.contents;
 

may lead to a correct output—although this is definitely not the right way to operate—but the code above is problematic in the perspective of the JavaScript object (myParagraph) which actually is a Text specifier. It is superficially encoded as a path in the DOM, and internally resolved (each time we send a command) as a range of character indexes within the parent story. To realize this, let's study the following code:

// Declares myParagraph, e.g.:
// ---
var myParagraph = app.activeDocument.stories[0].paragraphs[1];
 
// Displays the 'unresolved' path:
// ---
alert( myParagraph.toSpecifier() );
// => sth like: /document[@id=1]/story[0]/paragraph[1]
 
// Resolves myParagraph *now* and displays the underlying path:
// ---
alert( myParagraph.getElements()[0].toSpecifier() );
// => sth like: (.../character[4] to .../character[7])
 

The problem in directly affecting the Paragraph.contents property is that this mutely resolves the specifier at the moment we are sending the command, then the character range is replaced by something different—a string that may contain more characters than the allocated range, a string that may or may not contain multiple "\r", etc.—while the specifier itself, myParagraph, is not intrinsically updated in the sense that it still points out to the original, unresolved path. Under some circumstances this may lead to very weird issues.

Another point is that we have no clue about how and when the contents property is internally updated. There are many reasons to suppose that a cache is used, which may backup the string in the JS object just after we read the text. Anyway, I don't think that the = operator works here as we could expect to. A statement like: myParagraph.contents = "XXX" + myParagraph.contents first evaluates the right-sided value—which resolves the specifier and hits the contents—then a command is sent to InDesign that changes the text but does not seem to re-affect the contents property of the JS object:

var myParagraph = app.activeDocument.stories[0].paragraphs[1];
 
// Changes the contents (the wrong way)
// ---
myParagraph.contents = "XXX" + myParagraph.contents;
 
// Attempt to display the contents property
// ---
alert( myParagraph.contents );
// => you will generaly obtain an out-of-sync string
 

Interestingly, you will also observe that myParagraph.index is out of sync after the change. The character index has been shifted by "XXX".length, which should not be the case if the properties of the Paragraph were properly updated.

But once again we have to remember that the Paragraph object is a fake entity—basically a Text object—that addresses an index range within the story. So each time we change the story contents upstream of the paragraph or at the paragraph level, we need to resolve again the original specifier—provided that it is still meaningful—if we want to access to fresh properties. We can do that through myParagraph = myParagraph.getElements()[0] since this recalculates the text offsets from the original path (/document[@id=1]/story[0]/paragraph[1]) but this seems a bad practice to me in this particular case, because myParagraph is then attached to an immutable character range.

A better way to quickly re-resolve any Text entity without re-affecting the specifier is to use the underestimated .texts[0] shortcut:

var myParagraph = app.activeDocument.stories[0].paragraphs[1];
 
// Changes the contents (the wrong way)
// ---
myParagraph.contents = "XXXX" + myParagraph.contents;
 
// Accessing the property
// ---
alert( myParagraph.contents );            // WRONG (out of sync)
alert( myParagraph.texts[0].contents );   // CORRECT
 

The last line works because the final specifier, myParagraph.texts[0], needs the parent location, myParagraph, to be resolved first. Therefore, the paragraph scope is recalculated and all is fine.

• Original discussion: http://forums.adobe.com/message/4042847#4042847