├── patches ├── test │ ├── fixtures │ │ ├── .ext │ │ ├── file │ │ └── file.ext │ ├── application.specs │ ├── conversions.specs │ └── extendscript.specs ├── __all__.jsx ├── date.jsx ├── math.jsx ├── dom.application.jsx ├── dom.suite.jsx ├── file.jsx ├── object.jsx ├── error.jsx ├── string.jsx └── object.conversions.jsx ├── core-packages ├── ui │ ├── test │ │ ├── mixins.specs │ │ └── ui.specs │ ├── doc │ │ ├── layout.png │ │ └── reference.rst │ └── lib │ │ └── _ideas_.jsx ├── io │ ├── lib │ │ ├── index.jsx │ │ └── octals.jsx │ └── test │ │ └── io.specs ├── testing │ ├── templates │ │ ├── partial.environment.html │ │ ├── partial.test.html │ │ ├── partial.suite.html │ │ └── report.html │ ├── doc │ │ ├── unit-testing.png │ │ └── readme.rst │ └── lib │ │ └── index.jsx ├── templating │ ├── test │ │ └── templating.specs │ ├── lib │ │ └── index.jsx │ └── doc │ │ └── readme.rst ├── __future__ │ └── lib │ │ ├── ideas.txt │ │ ├── exceptions │ │ └── index.jsx │ │ ├── persistence │ │ ├── engines.jsx │ │ └── index.jsx │ │ └── preview │ │ └── idea.txt ├── persistence │ └── lib │ │ └── index.jsx ├── utils │ └── lib │ │ ├── date.jsx │ │ ├── index.jsx │ │ ├── math.jsx │ │ ├── folder.jsx │ │ ├── file.jsx │ │ ├── object.jsx │ │ ├── error.jsx │ │ ├── string.jsx │ │ └── object.conversions.jsx ├── http │ ├── lib │ │ ├── browser.jsx │ │ ├── auth.jsx │ │ └── url.jsx │ ├── doc │ │ └── readme.rst │ └── test │ │ └── http.specs └── logging │ ├── doc │ └── readme.rst │ ├── test │ └── logging.specs │ └── lib │ └── index.jsx ├── log └── .gitignore ├── test ├── fixtures │ └── core-packages │ │ ├── nonpackage-b.txt │ │ ├── package-b │ │ ├── test │ │ │ └── testspecs.specs │ │ ├── lib │ │ │ ├── real.jsx │ │ │ ├── subpackage │ │ │ │ └── index.jsx │ │ │ ├── empty.jsx │ │ │ └── index.jsx │ │ └── subpackage │ │ │ └── index.jsx │ │ └── package-a.jsx ├── runner.patches.jsx ├── runner.framework.jsx ├── runner.packages.jsx ├── bootstrapper.jsx └── loader.specs ├── doc ├── _themes │ └── theme.conf ├── homepage │ ├── extendables.png │ └── index.html ├── patches │ ├── object.rst │ ├── string.rst │ ├── application.rst │ ├── file-and-folder.rst │ ├── array.rst │ ├── index.rst │ ├── application-specific │ │ └── indesign.rst │ └── error.rst ├── core │ ├── index.rst │ ├── changes.rst │ ├── roadmap.rst │ ├── design-patterns.rst │ ├── writing-a-module.rst │ └── contribute.rst ├── _ext │ └── glob_include.py ├── jsdoc.conf ├── license.rst ├── about.rst ├── index.rst ├── make.bat ├── Makefile ├── get-started.rst └── conf.py ├── .gitignore ├── CONTRIBUTORS ├── .gitmodules ├── tools ├── testrunner.jsx └── scaffold │ ├── doc │ └── readme.rst │ ├── test │ └── specs.specs │ └── lib │ └── index.jsx ├── settings.jsx ├── examples ├── ui.jsx ├── files-and-folders.jsx ├── getter-setters.jsx ├── arrays.jsx ├── test │ └── examples.specs ├── basic.jsx └── xml-traversal.jsx ├── context.jsx ├── README ├── LICENSE ├── extendables.jsx ├── fabfile.py ├── dependencies └── base64.js ├── minify.py └── loader.jsx /patches/test/fixtures/.ext: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /patches/test/fixtures/file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /patches/test/fixtures/file.ext: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core-packages/ui/test/mixins.specs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /test/fixtures/core-packages/nonpackage-b.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/core-packages/package-b/test/testspecs.specs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core-packages/io/lib/index.jsx: -------------------------------------------------------------------------------- 1 | exports.octals = require("io/octals"); -------------------------------------------------------------------------------- /test/fixtures/core-packages/package-b/lib/real.jsx: -------------------------------------------------------------------------------- 1 | exports.this_is = 'Package B submodule' -------------------------------------------------------------------------------- /core-packages/testing/templates/partial.environment.html: -------------------------------------------------------------------------------- 1 | {key}: {value}
2 | -------------------------------------------------------------------------------- /test/fixtures/core-packages/package-b/subpackage/index.jsx: -------------------------------------------------------------------------------- 1 | exports.this_is = 'Package B sub-submodule'; -------------------------------------------------------------------------------- /doc/_themes/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = nature 3 | stylesheet = nature.css 4 | pygments_style = tango -------------------------------------------------------------------------------- /test/fixtures/core-packages/package-b/lib/subpackage/index.jsx: -------------------------------------------------------------------------------- 1 | exports.this_is = 'Package B sub-submodule'; -------------------------------------------------------------------------------- /test/fixtures/core-packages/package-b/lib/empty.jsx: -------------------------------------------------------------------------------- 1 | // empty, shouldn't be registered as a package or subpackage -------------------------------------------------------------------------------- /doc/homepage/extendables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debrouwere/Extendables/HEAD/doc/homepage/extendables.png -------------------------------------------------------------------------------- /core-packages/ui/doc/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debrouwere/Extendables/HEAD/core-packages/ui/doc/layout.png -------------------------------------------------------------------------------- /core-packages/testing/doc/unit-testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debrouwere/Extendables/HEAD/core-packages/testing/doc/unit-testing.png -------------------------------------------------------------------------------- /core-packages/templating/test/templating.specs: -------------------------------------------------------------------------------- 1 | describe('Templating', function () { 2 | it('has unit tests', function () { 3 | expect(false).toBeTruthy(); 4 | }); 5 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | _experiments_ 3 | doc/_build 4 | doc/jsdoc 5 | patches/doc/jsdoc 6 | core-packages/*/doc/jsdoc 7 | *.pyc 8 | *.old 9 | *.orig 10 | *(Autosaved)* 11 | -------------------------------------------------------------------------------- /core-packages/testing/templates/partial.test.html: -------------------------------------------------------------------------------- 1 | 2 | {result} 3 | {name} 4 | {problem} 5 | -------------------------------------------------------------------------------- /test/fixtures/core-packages/package-a.jsx: -------------------------------------------------------------------------------- 1 | var dirty = "this variable would pollute the global namespace and shouldn't survive module loading"; 2 | 3 | exports.this_is = "Package A"; -------------------------------------------------------------------------------- /test/fixtures/core-packages/package-b/lib/index.jsx: -------------------------------------------------------------------------------- 1 | var submodule = require("package-b/real"); 2 | 3 | exports.this_is = 'Package B core'; 4 | exports.id = module.id; 5 | exports.uri = module.uri; -------------------------------------------------------------------------------- /doc/patches/object.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | ``Object`` methods 3 | ================== 4 | 5 | .. contents:: 6 | 7 | .. include:: ../../patches/doc/jsdoc/Object.rst 8 | :start-after: class-methods -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Portions of this software were written and kindly contributed to Extendables by: 2 | 3 | * Bob Stucky 4 | * Gabe Harbs (http://in-tools.com/) 5 | * Marc Autret (http://www.indiscripts.com/) 6 | -------------------------------------------------------------------------------- /doc/core/index.rst: -------------------------------------------------------------------------------- 1 | .. _core: 2 | 3 | ==== 4 | Core 5 | ==== 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | writing-a-module 11 | contribute 12 | design-patterns 13 | roadmap 14 | changes -------------------------------------------------------------------------------- /core-packages/__future__/lib/ideas.txt: -------------------------------------------------------------------------------- 1 | - set implementation: http://gist.github.com/427161 2 | - an "errors" library that implements more verbose user-facing error messages and refers them to a webpage for additional information. 3 | - a config manager on top of the persistence module -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "doc/_themes/jsdoc-for-sphinx"] 2 | path = doc/_themes/jsdoc-for-sphinx 3 | url = git@github.com:stdbrouw/jsdoc-for-sphinx.git 4 | [submodule "doc/_themes/nature-fixedwidth"] 5 | path = doc/_themes/nature-fixedwidth 6 | url = git@github.com:stdbrouw/nature-fixedwidth.git 7 | -------------------------------------------------------------------------------- /patches/test/application.specs: -------------------------------------------------------------------------------- 1 | describe('Patches: application', function () { 2 | it('can determine which host app is running', function () { 3 | expect(app.is('extendscript')).toBeTruthy(); 4 | expect(app.is('extendscript', 15)).toBeFalsy(); 5 | expect(app.is('i dunno')).toBeFalsy(); 6 | }); 7 | }); -------------------------------------------------------------------------------- /doc/patches/string.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | ``String`` methods 3 | ================== 4 | 5 | Extendables provides a number of additional string munging and manipulation methods for `String` objects. Most of these will look familiar if you've coded in other languages before. 6 | 7 | .. contents:: 8 | 9 | .. include:: ../../patches/doc/jsdoc/String.rst 10 | :start-after: class-methods -------------------------------------------------------------------------------- /tools/testrunner.jsx: -------------------------------------------------------------------------------- 1 | var root = new File($.fileName).parent.parent; 2 | 3 | // Executes these subrunners entirely separate so they can't influence each other. 4 | // That means $.evalFile instead of #include 5 | $.evalFile(root + "/test/runner.patches.jsx"); 6 | $.evalFile(root + "/test/runner.framework.jsx"); 7 | $.evalFile(root + "/test/runner.packages.jsx"); 8 | $.writeln("Finished test run."); -------------------------------------------------------------------------------- /patches/__all__.jsx: -------------------------------------------------------------------------------- 1 | #include "object.jsx" 2 | #include "string.jsx" 3 | #include "array.jsx" 4 | #include "object.conversions.jsx" 5 | #include "error.jsx" 6 | #include "file.jsx" 7 | #include "date.jsx" 8 | #include "math.jsx" 9 | #include "dom.application.jsx" 10 | if (!app.is("toolkit")) { 11 | #include "dom.suite.jsx" 12 | } 13 | if (app.is("indesign")) { 14 | #include "dom.indesign.jsx" 15 | } -------------------------------------------------------------------------------- /settings.jsx: -------------------------------------------------------------------------------- 1 | var settings = {}; 2 | 3 | /* configuration */ 4 | $.level = 2; 5 | $.strict = false; 6 | 7 | /* extendables settings */ 8 | settings.package_directories = ['./core-packages', './site-packages']; 9 | 10 | /* module settings */ 11 | // don't log debug messages, but do log everything else 12 | settings.LOGGING_LOG_LEVEL = 4; 13 | settings.LOGGING_FOLDER = new Folder("log").at(Folder.extendables); -------------------------------------------------------------------------------- /test/runner.patches.jsx: -------------------------------------------------------------------------------- 1 | #include "bootstrapper.jsx" 2 | 3 | var base = new Folder("patches/test").at(Folder.extendables); 4 | var specs = base.getFiles("*.specs"); 5 | 6 | specs.forEach(function (specfile) { 7 | try { 8 | $.evalFile(specfile); 9 | } catch (error) { 10 | $.writeln(specfile + " is not a valid specifications file.\n" + error); 11 | } 12 | }); 13 | 14 | tests.to_html("tests.patches.html"); -------------------------------------------------------------------------------- /core-packages/testing/templates/partial.suite.html: -------------------------------------------------------------------------------- 1 |
2 |

Suite: {name} ({total} tests, {failed} failures)

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {specs => partial.test.html} 12 | 13 |
statustest
14 |
-------------------------------------------------------------------------------- /test/runner.framework.jsx: -------------------------------------------------------------------------------- 1 | #include "../extendables.jsx" 2 | extract("testing"); 3 | 4 | var specs = new File($.fileName).parent.files("*.specs"); 5 | specs = specs.concat(new Folder("examples/test").at(Folder.extendables).files("*.specs")); 6 | 7 | specs.forEach(function (specfile) { 8 | try { 9 | $.evalFile(specfile); 10 | } catch (error) { 11 | $.writeln(specfile + " is not a valid specifications file.\n" + error); 12 | } 13 | }); 14 | 15 | tests.to_html("tests.framework.html"); -------------------------------------------------------------------------------- /tools/scaffold/doc/readme.rst: -------------------------------------------------------------------------------- 1 | ================================================= 2 | ``new project``: this is a new Extendables module 3 | ================================================= 4 | 5 | .. 6 | This readme should give a general overview of what this module contains 7 | and how other modules and scripts can use the functionality this module 8 | provides. 9 | 10 | You can add in autodocs from the JSDOC toolkit using e.g. 11 | 12 | .. include:: jsdoc/MyClass.rst -------------------------------------------------------------------------------- /doc/patches/application.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | ``Application`` methods 3 | ======================= 4 | 5 | Most of Extendables' functionality is either at the application-specific level or at the ExtendScript level. However, each ``app`` object, regardless of the host application, does get a ``is`` method added in that makes it easy to write code that targets a specific application or version. 6 | 7 | .. contents:: 8 | 9 | .. include:: ../../patches/doc/jsdoc/Application.rst 10 | :start-after: class-methods -------------------------------------------------------------------------------- /doc/patches/file-and-folder.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | ``File`` and ``Folder`` objects 3 | =============================== 4 | 5 | .. contents:: 6 | 7 | .. literalinclude:: ../../examples/files-and-folders.jsx 8 | :language: extendscript 9 | 10 | ``File`` methods 11 | ================ 12 | 13 | .. include:: ../../patches/doc/jsdoc/File.rst 14 | :start-after: class-methods 15 | 16 | ``Folder`` methods 17 | ================== 18 | 19 | .. include:: ../../patches/doc/jsdoc/Folder.rst 20 | :start-after: class-methods -------------------------------------------------------------------------------- /doc/patches/array.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | ``Array`` methods 3 | ================= 4 | 5 | Extendables adds a considerable amount of methods to the ``Array`` object prototype. Most of these will be familiar from either Javascript 1.6+ (modern browsers implement Javascript 1.8 or higher) or from Jeremy Ashkenas' `underscore.js `_ library. 6 | 7 | .. contents:: 8 | 9 | .. literalinclude:: ../../examples/arrays.jsx 10 | :language: extendscript 11 | 12 | .. include:: ../../patches/doc/jsdoc/Array.rst 13 | :start-after: class-methods -------------------------------------------------------------------------------- /core-packages/ui/doc/reference.rst: -------------------------------------------------------------------------------- 1 | .. _ui-reference: 2 | 3 | ================ 4 | ``ui`` reference 5 | ================ 6 | 7 | .. contents:: 8 | 9 | Layout elements 10 | =============== 11 | 12 | .. include:: jsdoc/UIShortcuts.rst 13 | :start-after: class-methods 14 | 15 | Event names 16 | =========== 17 | 18 | * change 19 | * move 20 | * resize 21 | * show 22 | * focus 23 | * mousedown 24 | * mousemove 25 | * keyup 26 | * click 27 | * changing 28 | * moving 29 | * resizing 30 | * enterKey 31 | * blur 32 | * mouseup 33 | * mouseover 34 | * mouseout 35 | * keydown 36 | 37 | .. 38 | onInvoke? -------------------------------------------------------------------------------- /test/runner.packages.jsx: -------------------------------------------------------------------------------- 1 | var only_test = []; 2 | 3 | #include "../extendables.jsx" 4 | extract("testing"); 5 | 6 | var specfiles = []; 7 | __modules__.values().forEach(function (module) { 8 | if (only_test.length && only_test.indexOf(module.id) == -1) return; 9 | 10 | module.get_tests().forEach(function (specs) { 11 | specfiles.push(specs); 12 | }); 13 | }); 14 | 15 | specfiles.forEach(function (specfile) { 16 | try { 17 | $.evalFile(specfile); 18 | } catch (error) { 19 | $.writeln(specfile + " is not a valid specifications file.\n" + error); 20 | } 21 | }); 22 | 23 | tests.to_html("tests.packages.html"); -------------------------------------------------------------------------------- /test/bootstrapper.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Bootstrap 3 | * 4 | * This script tests the loader among other things, so instead of using the loader system 5 | * we're bootstrapping this stuff ourselves. 6 | */ 7 | 8 | var exports = {}; 9 | #include "../patches/__all__.jsx" 10 | #include "../core-packages/templating/lib/index.jsx" 11 | function require () { 12 | return { 13 | Template: exports.Template.clone() 14 | }; 15 | } 16 | var module = { 17 | 'id': 'testing', 18 | 'uri': new File("core-packages/testing/lib/index.jsx").at(Folder.extendables) 19 | } 20 | 21 | #include "../core-packages/testing/lib/index.jsx" 22 | 23 | for (name in exports) { 24 | $.global[name] = exports[name]; 25 | } -------------------------------------------------------------------------------- /core-packages/persistence/lib/index.jsx: -------------------------------------------------------------------------------- 1 | exports.Store = function (uri) { 2 | var self = this; 3 | this.uri = uri; 4 | this.file = new File(uri); 5 | this.exists = this.file.exists; 6 | 7 | this.save = function () { 8 | var data = this.data.serialize('json'); 9 | this.file.open('w'); 10 | this.file.write(data); 11 | this.file.close(); 12 | } 13 | 14 | this.refresh = function () { 15 | this.file.open('r'); 16 | this.data = this.file.read().deserialize('json'); 17 | this.file.close(); 18 | } 19 | 20 | this.destroy = function () { 21 | this.data = {}; 22 | this.file.remove(); 23 | } 24 | 25 | if (this.file.exists) { 26 | this.refresh(); 27 | } else { 28 | this.data = {}; 29 | } 30 | } -------------------------------------------------------------------------------- /doc/_ext/glob_include.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is experimental 3 | """ 4 | 5 | from docutils.parsers.rst import directives 6 | import glob 7 | import copy 8 | 9 | class Include(directives.misc.Include): 10 | def run(self): 11 | if self.arguments[0].endswith('*'): 12 | out = list() 13 | paths = glob.glob(self.arguments[0]) 14 | for path in paths: 15 | directive = copy.copy(super(Include, self)) 16 | directive.arguments[0] = directives.path(path) 17 | out = out + directive.run() 18 | return out 19 | else: 20 | return super(Include, self).run() 21 | 22 | def setup(sphinx): 23 | pass 24 | #sphinx.add_directive('include', Include) -------------------------------------------------------------------------------- /examples/ui.jsx: -------------------------------------------------------------------------------- 1 | #include "../extendables.jsx" 2 | 3 | var ui = require("ui"); 4 | 5 | /* stylings */ 6 | var mixins = { 7 | 'centered': { 8 | 'size': [180, 50], 9 | 'justify': 'center' 10 | }, 11 | 'square': { 12 | 'size': [200, 120] 13 | }, 14 | 'help': { 15 | 'helpTip': 'clickerdy click', 16 | }, 17 | 'button': ['centered', 'help'] 18 | }; 19 | 20 | /* structure */ 21 | var dialog = new ui.Dialog('A friendly welcome').with(mixins); 22 | dialog.column('stuff').using('square') 23 | .text('welcome', 'hello there').using('centered') 24 | .button('confirm', 'OK!').using('button'); 25 | 26 | 27 | /* event handlers */ 28 | dialog.stuff.confirm.on('click').do(function () { 29 | this.window.close(); 30 | }); 31 | 32 | dialog.window.show(); -------------------------------------------------------------------------------- /core-packages/utils/lib/date.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc A simple timer. Makes it easy to know how long it takes to execute something. 3 | * Exists as both a static method on ``Date`` 4 | * and a regular method on each ``Function`` object. 5 | * @param {String} format Choose whether the elapsed time should be formatted in 6 | * milliseconds (``ms`` or no argument) or seconds, rounded to two decimals (``s``). 7 | * @default ``ms`` 8 | */ 9 | 10 | exports.timer = { 11 | 'set': function () { 12 | this.start = new Date(); 13 | }, 14 | 'get': function (format) { 15 | var duration = new Date().getTime() - this.start.getTime(); 16 | if (format == 's') { 17 | return (duration/1000).toFixed(2); 18 | } else { 19 | return duration; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /context.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {String} item Can be any one of ``window``, ``doc``, ``page`` or ``spread``. 3 | */ 4 | 5 | // refactor: should pay mind to how InDesign-tied this implementation is, 6 | // probably needs a variant for every CS application 7 | 8 | function current (item) { 9 | var items = { 10 | 'window': app.layoutWindows.item(0), 11 | 'document': undefined, 12 | 'page': undefined, 13 | 'spread': undefined 14 | } 15 | 16 | if (app.documents.length) { 17 | items.merge({ 18 | 'document': app.documents.item(0), 19 | 'page': app.documents.item(0).pages.item(0), 20 | 'spread': app.documents.item(0).spreads.item(0) 21 | }); 22 | } 23 | 24 | if (item in items) { 25 | return items[item]; 26 | } else { 27 | throw RangeError(); 28 | } 29 | } -------------------------------------------------------------------------------- /core-packages/__future__/lib/exceptions/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | TODO 3 | 4 | A couple of things to aid in handling exceptions for end users: 5 | 1. if it's an error a user can do something about (they entered the wrong parameters or something), coders need to be able to 6 | easily throw an alert that points out the mistake (optionally giving users the possibility to continue or cancel the running script) 7 | and may point to additional documentation on- or offline 8 | 2. if it's an internal error, users should just be informed that something went wrong, and be given the possibility to generate 9 | an error report (using the templating module) to send back to the creator (either manually or over http) 10 | 11 | These are optional helpers coders can use or not use at their liberty. 12 | */ -------------------------------------------------------------------------------- /patches/date.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc A simple timer. Makes it easy to know how long it takes to execute something. 3 | * Exists as both a static method on ``Date`` 4 | * and a regular method on each ``Function`` object. 5 | * @param {String} format Choose whether the elapsed time should be formatted in 6 | * milliseconds (``ms`` or no argument) or seconds, rounded to two decimals (``s``). 7 | * @default ``ms`` 8 | */ 9 | 10 | Date.timer = { 11 | 'set': function () { 12 | this.start = new Date(); 13 | }, 14 | 'get': function (format) { 15 | var duration = new Date().getTime() - this.start.getTime(); 16 | if (format == 's') { 17 | return (duration/1000).toFixed(2); 18 | } else { 19 | return duration; 20 | } 21 | } 22 | } 23 | 24 | Function.prototype.timer = Date.timer; -------------------------------------------------------------------------------- /doc/jsdoc.conf: -------------------------------------------------------------------------------- 1 | /* 2 | This is an example of one way you could set up a configuration file to more 3 | conveniently define some commandline options. You might like to do this if 4 | you frequently reuse the same options. Note that you don't need to define 5 | every option in this file, you can combine a configuration file with 6 | additional options on the commandline if your wish. 7 | 8 | You would include this configuration file by running JsDoc Toolkit like so: 9 | java -jar jsrun.jar app/run.js -c=conf/sample.conf 10 | 11 | */ 12 | 13 | { 14 | // document all functions, even uncommented ones 15 | a: true, 16 | 17 | // including those marked @private 18 | p: false, 19 | 20 | // use this directory as the output directory 21 | d: "docs/source", 22 | 23 | // use this template 24 | t: "docs/_templates/rst" 25 | } -------------------------------------------------------------------------------- /doc/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | ======= 4 | License 5 | ======= 6 | 7 | .. literalinclude:: ../LICENSE 8 | 9 | Externally maintained libraries 10 | =============================== 11 | 12 | * Base64 (David Lindquist) (MIT) 13 | 14 | Forked libraries 15 | ================ 16 | 17 | * Jasmine (MIT) (hopefully soon just externally maintained) 18 | * url handling from node.js (MIT) 19 | 20 | Other included code 21 | =================== 22 | 23 | * a couple of Array methods from the Mozilla Developer Network (MIT) 24 | * a couple of Array and Object methods inspired on or taken from Jeremy Ashkenas' underscore.js (MIT) 25 | 26 | Thanks 27 | ====== 28 | 29 | Thanks to Kris Coppieters and Dave Saunders for leading the way in the ExtendScript community. Thanks to Jacob Kaplan-Moss for `teaching me the value of great documentation `_. -------------------------------------------------------------------------------- /examples/files-and-folders.jsx: -------------------------------------------------------------------------------- 1 | #include ../extendables.jsx 2 | 3 | /* 4 | * ``file.at`` / ``folder.at`` works similarly to File#getRelativeURI, 5 | * but returns a new File object instead of a path. 6 | */ 7 | 8 | var documentation = new Folder("doc").at(Folder.extendables); 9 | var here = new File($.fileName); 10 | var examples = here.parent.files(); 11 | var docfiles = documentation.files("*.rst"); 12 | 13 | $.writeln("The Extendables' documentation root directory contains {} files".format(docfiles.length)); 14 | $.writeln("You're currently running the example script '{}'".format(here.component('name'))); 15 | if (here.component('extension') == 'jsx') { 16 | $.writeln("This is an ExtendScript script"); 17 | } 18 | $.writeln("Other examples include: "); 19 | examples.forEach(function (example) { 20 | if (example.name != here.name) $.writeln(" - " + example.component('basename')); 21 | }); -------------------------------------------------------------------------------- /tools/scaffold/test/specs.specs: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc Feel free to name this file any way you like, as long as the .specs extension 3 | * remains intact. You may add as many specs files to the test folder as you wish. 4 | * They will all be picked up by the Extendables test runner. 5 | * 6 | * A specs file is a regular Javascript file, so use any Javascript code you wish. 7 | * Learn more about how to write tests at http://pivotal.github.com/jasmine/ and 8 | * also peruse http://stdbrouw.github.com/Extendables/docs/packages/testing/doc/readme.html 9 | * 10 | * Don't worry about importing the ``describe``, ``it``, ``expect`` et cetera: the 11 | * test runner will make these available for any specs, so you don't have to 12 | * import any of them yourself. 13 | */ 14 | 15 | describe('mymodule', function () { 16 | it('needs proper unit tests', function () { 17 | expect(false).toBeTruthy(); 18 | }); 19 | }); -------------------------------------------------------------------------------- /core-packages/http/lib/browser.jsx: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | 3 | /** 4 | * @description Open a URL in a browser 5 | * @author Gabe Harbs 6 | * @requires CS4+ 7 | */ 8 | function URL(location) { 9 | this.location = location; 10 | } 11 | 12 | /** 13 | * @description Fetch a URL 14 | */ 15 | URL.prototype.open = function () { 16 | return http.get(this.location); 17 | } 18 | 19 | /** 20 | * @description Open a URL in a browser 21 | */ 22 | URL.prototype.visit = function () { 23 | if (File.fs == "Macintosh") { 24 | var body = 'tell application "Finder"\ropen location "{}"\rend tell'.format(this.location); 25 | app.doScript(body,ScriptLanguage.APPLESCRIPT_LANGUAGE); 26 | } else { 27 | var body = 'dim objShell\rset objShell = CreateObject("Shell.Application")\rstr = "{}"\robjShell.ShellExecute str, "", "", "open", 1 '.format(this.location); 28 | app.doScript(body,ScriptLanguage.VISUAL_BASIC); 29 | } 30 | } -------------------------------------------------------------------------------- /doc/patches/index.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Enhancements 3 | ============ 4 | 5 | While Extendables includes a number of enhancements to the DOM objects you'll find in Adobe software, it works most of its magic at the Javascript level: it adds all kinds of methods to ``Array``, ``String``, ``Object``, ``Function`` and other prototypes, allowing for more expressive code with less boilerplate. 6 | 7 | These helper methods range from ``obj.has(property)`` which checks whether an object contains a value for the specified property, to ``array.flatten()`` which flattens a nested array, to ``str.to('lower')`` which lowercases a string. 8 | 9 | Extendables contains a number of array methods for `functional programming `_ which come in handy when filtering and manipulating arrays. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :glob: 14 | 15 | application-specific/* 16 | * -------------------------------------------------------------------------------- /examples/getter-setters.jsx: -------------------------------------------------------------------------------- 1 | function Person () { 2 | this.is_child = false; 3 | 4 | // private attribute 5 | this._age = null; 6 | 7 | this.age = function (value) { 8 | if (value) { 9 | // setter 10 | this._age = value; 11 | if (this._age < 18) { 12 | this.is_child = true; 13 | } 14 | } else { 15 | // getter 16 | if (this._age instanceof Number) { 17 | return "{_age} years old".format(this); 18 | } else { 19 | return this._age; 20 | } 21 | } 22 | } 23 | } 24 | 25 | var timmy = new Person(); 26 | // this sets the person's age 27 | timmy.age(13); 28 | // this returns the person's age 29 | $.writeln(timmy.age()); 30 | // this allows our object to do more than just setting an attribute 31 | $.writeln(timmy.is_child == true); 32 | -------------------------------------------------------------------------------- /core-packages/utils/lib/index.jsx: -------------------------------------------------------------------------------- 1 | exports.object = require("utils/object"); 2 | exports.array = require("utils/array"); 3 | 4 | function mix (cls, mixin, name) { 5 | if (!name) var name = mixin.name; 6 | var type = typeof mixin; 7 | 8 | if (type === 'function') { 9 | cls.prototype[name] = function () { 10 | return mixin.apply(this, [this].concat(Array.prototype.slice.call(arguments))); 11 | } 12 | } else { 13 | cls.prototype[name] = mixin; 14 | } 15 | } 16 | 17 | function extend (cls, patches) { 18 | for (var name in patches) { 19 | if (patches.hasOwnProperty(name)) { 20 | // safer not to replace existing properties 21 | if (!cls.hasOwnProperty(name)) { 22 | mix(cls, patches[name], name); 23 | } 24 | } 25 | } 26 | } 27 | 28 | /* Monkeypatching, for they who so choose */ 29 | function patch () { 30 | extend(Object, exports.object); 31 | extend(Array, exports.array); 32 | } 33 | 34 | exports.mix = mix; 35 | exports.extend = extend; -------------------------------------------------------------------------------- /core-packages/__future__/lib/persistence/engines.jsx: -------------------------------------------------------------------------------- 1 | exports.RESTEngine = function (path) { 2 | this.create = function () {} 3 | 4 | this.read = function () {} 5 | 6 | this.update = function () {} 7 | 8 | this.destroy = function () {} 9 | 10 | this.toString = function () { 11 | return 'RESTEngine for resource {}'.format(path); 12 | } 13 | } 14 | 15 | // we kunnen dit gebruiken om configuraties weg te schrijven 16 | // de veiligste manier om dit te laten werken is om deze engine 17 | // steeds de volledige resource te laten opvragen 18 | exports.FileBasedEngine = function (path) { 19 | this._file = new File(path); 20 | 21 | this.create = function (id) { 22 | } 23 | 24 | this.read = function (id /* optional */) { 25 | 26 | } 27 | 28 | this.update = function (id) { 29 | 30 | } 31 | 32 | this.destroy = function (id /* optional */) { 33 | 34 | } 35 | 36 | this.toString = function () { 37 | return 'FileBasedEngine for resource {}'.format(path); 38 | } 39 | } -------------------------------------------------------------------------------- /examples/arrays.jsx: -------------------------------------------------------------------------------- 1 | #include "../extendables.jsx"; 2 | 3 | var people = [{ 4 | 'first': 'Abraham', 5 | 'last': 'Lincoln', 6 | 'statesman': true, 7 | 'title': 'president' 8 | }, 9 | { 10 | 'first': 'Joe', 11 | 'last': 'Pesci', 12 | 'statesman': false 13 | }, 14 | { 15 | 'first': 'Zed', 16 | 'last': 'Alastair', 17 | 'statesman': false 18 | }]; 19 | 20 | // 21 | var greetings = people.map(function (person) { 22 | return "Hello there {first} {last}, how are you?".format(person); 23 | }); 24 | greetings.forEach(function (greeting) { 25 | alert(greeting); 26 | }) 27 | 28 | // select all people who are statesmen and give 29 | // them an especially warm welcome 30 | people.select(function (person) { 31 | return person.statesman; 32 | }).forEach(function (person) { 33 | if (person.has('title')) { 34 | var title = person.title; 35 | } else { 36 | var title = 'Mr.'; 37 | } 38 | alert("And I'd especially like to welcome {} {}".format(title, person.last)); 39 | }); -------------------------------------------------------------------------------- /examples/test/examples.specs: -------------------------------------------------------------------------------- 1 | describe('Example code', function () { 2 | var examples = new Folder("examples").at(Folder.extendables); 3 | 4 | it('has a working basic code sample', function () { 5 | $.evalFile(new File("basic.jsx").at(examples)); 6 | }); 7 | it('has a working code sample showcasing array functionality', function () { 8 | $.evalFile(new File("arrays.jsx").at(examples)); 9 | }); 10 | it('has a working code sample showcasing methods on File and Folder', function () { 11 | $.evalFile(new File("files-and-folders.jsx").at(examples)); 12 | }); 13 | it('has a working code sample showing the getter/setter design pattern', function () { 14 | $.evalFile(new File("getter-setters.jsx").at(examples)); 15 | }); 16 | it('has a working code sample that constructs a user interface', function () { 17 | $.evalFile(new File("ui.jsx").at(examples)); 18 | }); 19 | it('has a working code sample that does XML traversal', function () { 20 | $.evalFile(new File("xml-traversal.jsx").at(examples)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Extendables is an MIT-licensed developers' framework for Adobe ExtendScript. It is currently unmaintained and there are no guarantees it will continue to work with the latest versions of the Creative Suite and/or ExtendScript. 2 | 3 | Extendables comes with three big blocks of functionality. 4 | 5 | 1. Additional methods on built-in objects like String and Array that give you the Javascript 1.8 features you're used to (think `forEach`), conveniences for functional programming (think `map`, `reduce`, `filter`), easy serialization to JSON or base64 and more. 6 | 2. Additional methods on InDesign DOM objects that make coding in InDesign less verbose. 7 | 3. Packages for logging, unit testing, http, user interface development, and a couple of other goodies. 8 | 9 | If you're writing heavy-duty automations for a Creative Suite app like InDesign, or anything more than just a throwaway script, this is for you. 10 | 11 | Read the documentation at http://debrouwere.github.com/Extendables/docs/ to learn more and get started. 12 | -------------------------------------------------------------------------------- /patches/math.jsx: -------------------------------------------------------------------------------- 1 | Math.sum = function () { 2 | return arguments.to('array').sum(); 3 | } 4 | 5 | /** 6 | * @desc A factory method that creates arithmetic progressions, similar to what you'll find in PHP and Python. 7 | * @returns {Array} 8 | * @example 9 | * > Number.range(10); 10 | * [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 11 | * > Number.range(1, 11); 12 | * [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 13 | * > Number.range(0, 30, 5); 14 | * [0, 5, 10, 15, 20, 25] 15 | * > Number.range(0, 10, 3); 16 | * [0, 3, 6, 9] 17 | * > Number.range(0, -10, -1); 18 | * [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] 19 | * > Number.range(0); 20 | * [] 21 | * > Number.range(1, 0); 22 | * [] 23 | */ 24 | 25 | Number.range = function () { 26 | var step = arguments[2] || 1; 27 | if (arguments.length === 1) { 28 | var from = 0; 29 | var to = arguments[0]; 30 | } else { 31 | var from = arguments[0]; 32 | var to = arguments[1]; 33 | } 34 | 35 | var range = []; 36 | for (var i = from; Math.abs(i) < Math.abs(to); i = i+step) { 37 | range.push(i); 38 | } 39 | return range; 40 | } -------------------------------------------------------------------------------- /core-packages/utils/lib/math.jsx: -------------------------------------------------------------------------------- 1 | exports.sum = function () { 2 | return arguments.to('array').sum(); 3 | } 4 | 5 | /** 6 | * @desc A factory method that creates arithmetic progressions, similar to what you'll find in PHP and Python. 7 | * @returns {Array} 8 | * @example 9 | * > Number.range(10); 10 | * [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 11 | * > Number.range(1, 11); 12 | * [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 13 | * > Number.range(0, 30, 5); 14 | * [0, 5, 10, 15, 20, 25] 15 | * > Number.range(0, 10, 3); 16 | * [0, 3, 6, 9] 17 | * > Number.range(0, -10, -1); 18 | * [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] 19 | * > Number.range(0); 20 | * [] 21 | * > Number.range(1, 0); 22 | * [] 23 | */ 24 | 25 | Number.range = function () { 26 | var step = arguments[2] || 1; 27 | if (arguments.length === 1) { 28 | var from = 0; 29 | var to = arguments[0]; 30 | } else { 31 | var from = arguments[0]; 32 | var to = arguments[1]; 33 | } 34 | 35 | var range = []; 36 | for (var i = from; Math.abs(i) < Math.abs(to); i = i+step) { 37 | range.push(i); 38 | } 39 | return range; 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011 Stijn Debrouwere 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /doc/patches/application-specific/indesign.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | InDesign enhancements 3 | ===================== 4 | 5 | .. contents:: 6 | 7 | XML traversal 8 | ============= 9 | 10 | Extendables includes a number of shortcuts methods on :func:`XMLElement` that make XML traversal less verbose. 11 | 12 | .. note:: 13 | 14 | For handling straight-up XML (not InDesign-aware XMLElement objects), 15 | use the built-in XML support in ExtendScript 16 | 17 | Here's an example to get you started: 18 | 19 | .. literalinclude:: ../../../examples/xml-traversal.jsx 20 | :language: extendscript 21 | 22 | ``XMLElement`` 23 | -------------- 24 | 25 | .. include:: ../../../patches/doc/jsdoc/XMLElement.rst 26 | :start-after: class-methods 27 | 28 | Handy helpers 29 | ============= 30 | 31 | .. include:: ../../../patches/doc/jsdoc/Page.rst 32 | :start-after: class-title 33 | 34 | .. include:: ../../../patches/doc/jsdoc/LayoutWindow.rst 35 | :start-after: class-title 36 | 37 | Placing library assets 38 | ====================== 39 | 40 | .. include:: ../../../patches/doc/jsdoc/Asset.rst 41 | :start-after: class-title 42 | 43 | -------------------------------------------------------------------------------- /core-packages/logging/doc/readme.rst: -------------------------------------------------------------------------------- 1 | =============================================== 2 | ``logging``: a logging utility for ExtendScript 3 | =============================================== 4 | 5 | Logging is useful when debugging (nobody likes code interspersed with ``alert()`` statements). Logging is also beneficial in production settings — if something goes wrong, good logs will make it easy to know what happened and how to remedy the problem. 6 | 7 | Extendables comes with a logging library, loosely inspired on the eponymous `Python library `_. 8 | 9 | Initialize a log using ``var log = new logging.Log("logfile.log");``. 10 | 11 | After that, you can log messages using ``log.debug(msg)``, ``log.info(msg)``, ``log.warning(msg)``, ``log.error(msg)`` and ``log.critical(msg)``. 12 | 13 | All logs reside in the ``log`` directory under the Extendables root. 14 | 15 | Log levels 16 | ========== 17 | 18 | ===== ======== 19 | level meaning 20 | ===== ======== 21 | 0 NOTSET 22 | 1 CRITICAL 23 | 2 ERROR 24 | 3 WARNING 25 | 4 INFO 26 | 5 DEBUG 27 | ===== ======== 28 | 29 | .. include:: jsdoc/Log.rst -------------------------------------------------------------------------------- /patches/test/conversions.specs: -------------------------------------------------------------------------------- 1 | describe('Patches: object serialization', function () { 2 | it('can serialize key-value pairs', function () { 3 | var obj = {'key1': 'value1', 'key2': 'value2'} 4 | var str = obj.serialize('key-value', {'separator': ': ', 'eol': '\n'}); 5 | expect(str).toEqual("key1: value1\nkey2: value2\n"); 6 | }); 7 | 8 | it('can deserialize key-value pairs', function () { 9 | var kv = "key1: value1\nkey2: value2\n"; 10 | var obj = kv.deserialize('key-value', {'separator': ': ', 'eol': '\n'}); 11 | expect(obj.key1).toEqual('value1'); 12 | expect(obj.key2).toEqual('value2'); 13 | }); 14 | 15 | it('can base64-encode utf8 data', function () { 16 | expect('tested').toEqual(false); 17 | }); 18 | 19 | it('can base64-encode binary data', function () { 20 | expect('tested').toEqual(false); 21 | }); 22 | }); 23 | 24 | describe('Patches: object conversions', function () { 25 | it('can do a number of object conversions', function () { 26 | expect("§This 1 works!!!".to('alphanumeric')).toEqual("This 1 works"); 27 | }); 28 | }); -------------------------------------------------------------------------------- /doc/patches/error.rst: -------------------------------------------------------------------------------- 1 | .. _error-methods: 2 | 3 | ============== 4 | Error handling 5 | ============== 6 | 7 | ----------------- 8 | ``Error`` methods 9 | ----------------- 10 | 11 | .. contents:: 12 | 13 | .. include:: ../../patches/doc/jsdoc/Error.rst 14 | :start-after: class-methods 15 | 16 | ----------- 17 | Error types 18 | ----------- 19 | 20 | .. include:: ../../patches/doc/jsdoc/ArithmeticError.rst 21 | 22 | .. include:: ../../patches/doc/jsdoc/EnvironmentError.rst 23 | 24 | .. include:: ../../patches/doc/jsdoc/EvalError.rst 25 | 26 | .. include:: ../../patches/doc/jsdoc/ImportError.rst 27 | 28 | .. include:: ../../patches/doc/jsdoc/IOError.rst 29 | 30 | .. include:: ../../patches/doc/jsdoc/NotImplementedError.rst 31 | 32 | .. include:: ../../patches/doc/jsdoc/ParseError.rst 33 | 34 | .. include:: ../../patches/doc/jsdoc/RangeError.rst 35 | 36 | .. include:: ../../patches/doc/jsdoc/ReferenceError.rst 37 | 38 | .. include:: ../../patches/doc/jsdoc/SyntaxError.rst 39 | 40 | .. include:: ../../patches/doc/jsdoc/SystemError.rst 41 | 42 | .. include:: ../../patches/doc/jsdoc/TypeError.rst 43 | 44 | .. include:: ../../patches/doc/jsdoc/ValidationError.rst -------------------------------------------------------------------------------- /extendables.jsx: -------------------------------------------------------------------------------- 1 | // we need to log somethings before the log module is loaded, 2 | // so we buffer these messages 3 | var log_buffer = []; 4 | 5 | #include "patches/__all__.jsx"; 6 | var default_settings = new File("settings.jsx").at(Folder.extendables); 7 | var project_specific_settings = new File("settings.jsx").at(Folder.extendables.parent); 8 | if (project_specific_settings.exists) { 9 | // allows for project-specific settings, so nobody 10 | // has to override anything within /extendables 11 | // (this feature is currently undocumented) 12 | log_buffer.push([4, "Loading Extendables with project-specific settings at {}", project_specific_settings]); 13 | $.evalFile(project_specific_settings); 14 | } else { 15 | log_buffer.push([4, "Loading Extendables with default settings"]); 16 | $.evalFile(default_settings); 17 | } 18 | #include "loader.jsx"; 19 | load_modules(settings.package_directories); 20 | #include "context.jsx"; 21 | 22 | // write away buffered log messages 23 | 24 | var logging = require("logging"); 25 | var syslog = new logging.Log("extendables.log"); 26 | 27 | log_buffer.forEach(function (message) { 28 | syslog.log.apply(null, message); 29 | }); -------------------------------------------------------------------------------- /tools/scaffold/lib/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc This is a new module scaffold. Replace this description with a 3 | * short summary of what this module does. 4 | * 5 | * You can make anything from within this module available to scripts 6 | * by adding it to the ``exports`` variable, e.g. 7 | * 8 | * // in this module: 9 | * exports.MyClass = function MyClass () { } 10 | * 11 | * // in your scripts: 12 | * var MyClass = require("thismodule").MyClass; 13 | * 14 | * All modules have access to two global variables, outside of what 15 | * ExtendScript offers by itself: ``exports`` and ``module``, which 16 | * contains the module id and its path. 17 | * 18 | * __core__.jsx serves as the main module. You may add other files in this directory. 19 | * They will be available as submodules, and can be accessed using e.g. 20 | * ``require("thismodule/submod")`` 21 | * 22 | * Note that modules may require other modules as well as submodules, 23 | * so avoid using #include wherever possible. 24 | * 25 | * If you're unsure as to how to proceed, check Extendables' built-in modules. 26 | * Those modules will give you a better feel for how things work. 27 | */ 28 | -------------------------------------------------------------------------------- /core-packages/__future__/lib/preview/idea.txt: -------------------------------------------------------------------------------- 1 | It'd be cool to have some functionality that makes it easy to preview a layout change, without actually executing it -- that way, all kinds of plugins that help with layouting could show the results of what they'd do before actually doing it. 2 | 3 | * a highlighter: when e.g. selecting a layer or a PageItem in a dialog, switching the screen mode (LayoutWindow.screenMode) to 'normal' (i.e. with guides and item box contours visible) and blinking that item or each item on that layer. 4 | * a previewer: clones all the layers on a page (e.g. baselayer -> preview.baselayer), hides the original ones, and executes changes on those preview layers, which can be destroyed at will. Works like a wrapper on the page object, so should be transparent to whichever code should execute. Its only two (own) methods should be: 5 | - a show() method which makes the preview the active page. 6 | - a destroy() method that destroys the preview layers and 7 | unhides the original ones. 8 | 9 | Note that, in some cases, a simple app.undo() would work better, and that the previewer class does not prevent people from doing all sorts of things that wouldn't get rolled back on destroy(), like whenever they modify something without going through the preview object. -------------------------------------------------------------------------------- /core-packages/utils/lib/folder.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @name Folder 4 | */ 5 | 6 | /** 7 | * @desc The extendables base directory. Other notable class properties 8 | * include ``current``, ``desktop``, ``userData``, ``temp`` and ``trash``. 9 | */ 10 | 11 | exports.extendables = new File($.fileName).parent.parent; 12 | 13 | /** 14 | * @function 15 | * @desc Get a file or folder starting from an existing path. 16 | * A foolproof way to join paths together. 17 | * 18 | * Similar to ``File#getRelativeURI``, but returns a new Folder object 19 | * instead of a path. 20 | */ 21 | 22 | exports.at = require("utils/file").at; 23 | 24 | /** 25 | * @desc Works just like ``Folder#getFiles``, but returns only files, not folders. 26 | * @param {String|Function} [mask] 27 | */ 28 | 29 | exports.files = function (self, mask) { 30 | return self.getFiles(mask).reject(function (file_or_folder) { 31 | return file_or_folder.is(Folder); 32 | }); 33 | } 34 | 35 | /** 36 | * @desc Works just like ``Folder#getFiles``, but returns only folders, not files. 37 | * @param {String|Function} [mask] 38 | */ 39 | 40 | exports.folders = function (self, mask) { 41 | return self.getFiles(mask).reject(function (file_or_folder) { 42 | return file_or_folder.is(File); 43 | }); 44 | } -------------------------------------------------------------------------------- /doc/core/changes.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 0.1 (2009) 6 | ========== 7 | 8 | * Initial, internal helper library, with helpers for http and logging. 9 | 10 | 0.2 (09/2010) 11 | ============= 12 | 13 | A huge transformation from some helper libraries into a small but solid unit-tested framework. Also the first (silent) public release on GitHub. 14 | 15 | * Hugely refactored the helper library codebase into a framework with a module loader, monkeypatches, assorted modules. 16 | * Added in ``io``, ``templating``, ``testing`` and ``ui`` as Extendables packages. 17 | * Complete overhaul of the ``http`` module to support a subset of HTTP 1.1. 18 | 19 | 0.3 (1/11/2010) 20 | =============== 21 | 22 | The first publicized release, including a minimalistic website. This release considerably fleshes out the unit tests and the documentation. 23 | 24 | * Added support for partials in the ``templating`` module. 25 | * Added support for named placeholders to ``String#format``. 26 | * Added in File and Folder monkeypatches, and removed the ``path`` module. 27 | * Improvements to ``XMLElement#repr()`` 28 | * Added a ``Number.range()`` class method. 29 | 30 | 0.4 (pending) 31 | ============= 32 | 33 | * Further improved the documentation 34 | * Improvements to the UI module -------------------------------------------------------------------------------- /core-packages/io/test/io.specs: -------------------------------------------------------------------------------- 1 | describe('IO: octals', function () { 2 | var ByteString = require("io/octals").ByteString; 3 | var utf = "üàërhaupt"; 4 | var bstring = new ByteString(utf); 5 | 6 | it('has a ByteString implementation that can count bytes, not characters', function () { 7 | expect(utf.length).toEqual(9); 8 | expect(bstring.length).toEqual(12); 9 | }); 10 | 11 | it('works with the funkiest of utf8 characters', function () { 12 | expect(ByteString.count_bytes("ü")).toEqual(2); 13 | expect(ByteString.count_bytes("₯")).toEqual(3); 14 | }); 15 | 16 | it('can take an array of bytes as input, which speeds up performance', function () { 17 | 18 | var new_str = new ByteString(bstring.as_bytearray()); 19 | expect(bstring.as_bytearray()).toEqual(new_str.as_bytearray()); 20 | }); 21 | 22 | it('has the usual string methods on ByteString: slice', function () { 23 | expect(bstring.slice(2, 6).toString()).toEqual("àë"); 24 | }); 25 | 26 | it('has the usual string methods on ByteString: substr', function () { 27 | expect(bstring.substr(2, 4).toString()).toEqual("àë"); 28 | }); 29 | 30 | it('has the usual string methods on ByteString: indexOf', function () { 31 | expect(bstring.indexOf("ë")).toEqual(5); 32 | }); 33 | }); -------------------------------------------------------------------------------- /patches/dom.application.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @name Application 4 | * @desc An instance of this class is available as ``app`` in every Adobe 5 | * application with an ExtendScript engine. 6 | */ 7 | 8 | /** 9 | * @desc Check the host app. 10 | * @param {String} application The application name. Case-insensitive. 11 | * @param {String|Number} [version] 12 | * The application version number. Add two to your CS version number. 13 | * or pass in the version number as a string prefixed with 'CS', like ``app.is('indesign', 'CS5')``. 14 | * 15 | * @example 16 | * alert(app.is('toolkit')); // any version 17 | * alert(app.is('indesign', 'CS2')); // Creative Suite 2 18 | * alert(app.is('indesign', 4)); // Creative Suite 2 19 | * alert(app.is('indesign', '6.0')); // Creative Suite 4.0 20 | */ 21 | 22 | Application.prototype.is = function (application, version) { 23 | if (version && version.to('lower').contains('cs')) { 24 | if (!application.contains('toolkit')) { 25 | version = version.replace(/cs/gi, "").to('int') + 2; 26 | } 27 | } 28 | var version = version || this.version; 29 | var is_app = this.name.to('lower').contains(application.to('lower')); 30 | var is_version = this.version.to('string').startswith(version); 31 | return is_app && is_version; 32 | } -------------------------------------------------------------------------------- /core-packages/logging/test/logging.specs: -------------------------------------------------------------------------------- 1 | describe('logging', function () { 2 | var logging = require("logging"); 3 | 4 | it('exports a Log constructor', function () { 5 | expect(logging.Log).toBeDefined(); 6 | }); 7 | 8 | it('logs to the log directory', function () { 9 | var log = new logging.Log("test.logging.log"); 10 | if (log.file.exists) log.file.remove(); 11 | expect(log.file.exists).toEqual(false); 12 | log.critical("Just a drill."); 13 | expect(log.file.exists).toEqual(true); 14 | }); 15 | 16 | it('can truncate a log file', function () { 17 | var log = new logging.Log("test.logging.log"); 18 | log.truncate(true); 19 | expect(log.file.length).toEqual(0); 20 | }); 21 | 22 | it('only logs messages under a set log level', function () { 23 | var log = new logging.Log("test.logging.log", 3); 24 | log.truncate(true); 25 | log.debug("Shouldn't."); 26 | log.info("Shouldn't."); 27 | log.warning("Should."); 28 | log.error("Should."); 29 | log.critical("Should."); 30 | log.file.open("r"); 31 | var logs = log.file.read(); 32 | log.file.close(); 33 | expect(logs.contains("Should.")).toBeTruthy(); 34 | expect(logs.contains("Shouldn't.")).toBeFalsy(); 35 | }); 36 | }); -------------------------------------------------------------------------------- /core-packages/http/lib/auth.jsx: -------------------------------------------------------------------------------- 1 | // this is just a rough prototype, doesn't actually work yet. 2 | 3 | exports.AuthenticationDialog = AuthenticationDialog; 4 | 5 | var ui = require("ui"); 6 | 7 | function AuthenticationDialog (request) { 8 | var mixins = { 9 | 'masked': {} 10 | } 11 | // 1. dialog 12 | var dialog = this.dialog = new ui.Dialog().with(mixins); 13 | dialog.row('username').text('label', 'Username').input('value', '').using('masked'); 14 | dialog.row('password').text('label', 'Username').input('value', '').using('masked'); 15 | dialog.row('confirmation').text('status', '').button('cancel', 'Cancel').button('ok', 'Authenticate'); 16 | 17 | dialog.confirmation.cancel.on('click').do(function () { 18 | this.window.close(); 19 | }); 20 | 21 | dialog.confirmation.ok.on('click').do(function () { 22 | request.auth.basic(username, password); 23 | this.window.close(); 24 | }); 25 | 26 | dialog.confirmation.status.update = function (message) { 27 | this.dialog.confirmation.status.text = message; 28 | } 29 | 30 | this.do = function () { 31 | this.dialog.show(); 32 | return request.do(); 33 | } 34 | 35 | 36 | } 37 | 38 | /* 39 | usage example: 40 | 41 | while (response.status == 401) { 42 | var response = new AuthenticationDialog(request).do(); 43 | // retry login 44 | dialog.confirmation.status.update("Authentication failed. Please try again."); 45 | } 46 | */ -------------------------------------------------------------------------------- /core-packages/utils/lib/file.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @name File 4 | */ 5 | 6 | function from_basepath 7 | 8 | /** 9 | * @function 10 | * @desc Get a file or folder starting from an existing path. 11 | * A foolproof way to join paths together. 12 | * 13 | * Similar to ``File#getRelativeURI``, but returns a new File object 14 | * instead of a path. 15 | */ 16 | 17 | exports.at = function (self, folder) { 18 | if (folder.is(String)) folder = new Folder(folder); 19 | 20 | var path = [folder.relativeURI, self.relativeURI].join('/'); 21 | return new self.constructor(path); 22 | }; 23 | 24 | /** 25 | * @desc Easy extraction of path, name, basename and extension from a 26 | * :func:`File` object. 27 | * @param {String} type ``path``, ``name``, ``basename`` or ``extension`` 28 | */ 29 | 30 | exports.component = function (self, type) { 31 | switch (type) { 32 | case 'path': 33 | return self.path; 34 | break; 35 | case 'name': 36 | return self.name; 37 | break; 38 | case 'basename': 39 | var extlen = self.component('extension').length; 40 | if (extlen) { 41 | return self.name.slice(0, -1 * extlen).rtrim('.'); 42 | } else { 43 | return self.name; 44 | } 45 | break; 46 | case 'extension': 47 | var name = self.name.split('.'); 48 | if (name.length > 1) { 49 | return name.last(); 50 | } else { 51 | return ''; 52 | } 53 | break; 54 | } 55 | } -------------------------------------------------------------------------------- /examples/basic.jsx: -------------------------------------------------------------------------------- 1 | #include "../extendables.jsx"; 2 | 3 | // namespaces, so there are no variables all over the place 4 | var ui = require("ui"); 5 | var http = require("http"); 6 | 7 | function fetch_recipes (amount) { 8 | if (!http.has_internet_access()) throw new Error("No internet, no recipes."); 9 | // range returns an array containing the asked range of numbers, 10 | // and each number gets mapped to / replaced by a recipe 11 | return Number.range(amount).map(function () { 12 | var response = http.get("http://whatthefuckshouldimakefordinner.com/"); 13 | // the first link on the page is a recipe; 14 | // don't do this at home -- parsing html with 15 | // regular expressions is evil 16 | return response.body.match(/(.+)<\/a>/)[1]; 17 | }); 18 | } 19 | 20 | // the UI library allows us to separate style from structure, 21 | // leading to cleaner code 22 | var styling = { 23 | 'big': { 24 | 'size': [400, 15], 25 | 'justify': 'center' 26 | } 27 | } 28 | var dialog = new ui.Dialog("I'm hungry").with(styling); 29 | var suggestion = dialog.row('suggestion'); 30 | dialog.text('food', 'Want some food suggestions?').using('big') 31 | .button('ok', 'Sure thing!') 32 | .button('no', 'No thanks'); 33 | 34 | // event handlers, the easy way 35 | dialog.ok.on('click').do(function(){ 36 | dialog.food.text = "Wait a sec, coming up!"; 37 | dialog.food.text = fetch_recipes(1).first(); 38 | dialog.ok.text = "Hmm..."; 39 | }); 40 | dialog.no.on('click').do(function(){ 41 | this.window.close(); 42 | }); 43 | 44 | // let's get started 45 | dialog.window.show(); -------------------------------------------------------------------------------- /doc/core/roadmap.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | A roadmap 3 | ========= 4 | 5 | Extendables is strictly scratch-your-own-itch material, so there's no strict roadmap, but here are a couple of things under consideration for future releases. No promises. 6 | 7 | * a persistence module that would make it easy to persist objects either in an external database (using a REST interface) or locally in a serialized file. 8 | * improvements to the UI library, making it possible to apply a wider range of stylings than is currently possible. 9 | * a preview module, providing an easy way for scripts to show a preview of the action they're about to undertake, without actually executing it (permanently). Similar to how the ``preview`` button works in many native Adobe dialogs. 10 | * more consistent error handling across the framework (proper error types, limited try/catches, throwing errors instead of returning false or simply crashing, exception logging) 11 | * an exceptions module that provides a generic, standard way of alerting end-users when something went wrong, and which can create error reports for developers. 12 | * enhancements to the UI module (more styling options) 13 | 14 | Good documentation will remain a priority: 15 | 16 | * Tie in monkeypatch documentation with the existing Adobe DOM docs (user's choice of DOM version) so there's a single authoritative source of documentation. We don't want users to constantly have to switch between Extendables' documentation, the Object Model Viewer and the Javascript Tools Guide. The UI should be similar to the Object Model Viewer. 17 | * A separate project containing best practices that suggest standardized ways of handling certain problems, as well as a getting started guide for people new to ExtendScript. -------------------------------------------------------------------------------- /examples/xml-traversal.jsx: -------------------------------------------------------------------------------- 1 | #include "../extendables.jsx"; 2 | 3 | if (!app.is('indesign') { 4 | throw new EnvironmentError("This script only works in Adobe InDesign"); 5 | } 6 | 7 | // create a new document 8 | app.activeDocument = app.documents.add(); 9 | Number.range(10).forEach(function () { 10 | current('document').pages.add(); 11 | } 12 | 13 | // create a B-culture master 14 | current('document').masterSpreads.add(1, {'namePrefix': 'B', 'name': 'culture'}); 15 | 16 | var doc = current('document'); 17 | doc.importXML('stories.xml'); 18 | // q: what with multiple stories, does it return all of them as you'd expect? 19 | // var story = doc.xml().find('story'); 20 | 21 | // Apply the 'B-culture' master page if to page 5 if that page doesn't 22 | // derive from a master yet. 23 | // Page#master and LayoutWindow#page are both getter/setters. 24 | current('window').page(5); 25 | if (!current('page').master()) { 26 | current('page').master('B-culture'); 27 | } 28 | 29 | var frames = current('page').textFrames; 30 | 31 | // layout a page with all the story titles from the culture section 32 | doc.xml().children().forEach(function (story) { 33 | // attr returns the value for the specified attribute on an element, 34 | // in this case 'culture'. 35 | if (story.attr('section') == 'culture') { 36 | // val returns the value of an element, in this case the title 37 | var title = story.find('title').val(); 38 | var frame = frames.add(undefined, undefined, undefined, {'contents': title}); 39 | frame.move(undefined, [0, 10]); 40 | story.attr('processed', new Date().getTime()); 41 | } 42 | }); 43 | 44 | var pageitemtag = doc.pageItems.item(0).tag(); 45 | var xmltag = doc.xml().find(0).tag(); 46 | if (pageitemtag == xmltag) { 47 | "Associated page item 0 with a {} xml element.".format(xmltag).to_console(); 48 | } -------------------------------------------------------------------------------- /test/loader.specs: -------------------------------------------------------------------------------- 1 | __modules__ = {}; 2 | load_modules(['./test/fixtures/core-packages']); 3 | 4 | describe('Extendables module loader', function () { 5 | it('can determine the difference between a valid module and other files/folders', function () { 6 | // currently also registers empty modules, but that's pretty harmless. 7 | expect("package-b" in __modules__).toEqual(true); 8 | expect("nonpackage-b" in __modules__).toEqual(false); 9 | }); 10 | it('can fetch test specs from modules', function () { 11 | expect(__modules__["package-b"].get_tests().length).toEqual(1); 12 | expect(__modules__["package-b"].get_tests()[0].absoluteURI.endswith("testspecs.specs")).toEqual(true); 13 | }); 14 | it('can handle submodule files', function () { 15 | expect(require("package-b/real").this_is).toEqual('Package B submodule'); 16 | }); 17 | it('can handle submodule directories', function () { 18 | expect(require("package-b/subpackage").this_is).toEqual('Package B sub-submodule'); 19 | }); 20 | // see fixtures 21 | it('provides a context for each module containing an exports object, \ 22 | the module id and module uri', function () { 23 | var b = __modules__["package-b"].load().exports; 24 | expect(b.id).toEqual("index"); 25 | expect(b.uri.contains("package-b")).toEqual(true); 26 | }); 27 | it('is entirely CommonJS-compliant', function () { 28 | // our .id implementation is currently incorrect 29 | var b = __modules__["package-b"].load().exports; 30 | expect(b.id).toEqual("package-b"); 31 | }); 32 | it('has a require function that works according to the CommonJS specs', function () { 33 | var b = require("package-b"); 34 | expect(b.this_is).toEqual('Package B core'); 35 | }); 36 | it('has an extract function that can extract a module into the global namespace', function () { 37 | extract("package-b/real"); 38 | expect(this_is).toBeDefined(); 39 | }); 40 | }); -------------------------------------------------------------------------------- /core-packages/http/doc/readme.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | ``http``: sending and receiving http requests 3 | ============================================= 4 | 5 | The ``http`` library allows scripts to fetch and push data from and to the internet. It is a high-level interface with the ``Socket`` object. The library implements a good subset of the **HTTP 1.1** protocol. 6 | 7 | .. code-block:: extendscript 8 | 9 | #include "extendables/extendables.jsx"; 10 | var http = require("http"); 11 | var response = http.get("http://www.w3c.org") 12 | if (response.status_code == 200) { 13 | $.writeln(response.body); 14 | } else { 15 | $.writeln("Connection failed"); 16 | } 17 | 18 | Aside from high-level functions like ``get`` and ``post``, there's also a lower-level interface if you happen to need more flexibility. 19 | 20 | .. code-block:: extendscript 21 | 22 | var req = new http.HTTPRequest("GET", "http://nytimes.com"); 23 | req.follow_redirects(false); 24 | var timeout = req.timeout(); 25 | req.timeout(10); 26 | $.writeln("Changing timeout from {} to {} seconds".format(timeout, 10)); 27 | req.header("User-Agent", "My ExtendScript app"); 28 | var res = req.do(); 29 | $.writeln(res.status == 200); 30 | 31 | The ``Socket`` object is available in Adobe **Bridge**, Adobe **InDesign**, Adobe **InCopy**, Adobe **After Effects** and Adobe **Photoshop**, and you may also use it in the ExtendScript Toolkit. No luck for Illustrator fiends. 32 | 33 | Basic requests 34 | ============== 35 | 36 | .. include:: jsdoc/_global_.rst 37 | :start-after: class-methods 38 | 39 | Request objects 40 | =============== 41 | 42 | .. include:: jsdoc/HTTPRequest.rst 43 | :start-after: class-title 44 | 45 | Response objects 46 | ================ 47 | 48 | .. include:: jsdoc/HTTPResponse.rst 49 | :start-after: class-title -------------------------------------------------------------------------------- /core-packages/templating/lib/index.jsx: -------------------------------------------------------------------------------- 1 | exports.Template = Template; 2 | 3 | function Template (path, for_module) { 4 | var self = this; 5 | var template_file = new File(path).at("templates").at(new File(for_module.uri).parent.parent); 6 | if (!template_file.exists) { 7 | throw IOError("Couldn't open template {}".format(template_file)); 8 | } 9 | template_file.open("r"); 10 | this.template = template_file.read(); 11 | template_file.close(); 12 | 13 | this._output = false; 14 | 15 | this.process_partials = function (replacement_obj) { 16 | var out = self.template; 17 | var partial_syntax = new RegExp(/\{(\S+) => (\S+)\}/g); 18 | var matches = self.template.match(partial_syntax); 19 | if (!matches) return out; 20 | var replacements = matches.map(function (match) { 21 | var partial = partial_syntax.exec(self.template); 22 | var name = partial[1]; 23 | var obj = replacement_obj[name]; 24 | var path = partial[2]; 25 | // if we're dealing with an array, loop through it 26 | if (obj.is(Array)) { 27 | var output = obj.map(function (el) { 28 | return new Template(path, for_module).render(el); 29 | }).join(''); 30 | } else { 31 | var output = new Template(path, for_module).render(obj); 32 | } 33 | return {'from': match, 'to': output}; 34 | }); 35 | 36 | replacements.forEach(function (replacement) { 37 | out = out.replace(replacement.from, replacement.to); 38 | }); 39 | 40 | return out; 41 | } 42 | 43 | this.render = function () { 44 | // partials 45 | self._output = self.process_partials(arguments[0]); 46 | // string formatting 47 | self._output = self._output.format.apply(self._output, arguments); 48 | return self._output; 49 | } 50 | 51 | this.write_to = function (path) { 52 | var out = new File(path).at("log").at(Folder.extendables); 53 | if (this._output) { 54 | out.open("w"); 55 | out.write(this._output); 56 | out.close(); 57 | } else { 58 | throw new Error("There's no output to write. Did you call the render method first?"); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /core-packages/templating/doc/readme.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | ``templating``: minimalistic output templates 3 | ============================================= 4 | 5 | Templating is a small module that can write files according to a template. It acts as an abstraction on top of ``str.format``, substituting named and unnamed placeholders (a.k.a. curly braces) with the values you pass in. It is currently hardcoded to search for templates in the parent module's ``templates`` directory (if any) and it writes those away to the ``log`` dir. 6 | 7 | Usage: 8 | 9 | .. code-block: javascript 10 | 11 | var Template = require("templating").Template; 12 | // provided your module lives in a ``lib`` folder, will refer to ../templates/hello.txt 13 | // module is a global variable available to all modules 14 | var tpl = new Template("template.hello.txt", module) 15 | tpl.render("some", "replacements", "for placeholders"); 16 | // look for this file in the log dir 17 | tpl.write_to("hello.txt"); 18 | 19 | Aside from string formatting, ``templating`` also supports `partials `_. 20 | 21 | .. code-block: 22 | /* rendering */ 23 | var class = { 24 | 'room': 505, 25 | 'students': [{'name': 'Prez'}, {'name': 'Billy'}, {'name': 'Satchmo'}] 26 | } 27 | var tpl = new Template("class.txt", module).render(class); 28 | 29 | /* class.txt */ 30 | classroom: {room} 31 | {students => student.txt} 32 | 33 | /* student.txt */ 34 | * {name} 35 | 36 | // will output: 37 | classroom: 505 38 | * Prez 39 | * Billy 40 | * Satchmo 41 | 42 | You don't need to pass an array to a partial. If you pass an object, it'll just run through the partial template once as if you'd passed in a one-element array. 43 | 44 | .. note:: 45 | 46 | There are no plans to expand on this module, though it might become more potent if ``str.format`` does. Its sole use is to provide HTML output for the :ref:`testing ` module. Don't use this if you need more than the basics; try something like Mustache instead. -------------------------------------------------------------------------------- /doc/about.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | About this project 3 | ================== 4 | 5 | History 6 | ======= 7 | 8 | - me: doing a lot of automation work, but code looked way dirtier than it should, and way dirtier than it did when I work in Django -- but because of Javascript its flexibility, it's actually doable to give coding in ES more of a natural feel. 9 | - ExtendScript has been around for long, but now that javascript is in the spotlight as a viable all-round language rather than just a simple scripting tool for browsers, there's a cornucopia of usable code being released, standards being set and best practices being shared. So exactly the right time to give ExtendScripters the opportunity to share in the fun. 10 | 11 | Why is this free? 12 | ================= 13 | 14 | The ExtendScript community has a long history of both sharing and of trying to make an honest buck. This project tries to collect some of the common wisdom and approaches out there. It hopes to give ExtendScript coders a more meaningful and more permanent way in which they can share code, under the guiding principle that a rising tide lifts all boats. 15 | 16 | A solid development framework for ExtendScript should allow every one of us to do our jobs better and more quickly, and, yes, make some money off of the end products. For that reason, this framework comes with the very liberal MIT license, allowing it to be used in commercial projects without worry. 17 | 18 | Design considerations 19 | ===================== 20 | 21 | Proudly found elsewhere 22 | ----------------------- 23 | 24 | Re-use as much stuff from the CommonJS / server-side javascript sphere as possible, both ideas and actual code. However, often not possible to include stuff as straight CommonJS modules because they depend on a specific implementation (e.g. node.js modules that depend on C extensions we obviously can't use) or because the ExtendScript interpreter works slightly different than Spidermonkey or v8. So, while there are a couple of straight dependencies (in the `dependencies` directory), other elements of Extendables are either forks of other code (e.g. the `path` module derives from node.js) or are simply inspired by work and approaches in other languages, particularly Python, but with some Ruby influences as well. Yet other modules, are, of course, built from scratch, like HTTP and UI. 25 | 26 | -------------------------------------------------------------------------------- /patches/dom.suite.jsx: -------------------------------------------------------------------------------- 1 | if (typeof(Submenu) !== 'undefined') { 2 | Submenu.prototype.get_or_add = function (menu_title) { 3 | var item = this.menuItems.item(menu_title); 4 | 5 | // refactor: probably better, though untested: item == null 6 | if (!item.hasOwnProperty('title')) { 7 | var action = app.scriptMenuActions.add(menu_title); 8 | item = this.menuItems.add(action); 9 | } 10 | 11 | return item; 12 | } 13 | } 14 | 15 | /* 16 | var actions = app.scriptMenuActions; 17 | var menu = app.menus.item('$ID/RtMouseLayout'); 18 | var item = actions.add("Send feedback to editor"); 19 | var opt = menu.menuItems.add(item, LocationOptions.AT_BEGINNING); 20 | var sep = menu.menuSeparators.add(LocationOptions.AFTER, opt); 21 | 22 | var boo = function () { 23 | alert("harro"); 24 | } 25 | 26 | item.eventListeners.add("onInvoke", boo); 27 | */ 28 | 29 | /* 30 | var main = app.menus.item("$ID/Main"); 31 | var pubtalk = main.submenus.add("something"); 32 | // clear everything inside the Pubtalk menu, we want a fresh start 33 | pubtalk.menuElements.everyItem().remove(); 34 | 35 | // submenus 36 | var configuration = pubtalk.submenus.add("something below something"); 37 | 38 | // actions 39 | var todo = configuration.get_or_create("(not implemented yet)"); 40 | var run_script = pubtalk.get_or_create("Run a cool script"); 41 | 42 | // event handlers 43 | run_script.associatedMenuAction.eventListeners.add("onInvoke", function () { 44 | app.doScript(new File("script.jsx").at("wherever)); 45 | }); 46 | */ 47 | 48 | /* 49 | IDEAS / VAGUE THOUGHTS: 50 | - Maybe we should probably wrap this stuff in a similar way to the UI framework. 51 | That way, we can keep a registry of user-added menu items, in case we need 'em 52 | removed or need to change them later in the script. 53 | 54 | */ 55 | 56 | // note: this would probably work for xmlElement#children as well 57 | // and maybe for other collections too 58 | // perhaps a Collection wrapper class would be handy in that case, to homogenize 59 | // how we handle collections and to be able to handle them in a more Array-like way? 60 | 61 | /* 62 | var menus = app.menus.item("Main").submenus; 63 | var menus = app.menus; 64 | var menus = app.menus.item("Main").submenus.item("Window").submenus; 65 | for (var i = 0; i < menus.count(); i++) { 66 | $.writeln(menus.item(i).name); 67 | } 68 | */ 69 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Extendables documentation master file, created by 2 | sphinx-quickstart on Thu Sep 23 17:42:30 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Extendables' documentation! 7 | ====================================== 8 | 9 | Extendables is an :ref:`MIT-licensed ` developers' **framework for Adobe ExtendScript**. If you're writing heavy-duty automations for a Creative Suite app like **InDesign**, or anything more than just a throwaway script, this is for you. 10 | 11 | Extendables comes with three big blocks of functionality. 12 | 13 | 1. Additional methods on built-in objects like String and Array that give you the **Javascript 1.8 features** you're used to (think ``forEach``), conveniences for functional programming (think ``map``, ``reduce``, ``filter``), easy serialization to base64 and more. 14 | 2. Additional methods on InDesign DOM objects that make coding in InDesign **less verbose**. 15 | 3. Packages for **logging**, **unit testing**, **http requests**, **user interface development**, and a couple of other goodies. 16 | 17 | Include the framework in your code simply by including the ``extendables.jsx`` file. Code looks like this: 18 | 19 | .. literalinclude:: ../examples/basic.jsx 20 | :language: extendscript 21 | 22 | You'll code more quickly and more reliably. 23 | 24 | .. seealso:: 25 | :ref:`get-started` is a great place to get going. 26 | 27 | In addition, Extendables comes with a `CommonJS `_-compatible module loader and packaging system. This makes it easy to incorporate other CommonJS components into your project, and it also means all code is `namespaced `_. :ref:`Learn more about writing your own modules ` or about :ref:`the internals of the framework `. 28 | 29 | Packages 30 | -------- 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | 35 | packages/ui/doc/readme.rst 36 | packages/testing/doc/readme.rst 37 | packages/http/doc/readme.rst 38 | packages/logging/doc/readme.rst 39 | 40 | Javascript and DOM enhancements 41 | ------------------------------- 42 | 43 | .. toctree:: 44 | :maxdepth: 3 45 | 46 | patches/index 47 | 48 | More 49 | ---- 50 | 51 | .. toctree:: 52 | :maxdepth: 2 53 | 54 | core/index 55 | 56 | You can also :ref:`search through the docs `. -------------------------------------------------------------------------------- /core-packages/testing/doc/readme.rst: -------------------------------------------------------------------------------- 1 | .. _testing: 2 | 3 | ========================================= 4 | ``testing``: unit testing in ExtendScript 5 | ========================================= 6 | 7 | Extendables comes with `Pivotal Labs `_' excellent Jasmine unit-testing framework, ever-so-slightly modified to make it work with ExtendScript. Jasmine defines a very intuitive domain-specific language on top of Javascript: 8 | 9 | .. code-block:: javascript 10 | 11 | describe('Calculator', function () { 12 | var counter = 0 13 | 14 | it('can add a number', function () { 15 | counter = counter + 2; // counter was 0 before 16 | expect(bar).toEqual(2); 17 | }); 18 | 19 | it('can multiply a number', function () { 20 | counter = counter * 5; // counter was 2 before 21 | expect(bar).toEqual(10); 22 | }); 23 | }); 24 | 25 | You can `read the documentation to Jasmine `_ at GitHub. 26 | 27 | Where to place tests 28 | ==================== 29 | 30 | A test suite is simply a Javascript file which calls ``describe``. Conventionally, tests are placed inside ``/path/to/module/test/*.specs``. If you follow this convention, Extendables will find and run your tests as part of its built-in test suite. The test runner is located at ``tools/testrunner.jsx`` in the Extendables project root. 31 | 32 | Running tests 33 | ============= 34 | 35 | .. image:: unit-testing.png 36 | 37 | You don't need to use the Extendables test runner if you don't want to, or if you want to test a script that isn't a registered CommonJS module. 38 | 39 | Try this instead: 40 | 41 | .. code-block:: extendscript 42 | 43 | #include "extendables/extendables.jsx" 44 | // extracts describe, it, expect etc. into the global namespace 45 | extract("testing"); 46 | describe('My test suite', function () { /* your tests here */ }); 47 | tests.to_html("tests.mytestsuite.html"); 48 | // or alternatively: tests.to_console(); 49 | 50 | HTML test results will reside under ``log`` in the Extendables project root. 51 | 52 | .. note:: 53 | 54 | While it's usually not a good idea to extract an entire module into the global namespace, ``testing`` is the exception. It would be very tiresome to prefix every ``expect``, ``it``, ``describe`` etc. with a namespace, so we don't. To avoid polluting the global namespace of our script, test runners and specs are usually a separate script, rather than part of the script itself. -------------------------------------------------------------------------------- /core-packages/ui/lib/_ideas_.jsx: -------------------------------------------------------------------------------- 1 | #targetengine: "session" 2 | 3 | /* 4 | function Color (rgb_or_hex, opacity) { 5 | this.a = opacity; 6 | 7 | if (rgb_or_hex.is(Array)) { 8 | this.r = rgb_or_hex[0]; 9 | this.g = rgb_or_hex[1]; 10 | this.b = rgb_or_hex[2]; 11 | } else { 12 | var hex = rgb_or_hex.replace('#', ''); 13 | this.r = parseInt(hex.substring(0,2), 16); 14 | this.g = parseInt(hex.substring(2,4), 16); 15 | this.b = parseInt(hex.substring(4,6), 16); 16 | } 17 | 18 | this.rgba = function() { 19 | return [this.r, this.g, this.b, this.a]; 20 | } 21 | } 22 | */ 23 | 24 | Group.prototype.style = function (params) { 25 | function color (dest, color) { 26 | var brush = this.graphics.newBrush(this.graphics.BrushType.SOLID_COLOR, color); 27 | this.graphics[dest] = brush; 28 | } 29 | 30 | if (params['background'].is(Array)) { 31 | color('backgroundColor', params['background'][0]); 32 | color('disabledBackgroundColor', params['background'][1]); 33 | } else if (params['background'].is(Color)) { 34 | color('backgroundColor', params['background']); 35 | } 36 | } 37 | 38 | // dit vind ik lijk niet zo belangrijk omdat prutsen met kleurtjes niet bepaald 39 | // hetgeen is dat je als scripter zo broodnodig hebt, en waar shortcuts je 40 | // devtijd ongelooflijk kunnen verkorten. 41 | // misschien beter om gewoon bij item.properties = {} te blijven 42 | 43 | .style({ 44 | 'background': kleur of [kleur, disabled-kleur] 45 | 'foreground': kleur of [kleur, disabled-kleur] 46 | 'font': [family, style, size] 47 | }) 48 | 49 | 50 | .size(preferred, min, max) 51 | 52 | 53 | 54 | // group.children is standaard scriptui 55 | ui.row('password').children.forEach(function (control, i) { 56 | if (i == 0) control.size([200, undefined]); 57 | }) 58 | 59 | /* 60 | Op deze manier kan je een layout opdelen in: 61 | 62 | * styles & properties (= mixins) 63 | * structuur 64 | * events 65 | 66 | */ 67 | 68 | /* 69 | properties.fancypants = { 70 | 'bounds': [], 71 | 'styles': { 72 | 'background': y, 73 | 'foreground': z 74 | } 75 | } 76 | 77 | UI.prototype.button = function (id, text) { 78 | this[id] = this.add('button', undefined, text); 79 | return this; 80 | } 81 | 82 | UI.prototype.using = function () { 83 | arguments.forEach(function(mixin_name) { 84 | var mixin = self.mixins[mixin_name]; 85 | mixin.forEach(function(property) { 86 | if (property.name == 'styles') { 87 | this.style(property.value); 88 | } else { 89 | this[property.name] = property.value; 90 | } 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /patches/file.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @name Folder 4 | */ 5 | 6 | /** 7 | * @class 8 | * @name File 9 | */ 10 | 11 | /** 12 | * @desc The extendables base directory. Other notable class properties 13 | * include ``current``, ``desktop``, ``userData``, ``temp`` and ``trash``. 14 | */ 15 | Folder.extendables = new File($.fileName).parent.parent; 16 | 17 | function from_basepath (folder) { 18 | if (folder.is(String)) folder = new Folder(folder); 19 | 20 | var path = [folder.relativeURI, this.relativeURI].join('/'); 21 | return new this.constructor(path); 22 | } 23 | 24 | /** 25 | * @function 26 | * @desc Get a file or folder starting from an existing path. 27 | * A foolproof way to join paths together. 28 | * 29 | * Similar to ``File#getRelativeURI``, but returns a new File object 30 | * instead of a path. 31 | */ 32 | 33 | File.prototype.at = from_basepath; 34 | 35 | /** 36 | * @function 37 | * @desc Get a file or folder starting from an existing path. 38 | * A foolproof way to join paths together. 39 | * 40 | * Similar to ``File#getRelativeURI``, but returns a new Folder object 41 | * instead of a path. 42 | */ 43 | 44 | Folder.prototype.at = from_basepath; 45 | 46 | /** 47 | * @desc Easy extraction of path, name, basename and extension from a 48 | * :func:`File` object. 49 | * @param {String} type ``path``, ``name``, ``basename`` or ``extension`` 50 | */ 51 | 52 | File.prototype.component = function (type) { 53 | switch (type) { 54 | case 'path': 55 | return this.path; 56 | break; 57 | case 'name': 58 | return this.name; 59 | break; 60 | case 'basename': 61 | var extlen = this.component('extension').length; 62 | if (extlen) { 63 | return this.name.slice(0, -1 * extlen).rtrim('.'); 64 | } else { 65 | return this.name; 66 | } 67 | break; 68 | case 'extension': 69 | var name = this.name.split('.'); 70 | if (name.length > 1) { 71 | return name.last(); 72 | } else { 73 | return ''; 74 | } 75 | break; 76 | } 77 | } 78 | 79 | /** 80 | * @desc Works just like ``Folder#getFiles``, but returns only files, not folders. 81 | * @param {String|Function} [mask] 82 | */ 83 | 84 | Folder.prototype.files = function (mask) { 85 | return this.getFiles(mask).reject(function (file_or_folder) { 86 | return file_or_folder.is(Folder); 87 | }); 88 | } 89 | 90 | /** 91 | * @desc Works just like ``Folder#getFiles``, but returns only folders, not files. 92 | * @param {String|Function} [mask] 93 | */ 94 | 95 | Folder.prototype.folders = function (mask) { 96 | return this.getFiles(mask).reject(function (file_or_folder) { 97 | return file_or_folder.is(File); 98 | }); 99 | } -------------------------------------------------------------------------------- /core-packages/io/lib/octals.jsx: -------------------------------------------------------------------------------- 1 | exports.ByteString = ByteString; 2 | 3 | /** 4 | * @desc provides ByteString / ByteArray functionality for ExtendScript. 5 | * Handy when handling HTTP responses: UTF-8 can be multi-byte, but 6 | * the content-length header is always passed along as the number of octals, 7 | * not the number of characters. 8 | * 9 | * Be aware that the implementation is not particularly fast: it takes 10 | * about 75 milliseconds to process a 1000 character string. 11 | * 12 | * @param {String|Array} input Accepts either a plain string or a byte array; 13 | */ 14 | 15 | function ByteString (input) { 16 | var self = this; 17 | 18 | if (input.is(String)) { 19 | var string = input; 20 | this._bytearray = null; 21 | } else if (input.is(Array)) { 22 | var string = input.join(''); 23 | this._bytearray = input; 24 | } 25 | 26 | this.as_bytearray = function () { 27 | if (self._bytearray !== null) { 28 | return self._bytearray; 29 | } else { 30 | self._bytearray = []; 31 | } 32 | 33 | var characters = []; 34 | for (i = 0; i < string.length; i++) { 35 | characters.push(string.charAt(i)); 36 | } 37 | 38 | // map/reduce/flatten would've been more elegant 39 | // but it chucks performance down the drain 40 | characters.forEach(function (character) { 41 | var length = ByteString.count_bytes(character); 42 | for (i = 1; i <= length; i++) { 43 | if (i == length) { 44 | self._bytearray.push(character); 45 | } else { 46 | self._bytearray.push(''); 47 | } 48 | } 49 | }); 50 | return self._bytearray; 51 | } 52 | 53 | this.length = this.as_bytearray().length; 54 | 55 | this.slice = function (start, end) { 56 | var end = end || self.length; 57 | return new ByteString(self.as_bytearray().slice(start, end).join('')); 58 | } 59 | 60 | this.substr = function (start, length) { 61 | if (length) { 62 | var end = start + length; 63 | } else { 64 | var end = undefined; 65 | } 66 | return this.slice(start, end); 67 | } 68 | 69 | this.indexOf = function (character) { 70 | return this.as_bytearray().indexOf(character); 71 | } 72 | 73 | this.indexAfter = function (character) { 74 | var index = this.indexOf(character); 75 | if (index == -1) { 76 | return index; 77 | } else { 78 | return index + 1; 79 | } 80 | } 81 | 82 | this.toString = function () { 83 | return string; 84 | } 85 | } 86 | 87 | ByteString.count_bytes = function (character) { 88 | var code = character.charCodeAt(0); 89 | 90 | if (code >= 65535) { 91 | return 4; 92 | } else if (code >= 2048) { 93 | return 3; 94 | } else if (code >= 128) { 95 | return 2; 96 | } else { 97 | return 1; 98 | } 99 | } -------------------------------------------------------------------------------- /core-packages/ui/test/ui.specs: -------------------------------------------------------------------------------- 1 | var exports = {}; 2 | #include "../lib/__core__.jsx" 3 | var ui = exports; 4 | 5 | describe('UI: structure', function () { 6 | var dialog = new ui.Dialog("A test dialog"); 7 | 8 | it('has a DSL to define user interfaces', function () { 9 | dialog.text('message', 'Hello!').button('control', 'Close this dialog.'); 10 | 11 | expect(dialog.message).toBeDefined(); 12 | expect(dialog.control).toBeDefined(); 13 | }); 14 | 15 | it('makes sure adding new layout elements does not accidentally overwrite method names', function () { 16 | try { 17 | dialog.button('button', 'Close this dialog.'); 18 | } catch (error) { 19 | expect(error.description.contains("reserved")).toEqual(true); 20 | } 21 | }); 22 | 23 | it('can return the last added element', function () { 24 | var last = dialog.input('field').el() 25 | expect(last.type).toEqual("edittext"); 26 | }); 27 | }); 28 | 29 | describe('UI: mixins', function () { 30 | var mixins = { 31 | 'hello': { 32 | 'helpTip': 'hello there!' 33 | }, 34 | 'big': { 35 | 'size': [200, 200] 36 | }, 37 | 'warm-welcome': ['big', 'hello'] 38 | } 39 | var dialog = new ui.Dialog("A test dialog").with(mixins); 40 | 41 | it('can create layout groups', function () { 42 | dialog.row('a_row'); 43 | // test whether the current context is now the group 44 | expect(dialog.a_row.window.constructor.name).toEqual('Group'); 45 | // test whether mixins get passed along 46 | expect(dialog.a_row.mixins.hello).toBeDefined(); 47 | }); 48 | 49 | it('can apply layout properties to controls through mixins', function () { 50 | dialog.row('grouping').text('welcome', 'Hello!').using('hello').button('btn', 'Close this dialog.'); 51 | expect(dialog.grouping.welcome.helpTip).toEqual('hello there!'); 52 | }); 53 | 54 | it('can have mixins that are collections of other mixins', function () { 55 | dialog.button('btn2', 'Or explore further.').using('warm-welcome'); 56 | expect(dialog.btn2.helpTip).toEqual('hello there!'); 57 | expect(dialog.btn2.size.toString()).toEqual("200,200"); 58 | }); 59 | }); 60 | 61 | describe('UI: events', function () { 62 | var dialog = new Window("dialog", "A test dialog"); 63 | var control = dialog.add("button", undefined, "close this"); 64 | var ran_callback = false; 65 | 66 | it('has a DSL-ish syntax for adding event listeners to controls', function () { 67 | control.merge(new ControlMixins()); 68 | dialog.addEventListener("show", function () { 69 | // fake a click 70 | control.dispatchEvent(new UIEvent('click')); 71 | }); 72 | control.on('click').do(function () { 73 | this.window.close(); 74 | // if this function executes, we know the UI event DSL works. 75 | ran_callback = true; 76 | }); 77 | 78 | dialog.show(); 79 | expect(ran_callback).toEqual(true); 80 | }); 81 | }); -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fabric.api import * 3 | from fabric.context_managers import cd 4 | 5 | PACKAGEFOLDERS = ['./core-packages'] 6 | OTHER_FOLDERS = ['./', './patches'] 7 | JSDOC = "java -jar /Applications/jsdoc-toolkit/jsrun.jar /Applications/jsdoc-toolkit/app/run.js" 8 | 9 | class Folder(str): 10 | pass 11 | 12 | def _document_this(): 13 | folders = OTHER_FOLDERS 14 | for packagefolder in PACKAGEFOLDERS: 15 | for folder in os.listdir(packagefolder): 16 | if not folder.startswith("."): 17 | folder = Folder(packagefolder + "/" + folder) 18 | folder.package = "/lib" 19 | folders.append(folder) 20 | return folders 21 | 22 | def build_jsdoc(): 23 | for folder in _document_this(): 24 | local("{0} {1}{2} --template=doc/_themes/jsdoc-for-sphinx -x=js,jsx --directory={1}/doc/jsdoc".format(JSDOC, folder, getattr(folder, 'package', '')), capture=False) 25 | 26 | def build_sphinx(): 27 | with cd("doc"): 28 | local("make html", capture=False) 29 | 30 | def docbuild(part='all'): 31 | # jsdoc should be aliased to something like 32 | # java -jar jsdoc-toolkit/jsrun.jar jsdoc-toolkit/app/run.js 33 | 34 | if part == 'js': 35 | build_jsdoc() 36 | elif part == 'project': 37 | build_sphinx() 38 | elif part == 'clean': 39 | with cd("doc"): 40 | local("make clean") 41 | build_jsdoc() 42 | build_sphinx() 43 | else: 44 | build_jsdoc() 45 | build_sphinx() 46 | 47 | def ghpages(): 48 | local("mv doc/_build/html ../extendables-documentation") 49 | local("git stash") 50 | local("git checkout gh-pages -m") 51 | local("mv ../extendables-documentation ./docs") 52 | local("git add .") 53 | local('git commit -a -m "New docbuild"') 54 | local("git checkout master") 55 | local("git stash apply") 56 | 57 | def build(): 58 | docbuild() 59 | ghpages() 60 | 61 | def push(): 62 | local("git push origin --all") 63 | 64 | def commit(): 65 | if prompt("Do you want to do a docbuild first?", default='no') != 'no': 66 | build() 67 | if not prompt("Is this docbuild okay?", default='no') != 'no': 68 | abort("Halting commit.") 69 | 70 | # show any new files we could add 71 | new_files = local("git add . --dry-run") 72 | if len(new_files): 73 | print "Git found a few new files: " 74 | print new_files 75 | while prompt("Do you want to exclude some of these files first?", default='no') != 'no': 76 | local("nano .gitignore", capture=False) 77 | local("git add . --dry-run", capture=False) 78 | local("git add .") 79 | local("git commit -a", capture=False) 80 | 81 | if prompt("Commit to the central repository as well?", default='no') != 'no': 82 | push() 83 | 84 | def scaffold(name): 85 | local("cp -ri tools/scaffold ../site-packages/" + name, capture=False) -------------------------------------------------------------------------------- /dependencies/base64.js: -------------------------------------------------------------------------------- 1 | exports.encode64 = encoder('+/'); 2 | exports.decode64 = decoder('+/'); 3 | exports.urlsafeEncode64 = encoder('-_'); 4 | exports.urlsafeDecode64 = decoder('-_'); 5 | 6 | // base64.js - Base64 encoding and decoding functions 7 | // 8 | // Copyright (c) 2007, David Lindquist 9 | // Released under the MIT license 10 | // 11 | // Modified by TJ Holowaychuk for CommonJS module support. 12 | // Modified by Ben Weaver to use any alphabet. 13 | // Modified by Stijn Debrouwere for ExtendScript support. 14 | 15 | function encoder(extra) { 16 | var chars = alphabet(extra); 17 | 18 | return function(str) { 19 | str = str.toString(); 20 | var encoded = []; 21 | var c = 0; 22 | while (c < str.length) { 23 | var b0 = str.charCodeAt(c++); 24 | var b1 = str.charCodeAt(c++); 25 | var b2 = str.charCodeAt(c++); 26 | var buf = (b0 << 16) + ((b1 || 0) << 8) + (b2 || 0); 27 | var i0 = (buf & (63 << 18)) >> 18; 28 | var i1 = (buf & (63 << 12)) >> 12; 29 | var i2 = isNaN(b1) ? 64 : (buf & (63 << 6)) >> 6; 30 | var i3 = isNaN(b2) ? 64 : (buf & 63); 31 | encoded[encoded.length] = chars.charAt(i0); 32 | encoded[encoded.length] = chars.charAt(i1); 33 | encoded[encoded.length] = chars.charAt(i2); 34 | encoded[encoded.length] = chars.charAt(i3); 35 | } 36 | return encoded.join(''); 37 | }; 38 | } 39 | 40 | function decoder(extra) { 41 | var chars = alphabet(extra), 42 | invalid_char = new RegExp('[^' + regexp_escape(chars) + ']'); 43 | 44 | return function(str) { 45 | var invalid = { 46 | strlen: (str.length % 4 != 0), 47 | chars: invalid_char.test(str), 48 | equals: (new RegExp("/=/").test(str) && (new RegExp("/=[^=]/").test(str) || new RegExp("/={3}/").test(str))) 49 | }; 50 | if (invalid.strlen || invalid.chars || invalid.equals) 51 | throw new Error('Invalid base64 data'); 52 | var decoded = []; 53 | var c = 0; 54 | while (c < str.length) { 55 | var i0 = chars.indexOf(str.charAt(c++)); 56 | var i1 = chars.indexOf(str.charAt(c++)); 57 | var i2 = chars.indexOf(str.charAt(c++)); 58 | var i3 = chars.indexOf(str.charAt(c++)); 59 | var buf = (i0 << 18) + (i1 << 12) + ((i2 & 63) << 6) + (i3 & 63); 60 | var b0 = (buf & (255 << 16)) >> 16; 61 | var b1 = (i2 == 64) ? -1 : (buf & (255 << 8)) >> 8; 62 | var b2 = (i3 == 64) ? -1 : (buf & 255); 63 | decoded[decoded.length] = String.fromCharCode(b0); 64 | if (b1 >= 0) decoded[decoded.length] = String.fromCharCode(b1); 65 | if (b2 >= 0) decoded[decoded.length] = String.fromCharCode(b2); 66 | } 67 | return decoded.join(''); 68 | }; 69 | } 70 | 71 | /// --- Aux 72 | 73 | function alphabet(extra) { 74 | return 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 75 | + extra 76 | + '='; 77 | } 78 | 79 | function regexp_escape(expr) { 80 | return expr.replace(/([\^\$\/\.\*\-\+\?\|\(\)\[\]\{\}\\])/, '\\$1'); 81 | } -------------------------------------------------------------------------------- /doc/core/design-patterns.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Common design patterns 3 | ====================== 4 | 5 | For framework design issues currently under discussion, look at `issues in the GitHub issue tracker with a design label `_. 6 | 7 | getter/setters 8 | ============== 9 | 10 | Sometimes, when coding an interface, it's useful to be able to pretend that an object has a certain attribute people can read from and write to, while in reality there's more going on behind the screen: a certain calculation, creating an object, doing a lookup in different places and so on — in essence, properties that work like functions, or functions that look like properties. This is a very useful abstraction that makes code more domain-specific. 11 | 12 | Unfortunately, as opposed to Ruby and Python, Javascript has no support for making a function appear to be a property. Instead, you'll see the following idiom appear across different Javascript libraries: 13 | 14 | .. literalinclude:: ../../../examples/getter-setters.jsx 15 | 16 | Extendables makes considerable use of this idiom, which you're probably familiar with if you've ever used `jQuery `_ before. 17 | 18 | domain-specific language constructs 19 | =================================== 20 | 21 | Some parts of Extendables try to make coding a bit more elegant and a bit more like plain English by introducing a `domain-specific language `_ within Javascript. 22 | 23 | For example, in the UI module you would add styles to a dialog using ``var dialog = ui.Dialog("Hello there").with(styles);`` and not ``var dialog = ui.Dialog("Hello there"); dialog.add_styles(styles);`` 24 | 25 | In the Jasmine testing library, you define tests that look like 26 | 27 | .. code-block:: javascript 28 | 29 | it('needs proper unit tests', function () { 30 | expect(false).toBeTruthy(); 31 | }); 32 | 33 | By themselves, the function names ``it`` and ``expect`` are vague and undescriptive. But in context, they lead to very elegant code that makes immediate sense. Extendables uses domain-specific constructs wherever a single isolated task needs to be accomplished (building a user interface, testing your code) but avoids them elsewhere, in favor of more generally descriptive function and class names. 34 | 35 | In Extendables, modules that implement a domain-specific constructs are referred to as frameworks, because they frame your entire way of coding, whereas a library includes mostly helper classes and functions. 36 | 37 | extending prototypes 38 | ==================== 39 | 40 | Extendables does most of its work by extending the prototypes of built-in objects, like ``Number``, ``Array`` and ``String``. That way, we can keep our code entirely object-oriented instead of having 41 | to define a plethora of helper functions. 42 | 43 | If a developer wishes to extend object prototypes him- or herself, they need to be aware of which methods Extendables adds to the prototypes. Overriding prototype methods provided by Extendables could unpredictably alter how applications work, so some care needs to be taken. 44 | 45 | .. 46 | - factories (Error handling) 47 | - basic functional programming using select/reject/compact in Extendables' internal code -------------------------------------------------------------------------------- /core-packages/__future__/lib/persistence/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | stuff you do on the model: all, get, filter (= this is a manager) 3 | stuff you do on an individual instance: save, destroy, refresh 4 | stuff you do on collections / querysets: flush(), values() 5 | 6 | all of this, you do using an engine 7 | 8 | Inspiration: 9 | http://documentcloud.github.com/backbone/docs/backbone.html 10 | http://github.com/benpickles/js-model 11 | 12 | Note to self: test whether obj.watch() works like addEventListener (i.e. you can add a bunch of them) 13 | or whether you can only have a single watch function for each property. 14 | */ 15 | 16 | //var engines = require("persistence/engines"); 17 | // quick testing: 18 | var exports = {}; 19 | #include ../../../patches/__all__.jsx 20 | #include engines.jsx 21 | var engines = exports; 22 | 23 | function Manager (engine) { 24 | this._engine = engine; 25 | 26 | this.all = function () { 27 | return 'all objects'; 28 | } 29 | 30 | this.filter = function (args) { 31 | // dit zou kunnen werken in tandem met named paths: 32 | // /:collection_id/:id 33 | // waarbij je dan kan filteren met {'collection_id': 3, 'id': 5} 34 | // en wat niet ingevuld geraakt wordt weggelaten uit het pad 35 | // filter-opties _niet_ aanwezig in het pad kunnen weggefilterd 36 | // worden uit een array, met array.filter(function(el) { return el[filterfield] == filtervalue }) 37 | } 38 | 39 | this.get = function (args) { 40 | var objs = self.filter(args); 41 | if (!objs.length) throw Error("Does not exist"); 42 | if (objs.length > 1) throw Error("Returned multiple objects."); 43 | return objs[0]; 44 | } 45 | } 46 | 47 | function Collection () { 48 | this.flush = function () { 49 | } 50 | 51 | this.values = function () { 52 | } 53 | } 54 | Collection.prototype = new Array(); 55 | 56 | function model_factory (resource_path, engine) { 57 | function Model () { 58 | var self = this; 59 | 60 | this._attributes = {}; 61 | this._changes = {}; 62 | 63 | this.attributes = function () { 64 | return self._attributes.merge(self._changes); 65 | } 66 | 67 | this.refresh = function () { 68 | self = Model.objects.get({'id': self.id}); 69 | } 70 | 71 | this.validate = function () {} 72 | 73 | this.attr = function (name, value) { 74 | // tip overnemen van js-model: changes 75 | if (value) { 76 | self._changes[name] = value; 77 | } else { 78 | return self.attributes()[name]; 79 | } 80 | } 81 | 82 | this.save = function () { 83 | // men moet bij het aanmaken van een model zeggen welk het PK-veld is, 84 | // en op basis daarvan weten we of we moeten saven dan wel updaten. 85 | } 86 | 87 | this.destroy = function () { 88 | 89 | } 90 | } 91 | 92 | var engine = new engine(resource_path); 93 | Model.objects = new Manager(engine); 94 | return Model; 95 | } 96 | 97 | var Post = model_factory('this/is/a/path.conf', engines.RESTEngine); 98 | 99 | alert(Post.objects.all()); 100 | 101 | var post = new Post(); 102 | 103 | alert(post.save); 104 | 105 | /* 106 | als we de classmethods intact willen houden, hebben we een factory nodig die models maakt; 107 | ik zou puur ActiveRecord gaan, dus vrij close to the metal (of in dit geval, de JSON die we ontvangen) 108 | 109 | var Post = model_factory(resource_path, engine); 110 | */ -------------------------------------------------------------------------------- /minify.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | TODO: 5 | 6 | `var z = require("blah").z;` ... to support this kind of require, we just have to 7 | make sure we don't replace the entire line of code, but just the `require(whatev)` 8 | part, so we'll end up with: 9 | 10 | var z = require("blah").z; 11 | var z = exports.z; 12 | """ 13 | 14 | import os 15 | import sys 16 | import re 17 | 18 | PACKAGEFOLDERS = ['extendables/core-packages', 'site-packages'] 19 | 20 | all_packages = {} 21 | for folder in PACKAGEFOLDERS: 22 | packages = os.listdir(folder) 23 | for package in packages: 24 | if '.' not in package: 25 | all_packages[package] = folder + '/' + package 26 | 27 | class CodeFragment(object): 28 | def __init__(self, filename_or_string, basedir='', string=False): 29 | if string: 30 | self.code = filename_or_string 31 | self.basedir = basedir 32 | self.source = None 33 | else: 34 | if len(basedir): 35 | basedir += '/' 36 | self.source = basedir + filename_or_string 37 | with open(self.source) as script: 38 | self.code = script.read() 39 | self.basedir = basedir + "/".join(filename_or_string.split('/')[0:-1]) 40 | 41 | def inline_include(self, match): 42 | filename = match.group('filename') 43 | return CodeFragment(filename, self.basedir).inline() 44 | 45 | # todo: recursion 46 | # todo: support for var x = require("module").y; 47 | def inline_require(self, match): 48 | package = match.group('package') 49 | location = package.replace(package, all_packages.get(package, package), 1) 50 | location = location + '/lib' 51 | if not '/' in package: 52 | location = location + '/__core__' 53 | replacement = """undefined; 54 | var exports = {{}}; 55 | #include "{0}.jsx" 56 | var {1} = exports; 57 | exports = undefined; 58 | """.format(location, package) 59 | return replacement 60 | return CodeFragment(replacement, string=True).inline() 61 | 62 | # needs work 63 | def inline_extract(self, match): 64 | package = match.group('package') 65 | location = package.replace(package, all_packages.get(package, package), 1) 66 | if not '/' in location: 67 | location = location + '/__core__' 68 | replacement = """var exports = {{}}; 69 | #include "{0}.jsx"; 70 | for (name in exports) { 71 | $.global[name] = exports[name]; 72 | } 73 | exports = undefined; 74 | """.format(package) 75 | return replacement 76 | return CodeFragment(replacement, string=True).inline() 77 | 78 | def inline(self): 79 | code = self.code 80 | if self.source: 81 | code = code.replace( 82 | '$.fileName', 83 | 'new File($.fileName).parent + "/{source}"'.format(source=self.source) 84 | ) 85 | code = re.sub(r'#include "?(?P[^";]+)"?;?', self.inline_include, code) 86 | code = re.sub(r'(?.+)["\']\);?', self.inline_require, code) 87 | code = re.sub(r'(?.+)["\']\);?', self.inline_extract, code) 88 | return code 89 | 90 | def inline_code(): 91 | """ Give the filename to the script you wish to minify. """ 92 | script = sys.argv[1] 93 | print CodeFragment(script).inline() 94 | 95 | if __name__ == '__main__': 96 | inline_code() -------------------------------------------------------------------------------- /doc/core/writing-a-module.rst: -------------------------------------------------------------------------------- 1 | .. _writing-a-module: 2 | 3 | ======================= 4 | Writing your own module 5 | ======================= 6 | 7 | Modules versus scripts 8 | ---------------------- 9 | 10 | A module is something you *use* to build scripts, and a script is something you *run*. 11 | 12 | A module (or library) provides a bunch of functionality that you expect to re-use in different scripts, or want to share with the world, so other people can use your code to jumpstart their work. 13 | 14 | There's no simple way to ascertain what belongs in a module and what belongs in a script — you'll have to see what feels most convenient to you. For example, you could put all logic in a module and reduce your script to a two-liner: 15 | 16 | .. code-block:: extendscript 17 | 18 | var my_module = require("my_module"); 19 | my_module.do_stuff(); 20 | 21 | ... though chances are that module won't be easily reusable in other contexts. If you want to share a finished application, distribute it as a script, but if you want something other people can plug into their code, distribute it as a module. 22 | 23 | What goes where 24 | --------------- 25 | 26 | Extendables searches for modules in the ``core-packages`` directory (for built-in packages) and the ``site-packages`` (for your own modules and add-ons in general). You may have to create the ``site-packages`` directory yourself. 27 | 28 | .. tip:: 29 | 30 | If you take care never to put your own code into any other directory than ``site-packages``, you'll make life easier for yourself when upgrading to a newer version of Extendables: simply overwrite any existing directory except for ``site-packages``. 31 | 32 | Extendables follows a subset of `the CommonJS specifications `_. That means: 33 | 34 | * Binary files should be in the ``bin`` directory, 35 | * Javascript code should be under the ``lib`` directory 36 | * Documentation should be under the ``doc`` directory 37 | * Unit tests should be under the ``test`` directory 38 | 39 | Documentation should be in reStructuredText syntax, and files should have a `.rst` extension. Unit tests follow `the Jasmine DSL `_ and should have a `.specs` extension. 40 | 41 | For very small modules that don't require tests or documentation, a single ExtendScript file will also be recognized as a module when placed in the ``site-packages`` directory. 42 | 43 | .. warning:: 44 | 45 | If you're doing development on the Extendables core, make sure you don't put anything valuable inside ``extendables/site-packages`` — nothing in ``site-packages`` or ``log`` is tracked by the version control 46 | system, so whenever you change branches, everything inside of these directories will disappear. Instead, you could 47 | 48 | * use two different installations of Extendables: one for testing and development, 49 | and another as a production environment. That way, you can use a stable version 50 | for app development, but also the latest checkout of Extendables for working on the core. 51 | * place your own modules somewhere else, and place a symlink (OSX-only) inside of 52 | ``extendables/site-packages`` so your modules will get registered. 53 | 54 | Scaffolding 55 | ----------- 56 | 57 | If you have `Fabric `_ installed, you can create the scaffold for a module using ``fab scaffold:``. The result will reside in ``extendables/site-packages/``. If you'd rather not use Fabric, you may simply copy the scaffold from ``extendables/tools/scaffold``. 58 | 59 | Using the scaffold is highly encouraged. Its stub files include helpful comments and it takes care of the package layout for you. -------------------------------------------------------------------------------- /core-packages/http/lib/url.jsx: -------------------------------------------------------------------------------- 1 | // originally taken from node.js 2 | 3 | exports.parse = urlParse; 4 | 5 | // define these here so at least they only have to be compiled once on the first module load. 6 | var protocolPattern = /^([a-z0-9]+:)/; 7 | var portPattern = /:[0-9]+$/; 8 | var nonHostChars = ["/", "?", ";", "#"]; 9 | var hostlessProtocol = { 10 | "file":true, 11 | "file:":true 12 | } 13 | var slashedProtocol = { 14 | "http":true, "https":true, "ftp":true, "gopher":true, "file":true, 15 | "http:":true, "https:":true, "ftp:":true, "gopher:":true, "file:":true 16 | }; 17 | 18 | function urlParse (url) { 19 | if (url && typeof(url) === "object" && url.href) { 20 | throw TypeError("The unparsed url should be a string."); 21 | } 22 | 23 | var out = { href : url }, 24 | rest = url; 25 | 26 | var proto = protocolPattern.exec(rest); 27 | if (proto) { 28 | proto = proto[0]; 29 | out.protocol = proto; 30 | rest = rest.substr(proto.length); 31 | } 32 | 33 | // figure out if it's got a host 34 | // user@server is *always* interpreted as a hostname, and url 35 | // resolution will treat //foo/bar as host=foo,path=bar because that's 36 | // how the browser resolves relative URLs. 37 | if (proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) { 38 | var slashes = rest.substr(0, 2) === "//"; 39 | if (slashes && !(proto && hostlessProtocol[proto])) { 40 | rest = rest.substr(2); 41 | out.slashes = true; 42 | } 43 | } 44 | 45 | if (!hostlessProtocol[proto] && (slashes || (proto && !slashedProtocol[proto]))) { 46 | // there's a hostname. 47 | // the first instance of /, ?, ;, or # ends the host. 48 | // don't enforce full RFC correctness, just be unstupid about it. 49 | var firstNonHost = -1; 50 | for (var i = 0, l = nonHostChars.length; i < l; i ++) { 51 | var index = rest.indexOf(nonHostChars[i]); 52 | if (index !== -1 && (firstNonHost < 0 || index < firstNonHost)) firstNonHost = index; 53 | } 54 | 55 | if (firstNonHost !== -1) { 56 | out.host = rest.substr(0, firstNonHost); 57 | rest = rest.substr(firstNonHost); 58 | } else { 59 | out.host = rest; 60 | rest = ""; 61 | } 62 | 63 | // pull out the auth and port. 64 | var p = parseHost(out.host); 65 | out.merge(p); 66 | // we've indicated that there is a hostname, so even if it's empty, it has to be present. 67 | out.hostname = out.hostname || ""; 68 | } 69 | 70 | // now rest is set to the post-host stuff. 71 | // chop off from the tail first. 72 | var hash = rest.indexOf("#"); 73 | if (hash !== -1) { 74 | // got a fragment string. 75 | out.hash = rest.substr(hash); 76 | rest = rest.slice(0, hash); 77 | } 78 | var qm = rest.indexOf("?"); 79 | if (qm !== -1) { 80 | out.search = rest.substr(qm); 81 | out.query = rest.substr(qm+1); 82 | 83 | rest = rest.slice(0, qm); 84 | } 85 | 86 | if (rest) out.pathname = rest; 87 | return out; 88 | } 89 | 90 | function parseHost (host) { 91 | var out = {}; 92 | var at = host.indexOf("@"); 93 | if (at !== -1) { 94 | out.auth = host.substr(0, at); 95 | host = host.substr(at+1); // drop the @ 96 | } 97 | var port = portPattern.exec(host); 98 | if (port) { 99 | port = port[0]; 100 | out.port = port.substr(1).to('int'); 101 | host = host.substr(0, host.length - port.length); 102 | } 103 | if (host) out.hostname = host; 104 | return out; 105 | } -------------------------------------------------------------------------------- /patches/object.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Patches for functional programming. 3 | * Inspired by and sometimes copied from underscore.js 4 | */ 5 | 6 | /** 7 | * @desc Merge two objects together. This modifies the original object. 8 | * First use :func:`Object#clone` on the object if you want to keep the original object intact. 9 | * 10 | * @param {Object} obj The object to merge into this one. 11 | * 12 | * @returns {Object} Returns the merged object (``this``); 13 | */ 14 | 15 | Object.prototype.merge = function (obj) { 16 | if (!obj) return; 17 | 18 | var merged_obj = this; 19 | for (var name in obj) { 20 | merged_obj[name] = obj[name]; 21 | } 22 | return merged_obj; 23 | } 24 | 25 | /** 26 | * @function 27 | * @desc An alias for :func:`Object#merge` 28 | */ 29 | 30 | Object.prototype.extend = Object.prototype.merge 31 | 32 | /** 33 | * @desc Creates and returns a clone of the object. 34 | */ 35 | 36 | Object.prototype.clone = function () { 37 | // speeds things up if we're cloning an array 38 | if (this instanceof Array) return this.slice(0); 39 | if (this instanceof String) return this.substring(0); 40 | // the normal route for any other object 41 | // though it might not work on some built-in 42 | // application-specific objects 43 | return new this.constructor().merge(this); 44 | } 45 | 46 | /** 47 | * @desc 48 | * Returns only the keys (also known as 'names') of an object or associative array. 49 | * Will filter out any functions, as these are presumed to be object methods. 50 | * @returns {Array} An array with all the keys. 51 | */ 52 | 53 | Object.prototype.keys = function () { 54 | var keys = []; 55 | for (var key in this) { 56 | if (this.hasOwnProperty(key) && !(this[key] instanceof Function)) keys.push(key); 57 | } 58 | return keys; 59 | } 60 | 61 | /** 62 | * @desc Returns only the values of an object or associative array. 63 | * @returns {Array} An array with all the values. 64 | * 65 | * @example 66 | * > var nation = {'name': 'Belgium', 'continent': 'Europe'} 67 | * > nation.values(); 68 | * ['Belgium', 'Europe'] 69 | */ 70 | 71 | Object.prototype.values = function () { 72 | var self = this; 73 | return this.keys().map(function (key) { 74 | return self[key]; 75 | }); 76 | } 77 | 78 | /** 79 | * @desc An alias for ``this instanceof type``. 80 | * @returns {Bool} True or false. 81 | * 82 | * @example 83 | * > [].is(Array); 84 | * true 85 | */ 86 | Object.prototype.is = function(type) { 87 | return this instanceof type; 88 | } 89 | 90 | /** 91 | * @desc Checks whether the object has a value for the specified property. 92 | * @returns {Bool} True or false. 93 | */ 94 | 95 | Object.prototype.has = function (key) { 96 | // could be just null or an invalid object 97 | // either way, has() should return false 98 | if (this == null || this[key] == null) return false; 99 | 100 | if (key in this) { 101 | return new Boolean(this[key]) != false; 102 | } else { 103 | return false; 104 | } 105 | } 106 | 107 | /** 108 | * @desc Alias for ``obj.hasOwnProperty`` 109 | * @returns {Bool} True or false. 110 | */ 111 | 112 | Object.prototype.has_own = function (key) { 113 | return this.hasOwnProperty(key); 114 | } 115 | 116 | /** 117 | * @desc A debugging utility. When used without the ``dump`` argument, 118 | * equivalent to ``$.writeln(obj.toString())``. 119 | * @param {Bool} [dump=false] 120 | * Dump all properties of this object; 121 | * otherwise just returns a string representation. 122 | */ 123 | 124 | Object.prototype.to_console = function (dump) { 125 | if (dump) { 126 | var obj = this; 127 | var out = obj.reflect.properties.map(function (property) { 128 | return property.name + "\t => " + obj[property.name]; 129 | }).join("\n"); 130 | } else { 131 | var out = this.toString(); 132 | } 133 | return $.writeln(out); 134 | } -------------------------------------------------------------------------------- /loader.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * A more-or-less CommonJS-compliant module import system. 3 | * Namespaces for Javascript -- yay! 4 | */ 5 | 6 | var __modules__ = {}; 7 | function require (module_id) { 8 | // CommonJS: A module identifier is a String of "terms" 9 | var terms = module_id.split('/'); 10 | var module = terms.shift(); 11 | if (__modules__.hasOwnProperty(module)) { 12 | if (terms.length) { 13 | return __modules__[module].get_submodule(terms).load().exports; 14 | } else { 15 | return __modules__[module].load().exports; 16 | } 17 | } else { 18 | throw Error("No package named " + module_id); 19 | } 20 | } 21 | 22 | // extracts a module into the global namespace (like the eponymous PHP function); 23 | // to be avoided, but sometimes convenience trumps stringency 24 | function extract (module_id) { 25 | var module = require(module_id); 26 | for (var name in module) { 27 | $.global[name] = module[name]; 28 | } 29 | } 30 | 31 | function _is_valid_module (file_or_folder) { 32 | return file_or_folder.is(Folder) || file_or_folder.name.endswith(".jsx"); 33 | } 34 | 35 | function Module (file_or_folder, is_package) { 36 | var self = this; 37 | 38 | this.eval = function (file) { 39 | var exports = {}; 40 | var module = { 41 | 'id': self.id, 42 | 'uri': self.uri 43 | }; 44 | 45 | try { 46 | $.evalFile(file); 47 | } catch (error) { 48 | log_buffer.push([3, "Could not fully load " + module.id + "\n" + error]); 49 | } 50 | return exports; 51 | }; 52 | 53 | this.extract_submodules = function () { 54 | var base = file_or_folder; 55 | if (is_package) { 56 | base.changePath("./lib"); 57 | } 58 | var submodule_files = base.getFiles(_is_valid_module); 59 | 60 | submodule_files.forEach(function(submodule) { 61 | var submodule = new Module(submodule); 62 | self.submodules[submodule.id] = submodule; 63 | }); 64 | }; 65 | 66 | this.get_submodule = function (terms) { 67 | var submodule = self.submodules[terms.shift()] 68 | if (terms.length) { 69 | return submodule.get_submodule(terms); 70 | } else { 71 | return submodule; 72 | } 73 | }; 74 | 75 | this.get_subpackages = function () { 76 | return self.submodules.values().filter(function (submodule) { 77 | return submodule.packaged && submodule.id != 'tests'; 78 | }); 79 | } 80 | 81 | this.has_subpackages = function () { 82 | return !!self.get_subpackages().length; 83 | } 84 | 85 | this.get_tests = function () { 86 | var testfolder = new Folder("test").at(self.uri); 87 | if (testfolder.exists) { 88 | return testfolder.getFiles("*.specs"); 89 | } else { 90 | return []; 91 | } 92 | } 93 | 94 | this.load = function () { 95 | if (self.packaged) { 96 | self.exports = self.submodules['index'].load().exports; 97 | } else { 98 | self.exports = self.eval(self.uri); 99 | } 100 | return self 101 | } 102 | 103 | /* init */ 104 | this.id = file_or_folder.displayName.split('.')[0]; 105 | this.uri = file_or_folder.absoluteURI; 106 | this.packaged = file_or_folder.is(Folder); 107 | this.submodules = {}; 108 | if (this.packaged) { 109 | this.extract_submodules(); 110 | } 111 | } 112 | 113 | function load_modules (packagefolders) { 114 | packagefolders.forEach(function(packagefolder) { 115 | if (typeof packagefolder === 'string') { 116 | var folder = new Folder(packagefolder).at(Folder.extendables); 117 | } else { 118 | var folder = packagefolder; 119 | } 120 | var packages = folder.getFiles(_is_valid_module); 121 | 122 | packages.forEach(function(file_or_folder) { 123 | // An alias regists as a file in ExtendScript, even if it refers to a folder. 124 | // Check if the file is an alias and, if so, resolve it. 125 | if (file_or_folder.alias) file_or_folder = file_or_folder.resolve(); 126 | var module = new Module(file_or_folder, true); 127 | __modules__[module.id] = module; 128 | }); 129 | }); 130 | } -------------------------------------------------------------------------------- /core-packages/utils/lib/object.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Patches for functional programming. 3 | * Inspired by and sometimes copied from underscore.js 4 | */ 5 | 6 | /** 7 | * @desc Merge two objects together. This modifies the original object. 8 | * First use :func:`Object#clone` on the object if you want to keep the original object intact. 9 | * 10 | * @param {Object} obj The object to merge into this one. 11 | * 12 | * @returns {Object} Returns the merged object (``this``); 13 | */ 14 | 15 | exports.merge = function (self, obj) { 16 | if (!obj) return; 17 | 18 | var merged_obj = self; 19 | for (var name in obj) { 20 | merged_obj[name] = obj[name]; 21 | } 22 | return merged_obj; 23 | } 24 | 25 | /** 26 | * @function 27 | * @desc An alias for :func:`Object#merge` 28 | */ 29 | 30 | exports.extend = exports.merge 31 | 32 | /** 33 | * @desc Creates and returns a clone of the object. 34 | */ 35 | 36 | exports.clone = function (self) { 37 | // speeds things up if we're cloning an array 38 | if (this instanceof Array) return self.slice(0); 39 | if (this instanceof String) return self.substring(0); 40 | // the normal route for any other object 41 | // though it might not work on some built-in 42 | // application-specific objects 43 | return new self.constructor().merge(self); 44 | } 45 | 46 | /** 47 | * @desc 48 | * Returns only the keys (also known as 'names') of an object or associative array. 49 | * Will filter out any functions, as these are presumed to be object methods. 50 | * @returns {Array} An array with all the keys. 51 | */ 52 | 53 | exports.keys = function (self) { 54 | var keys = []; 55 | for (var key in self) { 56 | if (self.hasOwnProperty(key) && !(self[key] instanceof Function)) keys.push(key); 57 | } 58 | return keys; 59 | } 60 | 61 | /** 62 | * @desc Returns only the values of an object or associative array. 63 | * @returns {Array} An array with all the values. 64 | * 65 | * @example 66 | * > var nation = {'name': 'Belgium', 'continent': 'Europe'} 67 | * > nation.values(); 68 | * ['Belgium', 'Europe'] 69 | */ 70 | 71 | exports.values = function (self) { 72 | var keys = exports.keys(self); 73 | var values = []; 74 | for (var i = 0; i < keys.length; i++) { 75 | values.push(self[keys[i]]); 76 | } 77 | 78 | return values; 79 | } 80 | 81 | /** 82 | * @desc An alias for ``this instanceof type``. 83 | * @returns {Bool} True or false. 84 | * 85 | * @example 86 | * > [].is(Array); 87 | * true 88 | */ 89 | exports.is = function(self, type) { 90 | return self instanceof type; 91 | } 92 | 93 | /** 94 | * @desc Checks whether the object has a value for the specified property. 95 | * @returns {Bool} True or false. 96 | */ 97 | 98 | exports.has = function (self, key) { 99 | // could be just null or an invalid object 100 | // either way, has() should return false 101 | if (self == null || self[key] == null) return false; 102 | 103 | if (key in self) { 104 | return new Boolean(self[key]) != false; 105 | } else { 106 | return false; 107 | } 108 | } 109 | 110 | /** 111 | * @desc Alias for ``obj.hasOwnProperty`` 112 | * @returns {Bool} True or false. 113 | */ 114 | 115 | exports.has_own = function (self, key) { 116 | return self.hasOwnProperty(key); 117 | } 118 | 119 | /** 120 | * @desc A debugging utility. When used without the ``dump`` argument, 121 | * equivalent to ``$.writeln(obj.toString())``. 122 | * @param {Bool} [dump=false] 123 | * Dump all properties of this object; 124 | * otherwise just returns a string representation. 125 | */ 126 | 127 | exports.log = function (self, dump) { 128 | if (dump) { 129 | var props = exports.keys(self.reflect.properties); 130 | for (var i = 0; i < props.length; i++) { 131 | var property = props[i]; 132 | props[i] = property.name + "\t => " + self[property.name]; 133 | } 134 | var out = props.join("\n"); 135 | } else { 136 | var out = self.toString(); 137 | } 138 | return $.writeln(out); 139 | } -------------------------------------------------------------------------------- /core-packages/testing/lib/index.jsx: -------------------------------------------------------------------------------- 1 | #include ../../../dependencies/jasmine.js 2 | 3 | exports.jasmine = jasmine; 4 | exports.spyOn = spyOn; 5 | exports.it = it; 6 | exports.xit = xit; 7 | exports.expect = expect; 8 | exports.runs = runs; 9 | exports.waits = waits; 10 | exports.waitsFor = waitsFor; 11 | exports.beforeEach = beforeEach; 12 | exports.afterEach = afterEach; 13 | exports.describe = describe; 14 | exports.xdescribe = xdescribe; 15 | 16 | var Template = require("templating").Template; 17 | 18 | var TestRunner = function () { 19 | this._clean_results = function (suites, results) { 20 | var cleaned_results = suites.map(function(suite) { 21 | var total = suite.children.length; 22 | var passed = suite.children.filter(function(spec) { 23 | return (results[spec.id].result == "passed"); 24 | }).length; 25 | var specs = suite.children.map(function (spec) { 26 | return {'name': spec.name, 27 | 'result': results[spec.id].result, 28 | 'messages': results[spec.id].messages 29 | } 30 | }); 31 | 32 | return { 33 | 'name': suite.name, 34 | 'passed': passed, 35 | 'failed': new Number(total - passed), 36 | 'total': total, 37 | 'specs': specs 38 | }; 39 | }); 40 | return cleaned_results; 41 | } 42 | 43 | this.run = function () { 44 | var reporter = new jasmine.JsApiReporter(); 45 | jasmine.getEnv().addReporter(reporter); 46 | jasmine.getEnv().execute(); 47 | return this._clean_results(reporter.suites_, reporter.results()); 48 | } 49 | 50 | this.get_environment = function () { 51 | var env = { 52 | 'OS': $.os, 53 | 'ExtendScript build': $.build, 54 | 'ExtendScript version': $.version, 55 | 'path': $.includePath, 56 | 'locale': $.locale, 57 | 'app': app.name, 58 | 'app version': app.version 59 | } 60 | return env.keys().map(function (key) { 61 | return {'key': key, 'value': env[key]}; 62 | }); 63 | } 64 | 65 | // we'll add this into the html representation, 66 | // so people can upload structured test reports to our central server. 67 | this.as_json = function () { 68 | 69 | } 70 | 71 | this.to_console = function () { 72 | var results = this.run(); 73 | 74 | results.forEach(function(suite) { 75 | $.writeln("\nSuite: {} \tran {} tests, {} failure(s)".format(suite.name, suite.total, suite.failed)); 76 | suite.specs.forEach(function(spec) { 77 | $.writeln("\t" + spec.result.toUpperCase() + "\t" + spec.name); 78 | }); 79 | }); 80 | } 81 | 82 | this.to_log = function () { 83 | // todo 84 | } 85 | 86 | this.to_html = function (filename) { 87 | // some background info 88 | var datetime = new Date(); 89 | var date = datetime.toDateString(); 90 | var time = "{}:{}".format(datetime.getHours(), datetime.getMinutes()); 91 | var environment = this.get_environment(); 92 | 93 | // run tests 94 | var results = this.run(); 95 | 96 | // tidy up results 97 | results.forEach(function(suite) { 98 | suite.specs.forEach(function(spec) { 99 | if (spec.result == 'failed') { 100 | var messages = spec.messages.reject(function (message) { 101 | return message == 'Passed.'; 102 | }); 103 | spec.problem = '

{}

'.format(messages.join("
")); 104 | } else { 105 | spec.problem = ''; 106 | } 107 | }); 108 | }); 109 | 110 | var duration = ((new Date().getTime() - datetime.getTime())/1000).toFixed(2); 111 | 112 | var template = new Template("report.html", module); 113 | template.render({ 114 | 'date': date, 115 | 'time': time, 116 | 'duration': duration, 117 | 'suites': results, 118 | 'total': results.sum('total'), 119 | 'fails': results.sum('failed'), 120 | 'passes': results.sum('passed'), 121 | 'environment': environment 122 | }); 123 | template.write_to(filename); 124 | } 125 | 126 | // would be incredibly interesting to see usage patterns and whether certain tests 127 | // fail consistently on the same platform or app version or ... 128 | this.to_central_server = function () { 129 | // todo 130 | } 131 | } 132 | 133 | exports.tests = new TestRunner(); -------------------------------------------------------------------------------- /core-packages/logging/lib/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Basic file-based logging, loosely modelled after the logging module 3 | * in the Python standard library 4 | */ 5 | 6 | var SEVERITY = ["NOTSET", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]; 7 | 8 | var LogMessage = function (severity, message) { 9 | this.date = new Date().toLocaleString(); 10 | this.severity = severity; 11 | this.message = message; 12 | 13 | this.toString = function () { 14 | return "{} :: {}\t{}".format(this.date, SEVERITY[this.severity], this.message); 15 | } 16 | } 17 | 18 | /** 19 | * @class 20 | * @desc A log file object. Will resume an existing log if it exists, 21 | * or make a new one if it doesn't. 22 | * @param {String} name the filename for this log file 23 | * @param {String|Number} [log_level=4] the log level above which log messages don't get written, 24 | * either a name (e.g. CRITICAL) or the actual log level. 25 | * 26 | * @example 27 | * var logging = require("logging"); 28 | * // all logs end up in extendables/log 29 | * var log = new Log("example.log"); 30 | * try { 31 | * throw new Error(); 32 | * } catch (error) { 33 | * log.debug("Caught error, will log a debug message now"); 34 | * log.critical("Something happened: {error} ({env})", {'error': error, 'env': $.os}); 35 | * } 36 | */ 37 | 38 | var Log = function (name, log_level) { 39 | var self = this; 40 | this.name = name; 41 | // log level can be specified both by name or directly as a level. 42 | if (log_level && log_level.is(String)) log_level = SEVERITY.indexOf(log_level); 43 | this.log_level = log_level || settings.LOGGING_LOG_LEVEL || 4; 44 | 45 | this.truncate = function (forced) { 46 | // truncate the logfile if it gets bigger than half a megabyte 47 | self.file.open("e"); 48 | if (forced || self.file.length > 1024*512) { 49 | self.file.length = 0; 50 | } 51 | self.file.close(); 52 | } 53 | 54 | this.writeln = function (severity, message) { 55 | var log = self.file; 56 | var logmessage = new LogMessage(severity, message) 57 | log.open("e"); 58 | log.seek(log.length); 59 | log.writeln(logmessage); 60 | log.close(); 61 | } 62 | 63 | // basic logger 64 | this.log = function () { 65 | var arguments = arguments.to('array'); 66 | var severity = arguments.shift(); 67 | var template = arguments.shift(); 68 | var message = template.format.apply(template, arguments); 69 | // only log what's equal to or below the configured logging treshold 70 | if (severity <= self.log_level) { 71 | self.writeln(severity, message); 72 | } 73 | } 74 | 75 | /** 76 | * @desc log a debug message 77 | * @param {String} message The log message 78 | * @param {String|Object} [replacements] Can take any number of replacements, to be passed along to str.format() 79 | */ 80 | this.debug = function () { 81 | arguments = [5].concat(arguments.to('array')); 82 | self.log.apply(null, arguments); 83 | } 84 | /** 85 | * @desc log an info message 86 | * @param {String} message The log message 87 | * @param {String|Object} [replacements] Can take any number of replacements, to be passed along to str.format() 88 | */ 89 | this.info = function () { 90 | arguments = [4].concat(arguments.to('array')); 91 | self.log.apply(null, arguments); 92 | } 93 | /** 94 | * @desc log a warning 95 | * @param {String} message The log message 96 | * @param {String|Object} [replacements] Can take any number of replacements, to be passed along to str.format() 97 | */ 98 | this.warning = function () { 99 | arguments = [3].concat(arguments.to('array')); 100 | self.log.apply(null, arguments); 101 | } 102 | /** 103 | * @desc log an error 104 | * @param {String} message The log message 105 | * @param {String|Object} [replacements] Can take any number of replacements, to be passed along to str.format() 106 | */ 107 | this.error = function () { 108 | arguments = [2].concat(arguments.to('array')); 109 | self.log.apply(null, arguments); 110 | } 111 | /** 112 | * @desc log a critical error 113 | * @param {String} message The log message 114 | * @param {String|Object} [replacements] Can take any number of replacements, to be passed along to str.format() 115 | */ 116 | this.critical = function () { 117 | arguments = [1].concat(arguments.to('array')); 118 | self.log.apply(null, arguments); 119 | } 120 | 121 | // init 122 | var logfolder = settings.LOGGING_FOLDER || new Folder("log").at(Folder.extendables); 123 | this.file = new File(this.name).at(logfolder); 124 | this.truncate(); 125 | } 126 | 127 | exports.Log = Log; -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Extendables.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Extendables.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /patches/error.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc This method overloads :func:`Object#is` to combat a problem with some versions of ExtendScript 3 | * that leads to all error types being considered the base class Error. This problem makes it impossible to 4 | * do simple comparisons on errors, for example ``new EvalError() instanceof SyntaxError``. The previous 5 | * expression should return false but will return true. 6 | * 7 | * When testing whether you're dealing with a specific kind of error, use this method, and refrain 8 | * from using ``instanceof``. 9 | * 10 | * @param {Constructor} type Use the constructor itself, not a string or an instance. 11 | * 12 | * @example 13 | * try { 14 | * raise new SyntaxError(); 15 | * } catch (error if error.is(TypeError)) { 16 | * alert("This displays in case of a type error, but not in case of a syntax error."); 17 | * } 18 | * 19 | * @see :ref:`error-handling` has some useful advice on how to handle errors in ExtendScript. 20 | * 21 | * @returns {Bool} 22 | * True or false. Any error type matches the base class, so ``new SyntaxError().is(Error)`` would return ``true``. 23 | */ 24 | 25 | Error.prototype.is = function (type) { 26 | if (this instanceof type) { 27 | if ('_type' in this) { 28 | return type == Error || this._type == type; 29 | } else { 30 | throw new TypeError("This method only works on built-in error types \ 31 | and those created using the Error.factory class method."); 32 | } 33 | } else { 34 | return false; 35 | } 36 | } 37 | 38 | /** 39 | * @desc Use this classmethod to make sure your custom error types work 40 | * just like the built-in ones. 41 | * 42 | * @param {String} name Preferably the same name as the variable you're associating the error with. 43 | * 44 | * @example var DatabaseError = Error.factory("DatabaseError"); 45 | */ 46 | 47 | Error.factory = function (name) { 48 | var error = function (msg, file, line) { 49 | this.name = name; 50 | this.description = msg; 51 | this._type = error; 52 | } 53 | error.prototype = new Error(); 54 | return error; 55 | } 56 | 57 | /** 58 | * @class 59 | * @name Error 60 | * @desc A general-purpose error 61 | */ 62 | 63 | Error.prototype._type = Error; 64 | 65 | /** 66 | * @class 67 | * @name EvalError 68 | * @desc An error that occurs regarding the global function eval() 69 | */ 70 | 71 | EvalError.prototype._type = EvalError; 72 | 73 | /** 74 | * @class 75 | * @name RangeError 76 | * @desc An error that occurs when a numeric variable or parameter is outside of its valid range 77 | */ 78 | 79 | RangeError.prototype._type = RangeError; 80 | 81 | /** 82 | * @class 83 | * @name ReferenceError 84 | * @desc An error that occurs when de-referencing an invalid reference 85 | */ 86 | 87 | ReferenceError.prototype._type = ReferenceError; 88 | 89 | /** 90 | * @class 91 | * @name SyntaxError 92 | * @desc An error that occurs regarding the global function eval() 93 | */ 94 | 95 | SyntaxError.prototype._type = SyntaxError; 96 | 97 | /** 98 | * @class 99 | * @name TypeError 100 | * @desc An error that occurs when a variable or parameter is not of a valid type 101 | */ 102 | 103 | TypeError.prototype._type = TypeError; 104 | 105 | /** 106 | * @class 107 | * @name IOError 108 | * @desc Use when an IO operation (loading a file, writing to a file, an internet connection) fails. 109 | */ 110 | 111 | IOError.prototype._type = IOError; 112 | 113 | /** 114 | * @class 115 | * @name ArithmeticError 116 | * @desc Use when a calculation misbehaves. 117 | */ 118 | 119 | var ArithmeticError = Error.factory("ArithmeticError"); 120 | 121 | /** 122 | * @class 123 | * @name ImportError 124 | * @desc Use when an import fails. More specific than IOError. 125 | */ 126 | 127 | var ImportError = Error.factory("ImportError"); 128 | 129 | /** 130 | * @class 131 | * @name EnvironmentError 132 | * @desc Use for exceptions that have nothing to do with Extendables or ExtendScript. 133 | */ 134 | 135 | var EnvironmentError = Error.factory("EnvironmentError"); 136 | 137 | /** 138 | * @class 139 | * @name ParseError 140 | * @desc Much like EvalError, but for your own parsers. 141 | */ 142 | 143 | var ParseError = Error.factory("ParseError"); 144 | 145 | /** 146 | * @class 147 | * @name SystemError 148 | * @desc Use when the system (either the Creative Suite app or the operating system) malfunctions. 149 | */ 150 | 151 | var SystemError = Error.factory("SystemError"); 152 | 153 | /** 154 | * @class 155 | * @name NotImplementedError 156 | * @desc Use to warn people that a feature has not yet been implemented, as a placeholder 157 | * to remind yourself or to indicate that a subclass needs to overload the parent method. 158 | */ 159 | 160 | var NotImplementedError = Error.factory("NotImplementedError"); 161 | 162 | if (app.name.to('lower').contains("indesign")) { 163 | /** 164 | * @class 165 | * @name ValidationError 166 | */ 167 | ValidationError.prototype._type = ValidationError; 168 | } -------------------------------------------------------------------------------- /core-packages/utils/lib/error.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc This method overloads :func:`Object#is` to combat a problem with some versions of ExtendScript 3 | * that leads to all error types being considered the base class Error. This problem makes it impossible to 4 | * do simple comparisons on errors, for example ``new EvalError() instanceof SyntaxError``. The previous 5 | * expression should return false but will return true. 6 | * 7 | * When testing whether you're dealing with a specific kind of error, use this method, and refrain 8 | * from using ``instanceof``. 9 | * 10 | * @param {Constructor} type Use the constructor itself, not a string or an instance. 11 | * 12 | * @example 13 | * try { 14 | * raise new SyntaxError(); 15 | * } catch (error if error.is(TypeError)) { 16 | * alert("This displays in case of a type error, but not in case of a syntax error."); 17 | * } 18 | * 19 | * @see :ref:`error-handling` has some useful advice on how to handle errors in ExtendScript. 20 | * 21 | * @returns {Bool} 22 | * True or false. Any error type matches the base class, so ``new SyntaxError().is(Error)`` would return ``true``. 23 | */ 24 | 25 | Error.prototype.is = function (type) { 26 | if (this instanceof type) { 27 | if ('_type' in this) { 28 | return type == Error || this._type == type; 29 | } else { 30 | throw new TypeError("This method only works on built-in error types \ 31 | and those created using the Error.factory class method."); 32 | } 33 | } else { 34 | return false; 35 | } 36 | } 37 | 38 | /** 39 | * @desc Use this classmethod to make sure your custom error types work 40 | * just like the built-in ones. 41 | * 42 | * @param {String} name Preferably the same name as the variable you're associating the error with. 43 | * 44 | * @example var DatabaseError = Error.factory("DatabaseError"); 45 | */ 46 | 47 | Error.factory = function (name) { 48 | var error = function (msg, file, line) { 49 | this.name = name; 50 | this.description = msg; 51 | this._type = error; 52 | } 53 | error.prototype = new Error(); 54 | return error; 55 | } 56 | 57 | /** 58 | * @class 59 | * @name Error 60 | * @desc A general-purpose error 61 | */ 62 | 63 | Error.prototype._type = Error; 64 | 65 | /** 66 | * @class 67 | * @name EvalError 68 | * @desc An error that occurs regarding the global function eval() 69 | */ 70 | 71 | EvalError.prototype._type = EvalError; 72 | 73 | /** 74 | * @class 75 | * @name RangeError 76 | * @desc An error that occurs when a numeric variable or parameter is outside of its valid range 77 | */ 78 | 79 | RangeError.prototype._type = RangeError; 80 | 81 | /** 82 | * @class 83 | * @name ReferenceError 84 | * @desc An error that occurs when de-referencing an invalid reference 85 | */ 86 | 87 | ReferenceError.prototype._type = ReferenceError; 88 | 89 | /** 90 | * @class 91 | * @name SyntaxError 92 | * @desc An error that occurs regarding the global function eval() 93 | */ 94 | 95 | SyntaxError.prototype._type = SyntaxError; 96 | 97 | /** 98 | * @class 99 | * @name TypeError 100 | * @desc An error that occurs when a variable or parameter is not of a valid type 101 | */ 102 | 103 | TypeError.prototype._type = TypeError; 104 | 105 | /** 106 | * @class 107 | * @name IOError 108 | * @desc Use when an IO operation (loading a file, writing to a file, an internet connection) fails. 109 | */ 110 | 111 | IOError.prototype._type = IOError; 112 | 113 | /** 114 | * @class 115 | * @name ArithmeticError 116 | * @desc Use when a calculation misbehaves. 117 | */ 118 | 119 | exports.ArithmeticError = Error.factory("ArithmeticError"); 120 | 121 | /** 122 | * @class 123 | * @name ImportError 124 | * @desc Use when an import fails. More specific than IOError. 125 | */ 126 | 127 | exports.ImportError = Error.factory("ImportError"); 128 | 129 | /** 130 | * @class 131 | * @name EnvironmentError 132 | * @desc Use for exceptions that have nothing to do with Extendables or ExtendScript. 133 | */ 134 | 135 | exports.EnvironmentError = Error.factory("EnvironmentError"); 136 | 137 | /** 138 | * @class 139 | * @name ParseError 140 | * @desc Much like EvalError, but for your own parsers. 141 | */ 142 | 143 | exports.ParseError = Error.factory("ParseError"); 144 | 145 | /** 146 | * @class 147 | * @name SystemError 148 | * @desc Use when the system (either the Creative Suite app or the operating system) malfunctions. 149 | */ 150 | 151 | exports.SystemError = Error.factory("SystemError"); 152 | 153 | /** 154 | * @class 155 | * @name NotImplementedError 156 | * @desc Use to warn people that a feature has not yet been implemented, as a placeholder 157 | * to remind yourself or to indicate that a subclass needs to overload the parent method. 158 | */ 159 | 160 | exports.NotImplementedError = Error.factory("NotImplementedError"); 161 | 162 | var string = require("utils/string"); 163 | var is_indesign = string.contains(string.to(app.name, 'lower'), 'indesign'); 164 | if (is_indesign) { 165 | /** 166 | * @class 167 | * @name ValidationError 168 | */ 169 | ValidationError.prototype._type = ValidationError; 170 | } -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Extendables.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Extendables.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Extendables" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Extendables" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /core-packages/http/test/http.specs: -------------------------------------------------------------------------------- 1 | /* 2 | * Most of these tests will fail without a working internet connection. 3 | */ 4 | 5 | describe('HTTP: url parsing', function () { 6 | var url = require("http/url"); 7 | 8 | it('parses urls into host, pathname, port and auth parts', function () { 9 | var parsed_url = url.parse("http://one:two@extendables.org:80/a/path/"); 10 | expect(parsed_url.auth).toEqual("one:two"); 11 | expect(parsed_url.href).toEqual("http://one:two@extendables.org:80/a/path/"); 12 | expect(parsed_url.host).toEqual("one:two@extendables.org:80"); 13 | expect(parsed_url.hostname).toEqual("extendables.org"); 14 | expect(parsed_url.port).toEqual(80); 15 | expect(parsed_url.pathname).toEqual("/a/path/"); 16 | }); 17 | }); 18 | 19 | describe('HTTP: request and response objects', function () { 20 | var http = require("http"); 21 | 22 | it('exports a HTTPRequest constructor', function () { 23 | expect(http.HTTPRequest).toBeDefined(); 24 | }); 25 | 26 | it('exports a bunch of shortcut functions', function () { 27 | ['get', 'head', 'post', 'put', 'del'].forEach(function (method) { 28 | expect(http[method]).toBeDefined(); 29 | }) 30 | }); 31 | 32 | it('gives users control over port, method and timeout', function () { 33 | var req = new http.HTTPRequest("HEAD", "http://www.w3.org/"); 34 | req.method("GET"); 35 | req.timeout(10); 36 | req.port(20000); 37 | expect(req.method()).toEqual("GET"); 38 | expect(req.timeout()).toEqual(10); 39 | expect(req.port()).toEqual(20000); 40 | }); 41 | 42 | it('has an interface to add and view http headers', function () { 43 | var req = new http.HTTPRequest("HEAD", "http://www.w3.org/"); 44 | expect(req.header("User-Agent")).toEqual("Adobe ExtendScript"); 45 | req.header("User-Agent", "Something completely different."); 46 | expect(req.header("User-Agent")).toEqual("Something completely different."); 47 | }); 48 | 49 | it('signals redirects (301, 302, 303, 307) in the response object', function () { 50 | // nytimes.com redirects to www.nytimes.com using a 301 redirect 51 | var req = new http.HTTPRequest("GET", "http://nytimes.com"); 52 | req.follow_redirects(false); 53 | var res = req.do(); 54 | expect(res.is_redirect).toEqual(true); 55 | expect(res.redirection_type).toEqual('repeat'); 56 | }); 57 | }); 58 | 59 | describe('HTTP: requests', function () { 60 | var http = require("http"); 61 | 62 | beforeEach(function () { 63 | // let's not spam w3.org 64 | $.sleep(500); 65 | }); 66 | 67 | it('can check whether the script has internet access', function () { 68 | expect(http.has_internet_access()).toEqual(true); 69 | }); 70 | 71 | it('has a high-level shortcut for GET requests', function () { 72 | expect(http.get("http://www.w3.org/").status).toEqual(200); 73 | }); 74 | 75 | it('has a high-level shortcut for HEAD requests', function () { 76 | expect(http.head("http://www.w3.org/").status).toEqual(200); 77 | }); 78 | 79 | it('has a high-level shortcut for POST requests', function () { 80 | expect(http.post("http://www.w3.org/").status).toEqual(200); 81 | }); 82 | 83 | it('has a high-level shortcut for PUT requests', function () { 84 | expect(http.put("http://www.w3.org/").status).toEqual(307); 85 | }); 86 | 87 | it('has a high-level shortcut for DEL requests', function () { 88 | expect(http.head("http://www.w3.org/").status).toEqual(200); 89 | }); 90 | 91 | it('can follow redirects', function () { 92 | // nytimes.com redirects to www.nytimes.com using a 301 redirect 93 | var req = new http.HTTPRequest("GET", "http://nytimes.com"); 94 | var res = req.do(); 95 | expect(res.status).toEqual(200); 96 | expect(res.redirects).toEqual(["http://nytimes.com"]); 97 | expect(res.for_request.url().href).toEqual("http://www.nytimes.com/"); 98 | }); 99 | 100 | it('can follow redirects manually, using the .follow() method', function () { 101 | var req = new http.HTTPRequest("GET", "http://nytimes.com"); 102 | req.follow_redirects(false); 103 | var res = req.do(); 104 | res = res.follow(); 105 | expect(res.status).toEqual(200); 106 | }); 107 | 108 | it('by default, only follows redirects for GET and HEAD requests', function () { 109 | var req = new http.HTTPRequest("GET", "http://www.example.com/"); 110 | expect(req.follow_redirects()).toEqual(true); 111 | req = new http.HTTPRequest("POST", "http://www.example.com/"); 112 | expect(req.follow_redirects()).toEqual(false); 113 | }); 114 | 115 | it('can be configured to never redirect', function () { 116 | // nytimes.com redirects to www.nytimes.com using a 301 redirect 117 | var req = new http.HTTPRequest("GET", "http://nytimes.com"); 118 | req.follow_redirects(false); 119 | var res = req.do(); 120 | expect(res.status).toEqual(301); 121 | }); 122 | 123 | it('is thoroughly stress-tested', function () { 124 | // So far, the http library has been able to handle everything 125 | // I've thrown at it, with the sole exception of example. 126 | // urls. No clue why, as connecting via telnet works just fine. 127 | expect(http.get("http://www.example.com/").status).toEqual(200); 128 | }); 129 | }); -------------------------------------------------------------------------------- /patches/string.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc This is a simple string formatting method, loosely inspired on the one in Python 3. 3 | * 4 | * * In unnamed mode, specify placeholders with the **{}** symbol. 5 | * * In named mode, specify placeholders with **{propname}**. 6 | * 7 | * @param {String} replacements 8 | * For each **{}** symbol in the text, ``format`` expects a replacement argument. 9 | * Calls `.toString()` on each replacement, so you can pass in any data type. 10 | * You may also specify a single replacement object, which will do named formatting. 11 | * 12 | * @example 13 | * > var person = {'salutation': 'mister', 'name': 'John Smith'}; 14 | * > var hello = "Hello there, {}, I've heard your name is {}!".format(person.salutation, person.name); 15 | * > $.writeln(hello); 16 | * "Hello there, mister, I've heard your name is John Smith" 17 | * 18 | * @example 19 | * > var person = {'salutation': 'mister', 'name': 'John Smith'}; 20 | * > var hello = "Hello there, {salutation}, I've heard your name is {name}!".format(person); 21 | * > $.writeln(hello); 22 | * "Hello there, mister, I've heard your name is John Smith" 23 | */ 24 | 25 | String.prototype.format = function() { 26 | var str = this; 27 | var replacements = arguments.to('array'); 28 | var named = replacements.length == 1 && replacements[0].reflect.name == 'Object'; 29 | 30 | if (named) { 31 | var dict = replacements[0]; 32 | dict.keys().forEach(function (key) { 33 | // replace globally (flagged g) 34 | str = str.replace("{" + key + "}", dict[key], "g"); 35 | }); 36 | return str; 37 | } else { 38 | // split the string into parts around the substring replacement symbols ({}). 39 | var chunks = str.split("{}"); 40 | // fill in the replacements 41 | for (var i in chunks) { 42 | var replacement = replacements.shift(); 43 | if (replacement) chunks[i] += replacement.toString(); 44 | } 45 | // join everything together 46 | return chunks.join(''); 47 | } 48 | } 49 | 50 | /** 51 | * @desc Tests whether the string starts with the specified substring. 52 | * @param {String} substring 53 | * @returns {Bool} True or false. 54 | */ 55 | 56 | String.prototype.startswith = function (substring) { 57 | return new Boolean(this.length && this.indexOf(substring) === 0).valueOf(); 58 | } 59 | 60 | /** 61 | * @desc Tests whether the string ends with the specified substring. 62 | * @param {String} substring 63 | * @returns {Bool} True or false. 64 | */ 65 | 66 | String.prototype.endswith = function (substring) { 67 | return new Boolean(this.length && this.indexOf(substring) == (this.length - substring.length)).valueOf(); 68 | } 69 | 70 | /** 71 | * @desc Tests whether the string contains the specified substring. 72 | * This is equal to ``str.indexOf(substring) != -1``. 73 | * @param {String} substring 74 | * @returns {Bool} True or false. 75 | */ 76 | 77 | String.prototype.contains = function (substring) { 78 | return this.indexOf(substring) != -1; 79 | } 80 | 81 | /** 82 | * @desc Does what it says. 83 | * Does not check whether the string actually extends beyond the the substring. 84 | */ 85 | 86 | String.prototype.indexAfter = function (substring) { 87 | var index = this.indexOf(substring); 88 | if (index == -1) { 89 | return index; 90 | } else { 91 | return index + substring.length; 92 | } 93 | } 94 | 95 | /** 96 | * @desc Removes leading whitespace characters, including tabs, line endings and the like. 97 | * @param {String} [character] if specified, removes leading characters matching the parameter 98 | * instead of whitespace. 99 | * 100 | * @example 101 | * > $.writeln(" hello there ".trim()); 102 | * "hello there " 103 | */ 104 | 105 | String.prototype.ltrim = function(character) { 106 | if (character) { 107 | if (this.endswith(character) == true) { 108 | return this.substr(1).ltrim(character); 109 | } else { 110 | return this; 111 | } 112 | } else { 113 | return this.replace(/^\s+/, ""); 114 | } 115 | } 116 | 117 | /** 118 | * @desc Removes trailing whitespace characters, including tabs, line endings and the like. 119 | * @param {String} [character] if specified, removes trailing characters matching the parameter 120 | * instead of whitespace. 121 | * 122 | * @example 123 | * > $.writeln(" hello there ".trim()); 124 | * " hello there" 125 | */ 126 | 127 | String.prototype.rtrim = function (character) { 128 | if (character) { 129 | if (this.endswith(character) == true) { 130 | return this.slice(0, -1).rtrim(character); 131 | } else { 132 | return this; 133 | } 134 | } else { 135 | return this.replace(/\s+$/, ""); 136 | } 137 | } 138 | 139 | /** 140 | * @desc Removes leading and trailing whitespace characters, including tabs, line endings and the like. 141 | * @param {String} [character] if specified, removes leading and trailing characters matching the 142 | * parameter instead of whitespace. 143 | * 144 | * @example 145 | * > $.writeln(" hello there ".trim()); 146 | * "hello there" 147 | */ 148 | 149 | String.prototype.trim = function(character) { 150 | if (character) { 151 | return this.ltrim(character).rtrim(character); 152 | } else { 153 | return this.replace(/^\s+|\s+$/g, ""); 154 | } 155 | } -------------------------------------------------------------------------------- /core-packages/utils/lib/string.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc This is a simple string formatting method, loosely inspired on the one in Python 3. 3 | * 4 | * * In unnamed mode, specify placeholders with the **{}** symbol. 5 | * * In named mode, specify placeholders with **{propname}**. 6 | * 7 | * @param {String} replacements 8 | * For each **{}** symbol in the text, ``format`` expects a replacement argument. 9 | * Calls `.toString()` on each replacement, so you can pass in any data type. 10 | * You may also specify a single replacement object, which will do named formatting. 11 | * 12 | * @example 13 | * > var person = {'salutation': 'mister', 'name': 'John Smith'}; 14 | * > var hello = "Hello there, {}, I've heard your name is {}!".format(person.salutation, person.name); 15 | * > $.writeln(hello); 16 | * "Hello there, mister, I've heard your name is John Smith" 17 | * 18 | * @example 19 | * > var person = {'salutation': 'mister', 'name': 'John Smith'}; 20 | * > var hello = "Hello there, {salutation}, I've heard your name is {name}!".format(person); 21 | * > $.writeln(hello); 22 | * "Hello there, mister, I've heard your name is John Smith" 23 | */ 24 | 25 | exports.format = function() { 26 | var replacements = arguments.to('array'); 27 | var str = replacements.shift(); 28 | var named = replacements.length == 1 && replacements[0].reflect.name == 'Object'; 29 | 30 | if (named) { 31 | var dict = replacements[0]; 32 | dict.keys().forEach(function (key) { 33 | // replace globally (flagged g) 34 | str = str.replace("{" + key + "}", dict[key], "g"); 35 | }); 36 | return str; 37 | } else { 38 | // split the string into parts around the substring replacement symbols ({}). 39 | var chunks = str.split("{}"); 40 | // fill in the replacements 41 | for (var i in chunks) { 42 | var replacement = replacements.shift(); 43 | if (replacement) chunks[i] += replacement.toString(); 44 | } 45 | // join everything together 46 | return chunks.join(''); 47 | } 48 | } 49 | 50 | /** 51 | * @desc Tests whether the string starts with the specified substring. 52 | * @param {String} substring 53 | * @returns {Bool} True or false. 54 | */ 55 | 56 | exports.startswith = function (self, substring) { 57 | return new Boolean(selflength && self.indexOf(substring) === 0).valueOf(); 58 | } 59 | 60 | /** 61 | * @desc Tests whether the string ends with the specified substring. 62 | * @param {String} substring 63 | * @returns {Bool} True or false. 64 | */ 65 | 66 | exports.endswith = function (self, substring) { 67 | return new Boolean(self.length && self.indexOf(substring) == (self.length - substring.length)).valueOf(); 68 | } 69 | 70 | /** 71 | * @desc Tests whether the string contains the specified substring. 72 | * This is equal to ``str.indexOf(substring) != -1``. 73 | * @param {String} substring 74 | * @returns {Bool} True or false. 75 | */ 76 | 77 | exports.contains = function (self, substring) { 78 | return self.indexOf(substring) != -1; 79 | } 80 | 81 | /** 82 | * @desc Does what it says. 83 | * Does not check whether the string actually extends beyond the the substring. 84 | */ 85 | 86 | exports.indexAfter = function (self, substring) { 87 | var index = self.indexOf(substring); 88 | if (index == -1) { 89 | return index; 90 | } else { 91 | return index + substring.length; 92 | } 93 | } 94 | 95 | /** 96 | * @desc Removes leading whitespace characters, including tabs, line endings and the like. 97 | * @param {String} [character] if specified, removes leading characters matching the parameter 98 | * instead of whitespace. 99 | * 100 | * @example 101 | * > $.writeln(" hello there ".trim()); 102 | * "hello there " 103 | */ 104 | 105 | exports.ltrim = function(self, character) { 106 | if (character) { 107 | if (exports.endswith(self, character) == true) { 108 | return exports.ltrim(self.substr(1), character); 109 | } else { 110 | return self; 111 | } 112 | } else { 113 | return self.replace(/^\s+/, ""); 114 | } 115 | } 116 | 117 | /** 118 | * @desc Removes trailing whitespace characters, including tabs, line endings and the like. 119 | * @param {String} [character] if specified, removes trailing characters matching the parameter 120 | * instead of whitespace. 121 | * 122 | * @example 123 | * > $.writeln(" hello there ".trim()); 124 | * " hello there" 125 | */ 126 | 127 | exports.rtrim = function (self, character) { 128 | if (character) { 129 | if (exports.endswith(self, character) == true) { 130 | return exports.rtrim(self.slice(0, -1), character); 131 | } else { 132 | return self; 133 | } 134 | } else { 135 | return self.replace(/\s+$/, ""); 136 | } 137 | } 138 | 139 | /** 140 | * @desc Removes leading and trailing whitespace characters, including tabs, line endings and the like. 141 | * @param {String} [character] if specified, removes leading and trailing characters matching the 142 | * parameter instead of whitespace. 143 | * 144 | * @example 145 | * > $.writeln(" hello there ".trim()); 146 | * "hello there" 147 | */ 148 | 149 | exports.trim = function(self, character) { 150 | if (character) { 151 | return exports.rtrim(exports.ltrim(self, character), character); 152 | } else { 153 | return self.replace(/^\s+|\s+$/g, ""); 154 | } 155 | } -------------------------------------------------------------------------------- /patches/object.conversions.jsx: -------------------------------------------------------------------------------- 1 | var exports = {}; 2 | var base64 = exports; 3 | #include "../dependencies/base64.js" 4 | #include "../dependencies/json2.js" 5 | 6 | // keyvalue encoding comes in handy to create things like INI files and HTTP headers 7 | 8 | var keyvalue = {}; 9 | keyvalue.encode = function (obj, options) { 10 | var separator = options["separator"] || "="; 11 | var eol = options["eol"] || "\n"; 12 | var output = ""; 13 | var properties = obj.reflect.properties.reject(function (property) { 14 | return property.name.startswith("_") || property.name == 'reflect'; 15 | }); 16 | properties.forEach(function (property) { 17 | output += property.name + separator + obj[property.name] + eol; 18 | }); 19 | return output; 20 | } 21 | keyvalue.decode = function (str, options) { 22 | var separator = options["separator"] || "="; 23 | var eol = options["eol"] || "\n"; 24 | var obj = {}; 25 | var pairs = str.split(eol); 26 | pairs.forEach(function (pair) { 27 | pair = pair.split(separator); 28 | obj[pair[0]] = pair[1]; 29 | }); 30 | return obj; 31 | } 32 | 33 | /** 34 | * @desc Object serialization. 35 | * 36 | * The result of serialization followed by deserialization is the original object, whereas 37 | * a conversion is not reversible. 38 | * 39 | * @param {String} type Either ``base64`` or ``key-value``. 40 | * @param {Object} [options] Options, if applicable for the serialization type. 41 | * 42 | * @example 43 | * > var obj = {'key1': 'value1', 'key2': 'value2'}; 44 | * > obj.serialize('key-value', {'separator': ': ', 'eol': '\n'}); 45 | * "key1: value1\nkey2: value2\n" 46 | */ 47 | 48 | Object.prototype.serialize = function (type, options) { 49 | var obj = this; 50 | var options = options || {}; 51 | // type: json, keyvalue 52 | var serializations = { 53 | 'xml': function () { throw new NotImplementedError(); }, 54 | 'json': function () { return JSON.stringify(obj); }, 55 | 'base64': function () { return base64.encode64(obj); }, 56 | 'key-value': function () { return keyvalue.encode(obj, options); } 57 | }; 58 | 59 | if (serializations.hasOwnProperty(type)) { 60 | return serializations[type](); 61 | } else { 62 | throw RangeError("This method cannot convert from {} to {}".format(obj.prototype.name, type)); 63 | } 64 | } 65 | 66 | /** 67 | * @desc Object deserialization. 68 | * 69 | * @param {String} type Either ``xml``, ``base64`` or ``key-value``. 70 | * @param {Object} [options] Options, if applicable for the deserialization type. 71 | */ 72 | 73 | Object.prototype.deserialize = function (type, options) { 74 | var obj = this; 75 | 76 | var deserializations = { 77 | 'xml': function () { return new XML(obj); }, 78 | 'json': function () { return JSON.parse(obj); }, 79 | 'base64': function () { return base64.decode64(obj); }, 80 | 'key-value': function () { return keyvalue.decode(obj, options); } 81 | } 82 | 83 | if (deserializations.hasOwnProperty(type)) { 84 | return deserializations[type](); 85 | } else { 86 | throw RangeError("This method cannot convert from {} to {}".format(obj.prototype.name, type)); 87 | } 88 | } 89 | 90 | /** 91 | * @desc Provides easy shortcuts to a number of common conversions, like lowercasing a string or 92 | * converting the ``arguments`` object to an array. 93 | * 94 | * All of these conversions return a new object, they do not modify the original. 95 | * 96 | * A ``slug`` is a string that's usable as a filename or in an URL: it's 97 | * a lowercased string with all non-alphanumeric characters stripped out, and spaces replaced by 98 | * hyphens. 99 | * 100 | * Use this method instead of functions like ``parseInt`` and methods like ``str.toLowerCase()``. 101 | * 102 | * @param {String} type 103 | * One of ``boolean``, ``number``, ``int``, ``float``, ``string``, ``array``, ``alphanumeric``, ``slug``, ``lower`` and ``upper``. 104 | * 105 | * @example 106 | * > var list = [1.4, 2.2, 4.3]; 107 | * > function to_integers () { 108 | * ... return arguments.to('array').map(function (item) { return item.to('int'); }); 109 | * ... } 110 | * > to_integers(list); 111 | * [1,2,3] 112 | */ 113 | 114 | Object.prototype.to = function (type) { 115 | // never, ever modify the original object 116 | var result = this.clone(); 117 | 118 | var conversions = { 119 | /* types */ 120 | // REFACTOR: 'int' should be 'number', to correspond to the class name! 121 | 'boolean': function () { return !!result; }, 122 | 'number': function () { return new Number(result); }, 123 | 'int': function () { return parseInt(result); }, 124 | 'float': function () { return parseFloat(result); }, 125 | 'string': function () { return result.toString() }, 126 | 'array': function () { return Array.prototype.slice.call(result); }, 127 | /* other conversions */ 128 | 'alphanumeric': function () { return result.replace(/[^a-zA-Z0-9 ]/g, ""); }, 129 | 'slug': function () { return result.replace(/[^a-zA-Z0-9 ]/g, "").toLowerCase().replace(" ", "-"); }, 130 | 'lower': function () { return result.toLowerCase(); }, 131 | 'upper': function () { return result.toUpperCase(); } 132 | }; 133 | 134 | if (conversions.hasOwnProperty(type)) { 135 | return conversions[type](); 136 | } else { 137 | throw RangeError("This method cannot convert from {} to {}".format(this.prototype.name, type)); 138 | } 139 | } -------------------------------------------------------------------------------- /core-packages/utils/lib/object.conversions.jsx: -------------------------------------------------------------------------------- 1 | var exports = {}; 2 | var base64 = exports; 3 | #include "../dependencies/base64.js" 4 | #include "../dependencies/json2.js" 5 | 6 | // keyvalue encoding comes in handy to create things like INI files and HTTP headers 7 | 8 | var object = require("utils/object"); 9 | var string = require("utils/string"); 10 | var array = require("utils/array"); 11 | 12 | var keyvalue = {}; 13 | keyvalue.encode = function (obj, options) { 14 | var separator = options["separator"] || "="; 15 | var eol = options["eol"] || "\n"; 16 | var output = ""; 17 | var properties = array.reject(obj.reflect.properties, function (property) { 18 | return string.startswith(property.name, "_") || property.name == 'reflect'; 19 | }); 20 | array.forEach(properties, function (property) { 21 | output += property.name + separator + obj[property.name] + eol; 22 | }); 23 | return output; 24 | } 25 | keyvalue.decode = function (str, options) { 26 | var separator = options["separator"] || "="; 27 | var eol = options["eol"] || "\n"; 28 | var obj = {}; 29 | var pairs = str.split(eol); 30 | array.forEach(pairs, function (pair) { 31 | pair = pair.split(separator); 32 | obj[pair[0]] = pair[1]; 33 | }); 34 | return obj; 35 | } 36 | 37 | /** 38 | * @desc Object serialization. 39 | * 40 | * The result of serialization followed by deserialization is the original object, whereas 41 | * a conversion is not reversible. 42 | * 43 | * @param {String} type Either ``base64`` or ``key-value``. 44 | * @param {Object} [options] Options, if applicable for the serialization type. 45 | * 46 | * @example 47 | * > var obj = {'key1': 'value1', 'key2': 'value2'}; 48 | * > obj.serialize('key-value', {'separator': ': ', 'eol': '\n'}); 49 | * "key1: value1\nkey2: value2\n" 50 | */ 51 | 52 | exports.serialize = function (self, type, options) { 53 | var options = options || {}; 54 | // type: json, keyvalue 55 | var serializations = { 56 | 'xml': function () { throw new NotImplementedError(); }, 57 | 'json': function () { return JSON.stringify(self); }, 58 | 'base64': function () { return base64.encode64(self); }, 59 | 'key-value': function () { return keyvalue.encode(self, options); } 60 | }; 61 | 62 | if (serializations.hasOwnProperty(type)) { 63 | return serializations[type](); 64 | } else { 65 | throw RangeError(string.format("This method cannot convert from {} to {}", self.prototype.name, type)); 66 | } 67 | } 68 | 69 | /** 70 | * @desc Object deserialization. 71 | * 72 | * @param {String} type Either ``xml``, ``base64`` or ``key-value``. 73 | * @param {Object} [options] Options, if applicable for the deserialization type. 74 | */ 75 | 76 | exports.deserialize = function (self, type, options) { 77 | var deserializations = { 78 | 'xml': function () { return new XML(self); }, 79 | 'json': function () { return JSON.parse(self); }, 80 | 'base64': function () { return base64.decode64(self); }, 81 | 'key-value': function () { return keyvalue.decode(self, options); } 82 | } 83 | 84 | if (deserializations.hasOwnProperty(type)) { 85 | return deserializations[type](); 86 | } else { 87 | throw RangeError(string.format("This method cannot convert from {} to {}", self.prototype.name, type)); 88 | } 89 | } 90 | 91 | /** 92 | * @desc Provides easy shortcuts to a number of common conversions, like lowercasing a string or 93 | * converting the ``arguments`` object to an array. 94 | * 95 | * All of these conversions return a new object, they do not modify the original. 96 | * 97 | * A ``slug`` is a string that's usable as a filename or in an URL: it's 98 | * a lowercased string with all non-alphanumeric characters stripped out, and spaces replaced by 99 | * hyphens. 100 | * 101 | * Use this method instead of functions like ``parseInt`` and methods like ``str.toLowerCase()``. 102 | * 103 | * @param {String} type 104 | * One of ``boolean``, ``number``, ``int``, ``float``, ``string``, ``array``, ``alphanumeric``, ``slug``, ``lower`` and ``upper``. 105 | * 106 | * @example 107 | * > var list = [1.4, 2.2, 4.3]; 108 | * > function to_integers () { 109 | * ... return arguments.to('array').map(function (item) { return item.to('int'); }); 110 | * ... } 111 | * > to_integers(list); 112 | * [1,2,3] 113 | */ 114 | 115 | exports.to = function (self, type) { 116 | // never, ever modify the original object 117 | var result = object.clone(self); 118 | 119 | var conversions = { 120 | /* types */ 121 | // REFACTOR: 'int' should be 'number', to correspond to the class name! 122 | 'boolean': function () { return !!result; }, 123 | 'number': function () { return new Number(result); }, 124 | 'int': function () { return parseInt(result); }, 125 | 'float': function () { return parseFloat(result); }, 126 | 'string': function () { return result.toString() }, 127 | 'array': function () { return Array.prototype.slice.call(result); }, 128 | /* other conversions */ 129 | 'alphanumeric': function () { return result.replace(/[^a-zA-Z0-9 ]/g, ""); }, 130 | 'slug': function () { return result.replace(/[^a-zA-Z0-9 ]/g, "").toLowerCase().replace(" ", "-"); }, 131 | 'lower': function () { return result.toLowerCase(); }, 132 | 'upper': function () { return result.toUpperCase(); } 133 | }; 134 | 135 | if (conversions.hasOwnProperty(type)) { 136 | return conversions[type](); 137 | } else { 138 | throw RangeError(string.format("This method cannot convert from {} to {}", self.prototype.name, type)); 139 | } 140 | } -------------------------------------------------------------------------------- /doc/get-started.rst: -------------------------------------------------------------------------------- 1 | .. _get-started: 2 | 3 | ================================ 4 | Getting started with Extendables 5 | ================================ 6 | 7 | .. 8 | why a framework comes in handy; mention the low barrier to entry: it's free, it's easy, don't use what you don't need 9 | 10 | Download 11 | ======== 12 | 13 | You can download the latest version of Extendables at http://github.com/stdbrouw/extendables/zipball/master 14 | 15 | First steps 16 | =========== 17 | 18 | .. warning:: 19 | 20 | This part of the documentation is high on our to-do list, but currently you're better of skipping to the documentation for individual packages. The ScriptUI documentation in particular contains a pretty good beginner's walkthrough. 21 | 22 | .. include:: jsdoc/_global_.rst 23 | :start-after: class-title 24 | 25 | ... now let's see whether that worked. 26 | 27 | * loading a package (e.g. http) and the concept of namespaces 28 | * doing things with objects (e.g. reject, is, to, indexAfter) 29 | * manipulating stuff in InDesign (current, dom object shortcuts) 30 | * wrap things up with a basic interface (ui + menu) 31 | 32 | Learn more: 33 | 34 | * javascript 35 | * functional methods 36 | * conversions, serializations, object types and checks 37 | * InDesign: see ``dom`` docs 38 | * ScriptUI: see ``ui`` docs 39 | * Best practices 40 | * OO in javascript 41 | * Error handling 42 | * Testing 43 | * About this project 44 | 45 | Porting existing scripts to Extendables 46 | ======================================= 47 | 48 | Porting existing scripts from plain ExtendScript to an Extendables-enhanced is easy. Just keep your existing code, and refactor parts of it to use all the convenience methods Extendables provides when it seems sensible and when time allows. 49 | 50 | There is, however, one caveat: because Extendables enhances built-in object prototypes, ``for ... in`` loops won't work as you're used to. 51 | 52 | .. code-block:: extendscript 53 | 54 | #include "extendables/extendables.jsx"; 55 | var people = { 56 | "jack": 33, 57 | "jill": 11, 58 | "jonas": 5 59 | }; 60 | 61 | // this won't work anymore 62 | for (var person in people) { 63 | $.writeln("{} is {} years old".format(person, people[person])) 64 | } 65 | 66 | // this will work 67 | people.keys().forEach(function(person){ 68 | $.writeln("{} is {} years old".format(person, people[person])) 69 | }); 70 | 71 | // if you if you absolutely insist on using for...in, this'll work too 72 | for (var person in people) { 73 | if (!x.propertyIsEnumerable(person)) continue; 74 | $.writeln("{} is {} years old".format(person, people[person])) 75 | } 76 | 77 | // on arrays, this is better if you just want the ages 78 | var people = [{'name': 'Alfred', age: 33}, {'name': 'Zed', age: 45}]; 79 | people.pluck('age') 80 | 81 | // and this is better if you want a list of strings 82 | people.map(function(person){ 83 | return "{name} is {age} years old".format(person) 84 | }); 85 | 86 | Please keep in mind that, regardless of whether you use Extendables, `Mozilla specifically warns against `_ using ``for...in`` as a way to loop through arrays and hashes: 87 | 88 | Although it may be tempting to use this as a way to iterate over an Array, this is a bad idea. The ``for...in`` statement iterates over user-defined properties in addition to the array elements, so if you modify the array's non-integer or non-positive properties (e.g. by adding a "``foo``" property to it or even by adding a method or property to Array.prototype), the for...in statement will return the name of your user-defined properties in addition to the numeric indexes. Also, because order of iteration is arbitrary, iterating over an array may not visit elements in numeric order. Thus it is better to use a traditional for loop with a numeric index when iterating over arrays. Similar arguments might be used against even using ``for...in`` at all (at least without propertyIsEnumerable() or hasOwnProperty() checks), since it will also iterate over Object.prototype (which, though usually discouraged, can, as in the case of Array.prototype, be usefully extended by the user where are no namespacing concerns caused by inclusion of other libraries which might not perform the above checks on such iterations and where they are aware of the effect such extension will have on their own use of iterators such as ``for...in``). 89 | 90 | Installing a new Extendables module 91 | =================================== 92 | 93 | Extendables comes with a bunch of built-in modules, but you can also `write your own `_ and install modules other people have contributed. Contributed modules are installed by simply downloading them and putting them inside of ``extendables/site-packages``. 94 | 95 | .. note:: 96 | 97 | Because Extendables is so new, there are currently no contributed modules the author is aware of. However, even if you're not planning on open-sourcing any of your own work, it still makes sense to put code that you plan to reuse in different projects into its own module. Code in a module gets its own namespace and the package layout makes it easy to include documentation and unit tests alongside your code. Modules make it easy to keep track of library code and they keep a project directory from getting cluttered with files full of helper functions. 98 | 99 | Issues 100 | ====== 101 | 102 | Any issues or feature requests should be posted on `our GitHub page `_. You can also reach the maintainer at stijn@stdout.be. -------------------------------------------------------------------------------- /patches/test/extendscript.specs: -------------------------------------------------------------------------------- 1 | describe('Monkeypatches on Object', function () { 2 | it('gives quick access to objects\' keys and values', function () { 3 | var nation = {'name': 'Belgium', 'continent': 'Europe'} 4 | expect(nation.values()).toEqual(['Belgium', 'Europe']); 5 | expect(nation.keys()).toEqual(['name', 'continent']); 6 | }); 7 | 8 | it('can merge an object with another', function () { 9 | var nation = {'name': 'Belgium'}.merge({'continent': 'Europe'}); 10 | expect(nation.has('continent')).toBeTruthy(); 11 | }); 12 | 13 | it('can test whether an object has a value for a property', function () { 14 | var nation = {'name': 'Belgium', 'continent': ''}; 15 | expect(nation.has('name')).toBeTruthy(); 16 | expect(nation.has('continent')).toBeFalsy(); 17 | }); 18 | }); 19 | 20 | describe('Monkeypatches on Array', function () { 21 | it('has a number of already vetted methods, \ 22 | courtesy of underscore.js and the Mozilla Developer Network', function () { 23 | /* 24 | these include: indexOf, lastIndexOf, every, filter, forEach, 25 | map, some, reduce, reduceRight, select, reject and flatten 26 | */ 27 | expect(true).toBeTruthy(); 28 | }); 29 | 30 | it('has min and max methods that work on objects', function () { 31 | var array = [ 32 | {'first': 'Abraham', 'last': 'Lincoln'}, 33 | {'first': 'Joe', 'last': 'Pesci'}, 34 | {'first': 'Zed', 'last': 'Alastair'} 35 | ]; 36 | function get_first (obj) { 37 | return obj.first; 38 | } 39 | function get_last (obj) { 40 | return obj.last; 41 | } 42 | expect(array.min(get_first).first).toEqual('Abraham'); 43 | expect(array.min('first').first).toEqual('Abraham'); 44 | expect(array.max(get_first).first).toEqual('Zed'); 45 | expect(array.max('first').first).toEqual('Zed'); 46 | expect(array.min(get_last).first).toEqual('Zed'); 47 | expect(array.min('last').first).toEqual('Zed'); 48 | expect(array.max(get_last).first).toEqual('Joe'); 49 | expect(array.max('last').first).toEqual('Joe'); 50 | }); 51 | 52 | it('has a pluck method', function () { 53 | var people = [{'name': 'Alfred', age: 33}, {'name': 'Zed', age: 45}]; 54 | expect(people.pluck('age')).toEqual([33,45]); 55 | expect(people.pluck('age').sum()).toEqual(78); 56 | expect(people.sum('age')).toEqual(78); 57 | expect(people.sum(function (person) { return person.age })).toEqual(78); 58 | }); 59 | 60 | it('has a sum method that works on objects', function () { 61 | var numbers = [3, 2, 5]; 62 | var persons = [ 63 | {'name': 'Abraham', 'children': 5}, 64 | {'name': 'Joe', 'children': 3}, 65 | {'name': 'Zed', 'children': 0} 66 | ]; 67 | 68 | function get_children (obj) { 69 | return obj.children; 70 | } 71 | 72 | expect(numbers.sum()).toEqual(10); 73 | expect(persons.sum(get_children)).toEqual(8); 74 | expect(persons.sum('children')).toEqual(8); 75 | }); 76 | }); 77 | 78 | describe('Monkeypatches on String', function () { 79 | it('does unnamed string formatting', function () { 80 | expect("hello {} {}!".format("mister", "Jones")).toEqual("hello mister Jones!"); 81 | }); 82 | 83 | it('does named string formatting', function () { 84 | var person = {'salutation': 'mister', 'name': 'John Smith'}; 85 | var unnamed = "Hello there, {}, I've heard your name is {}!".format(person.salutation, person.name); 86 | var named = "Hello there, {salutation}, I've heard your name is {name}!".format(person); 87 | expect(named).toEqual(unnamed); 88 | }); 89 | }); 90 | 91 | describe('Monkeypatches on Date', function () { 92 | it('has a simple timer', function () { 93 | Date.timer.set(); 94 | $.sleep(100); 95 | var time_passed = Date.timer.get(); 96 | var time_passed_s = Date.timer.get('s'); 97 | expect(time_passed < 200).toBeTruthy(); 98 | expect(time_passed_s < 1).toBeTruthy(); 99 | }); 100 | }); 101 | 102 | describe('Monkeypatches on Number', function () { 103 | it('has a range class method that works like the Python range() implementation', function () { 104 | // these examples come straight out of Python docs 105 | expect(Number.range(10)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 106 | expect(Number.range(1, 11)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 107 | expect(Number.range(0, 30, 5)).toEqual([0, 5, 10, 15, 20, 25]); 108 | expect(Number.range(0, 10, 3)).toEqual([0, 3, 6, 9]); 109 | expect(Number.range(0, -10, -1)).toEqual([0, -1, -2, -3, -4, -5, -6, -7, -8, -9]); 110 | expect(Number.range(0)).toEqual([]); 111 | expect(Number.range(1, 0)).toEqual([]); 112 | }); 113 | }); 114 | 115 | describe('Monkeypatches on File and Folder', function () { 116 | it('can join paths', function () { 117 | var identical = []; 118 | identical[0] = new File("file.ext").at("patches/test/fixtures").at(Folder.extendables); 119 | identical[1] = new File($.fileName).parent; 120 | identical[1].changePath("fixtures/file.ext"); 121 | identical[2] = new File("patches/test/fixtures/file.ext").at(Folder.extendables); 122 | identical[3] = new File("fixtures/file.ext").at(new Folder("patches/test")).at(Folder.extendables); 123 | identical[4] = new File("/fixtures/file.ext").at(new Folder("/patches/test")).at(Folder.extendables); 124 | 125 | var uris = identical.map(function (file) { 126 | return file.absoluteURI; 127 | }); 128 | var is_identical = (uris[0] == uris[1] && uris[1] == uris[2] && uris[2] == uris[3] && uris[3] == uris[4]); 129 | expect(is_identical).toBeTruthy(); 130 | 131 | var all_exist = identical.map(function (file) { 132 | return file.exists; 133 | }).every(function (exists) { 134 | return exists == true; 135 | }); 136 | expect(all_exist).toBeTruthy(); 137 | }); 138 | 139 | it('can return the directory name', function () { 140 | var dirname = new File("/one/two/three.jsx").component('path'); 141 | expect(dirname).toEqual("/one/two"); 142 | }); 143 | 144 | it('can return the base name (file name)', function () { 145 | var basename = new File("/one/two/three.jsx").component('basename'); 146 | expect(basename.toString()).toEqual("three"); 147 | }); 148 | 149 | it('can return the file extension', function () { 150 | var ext = new File("/one/two/three.jsx").component('extension'); 151 | expect(ext).toEqual("jsx"); 152 | }); 153 | }); -------------------------------------------------------------------------------- /core-packages/testing/templates/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Extendables test report 6 | 149 | 150 | 161 | 162 | 163 | 168 |

Extendables test report

169 |

{date} at {time}

170 |
171 | Jasmine ran {total} tests in {duration} seconds 172 |
173 | 174 | 175 | 176 | 177 |
178 |
179 | {suites => partial.suite.html} 180 |

{environment => partial.environment.html}

181 | 182 | 183 | -------------------------------------------------------------------------------- /doc/homepage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Extendables: a stalwart little framework for Adobe ExtendScript 5 | 6 | 89 | 100 | 101 | 102 |
103 | Fork me on GitHub 104 | 105 | 109 |

Extendables is a developers' framework for Adobe ExtendScript. Writing heavy-duty automations for InDesign, or anything more than just a throwaway script? This is for you.

110 |
    111 |
  • Javascript goodies: we've added in as much features from Javascript 1.6+ as we could, like the .forEach() and .filter() methods on arrays.
  • 112 |
  • Helpful shortcuts make your code less verbose. We've monkeypatched a bunch of additional methods on DOM objects like Document and XMLElement.
  • 113 |
  • (Some) batteries included, like an HTTP library and a UI mini-framework.
  • 114 |
  • Keep your sanity with support for logging and unit testing (using the excellent Jasmine framework). And we've got CommonJS-inspired namespaced modules. Because namespaces are one honking great idea.
  • 115 |
  • Free as in freedom. Extendables comes with a very permissive MIT license. Feel free to use this framework in commercial closed-source projects — it's totally okay.
  • 116 |
117 |

→ Come on now, show me a code sample!

118 |

Check out the documentation. Just include extendables.jsx at the top of your script to get started. Porting an existing library to an Extendables library is easy too.

119 |

A few caveats. Extendables is still pretty young. Any Creative Suite scripter will find a lot to like, but the framework was created with InDesign scripting in mind. There's a ton of potential cool stuff that's not in there right now.

120 |

But maybe, just maybe, even in its current incarnation, you'll like what you see.

121 |
122 | 123 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Extendables documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Sep 23 17:42:30 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('./_ext')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['glob_include', 'sphinxcontrib.googleanalytics'] 29 | googleanalytics_id = 'UA-19440923-1' 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Extendables' 45 | copyright = u'2010, Stijn Debrouwere' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.3' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.3a' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'nature-fixedwidth' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | html_theme_path = ["_themes"] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_domain_indices = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 154 | #html_show_sphinx = True 155 | 156 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 157 | #html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'Extendablesdoc' 169 | 170 | primary_domain = 'js' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | # The paper size ('letter' or 'a4'). 176 | #latex_paper_size = 'letter' 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #latex_font_size = '10pt' 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ('index', 'Extendables.tex', u'Extendables Documentation', 185 | u'Stijn Debrouwere', 'manual'), 186 | ] 187 | 188 | # The name of an image file (relative to this directory) to place at the top of 189 | # the title page. 190 | #latex_logo = None 191 | 192 | # For "manual" documents, if this is true, then toplevel headings are parts, 193 | # not chapters. 194 | #latex_use_parts = False 195 | 196 | # If true, show page references after internal links. 197 | #latex_show_pagerefs = False 198 | 199 | # If true, show URL addresses after external links. 200 | #latex_show_urls = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'extendables', u'Extendables Documentation', 218 | [u'Stijn Debrouwere'], 1) 219 | ] 220 | -------------------------------------------------------------------------------- /doc/core/contribute.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Contribute 3 | ========== 4 | 5 | You can contribute to Extendables simply by using the framework. The more people use the framework, the more bugs we'll find and squash, and the more reliable the framework will thus become. If you encounter an issue, please run the test suite (so we have as much diagnostic information as possible) and report your issue using the GitHub issue tracker. 6 | 7 | Another way to contribute is by writing your own libraries and open-sourcing those. :ref:`Learn more about writing your own modules. ` 8 | 9 | Last but not least, contributions — bugfixes, new code or documentation — to Extendables itself are much appreciated. You can check the issue tracker on GitHub to see open issues or feature requests, or you can implement new features by forking the repository on GitHub and doing a push request once you have something you want to push back into the main repository. Read on to learn more. 10 | 11 | Running the test suite 12 | ====================== 13 | 14 | To run the test suite, open ``tools/testrunner.jsx`` in the ExtendScript Toolkit, and run the script. Test results will reside in Extendables' ``log`` directory. 15 | 16 | Writing documentation 17 | ===================== 18 | 19 | Extendables uses the `JSDoc toolkit `_ to autogenerate documentation, a toolkit template that outputs to `reStructuredText `_ and finally the Sphinx documentation generator to bundle that automatically extracted documentation together with free-flowing documentation. The combination of JSDoc and Sphinx makes it possible to have documentation that serves as both a complete reference and a broader overview of how different components of the framework hang together and how best to use certain libraries. 20 | 21 | JSDoc 22 | ----- 23 | 24 | JSDoc is a special syntax for documenting Javascript code inline. It looks like this: 25 | 26 | .. code-block:: extendscript 27 | 28 | /** 29 | * @desc Performs a POST request on the specified resource. 30 | * @param {String} url The URL for the resource 31 | * @param {Object|String} data Either an object, to be urlencoded by this function, or a string that 32 | * will be passed along unchanged. 33 | * @param {Object} [basic_auth] Basic authentication — any object with ``username`` and ``password`` 34 | * properties will do. 35 | * @param {Number} [timeout=1] How long before the http client should give up. 36 | */ 37 | 38 | function post (url, data, basic_auth, timeout) { 39 | var request = new HTTPRequest("POST", url); 40 | return _push(request, data, basic_auth, timeout); 41 | } 42 | 43 | JSDoc syntax gets picked up by the JSDoc toolkit and from there finds its way into the Sphinx documentation. With JSDoc, it's easier to keep code documentation in sync with the code — whenever you change an interface, change the associated inline documentation. 44 | 45 | .. seealso:: 46 | 47 | A full JSDoc reference document is up at http://code.google.com/p/jsdoc-toolkit/wiki/TagReference. 48 | 49 | Sphinx 50 | ------ 51 | 52 | `Sphinx `_ is a documentation generator originally out of the `Python `_ world, but with good support for documenting Javascript (and thus ExtendScript) projects. 53 | 54 | Sphinx works with reStructuredText. While its syntax can be a bit off-putting at first, it remains the best way to create beautiful documentation for software projects. 55 | 56 | Documentation for Sphinx lives at http://sphinx.pocoo.org/ 57 | 58 | What goes where 59 | --------------- 60 | 61 | The documentation to Extendables resides in the ``doc`` directory. Per `the CommonJS specifications `_, documentation for modules belongs in a ``doc`` dir under each module's root directory, e.g. ``site-packages/http/doc/readme.rst``. 62 | 63 | .. seealso: 64 | 65 | If you're writing documentation for a module, you'll find more information at :ref:`writing-a-module`. 66 | 67 | Committing and making a new build 68 | ================================= 69 | 70 | Making a new build takes a couple of steps: 71 | 72 | * generating new autodocs 73 | * generating new Sphinx docs 74 | * committing any new files to the repository and deleting old ones 75 | * building the documentation and committing it to the ``gh-pages`` branch 76 | * finally pushing the new version to GitHub 77 | 78 | However, if you're on a UNIX-compatible system — either Apple OS X or Microsoft Windows using an emulator like `Cygwin `_ or `MinGW `_), you can use `Fabric `_ to do one-click builds. 79 | 80 | Fabric 81 | ------ 82 | 83 | `Fabric `_ helps automate administrative tasks. Installing Fabric is outside the scope of this documentation. Once installed, open a command-line interface, navigate to the ``extendables`` directory and use ``fab --list`` to get an overview of all tasks you can execute. 84 | 85 | Notably, use ``fab docbuild`` to build both the autodocs and Sphinx docs, ``fab commit`` to commit to the Git repository and optionally push to the central repository at http://github.com/stdbrouw/Extendables as well. 86 | 87 | .. tip:: 88 | 89 | If you know a little bit of Python, it's easy to add new Fabric commands. Take a quick look at ``fabfile.py`` where all tasks are defined. 90 | 91 | Git and GitHub 92 | -------------- 93 | 94 | Extendables is under revision control, using the excellent Git VCS. The central repository is over at http://github.com/stdbrouw/Extendables. GitHub has `excellent help pages `_ that will get you started with both Git and GitHub. 95 | 96 | If you're uncomfortable using Git, just use ``fab commit`` instead and it will guide you through the commit process. 97 | 98 | .. warning:: 99 | 100 | If you're doing development on the Extendables core, make sure you don't put anything valuable inside ``extendables/site-packages`` — nothing in ``site-packages`` or ``log`` is tracked by Git, so whenever you change branches, everything inside of these directories will disappear. Instead, you could 101 | 102 | * use two different installations of Extendables: one for testing and development, 103 | and another as a production environment. That way, you can use a stable version 104 | for app development, but also the latest checkout of Extendables for working on the core. 105 | * place your own modules somewhere else, and place a symlink (OSX-only) inside of 106 | ``extendables/site-packages`` so your modules will get registered. 107 | 108 | Guidelines 109 | ---------- 110 | 111 | * Versioning: see the `semver spec `_. Since we're currently still in the experimental phase (version 0), semver places no restrictions on how we use version numbers, but this will become important once we graduate to version 1. 112 | * In time, if necessary, we may adopt the `nvie branching strategy `_. 113 | * We don't commit things that aren't documented or tested -- there's no point having functionality in the framework if nobody knows it's there or if it's not dependable. We encourage people to show unfinished code, they just shouldn't expect it to get committed any time soon. 114 | * Keep commits as atomic as possible: smaller is better. 115 | * Version control messages: messages should be in the past tense, e.g. ``Improved XMLElement#repr``. If your commit closes a ticket in the issue tracker, start your commit message with ``Fixed #issue``, e.g. ``Fixed #33 -- improved XMLElement#repr``. 116 | * In the documentation, instance methods are referred to as ``MyClass#mymethod``, class methods are referred to as ``MyClass.mymethod``. 117 | * style guide: none yet; to be discussed. See the `Django style guide `_ for a good example of issues that need to be addressed (code style, reporting bugs, how to do unit tests, naming conventions et cetera) and how we can communicate them. --------------------------------------------------------------------------------