_.jsx

What is _.jsx?

_.jsx started as an attempt to bring the functionality of web javascript frameworks to extendscript for InDesign. (Most notably jQuery.)

One of its most significant features is the ability to address a whole collection of items through a single Proxy object. This is very similar to the functionality that can be seen in jQuery using selectors, as well as functionality already built into InDesign using the everyItem(), and itemByRange() functions in collection objects. However the key advantage to underscore is the usage of advance filtering through css style selectors, property object matching and filter functions.

It quickly grew from here to include optimizations to selectors and assignments specific to InDesign, an event-loop, a nice selection of web-inspired asynchronous operations and various helpful tools for quickly manipulating the InDesign DOM

To download _.jsx click here Or download the current development source here

To download the JSXConsole. (A extendscript console for easy testing and scripting from within InDesign) click here

To really understand what _.jsx has to offer it is recommended that you try the examples yourself and experiment using the JSXConsole. Remember that _.jsx only selects objects if they exist, and searches for non-existant items return undefined. So when searching for things .e.g textFrames. Make sure they exist on the document. (Or of course you can use _.jsx to populate your document!) e.g

                                
_('documents').add();

_(10).times(function(){
        _('textFrames').add({
            geometricBounds: _.randArray(0, 100, 4)
        });
});
                                
                                

Selectors.

_.jsx supports a large number of methods to select content in InDesign.


    // Selects the app.activeDocument.textFrames collection.
    _('textFrames');
    => [ProxyCollection [object TextFrames]]

                    

By default _.jsx uses the 'app' object as search scope. If it doesn't find the specified property it will search in the 'activeDocument' scope. This allows you to use short selectors in many cases.
However: sometimes you may wish to search within another scope or explicitly search inside a document for a property also present in the 'app' object. Just like everyItem() collections in InDesign, selectors can be chained. So specifying an alternate scope is as easy as changing the value in the first selector.


// Selects insertion Points of all texts, of all textframes.
_('textFrames')._('texts')._('insertionPoints');
=> [[ProxyCollection [object InsertionPoints]]

/** (The default scope is 'app'. The characterStyles property
* is present in both the app and the activeDocument,
* in this case the 'app' property is returned). */
_('characterStyles');
=> [ProxyCollection [object CharacterStyles]]

/** (By explicitly passing the activeDocument as the first
* parameter we set the scope and get the documents
* characterStyles collection instead. */
_('activeDocument')._('characterStyles');
=> [ProxyCollection [object CharacterStyles]].


// Any object can be wrapped to turn it into a proxy.
_(app.activeDocument);
=> [Proxy Document: Untitled-1]

//Which is the same as
_('activeDocument');
=> [Proxy Document: Untitled-1]


                            

Filtering.

_.jsx gets very powerful when it comes to filtering collections. On its own the InDesign everyItem function already returns objects that represent entire collections and allows you to modify them all as one. _.jsx's filtering implements a more advance version of this with support for conditional selection. The various methods of selection are

  • CSS style string filters
  • Filter Objects
  • Filter Functions
All filters can be intermixed with one another in selection chains.

The simplest to use are web inspired CSS selectors:

CSS Style Filtering



    /* You can select items by ID. Using the '#' specifier */
    _('textFrames#231');
    => [Proxy TextFrame: ]

                        


    // You can select items by name. Using the '.' specifier
    _('layers.Layer 1');
    => [Proxy Layer: Layer 1]


                        

The square brackets selections allow some more advanced selections involving wildcards and negation



    /* Selects all overset textFrames. */
    _("textFrames[overflows=true]");
    => [ProxyCollection [object TextFrames]]

    //Selects all rectangles where label = "myLabel"
    _('rectangles[label=myLabel]');
    => [ProxyCollection [object Rectangles]]


    /** An example of selecting all empty textFrames,
    * and using the negation operator to select all non-empty text-frames.
    */
    _('textFrames[contents=]');
    => [ProxyCollection [object TextFrames]]

    _('textFrames[!contents=]');
    => [ProxyCollection [object TextFrames]]


                        

    /* USING WILDCARDS
    *
    * Return all layers whose names start with 'Layer'
    */
    _('layers[name=Layer *]');
    => [ProxyCollection [object Layers]]

    /* Return all cells in the first column of the table with id [21]. */
    _('textFrames')._('tables#21')._('cells[name=0:*]');
    => [ProxyCollection [object Rectangles]]


    /* Return all texFrames that do not have 'badword' anywhere in the contents. */
    _('textFrames[!contents=*badword*]');
    => [ProxyCollection [object TextFrames]]

                        

Filters can be added at any point within the selection chain, and multiple filters can be chained together.



    //Multiple filters within one selection. Selects all non-overflowing locked text-frames.
    _('textFrames')._('[overflows=false]')._('[locked=true]');
    => [ProxyCollection [object TextFrames]]

    //Filters at multiple selection points. E.g selects all underlined texts, whose parent is a non-overflowing textFrame.
    _('textFrames[overflows=false]')._('texts[underline=true]');
    => [ProxyCollection [object Texts]]


                        

Using Filter Objects

While in many cases CSS style filtering is more than enough, in some cases a selection needs to be very specific and achieving this through string filters could result in horrendously long chains. In this case object filtering is often handy. Object filtering is simply passing in an object full of properties which each child in the filtered selection will match.


    // Here we use a complex filter object to avoid a very long css style  selection chain.

_('textFrames')._({
    overflows:false,
    hidden:false,
    locked:false,
    label:'myScriptLabel',
    itemLayer:_('layers.Layer 1')
});

=> [ProxyCollection [object TextFrames]];

                

Notice in the above example we specify that the itemLayer must be the layer pointed to by _('layers.Layer 1'). This works because proxies inherit the equality function from their wrapped value.

Using Filter Functions

Sometimes simple comparisons of properties isn't enough to make the selection you want. When it is no longer trivial to make a selection on simple characteristics of the type of object you are selecting, you need to pass in a filter function instead.


// In this example we want to select all Rectangles which have a height that is larger than 10 (of our current document measurement unit). This is not something we can specify in a css selector, or a filter object so we need a filter function./

var  taller_than_10_filter = function(rectangle){
    var bounds = rectangle.geometricBounds;
    return bounds[2]-bounds[0] > 10;
}

_('rectangles')._(taller_than_10_filter);

=> [ProxyCollection [object Rectangles]];

            

A filter function can be any function at all that takes one parameter - the child to test. If it returns true, the object is included in the output, otherwise it isn't.

Notice in the above example we specify that the itemLayer must be the layer pointed to by _('layers.Layer 1'). This works because proxies inherit the equality function from their wrapped value.

As well as filtering, _.jsx also supports the functional programming paradigm of mapping. Instead of returning a boolean you can return any kind of object instead, and the resulting collection will be of the mapped objects. e.g


/* In this example we would like to have a collection of all graphics and all
of their parent rectangles so we can set the bounds of images and their frames
at the same time.
Our filter function is passed in a graphic, and returns an array
containing the graphic and its parent. */

var complex = _('rectangles')._('graphics')._(function(gffx){
    return [gffx, gffx.parent];
});
=> [ProxyCollection [object Rectangles]];

//We can then act upon the mapped objects.
complex.geometricBounds = [0,0,50,50];

            

Another handy alternate use to the filter function is ignoring the result and simply using it as a "for-each" loop. Any InDesign collections are resolved to arrays before use meaning the collections are safe for looping. e.g


/* Using a filter function as a loop. */

var layerCounts = {};
_('textFrames')._(function(tf){
    var layerName = tf.itemLayer.name;
    if(!layerCounts[layerName])
        layerCounts[layerName] = 0
    layerCounts[layerName]++;
});


    

Working With a Selection

So what can you do once you have made a selection?



var selection = _('textFrames')._(['overflows=true'])

/* You can assign to the properties of its children. */
selection.label = "I am overflowing!"

/* You can retreive the properties of its children as an array. */

selection.id()
=> 12,46,99

/*   Remember unlike the objects they represent, properties are accessed
using a function of the same name, and return an array with the property for each child.
*/

/* You can call functions on it */
selection.fit(FitOptions.CONTENT_TO_FRAME);

/**
* You can access the underlying collection or object using the .value property.
* e.g
*/
_('textFrames#231').value
=> [object TextFrame]

//Page with two text-frames filled with texts.
_('textFrames')._('texts').value.
=> [object Texts],[object Texts]



    

NOTE: Operations on selections will work as long as the selection is valid. Invalid selections will throw an error. Hence you can't do anything to _('textFrames') if there are no textFrames in the activeDocument.
Selectors return undefined if a specifier doesn't resolve to anything.

Selections represent both the collections they hold, and the items within the collections, meaning the Proxy inherits functions from both. An example can better explain this:


/*  A document with 4 textFrames, and 2 2x2 tables, with contents 'test' in every cell.

You can see that while the Proxy for the selection below contains 4
Tables collections, it only contains two actual tables. */

_('textFrames')._('tables')
=> [ProxyCollection [object Tables],[object Tables],[object Tables],[object Tables]];

_('textFrames')._('tables').size()
=> 2

/* Accessing properties or functions on the children in the collection
only accesses the valid children, in this case 2 tables */
_('textFrames')._('tables').contents()
test,test,test,test, ,test,test,test,test,

/* Calling functions owned by the Tables collection(rather than individual table) targets every collection object. */
_('textFrames')._('tables').add()

// We now have 6 tables. (The original 2 + the 4 we just added. One in each collection.
_('textFrames')._('tables').size()
=> 6


    

More detail on Assigning to Properties

Just like the items returned when calling everyItem() in InDesign, proxies returned by _.jsx selections return objects that allow setting of properties of multiple children at once. It also has a few added advantages that make it more advanced than what extendscript currently offers. Read on:


var selection = _('textFrames')._('texts');
// You can set a property for all children the normal way
selection.underline = true;

//Or pass the property as a parameter to a function
selection.underline(true);

//When using the setter function you can chain setter calls.
selecion.underline(true).strikeThru(true)

//Since the scope stays the same you can even do things like 'select'=> 'set a property'=> 'refine selection'=> 'set another property'. Although for ease of readability this is not ideal. e.g
_('textFrames').strokeColor('Black')._('texts').fillColor = 'Paper'

//Sometimes when you set a property you may want to set it to something relative to its current value. For example to set all invisible textFrames to visible, and vice-versa you could do:

_('textFrames').visible = function(tf){return !tf.visible};

/**Passing a function as a value to a property evaluates the function
* with the child being assigned to as the first Parameter.
* It sets the property to the result of the function.
* Similarly you can wrap a function in square brackets. This does exactly
* the same except instead of passing the child as the first
* value - it passes the value of the property as both the scope, and first parameter.
* The usefulness of this can be seen in examples such as the following:
*/

//Convert all story content to upper case.
_('stories').contents = [String.prototype.toUpperCase];

//Switch aspect ratio of all rectangles
_('rectangles').geometricBounds = [Array.prototype.reverse];

//Surround contents in all cells of all tables with square brackets.
_('textFrames')._('tables')._('cells').contents = [
    function(c){
        return "["+c+"]";
    }
]

//Functions work as normal and expected.e.g
_('rectangles')._({itemLayer:_('layers.To Remove')}).remove();

//Functions can also be chained. Simply prepend an underscore to the function name and it will return the current scope instead of its default return value. e.g:

_('textFrames')._duplicate([30,30])._move(null, [30,30]);

Timers

_.jsx has a Timer class that is useful for repeated actions. It can be used for a one-time timed action also but in this case it may be simpler to use the _.setTimeout() function. The syntax for creating a new Timer object is as follows


/**
*  _.Timer(interval,[repeats]);
* @param  {number} interval Milliseconds between timer events.
* @return {number} repeats [optional] number of repeats before timer stops.
*
* If no repeats are specified the timer repeats indefinitely or until timer.stop()
* is called.
*/
var timer = new _.Timer(100, 5);
timer.addEventListener(_.TimerEvent, _('documents').add)
timer.addEventListener(_.TimerFinishedEvent, function(){
    alert('done!');
})
timer.start();


Events

_.jsx supports a number of events mechanisms. You can attach event listeners to any textFrames and rectangles to detect a number of custom events. These are:

  • _.MOVE
  • _.SELECT
  • _.DESELCT
  • _.UP (These are key listeners for the arrow keys when the object is selected)
  • _.DOWN
  • _.LEFT
  • _.RIGHT
For example to attach listeners for all these events to all textframes, and append the event type to the textFrames we can do:


_.each({
    up:_.UP,
    down:_.DOWN,
    left:_.LEFT,
    right:_.RIGHT,
    select:_.SELECT,
    deselect:_.DESELECT,
    move:_.MOVE}, function(value, key){
         _('textFrames').addEventListener(value, function(e){
                                e.target.contents +=" "+key
                                });
 });



Event listeners of any type can also be added/removed from the global _ object and likewise any event can be dispatched using:

  • _.addEventListener = function(type, callback)
  • _.removeEventListener = function(type, callback)
  • _.dispatchEvent = function(type, [target]) (The target can be overridden, otherwise it is the global object).
e.g:


_.addEventListener("MyCustomEvent", function(e){
    alert('my event was triggered from ' + e.target);
});
//Trigger my custom event after 2 seconds.
_.setTimeout(function(){
    _.dispatchEvent("MyCustomEvent", _('activeDocument'))
}, 2000);

HTTP

_.jsx supports a number of basic HTTP calls in async and synchronous variants. These are:
(In Format function(params):response)

  • _.get(url, callback):void
  • _.getSync(url):responseData
  • _.post(url, data, callback):void
  • _.postSync(url, data):responseData
  • _.head(url, callback):void
  • _.headSync(url):responseData
There is also support for downloading images to a local file. If no file is specified a temporary file is used.
  • _.getImg(url, callback)
  • _.getImg(url, fileToSaveTo, callback)
  • _.getImgSync(url, [optionalFileToSaveTo]):File

    
/**
* The response data for the get, post, and head is always an object with following properties
*/
{
    code:['The HTTP response code'],
    headers:['An array of HTTP response headers.'],
    data:['The HTTP response body text']
}
    
    

Here are some examples:


/**
* Example showing the difference between synchronous and asynchronous
* versions of the HTTP calls. This example gets the source code for google.
*
*/
var data = _.getSync('www.google.com');
alert('Response code is ' + data.code);

_.get('www.google.com', function(data){
    alert("Response headers are "+ data.headers);
});

/**
* Example placing image straight from the web into a document.
*/
var googleImageUrl = "http://www.google.com/images/srpr/logo4w.png";
_('activeDocument').place(_.getImgSync(googleImageUrl));


Animation

_.jsx can animate any item that has the geometricBounds property. It has async and synchronous variants of the animation functions. Calls to 'animate' can take either, an array of 4 coordinates to animate to. Or an object with these optional properties:

    
{
    to:['An array of 4 coordinates'],
    rotate:['a positive or negative angle to rotate'],
    scale:['A scale multiplier to scale the size of an object']
}
    
    

Async animations have an optional callback parameter. When using the Async animation you can still interact with the document. Try moving or typing into a textFrame while it is rotating! Some examples are:

    
//Rotate every text frame 360 degrees over 3 seconds
_('textFrames').animate({rotate:360}, 3000);

//Double the size of all rectangles in the document over 3 seconds and inform us when the animation is over

_('rectangles').animate({scale:2}, 3000, function(){
    alert('done');
});
//Move all overset textFrames to the top left corner.
_('textFrames[overflows=true]').animate([0,0,20,20], 2000)
    
    

Debugging

While fairly trivial to most people sometimes writing "$.writeln" for debug statements can be quite cumbersome if it is repeated often. Underscore offers a shorthand that allows you to pipe objects to the underscore object. Underscore will then print them to JSXConsole if it is running, otherwise they will be piped to the extendscript toolkit console. The syntax for this shorthand is:

_ < 'hello world'
=> "hello world"

_ < "Number "+ 5 < "is alive"
=> Number 5
   is alive

Ranges.

Underscore also provides a helpful 'Range' type very similar to the kind present in the Python language. See examples below for how to use it.


//A single integer results in a lange of [n] length
_(10);
=> [0,1,2,3,4,5,6,7,8,9,10];


//Two integers results in a range of single integers between [n] and [m].
_(2, 11);
=> [2, 3, 4, 5, 6, 7, 8, 9, 10];

//Three numbers results in a range of single integers between [n] and [m]. using a specified step size. Negative step sizes are ok too!
_(2, 3, 0.1);
=> [2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9];

/*Ranges have a [times] function that can be used as a handy replacement for for(i = 0; i < n... loops. e.g
*Ranges also have an [each] function which is very similar to the [times] function but passes the current index of the callback function;
* Use [times for functions that take no parameters. e.g document.textFrames.add;
* otherwise use [each] for access to the current index. */

//Adds 10 textFrames.
_(10).times(
    _('textFrames').add;
);

//each passes the index to the function, times does not.
_(10, 0, -1).each(function(i){
    _ < ('hello ' + i)
});


Functions and Tools

There are a number of handy functions and tools offered by _.jsx. These are always attached to the _ object, as we intentionally avoid any modifying of object prototypes.



/**
* arrayEqual.
* Returns whether two arrays have equal contents
*/
_.arrayEqual(arr1, arr2)


/**
* assert
* Throws an error if condition doesn't evaluate to true.
*/
_.assert(condition);


/**
* capitalize
* returns the string with the first character capitalized.
*/
_.capitalize(string)


/**
* collectionType
* returns the type of object in the collection given as a string
*/
_.collectionType(collection);


/*
* each.
* Performs a safe for-each iteration over arrays, collections, and objects.
* takes a function to call for each item in the form of.
* function(value, [index]);
*/
_.each(collection, callback);


/**
* emptyCache
* Resets all caches in underscore.
*/
_.emptyCache()


/**
* eval
* Evaluates javascript code. This function is mostly useless in extendscript but provides
* an easy way to use eval from inside Flex or Actionscript extensions
* using US.swc
*/
_.eval(value, list): varies


/**
* Returns a collection b given collection a, using filter function [callback].
* items that return true are included, others are not.
*/
_.filter(collection, callback)


/**
* inArray
* returns a boolean value whether a value is present inside an array.
*/
_.inArray(value, list)


/**
* indexOf
* returns the index of [value] in [list] or -1;
*/
_.indexOf(value, list)


/*
* isCollection,
* returns whether an object. is an InDesign collection object.
*/
_.isCollection(collection);


/**
* isNum.
* Returns whether string evaluates to exactly a number. No extra characters.
*/
_.isNum(str)


/*
* isProxy,
* returns whether an object. is an underscore Proxy object.
*/
_.isProxy(collection);


/**
* map
* Returns a collection b given collection a, using mapping function [callback]
*/
_.map(collection, callback)


/**
* profile
* Returns number of seconds it takes to perform a function [n] times.
* default is 1.
*/
_.profile(func, [n]);


/**
* randArray
* returns an array of random numbers of length [n]. between [min] and [max]
*/
_.randArray(min,max,n);


/**
* random
* returns a random number. Optionally between [min] and [max]
*/
_.random([min],[max]);

/**
* Reset
* Resets all caches and listeners in underscore.
*/
_.reset()


/**
* setTempPref
* Gives you an easy way to set preferences in InDesign and returns a function
* that when called restores the prefs to how they were.
* [where] = scope of preference. [e.g app.activeDocument.viewPreferences]
* [what] = name of one or more preferences.
*           e.g ['horizontalMeasurementUnits', 'verticalMeasurementUnits'];
* [values] = single value for all prefs in 'what' or a unique value for each pref.
*           e.g MeasurementUnits.PIXELS; - Sets all to pixels
*           e.g [MeasurementUnits.AGATES, MeasurementUnits.INCHES]; - Sets different value * for each pref.

* Returns: function restorePrefs(); - Call this function to restore the prefs to the state
* before the call to setTempPref;
*/
_.setTempPref(where, what, values): restoretempPrefs()


/**
* setTimeout, call a function after a certain time period.
*/
_.setTimeout(callback, time);


/**
* snapshot
* returns a memory snapshot diff, since the last time snapshot was called.
* similar to $.summary(), except returned value is diff, not total memory usage.
*/
_.snapshot();


/**
* strip
* strip\trims an input string. returns stripped value.
*/
_.strip(str);



/*
* tempFolder
* tempFile
* Quick creation of temporary files.
* returns file or folder object.
*/
_.tempFolder()
_.tempFile()


/*
* toArray,
* Returns any collection object, array, or object as an array. Converting if neccessary.
*/
_.toArray(collection);


/**
*   uuid
*   returns a uuid
*/
_.uuid();


/**
* wraps a function call in an Undo, which appears as [undoName] in the InDesign menu.
* Can give an optional undoMode, default is UndoModes.FAST_ENTIRE_SCRIPT
*/
_.wrapInUndo(undoName, func, [undoMode]);