├── test ├── airenv │ ├── jsclass │ ├── app.xml │ └── index.html ├── xulenv │ ├── chrome │ │ ├── content │ │ │ ├── jsclass │ │ │ └── main.xul │ │ └── chrome.manifest │ ├── defaults │ │ └── preferences │ │ │ └── prefs.js │ └── application.ini ├── fixtures │ └── common.js ├── phantom.js ├── browser.html ├── console.js ├── examples │ ├── reformat.js │ ├── benchmarks.js │ ├── tracing.js │ └── async.js ├── specs │ ├── singleton_spec.js │ ├── test │ │ └── test_spec_helpers.js │ ├── method_spec.js │ ├── console_spec.js │ ├── interface_spec.js │ ├── forwardable_spec.js │ ├── proxy_spec.js │ ├── decorator_spec.js │ ├── tsort_spec.js │ └── comparable_spec.js └── runner.js ├── source ├── dom │ ├── _tail.js │ ├── _head.js │ ├── dom.js │ ├── event.js │ └── builder.js ├── test │ ├── _tail.js │ ├── reporters │ │ ├── exit_status.js │ │ ├── coverage.js │ │ ├── phantomjs.js │ │ ├── dot.js │ │ ├── test_swarm.js │ │ ├── composite.js │ │ ├── json.js │ │ ├── tap.js │ │ └── error.js │ ├── unit │ │ ├── failure.js │ │ ├── error.js │ │ ├── observable.js │ │ ├── test_result.js │ │ ├── assertion_message.js │ │ └── test_suite.js │ ├── helpers.js │ ├── ui │ │ ├── terminal.js │ │ └── browser.js │ ├── unit.js │ ├── context │ │ ├── test.js │ │ ├── context.js │ │ ├── suite.js │ │ ├── shared_behavior.js │ │ └── life_cycle.js │ ├── _head.js │ ├── mocking │ │ ├── dsl.js │ │ └── matchers.js │ ├── async_steps.js │ └── coverage.js ├── console │ ├── _tail.js │ ├── windows.js │ ├── phantom.js │ ├── _head.js │ ├── browser.js │ ├── node.js │ ├── rhino.js │ ├── config.js │ ├── browser_color.js │ └── base.js ├── package │ ├── browser.js │ ├── _tail.js │ ├── loaders │ │ ├── server.js │ │ ├── rhino.js │ │ ├── wsh.js │ │ ├── commonjs.js │ │ ├── xulrunner.js │ │ └── browser.js │ ├── _head.js │ ├── loader.js │ └── dsl.js ├── assets │ └── bullet_go.png ├── core │ ├── _tail.js │ ├── singleton.js │ ├── _head.js │ ├── interface.js │ ├── keywords.js │ ├── bootstrap.js │ ├── class.js │ ├── kernel.js │ └── utils.js ├── forwardable.js ├── comparable.js ├── constant_scope.js ├── decorator.js ├── observable.js ├── proxy.js ├── deferrable.js ├── benchmark.js ├── tsort.js ├── state.js └── command.js ├── site ├── config │ ├── compass.rb │ └── site.rb ├── site │ ├── images │ │ └── tracing.png │ ├── stylesheets │ │ └── github.css │ └── javascripts │ │ └── analytics.js └── src │ ├── pages │ ├── platforms │ │ └── node.haml │ ├── singletons.haml │ ├── index.haml │ ├── interfaces.haml │ ├── benchmark.haml │ ├── hooks.haml │ ├── binding.haml │ ├── platforms.haml │ ├── debugging.haml │ ├── packages.haml │ ├── packages │ │ ├── autoload.haml │ │ └── customloaders.haml │ ├── license.haml │ ├── forwardable.haml │ ├── proxies.haml │ ├── singletonmethods.haml │ ├── classmethods.haml │ ├── comparable.haml │ ├── tsort.haml │ ├── decorator.haml │ ├── modifyingmodules.haml │ ├── stacktrace.haml │ ├── kernel.haml │ ├── introduction.haml │ └── console.haml │ └── stylesheets │ └── screen.sass ├── .gitignore ├── .travis.yml ├── Gemfile ├── index.js ├── Rakefile ├── LICENSE └── README.markdown /test/airenv/jsclass: -------------------------------------------------------------------------------- 1 | ../.. -------------------------------------------------------------------------------- /test/xulenv/chrome/content/jsclass: -------------------------------------------------------------------------------- 1 | ../../../.. -------------------------------------------------------------------------------- /source/dom/_tail.js: -------------------------------------------------------------------------------- 1 | exports.DOM = DOM; 2 | }); 3 | 4 | -------------------------------------------------------------------------------- /source/test/_tail.js: -------------------------------------------------------------------------------- 1 | exports.Test = Test; 2 | }); 3 | 4 | -------------------------------------------------------------------------------- /source/console/_tail.js: -------------------------------------------------------------------------------- 1 | exports.Console = Console; 2 | }); 3 | 4 | -------------------------------------------------------------------------------- /test/xulenv/chrome/chrome.manifest: -------------------------------------------------------------------------------- 1 | content xultestenv file:content/ 2 | -------------------------------------------------------------------------------- /source/package/browser.js: -------------------------------------------------------------------------------- 1 | Package.loader = Package.BrowserLoader; 2 | 3 | -------------------------------------------------------------------------------- /site/config/compass.rb: -------------------------------------------------------------------------------- 1 | require "staticmatic/compass" 2 | 3 | project_type = :staticmatic -------------------------------------------------------------------------------- /source/assets/bullet_go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/js.class/master/source/assets/bullet_go.png -------------------------------------------------------------------------------- /source/core/_tail.js: -------------------------------------------------------------------------------- 1 | JS.extend(exports, JS); 2 | if (global.JS) JS.extend(global.JS, JS); 3 | }); 4 | 5 | -------------------------------------------------------------------------------- /site/site/images/tracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snitko/js.class/master/site/site/images/tracing.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | build 3 | node_modules 4 | site/site/stylesheets/screen.css 5 | site/site/*.html 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.6" 5 | - "0.8" 6 | - "0.10" 7 | - "0.11" 8 | 9 | -------------------------------------------------------------------------------- /test/xulenv/defaults/preferences/prefs.js: -------------------------------------------------------------------------------- 1 | pref("toolkit.defaultChromeURI", "chrome://xultestenv/content/main.xul"); 2 | -------------------------------------------------------------------------------- /source/package/_tail.js: -------------------------------------------------------------------------------- 1 | exports.Package = Package; 2 | exports.Packages = exports.packages = packages; 3 | exports.DSL = DSL; 4 | }); 5 | 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'staticmatic' 4 | gem 'compass', '~> 0.11.0' 5 | gem 'haml', '~> 3.1.0' 6 | gem 'RedCloth', '~> 3.0.0' 7 | 8 | -------------------------------------------------------------------------------- /source/core/singleton.js: -------------------------------------------------------------------------------- 1 | JS.Singleton = new JS.Class('Singleton', { 2 | initialize: function(name, parent, methods) { 3 | return new (new JS.Class(name, parent, methods)); 4 | } 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/common.js: -------------------------------------------------------------------------------- 1 | var Common = {name: 'CommonJS module'}; 2 | var HTTP = {name: 'CommonJS HTTP lib'}; 3 | 4 | if (typeof exports === 'object') { 5 | exports.Common = Common; 6 | exports.HTTP = HTTP; 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | cleanup = (typeof JSCLASS_PATH === 'undefined'); 3 | 4 | JSCLASS_PATH = path.dirname(__filename) + '/src'; 5 | module.exports = require(JSCLASS_PATH + '/loader'); 6 | 7 | if (cleanup) delete JSCLASS_PATH; 8 | 9 | -------------------------------------------------------------------------------- /test/xulenv/application.ini: -------------------------------------------------------------------------------- 1 | [App] 2 | Vendor=JSClass 3 | Name=XULTestEnvironment 4 | Version=0.1 5 | BuildID=20101002 6 | Copyright=Copyright (c) 2010 Aurélio A. Heckert 7 | ID=xultestenv@jsclass.jcoglan.com 8 | 9 | [Gecko] 10 | MinVersion=1.8 11 | MaxVersion=1.9.2.* 12 | 13 | -------------------------------------------------------------------------------- /source/dom/_head.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | -------------------------------------------------------------------------------- /test/phantom.js: -------------------------------------------------------------------------------- 1 | JSCLASS_PATH = '../build/src' 2 | var pkg = require(JSCLASS_PATH + '/loader') 3 | 4 | pkg.require('JS.Test', function(Test) { 5 | var page = new WebPage(), 6 | reporter = new Test.Reporters.PhantomJS({}, page) 7 | 8 | page.open('test/browser.html') 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /source/console/windows.js: -------------------------------------------------------------------------------- 1 | Console.extend({ 2 | Windows: new JS.Class(Console.Base, { 3 | coloring: function() { 4 | return false; 5 | }, 6 | 7 | echo: function(string) { 8 | WScript.Echo(string); 9 | }, 10 | 11 | exit: function(status) { 12 | WScript.Quit(status); 13 | } 14 | }) 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /source/console/phantom.js: -------------------------------------------------------------------------------- 1 | Console.extend({ 2 | Phantom: new JS.Class(Console.Base, { 3 | echo: function(string) { 4 | console.log(string); 5 | }, 6 | 7 | envvar: function(name) { 8 | return require('system').env[name] || null; 9 | }, 10 | 11 | exit: function(status) { 12 | phantom.exit(status); 13 | } 14 | }) 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /site/site/stylesheets/github.css: -------------------------------------------------------------------------------- 1 | .atn { color:#008080 } 2 | .atv { color:#008080 } 3 | .com { color:#999988 } 4 | .dec { color:#000000; font-weight:bold } 5 | .kwd { color:#000000; font-weight:bold } 6 | .lit { color:#009999 } 7 | .pln { color:#000000 } 8 | .pun { color:#666666 } 9 | .str { color:#dd1144 } 10 | .tag { color:#000080 } 11 | .typ { color:#445588 } 12 | 13 | -------------------------------------------------------------------------------- /source/console/_head.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS, 4 | 5 | Enumerable = js.Enumerable || require('./enumerable').Enumerable; 6 | 7 | if (E) exports.JS = exports; 8 | factory(js, Enumerable, E ? exports : js); 9 | 10 | })(function(JS, Enumerable, exports) { 11 | 'use strict'; 12 | 13 | -------------------------------------------------------------------------------- /site/site/javascripts/analytics.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 5 | 6 | ga('create', 'UA-873493-4', 'jcoglan.com'); 7 | ga('send', 'pageview'); 8 | 9 | -------------------------------------------------------------------------------- /test/airenv/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JSClass 4 | 3.0 5 | JSClass 6 | 7 | index.html 8 | true 9 | 720 10 | 800 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JS.Class test runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /source/package/loaders/server.js: -------------------------------------------------------------------------------- 1 | Package.ServerLoader = { 2 | usable: function() { 3 | return typeof Package._getObject('load') === 'function' && 4 | typeof Package._getObject('version') === 'function'; 5 | }, 6 | 7 | __FILE__: function() { 8 | return this._currentPath; 9 | }, 10 | 11 | loadFile: function(path, fireCallbacks) { 12 | this._currentPath = path; 13 | load(path); 14 | fireCallbacks(); 15 | } 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /source/core/_head.js: -------------------------------------------------------------------------------- 1 | <%- wake.comment(wake.file('LICENSE')) %> 2 | 3 | var JS = (typeof this.JS === 'undefined') ? {} : this.JS; 4 | 5 | (function(factory) { 6 | var $ = (typeof this.global === 'object') ? this.global : this, 7 | E = (typeof exports === 'object'); 8 | 9 | if (E) { 10 | exports.JS = exports; 11 | JS = exports; 12 | } else { 13 | $.JS = JS; 14 | } 15 | factory($, JS); 16 | 17 | })(function(global, exports) { 18 | 'use strict'; 19 | 20 | -------------------------------------------------------------------------------- /test/airenv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JS.Class test runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /source/package/_head.js: -------------------------------------------------------------------------------- 1 | <%- wake.comment(wake.file('LICENSE')) %> 2 | 3 | var JS = (typeof this.JS === 'undefined') ? {} : this.JS; 4 | JS.Date = Date; 5 | 6 | (function(factory) { 7 | var $ = (typeof this.global === 'object') ? this.global : this, 8 | E = (typeof exports === 'object'); 9 | 10 | if (E) { 11 | exports.JS = exports; 12 | JS = exports; 13 | } else { 14 | $.JS = JS; 15 | } 16 | factory($, JS); 17 | 18 | })(function(global, exports) { 19 | 'use strict'; 20 | 21 | -------------------------------------------------------------------------------- /source/test/reporters/exit_status.js: -------------------------------------------------------------------------------- 1 | Test.Reporters.extend({ 2 | ExitStatus: new JS.Class({ 3 | startSuite: function(event) {}, 4 | 5 | startContext: function(event) {}, 6 | 7 | startTest: function(event) {}, 8 | 9 | addFault: function(event) {}, 10 | 11 | endTest: function(event) {}, 12 | 13 | endContext: function(event) {}, 14 | 15 | update: function(event) {}, 16 | 17 | endSuite: function(event) { 18 | Console.exit(event.passed ? 0 : 1); 19 | } 20 | }) 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /site/config/site.rb: -------------------------------------------------------------------------------- 1 | # Default is 3000 2 | # configuration.preview_server_port = 3000 3 | 4 | # Default is localhost 5 | # configuration.preview_server_host = "localhost" 6 | 7 | # Default is true 8 | # When false .html & index.html get stripped off generated urls 9 | # configuration.use_extensions_for_page_links = true 10 | 11 | # Default is an empty hash 12 | # configuration.sass_options = {} 13 | 14 | # Default is an empty hash 15 | # http://haml-lang.com/docs/yardoc/file.HAML_REFERENCE.html#options 16 | # configuration.haml_options = {} -------------------------------------------------------------------------------- /source/package/loader.js: -------------------------------------------------------------------------------- 1 | var candidates = [ Package.XULRunnerLoader, 2 | Package.RhinoLoader, 3 | Package.BrowserLoader, 4 | Package.CommonJSLoader, 5 | Package.ServerLoader, 6 | Package.WshLoader ], 7 | 8 | n = candidates.length, 9 | i, candidate; 10 | 11 | for (i = 0; i < n; i++) { 12 | candidate = candidates[i]; 13 | if (candidate.usable()) { 14 | Package.loader = candidate; 15 | if (candidate.setup) candidate.setup(); 16 | break; 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /source/test/unit/failure.js: -------------------------------------------------------------------------------- 1 | Test.Unit.extend({ 2 | Failure: new JS.Class({ 3 | initialize: function(testCase, message) { 4 | this._testCase = testCase; 5 | this._message = message; 6 | }, 7 | 8 | metadata: function() { 9 | return { 10 | test: this.testMetadata(), 11 | error: this.errorMetadata() 12 | } 13 | }, 14 | 15 | testMetadata: function() { 16 | return this._testCase.metadata(); 17 | }, 18 | 19 | errorMetadata: function() { 20 | return { 21 | type: 'failure', 22 | message: this._message 23 | }; 24 | } 25 | }) 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /source/package/loaders/rhino.js: -------------------------------------------------------------------------------- 1 | Package.RhinoLoader = { 2 | usable: function() { 3 | return typeof java === 'object' && 4 | typeof require === 'function'; 5 | }, 6 | 7 | __FILE__: function() { 8 | return this._currentPath; 9 | }, 10 | 11 | loadFile: function(path, fireCallbacks) { 12 | var cwd = java.lang.System.getProperty('user.dir'), 13 | module = path.replace(/\.[^\.]+$/g, ''); 14 | 15 | var requirePath = new java.io.File(cwd, module).toString(); 16 | this._currentPath = requirePath + '.js'; 17 | var module = require(requirePath); 18 | fireCallbacks(module); 19 | 20 | return module; 21 | } 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /source/package/loaders/wsh.js: -------------------------------------------------------------------------------- 1 | Package.WshLoader = { 2 | usable: function() { 3 | return !!Package._getObject('ActiveXObject') && 4 | !!Package._getObject('WScript'); 5 | }, 6 | 7 | __FILE__: function() { 8 | return this._currentPath; 9 | }, 10 | 11 | loadFile: function(path, fireCallbacks) { 12 | this._currentPath = path; 13 | var fso = new ActiveXObject('Scripting.FileSystemObject'), file, runner; 14 | try { 15 | file = fso.OpenTextFile(path); 16 | runner = function() { eval(file.ReadAll()) }; 17 | runner(); 18 | fireCallbacks(); 19 | } finally { 20 | try { if (file) file.Close() } catch (e) {} 21 | } 22 | } 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /source/test/reporters/coverage.js: -------------------------------------------------------------------------------- 1 | Test.Reporters.extend({ 2 | Coverage: new JS.Class({ 3 | include: Console, 4 | 5 | startSuite: function(event) {}, 6 | 7 | startContext: function(event) {}, 8 | 9 | startTest: function(event) {}, 10 | 11 | addFault: function(event) {}, 12 | 13 | endTest: function(event) {}, 14 | 15 | endContext: function(event) {}, 16 | 17 | update: function(event) {}, 18 | 19 | endSuite: function(event) { 20 | var reports = Test.Unit.TestCase.reports; 21 | for (var i = 0, n = reports.length; i < n; i++) { 22 | this.reset(); 23 | this.puts(''); 24 | reports[i].report(); 25 | } 26 | } 27 | }) 28 | }); 29 | 30 | -------------------------------------------------------------------------------- /site/src/pages/platforms/node.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Installing with @npm@ 3 | 4 | If you want to use @jsclass@ with Node, there's an npm pacakge you can 5 | install: 6 | 7 |
npm install jsclass
8 | 9 | After installing this package, you can either use the 10 | "@JS.require()@":/packages.html API to load components, or use the standard 11 | @require()@ function. 12 | 13 |
// Using JS.require()
14 |   
15 |   var JS = require('jsclass');
16 | 
17 |   JS.require('JS.Set', function(Set) {
18 |       var s = new Set([1,2,3]);
19 |       // ...
20 |   });
21 |   
22 |   
23 |   // Using require()
24 |   
25 |   var Set = require('jsclass/src/set');
26 |   var s = new Set([1,2,3]);
27 | -------------------------------------------------------------------------------- /source/core/interface.js: -------------------------------------------------------------------------------- 1 | JS.Interface = new JS.Class('Interface', { 2 | initialize: function(methods) { 3 | this.test = function(object, returnName) { 4 | var n = methods.length; 5 | while (n--) { 6 | if (typeof object[methods[n]] !== 'function') 7 | return returnName ? methods[n] : false; 8 | } 9 | return true; 10 | }; 11 | }, 12 | 13 | extend: { 14 | ensure: function() { 15 | var args = JS.array(arguments), object = args.shift(), face, result; 16 | while (face = args.shift()) { 17 | result = face.test(object, true); 18 | if (result !== true) throw new Error('object does not implement ' + result + '()'); 19 | } 20 | } 21 | } 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /source/test/helpers.js: -------------------------------------------------------------------------------- 1 | Test.extend({ 2 | Helpers: new JS.Module({ 3 | $R: function(start, end) { 4 | return new Range(start, end); 5 | }, 6 | 7 | $w: function(string) { 8 | return string.split(/\s+/); 9 | }, 10 | 11 | forEach: function(list, block, context) { 12 | for (var i = 0, n = list.length; i < n; i++) { 13 | block.call(context, list[i], i); 14 | } 15 | }, 16 | 17 | its: function() { 18 | return new MethodChain(); 19 | }, 20 | 21 | map: function(list, block, context) { 22 | return new Enumerable.Collection(list).map(block, context) 23 | }, 24 | 25 | repeat: function(n, block, context) { 26 | while (n--) block.call(context); 27 | } 28 | }) 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /source/console/browser.js: -------------------------------------------------------------------------------- 1 | Console.extend({ 2 | Browser: new JS.Class(Console.Base, { 3 | backtraceFilter: function() { 4 | return new RegExp(window.location.href.replace(/(\/[^\/]+)/g, '($1)?') + '/?', 'g'); 5 | }, 6 | 7 | coloring: function() { 8 | if (this.envvar(Console.NO_COLOR)) return false; 9 | return Console.AIR; 10 | }, 11 | 12 | echo: function(string) { 13 | if (window.runtime) return window.runtime.trace(string); 14 | if (window.console) return console.log(string); 15 | alert(string); 16 | }, 17 | 18 | envvar: function(name) { 19 | return window[name] || null; 20 | }, 21 | 22 | getDimensions: function() { 23 | if (Console.AIR) return this.callSuper(); 24 | return [1024, 1]; 25 | } 26 | }) 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /source/test/ui/terminal.js: -------------------------------------------------------------------------------- 1 | Test.UI.extend({ 2 | Terminal: new JS.Class({ 3 | getOptions: function() { 4 | var options = {}, 5 | format = Console.envvar('FORMAT'), 6 | test = Console.envvar('TEST'); 7 | 8 | if (Console.envvar('TAP')) options.format = 'tap'; 9 | 10 | if (format) options.format = format; 11 | if (test) options.test = [test]; 12 | 13 | delete options.argv; 14 | options.test = options.test || []; 15 | return options; 16 | }, 17 | 18 | getReporters: function(options) { 19 | var R = Test.Reporters, 20 | Printer = R.get(options.format) || R.Dot; 21 | 22 | return [ 23 | new R.Coverage(options), 24 | new Printer(options), 25 | new R.ExitStatus(options) 26 | ]; 27 | } 28 | }) 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /source/test/unit.js: -------------------------------------------------------------------------------- 1 | var Test = new JS.Module('Test', { 2 | extend: { 3 | asyncTimeout: 5, 4 | 5 | filter: function(objects, suffix) { 6 | return Test.Runner.filter(objects, suffix); 7 | }, 8 | 9 | Reporters: new JS.Module({ 10 | extend: { 11 | METHODS: ['startSuite', 'startContext', 'startTest', 12 | 'update', 'addFault', 13 | 'endTest', 'endContext', 'endSuite'], 14 | 15 | _registry: {}, 16 | 17 | register: function(name, klass) { 18 | this._registry[name] = klass; 19 | }, 20 | 21 | get: function(name) { 22 | if (!name) return null; 23 | return this._registry[name] || null; 24 | } 25 | } 26 | }), 27 | 28 | UI: new JS.Module({}), 29 | Unit: new JS.Module({}) 30 | } 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /test/console.js: -------------------------------------------------------------------------------- 1 | if (this.ActiveXObject) load = function(path) { 2 | var fso = new ActiveXObject('Scripting.FileSystemObject'), file, runner; 3 | try { 4 | file = fso.OpenTextFile(path); 5 | runner = function() { eval(file.ReadAll()) }; 6 | runner(); 7 | } finally { 8 | try { if (file) file.Close() } catch (e) {} 9 | } 10 | }; 11 | 12 | (function() { 13 | var $ = (typeof global === 'object') ? global : this, 14 | path = $.JSCLASS_PATH = 'build/src/'; 15 | 16 | if (typeof phantom !== 'undefined') { 17 | $.JSCLASS_PATH = '../' + $.JSCLASS_PATH; 18 | $.CWD = '..'; 19 | } 20 | 21 | if (typeof require === 'function') { 22 | $.JS = require('../' + path + 'loader'); 23 | require('./runner'); 24 | } else { 25 | load(path + 'loader.js'); 26 | load('test/runner.js'); 27 | } 28 | })(); 29 | 30 | -------------------------------------------------------------------------------- /source/test/unit/error.js: -------------------------------------------------------------------------------- 1 | Test.Unit.extend({ 2 | Error: new JS.Class({ 3 | initialize: function(testCase, exception) { 4 | if (typeof exception === 'string') 5 | exception = new Error(exception); 6 | 7 | this._testCase = testCase; 8 | this._exception = exception; 9 | }, 10 | 11 | metadata: function() { 12 | return { 13 | test: this.testMetadata(), 14 | error: this.errorMetadata() 15 | } 16 | }, 17 | 18 | testMetadata: function() { 19 | return this._testCase.metadata(); 20 | }, 21 | 22 | errorMetadata: function() { 23 | return { 24 | type: 'error', 25 | message: this._exception.name + ': ' + this._exception.message, 26 | backtrace: Console.filterBacktrace(this._exception.stack) 27 | }; 28 | } 29 | }) 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /source/test/reporters/phantomjs.js: -------------------------------------------------------------------------------- 1 | // http://phantomjs.org/ 2 | 3 | Test.Reporters.extend({ 4 | PhantomJS: new JS.Class({ 5 | initialize: function(options, page) { 6 | this._options = options || {}; 7 | 8 | var format = Console.envvar('FORMAT'); 9 | 10 | if (Console.envvar('TAP')) format = format || 'tap'; 11 | this._options.format = this._options.format || format; 12 | 13 | var R = Test.Reporters, 14 | Printer = R.get(this._options.format) || R.Dot, 15 | reporter = new R.Composite(), 16 | bridge = new R.JSON.Reader(reporter); 17 | 18 | reporter.addReporter(new Printer(options)); 19 | reporter.addReporter(new R.ExitStatus()); 20 | 21 | page.onConsoleMessage = function(m) { 22 | if (!bridge.read(m)) console.log(m); 23 | }; 24 | } 25 | }) 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /source/package/loaders/commonjs.js: -------------------------------------------------------------------------------- 1 | Package.CommonJSLoader = { 2 | usable: function() { 3 | return typeof require === 'function' && 4 | typeof exports === 'object'; 5 | }, 6 | 7 | __FILE__: function() { 8 | return this._currentPath; 9 | }, 10 | 11 | loadFile: function(path, fireCallbacks) { 12 | var file; 13 | 14 | if (typeof process !== 'undefined') { 15 | var cwd = process.cwd(), 16 | module = path.replace(/\.[^\.]+$/g, ''), 17 | path = require('path'); 18 | 19 | file = path.resolve(module); 20 | } 21 | else if (typeof phantom !== 'undefined') { 22 | file = phantom.libraryPath.replace(/\/$/, '') + '/' + 23 | path.replace(/^\//, ''); 24 | } 25 | 26 | this._currentPath = file + '.js'; 27 | var module = require(file); 28 | fireCallbacks(module); 29 | 30 | return module; 31 | } 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /test/examples/reformat.js: -------------------------------------------------------------------------------- 1 | // This script reads a JS.Test JSON output stream from stdin and reformats it 2 | // using any of the terminal-based test reporters, e.g. dot, spec, error, tap. 3 | // 4 | // Try it out on the JS.Test suite: 5 | // 6 | // $ node test/console -f json | node test/examples/reformat -f tap 7 | 8 | JSCLASS_PATH = 'build/src' 9 | var JS = require('../../' + JSCLASS_PATH + '/loader') 10 | 11 | JS.require('JS.Test', function(Test) { 12 | var options = require('nopt')({format: String}), 13 | R = Test.Reporters, 14 | Printer = R.get(options.format), 15 | reporter = new R.Composite(), 16 | reader = new R.JSON.Reader(reporter) 17 | 18 | reporter.addReporter(new Printer()) 19 | reporter.addReporter(new R.ExitStatus()) 20 | 21 | process.stdin.on('data', function(data) { 22 | data.toString().split('\n').forEach(reader.method('read')) 23 | }) 24 | process.stdin.resume() 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /test/xulenv/chrome/content/main.xul: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /source/test/reporters/dot.js: -------------------------------------------------------------------------------- 1 | Test.Reporters.extend({ 2 | Dot: new JS.Class(Test.Reporters.Error, { 3 | SYMBOLS: { 4 | failure: 'F', 5 | error: 'E' 6 | }, 7 | 8 | startTest: function(event) { 9 | this._outputFault = false; 10 | }, 11 | 12 | addFault: function(event) { 13 | this._faults.push(event); 14 | if (this._outputFault) return; 15 | this._outputFault = true; 16 | this.consoleFormat('bold', 'red'); 17 | this.print(this.SYMBOLS[event.error.type]); 18 | this.reset(); 19 | }, 20 | 21 | endTest: function(event) { 22 | if (this._outputFault) return; 23 | this.consoleFormat('green'); 24 | this.print('.'); 25 | this.reset(); 26 | }, 27 | 28 | endSuite: function(event) { 29 | this.puts('\n'); 30 | 31 | for (var i = 0, n = this._faults.length; i < n; i++) 32 | this._printFault(i + 1, this._faults[i]); 33 | 34 | this._printSummary(event); 35 | } 36 | }) 37 | }); 38 | 39 | Test.Reporters.register('dot', Test.Reporters.Dot); 40 | 41 | -------------------------------------------------------------------------------- /source/test/context/test.js: -------------------------------------------------------------------------------- 1 | Test.Context.Test = new JS.Module({ 2 | it: function(name, opts, block) { 3 | var testName = 'test: ' + name; 4 | 5 | if (JS.indexOf(this.instanceMethods(false), testName) >= 0) 6 | throw new Error(testName + ' is already defined in ' + this.displayName); 7 | 8 | opts = opts || {}; 9 | 10 | if (typeof opts === 'function') { 11 | block = opts; 12 | } else { 13 | if (opts.before !== undefined) 14 | this.before_should_callbacks[testName] = opts.before; 15 | } 16 | 17 | this.define(testName, block, {_resolve: false}); 18 | }, 19 | 20 | should: function() { return this.it.apply(this, arguments) }, 21 | test: function() { return this.it.apply(this, arguments) }, 22 | tests: function() { return this.it.apply(this, arguments) }, 23 | 24 | beforeTest: function(name, block) { 25 | this.it(name, {before: block}, function() {}); 26 | } 27 | }); 28 | 29 | Test.Context.Test.alias({ 30 | beforeIt: 'beforeTest', 31 | beforeShould: 'beforeTest', 32 | beforeTests: 'beforeTest' 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /source/forwardable.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var Forwardable = new JS.Module('Forwardable', { 12 | defineDelegator: function(subject, method, alias, resolve) { 13 | alias = alias || method; 14 | this.define(alias, function() { 15 | var object = this[subject], 16 | property = object[method]; 17 | 18 | return (typeof property === 'function') 19 | ? property.apply(object, arguments) 20 | : property; 21 | }, {_resolve: resolve !== false}); 22 | }, 23 | 24 | defineDelegators: function() { 25 | var methods = JS.array(arguments), 26 | subject = methods.shift(), 27 | i = methods.length; 28 | 29 | while (i--) this.defineDelegator(subject, methods[i], methods[i], false); 30 | this.resolve(); 31 | } 32 | }); 33 | 34 | exports.Forwardable = Forwardable; 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /site/src/pages/singletons.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Singletons 3 | 4 | A singleton class is one which can only ever have one instance. The concept is 5 | more useful in Java, where you cannot create an object without first creating 6 | a class. JavaScript allows objects without classes (indeed, it has no classes, 7 | only objects) but using @JS.Singleton@ lets you create custom objects from 8 | existing @JS.Class@ classes, allowing you to inherit methods, include modules 9 | and use @method()@ et al. 10 | 11 |
var Camel = new JS.Singleton(Animal, {
12 |       fillHumpsWithWater: function() { ... }
13 |   });
14 | 
15 |   // You can call instance methods...
16 |   Camel.speak('the desert');    // from Animal
17 |   Camel.fillHumpsWithWater();
18 | 
19 |   var s = Camel.method('speak');
20 |   s('drinking');
21 | 
22 |   Camel.klass.superclass    // -> Animal
23 | 24 | @JS.Singleton@ just creates a class with the arguments you give it, 25 | immediately instantiates this new class and returns the instance. You can 26 | access the class through @Camel.klass@ as shown above. 27 | 28 | -------------------------------------------------------------------------------- /source/test/_head.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS, 4 | 5 | Console = js.Console || require('./console').Console, 6 | DOM = js.DOM || require('./dom').DOM, 7 | Enumerable = js.Enumerable || require('./enumerable').Enumerable, 8 | SortedSet = js.SortedSet || require('./set').SortedSet, 9 | Range = js.Range || require('./range').Range, 10 | Hash = js.Hash || require('./hash').Hash, 11 | MethodChain = js.MethodChain || require('./method_chain').MethodChain, 12 | Comparable = js.Comparable || require('./comparable').Comparable, 13 | StackTrace = js.StackTrace || require('./stack_trace').StackTrace; 14 | 15 | if (E) exports.JS = exports; 16 | factory(js, Console, DOM, Enumerable, SortedSet, Range, Hash, MethodChain, Comparable, StackTrace, E ? exports : js); 17 | 18 | })(function(JS, Console, DOM, Enumerable, SortedSet, Range, Hash, MethodChain, Comparable, StackTrace, exports) { 19 | 'use strict'; 20 | 21 | -------------------------------------------------------------------------------- /source/package/loaders/xulrunner.js: -------------------------------------------------------------------------------- 1 | Package.XULRunnerLoader = { 2 | jsloader: '@mozilla.org/moz/jssubscript-loader;1', 3 | cssservice: '@mozilla.org/content/style-sheet-service;1', 4 | ioservice: '@mozilla.org/network/io-service;1', 5 | 6 | usable: function() { 7 | try { 8 | var CC = (Components || {}).classes; 9 | return !!(CC && CC[this.jsloader] && CC[this.jsloader].getService); 10 | } catch(e) { 11 | return false; 12 | } 13 | }, 14 | 15 | setup: function() { 16 | var Cc = Components.classes, Ci = Components.interfaces; 17 | this.ssl = Cc[this.jsloader].getService(Ci.mozIJSSubScriptLoader); 18 | this.sss = Cc[this.cssservice].getService(Ci.nsIStyleSheetService); 19 | this.ios = Cc[this.ioservice].getService(Ci.nsIIOService); 20 | }, 21 | 22 | loadFile: function(path, fireCallbacks) { 23 | Package.log('[LOAD] ' + path); 24 | 25 | this.ssl.loadSubScript(path); 26 | fireCallbacks(); 27 | }, 28 | 29 | loadStyle: function(path) { 30 | var uri = this.ios.newURI(path, null, null); 31 | this.sss.loadAndRegisterSheet(uri, this.sss.USER_SHEET); 32 | } 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /source/console/node.js: -------------------------------------------------------------------------------- 1 | Console.extend({ 2 | Node: new JS.Class(Console.Base, { 3 | backtraceFilter: function() { 4 | return new RegExp(process.cwd() + '/', 'g'); 5 | }, 6 | 7 | coloring: function() { 8 | return !this.envvar(Console.NO_COLOR) && require('tty').isatty(1); 9 | }, 10 | 11 | envvar: function(name) { 12 | return process.env[name] || null; 13 | }, 14 | 15 | exit: function(status) { 16 | process.exit(status); 17 | }, 18 | 19 | getDimensions: function() { 20 | var width, height, dims; 21 | if (process.stdout.getWindowSize) { 22 | dims = process.stdout.getWindowSize(); 23 | width = dims[0]; 24 | height = dims[1]; 25 | } else { 26 | dims = process.binding('stdio').getWindowSize(); 27 | width = dims[1]; 28 | height = dims[0]; 29 | } 30 | return [width, height]; 31 | }, 32 | 33 | print: function(string) { 34 | require('sys').print(this.flushFormat() + string); 35 | }, 36 | 37 | puts: function(string) { 38 | require('sys').puts(this.flushFormat() + string); 39 | } 40 | }) 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /source/test/reporters/test_swarm.js: -------------------------------------------------------------------------------- 1 | // https://github.com/jquery/testswarm 2 | 3 | Test.Reporters.extend({ 4 | TestSwarm: new JS.Class({ 5 | extend: { 6 | create: function(options, browser) { 7 | if (JS.ENV.TestSwarm) return new this(options, browser); 8 | } 9 | }, 10 | 11 | initialize: function(options, browserReporter) { 12 | this._browserReporter = browserReporter; 13 | 14 | TestSwarm.serialize = function() { 15 | return browserReporter.serialize(); 16 | }; 17 | }, 18 | 19 | startSuite: function(event) {}, 20 | 21 | startContext: function(event) {}, 22 | 23 | startTest: function(event) {}, 24 | 25 | addFault: function(event) {}, 26 | 27 | endTest: function(event) { 28 | TestSwarm.heartbeat(); 29 | }, 30 | 31 | endContext: function(event) {}, 32 | 33 | update: function(event) {}, 34 | 35 | endSuite: function(event) { 36 | TestSwarm.submit({ 37 | fail: event.failures, 38 | error: event.errors, 39 | total: event.tests 40 | }); 41 | } 42 | }) 43 | }); 44 | 45 | Test.Reporters.register('testswarm', Test.Reporters.TestSwarm); 46 | 47 | -------------------------------------------------------------------------------- /site/src/pages/index.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. The cross-platform JavaScript class library 3 | 4 | @jsclass@ is a portable, modular JavaScript class library, influenced by the 5 | "Ruby":http://ruby-lang.org/ programming language. It provides a rich set of 6 | tools for building object-oriented JavaScript programs, and is designed to run 7 | on a wide variety of "client- and server-side platforms":/platforms.html. 8 | 9 | In particular, to support the writing of equally portable JavaScript code, it 10 | provides a "package manager":/packages.html and "testing framework":http://jstest.jcoglan.com 11 | that run on all supported platforms. 12 | 13 | h3. Features 14 | 15 | The library provides many of Ruby's powerful features, including: 16 | 17 | * An object system with classes, mixins, and singleton methods 18 | * Late-binding arguments-optional @super@ calls to parent classes and mixins 19 | * @included@, @extended@ and @inherited@ hooks 20 | * Context-preserving method binding 21 | * Reflection APIs for the object system 22 | * Conventions for object equality, comparison, iteration and hashing 23 | * Versions of various standard Ruby modules and data structures 24 | 25 | -------------------------------------------------------------------------------- /source/comparable.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var Comparable = new JS.Module('Comparable', { 12 | extend: { 13 | ClassMethods: new JS.Module({ 14 | compare: function(one, another) { 15 | return one.compareTo(another); 16 | } 17 | }), 18 | 19 | included: function(base) { 20 | base.extend(this.ClassMethods); 21 | } 22 | }, 23 | 24 | lt: function(other) { 25 | return this.compareTo(other) < 0; 26 | }, 27 | 28 | lte: function(other) { 29 | return this.compareTo(other) < 1; 30 | }, 31 | 32 | gt: function(other) { 33 | return this.compareTo(other) > 0; 34 | }, 35 | 36 | gte: function(other) { 37 | return this.compareTo(other) > -1; 38 | }, 39 | 40 | eq: function(other) { 41 | return this.compareTo(other) === 0; 42 | }, 43 | 44 | between: function(a, b) { 45 | return this.gte(a) && this.lte(b); 46 | } 47 | }); 48 | 49 | exports.Comparable = Comparable; 50 | }); 51 | 52 | -------------------------------------------------------------------------------- /source/console/rhino.js: -------------------------------------------------------------------------------- 1 | Console.extend({ 2 | Rhino: new JS.Class(Console.Base, { 3 | backtraceFilter: function() { 4 | return new RegExp(java.lang.System.getProperty('user.dir') + '/', 'g'); 5 | }, 6 | 7 | envvar: function(name) { 8 | var env = java.lang.System.getenv(); 9 | return env.get(name) || null; 10 | }, 11 | 12 | getDimensions: function() { 13 | var proc = java.lang.Runtime.getRuntime().exec(['sh', '-c', 'stty -a < /dev/tty']), 14 | is = proc.getInputStream(), 15 | bite = 0, 16 | out = '', 17 | width, height; 18 | 19 | while (bite >= 0) { 20 | bite = is.read(); 21 | if (bite >= 0) out += String.fromCharCode(bite); 22 | } 23 | 24 | var match = out.match(/rows\s+(\d+);\s+columns\s+(\d+)/); 25 | if (!match) return this._dimCache || this.callSuper(); 26 | 27 | return this._dimCache = [parseInt(match[2], 10), parseInt(match[1], 10)]; 28 | }, 29 | 30 | print: function(string) { 31 | java.lang.System.out.print(this.flushFormat() + string); 32 | }, 33 | 34 | puts: function(string) { 35 | java.lang.System.out.println(this.flushFormat() + string); 36 | } 37 | }) 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /source/core/keywords.js: -------------------------------------------------------------------------------- 1 | JS.Method.keyword('callSuper', function(method, env, receiver, args) { 2 | var methods = env.lookup(method.name), 3 | stackIndex = methods.length - 1, 4 | params = JS.array(args); 5 | 6 | if (stackIndex === 0) return undefined; 7 | 8 | var _super = function() { 9 | var i = arguments.length; 10 | while (i--) params[i] = arguments[i]; 11 | 12 | stackIndex -= 1; 13 | if (stackIndex === 0) delete receiver.callSuper; 14 | var returnValue = methods[stackIndex].apply(receiver, params); 15 | receiver.callSuper = _super; 16 | stackIndex += 1; 17 | 18 | return returnValue; 19 | }; 20 | 21 | return _super; 22 | }); 23 | 24 | JS.Method.keyword('blockGiven', function(method, env, receiver, args) { 25 | var block = Array.prototype.slice.call(args, method.arity), 26 | hasBlock = (typeof block[0] === 'function'); 27 | 28 | return function() { return hasBlock }; 29 | }); 30 | 31 | JS.Method.keyword('yieldWith', function(method, env, receiver, args) { 32 | var block = Array.prototype.slice.call(args, method.arity); 33 | 34 | return function() { 35 | if (typeof block[0] !== 'function') return; 36 | return block[0].apply(block[1] || null, arguments); 37 | }; 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /site/src/pages/interfaces.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Interfaces 3 | 4 | Though not found in Ruby, I've decided to include @Interface@ support in 5 | JS.Class. Interfaces are found in Java and can be very useful in JavaScript 6 | when used judiciously. The idea of an interface is that you create a set of 7 | method names with no implementations. You can then insist that objects/classes 8 | implement the named methods; if you require an object to have a certain set of 9 | methods, you can then throw an exception if it does not. 10 | 11 | To create an interface, just pass in an array of method names: 12 | 13 |
var IntComparable = new JS.Interface([
14 |       'compareTo', 'lt', 'lte', 'gt', 'gte', 'eq'
15 |   ]);
16 | 
17 |   var IntStateMachine = new JS.Interface([
18 |       'getInitialState', 'changeState'
19 |   ]);
20 | 21 | You can then test any object to find out whether it implements the required 22 | interfaces: 23 | 24 |
JS.Interface.ensure(someObject, IntComparable, IntStateMachine);
25 | 26 | @JS.Interface.ensure@ tests its first argument against all the supplied 27 | interfaces. If it fails one of the tests, an error is thrown that tells you 28 | the name of the first method found to be missing from the object. 29 | -------------------------------------------------------------------------------- /source/dom/dom.js: -------------------------------------------------------------------------------- 1 | var DOM = { 2 | ELEMENT_NODE: 1, 3 | ATTRIBUTE_NODE: 2, 4 | TEXT_NODE: 3, 5 | CDATA_SECTION_NODE: 4, 6 | ENTITY_REFERENCE_NODE: 5, 7 | ENTITY_NODE: 6, 8 | PROCESSING_INSTRUCTION_NODE: 7, 9 | COMMENT_NODE: 8, 10 | DOCUMENT_NODE: 9, 11 | DOCUMENT_TYPE_NODE: 10, 12 | DOCUMENT_FRAGMENT_NODE: 11, 13 | NOTATION_NODE: 12, 14 | 15 | ENV: this, 16 | 17 | toggleClass: function(node, className) { 18 | if (this.hasClass(node, className)) this.removeClass(node, className); 19 | else this.addClass(node, className); 20 | }, 21 | 22 | hasClass: function(node, className) { 23 | var classes = node.className.split(/\s+/); 24 | return JS.indexOf(classes, className) >= 0; 25 | }, 26 | 27 | addClass: function(node, className) { 28 | if (this.hasClass(node, className)) return; 29 | node.className = node.className + ' ' + className; 30 | }, 31 | 32 | removeClass: function(node, className) { 33 | var pattern = new RegExp('\\b' + className + '\\b\\s*', 'g'); 34 | node.className = node.className.replace(pattern, ''); 35 | } 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /source/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var methodsFromPrototype = function(klass) { 3 | var methods = {}, 4 | proto = klass.prototype; 5 | 6 | for (var field in proto) { 7 | if (!proto.hasOwnProperty(field)) continue; 8 | methods[field] = JS.Method.create(klass, field, proto[field]); 9 | } 10 | return methods; 11 | }; 12 | 13 | var classify = function(name, parentName) { 14 | var klass = JS[name], 15 | parent = JS[parentName]; 16 | 17 | klass.__inc__ = []; 18 | klass.__dep__ = []; 19 | klass.__fns__ = methodsFromPrototype(klass); 20 | klass.__tgt__ = klass.prototype; 21 | 22 | klass.prototype.constructor = 23 | klass.prototype.klass = klass; 24 | 25 | JS.extend(klass, JS.Class.prototype); 26 | klass.include(parent || JS.Kernel); 27 | klass.setName(name); 28 | 29 | klass.constructor = klass.klass = JS.Class; 30 | }; 31 | 32 | classify('Method'); 33 | classify('Module'); 34 | classify('Class', 'Module'); 35 | 36 | var eigen = JS.Kernel.instanceMethod('__eigen__'); 37 | 38 | eigen.call(JS.Method).resolve(); 39 | eigen.call(JS.Module).resolve(); 40 | eigen.call(JS.Class).include(JS.Module.__meta__); 41 | })(); 42 | 43 | JS.NotImplementedError = new JS.Class('NotImplementedError', Error); 44 | 45 | -------------------------------------------------------------------------------- /source/test/unit/observable.js: -------------------------------------------------------------------------------- 1 | Test.Unit.extend({ 2 | Observable: new JS.Module({ 3 | addListener: function(channelName, block, context) { 4 | if (block === undefined) throw new Error('No callback was passed as a listener'); 5 | 6 | this.channels()[channelName] = this.channels()[channelName] || []; 7 | this.channels()[channelName].push([block, context]); 8 | 9 | return block; 10 | }, 11 | 12 | removeListener: function(channelName, block, context) { 13 | var channel = this.channels()[channelName]; 14 | if (!channel) return; 15 | 16 | var i = channel.length; 17 | while (i--) { 18 | if (channel[i][0] === block) { 19 | channel.splice(i,1); 20 | return block; 21 | } 22 | } 23 | return null; 24 | }, 25 | 26 | notifyListeners: function(channelName, args) { 27 | var args = JS.array(arguments), 28 | channelName = args.shift(), 29 | channel = this.channels()[channelName]; 30 | 31 | if (!channel) return 0; 32 | 33 | for (var i = 0, n = channel.length; i < n; i++) 34 | channel[i][0].apply(channel[i][1] || null, args); 35 | 36 | return channel.length; 37 | }, 38 | 39 | channels: function() { 40 | return this.__channels__ = this.__channels__ || []; 41 | } 42 | }) 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /site/src/stylesheets/screen.sass: -------------------------------------------------------------------------------- 1 | body 2 | background: #fff 3 | border-top: 2em solid #95c3f0 4 | color: #30303c 5 | font: 300 18px/1.4 Open Sans, FreeSans, Helvetica, Arial, sans-serif 6 | margin: 0 7 | padding: 0 8 | 9 | .nav 10 | float: left 11 | margin: 2em 12 | 13 | h1 14 | font-size: 2em 15 | font-weight: 400 16 | margin: 0 17 | 18 | a 19 | color: #30303c 20 | 21 | h4, li 22 | font-size: 90% 23 | 24 | .content 25 | margin: 2em 0 3em 20em 26 | width: 40em 27 | 28 | h2 29 | font-size: 1.8em 30 | font-weight: 300 31 | margin: 0 0 1em 32 | 33 | h3 34 | font-size: 1em 35 | font-weight: bold 36 | margin: 2.8em 0 1.4em 37 | 38 | p, ul, pre 39 | margin: 1.4em 0 40 | 41 | ul 42 | margin: 1em 0 0 2em 43 | padding: 0 44 | list-style: circle outside 45 | 46 | ul ul 47 | font-size: 0.9em 48 | margin-top: 0 49 | 50 | a 51 | color: #3094c0 52 | text-decoration: none 53 | 54 | a:hover 55 | text-decoration: underline 56 | 57 | pre, code 58 | font-family: Inconsolata, Monaco, Lucida Console, Courier New, monospace 59 | 60 | pre, blockquote 61 | border-left: 1em solid #f0f0f0 62 | font-size: 0.9em 63 | margin-left: 0 64 | padding-left: 2em 65 | 66 | .footer 67 | border-top: 1px solid #d0d0d0 68 | clear: both 69 | margin: 2em 0 0 2em 70 | padding: 1em 0 2em 18em 71 | width: 40em 72 | 73 | -------------------------------------------------------------------------------- /source/console/config.js: -------------------------------------------------------------------------------- 1 | Console.BROWSER = (typeof window !== 'undefined'); 2 | Console.NODE = (typeof process === 'object') && !Console.BROWSER; 3 | Console.PHANTOM = (typeof phantom !== 'undefined'); 4 | Console.AIR = (Console.BROWSER && typeof runtime !== 'undefined'); 5 | Console.RHINO = (typeof java !== 'undefined' && typeof java.lang !== 'undefined'); 6 | Console.WSH = (typeof WScript !== 'undefined'); 7 | 8 | var useColor = false, ua; 9 | if (Console.BROWSER) { 10 | ua = navigator.userAgent; 11 | if (window.console && (/Firefox/.test(ua) || /Chrome/.test(ua))) 12 | useColor = true; 13 | } 14 | 15 | if (Console.PHANTOM) Console.adapter = new Console.Phantom(); 16 | else if (useColor) Console.adapter = new Console.BrowserColor(); 17 | else if (Console.BROWSER) Console.adapter = new Console.Browser(); 18 | else if (Console.NODE) Console.adapter = new Console.Node(); 19 | else if (Console.RHINO) Console.adapter = new Console.Rhino(); 20 | else if (Console.WSH) Console.adapter = new Console.Windows(); 21 | else Console.adapter = new Console.Base(); 22 | 23 | for (var type in Console.ESCAPE_CODES) { 24 | for (var key in Console.ESCAPE_CODES[type]) (function(type, key) { 25 | Console.define(key, function() { 26 | Console.adapter.format(type, key, arguments); 27 | }); 28 | })(type, key); 29 | } 30 | 31 | Console.extend(Console); 32 | 33 | -------------------------------------------------------------------------------- /source/test/context/context.js: -------------------------------------------------------------------------------- 1 | Test.extend({ 2 | Context: new JS.Module({ 3 | extend: { 4 | included: function(base) { 5 | base.extend(Test.Context.Context, {_resolve: false}); 6 | base.include(Test.Context.LifeCycle, {_resolve: false}); 7 | base.extend(Test.Context.Test, {_resolve: false}); 8 | base.include(Console); 9 | }, 10 | 11 | Context: new JS.Module({ 12 | context: function(name, block) { 13 | var klass = new JS.Class(name.toString(), this, {}, {_resolve: false}); 14 | klass.__eigen__().resolve(); 15 | block.call(klass); 16 | return klass; 17 | }, 18 | 19 | cover: function(module) { 20 | var logger = new Test.Coverage(module); 21 | this.before_all_callbacks.push(logger.method('attach')); 22 | this.after_all_callbacks.push(logger.method('detach')); 23 | Test.Unit.TestCase.reports.push(logger); 24 | } 25 | }) 26 | } 27 | }), 28 | 29 | describe: function(name, block) { 30 | var klass = new JS.Class(name.toString(), Test.Unit.TestCase, {}, {_resolve: false}); 31 | klass.include(Test.Context, {_resolve: false}); 32 | klass.__eigen__().resolve(); 33 | 34 | block.call(klass); 35 | return klass; 36 | } 37 | }); 38 | 39 | Test.Context.Context.alias({describe: 'context'}); 40 | 41 | Test.extend({ 42 | context: Test.describe 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /source/test/reporters/composite.js: -------------------------------------------------------------------------------- 1 | Test.Reporters.extend({ 2 | Composite: new JS.Class({ 3 | initialize: function(reporters) { 4 | this._reporters = reporters || []; 5 | this._queue = []; 6 | this._pointer = 0; 7 | }, 8 | 9 | addReporter: function(reporter) { 10 | if (!reporter) return; 11 | this._reporters.push(reporter); 12 | }, 13 | 14 | removeReporter: function(reporter) { 15 | var index = JS.indexOf(this._reporters, reporter); 16 | if (index >= 0) this._reporters.splice(index, 1); 17 | }, 18 | 19 | flush: function() { 20 | var queue = this._queue, method, event, i, n, fn; 21 | while (queue[this._pointer] !== undefined) { 22 | method = queue[this._pointer][0]; 23 | event = queue[this._pointer][1]; 24 | for (i = 0, n = this._reporters.length; i < n; i++) { 25 | fn = this._reporters[i][method]; 26 | if (fn) fn.call(this._reporters[i], event); 27 | } 28 | this._pointer += 1; 29 | } 30 | } 31 | }) 32 | }); 33 | 34 | (function() { 35 | var methods = Test.Reporters.METHODS, 36 | n = methods.length; 37 | 38 | while (n--) 39 | (function(i) { 40 | var method = methods[i]; 41 | Test.Reporters.Composite.define(method, function(event) { 42 | this._queue[event.eventId] = [method, event]; 43 | this.flush(); 44 | }); 45 | })(n); 46 | })(); 47 | 48 | -------------------------------------------------------------------------------- /source/core/class.js: -------------------------------------------------------------------------------- 1 | JS.Class = JS.makeClass(JS.Module); 2 | 3 | JS.extend(JS.Class.prototype, { 4 | initialize: function(name, parent, methods, options) { 5 | if (typeof name !== 'string') { 6 | options = arguments[2]; 7 | methods = arguments[1]; 8 | parent = arguments[0]; 9 | name = undefined; 10 | } 11 | if (typeof parent !== 'function') { 12 | options = methods; 13 | methods = parent; 14 | parent = Object; 15 | } 16 | JS.Module.prototype.initialize.call(this, name); 17 | options = options || {}; 18 | 19 | var klass = JS.makeClass(parent); 20 | JS.extend(klass, this); 21 | 22 | klass.prototype.constructor = 23 | klass.prototype.klass = klass; 24 | 25 | klass.__eigen__().include(parent.__meta__, {_resolve: options._resolve}); 26 | klass.setName(name); 27 | 28 | klass.__tgt__ = klass.prototype; 29 | 30 | var parentModule = (parent === Object) 31 | ? {} 32 | : (parent.__fns__ ? parent : new JS.Module(parent.prototype, {_resolve: false})); 33 | 34 | klass.include(JS.Kernel, {_resolve: false}) 35 | .include(parentModule, {_resolve: false}) 36 | .include(methods, {_resolve: false}); 37 | 38 | if (options._resolve !== false) klass.resolve(); 39 | 40 | if (typeof parent.inherited === 'function') 41 | parent.inherited(klass); 42 | 43 | return klass; 44 | } 45 | }); 46 | 47 | -------------------------------------------------------------------------------- /site/src/pages/benchmark.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Benchmark 3 | 4 | @JS.Benchmark@ provides a tool for measuring the execution time of blocks 5 | of JavaScript code. To take a measurement, you just supply a name for the 6 | measurement, the number of times to run it, and the function you want to 7 | execute: 8 | 9 |
JS.Benchmark.measure('String#join', 20, {
10 |       test: function() {
11 |           ['a', 'list', 'of', 'strings'].join(' ');
12 |       }
13 |   });
14 | 15 | @JS.Benchmark@ will take your @test@ function, run it the given number of 16 | times, and print the mean and standard deviation of the function's execution 17 | time to the "console":/console.html. 18 | 19 | If the operation you're benchmarking requires some setup, you probably don't 20 | want to include the setup time in the measurement. To help you measure the 21 | right thing, you can place any setup code in its own function. State can be 22 | shared between the @setup@ and @test@ functions by assigning values to @this@: 23 | 24 |
JS.Benchmark.measure('Module#ancestors', 10, {
25 |       setup: function() {
26 |           this.module = new JS.Module({ include: [JS.Comparable, JS.Enumerable] });
27 |       },
28 |       test: function() {
29 |           this.module.ancestors();
30 |       }
31 |   });
32 | 33 | The time reported by @Benchmark@ will not include the time it took to run the 34 | @setup@ function, only the @test@ function is included. 35 | -------------------------------------------------------------------------------- /source/test/ui/browser.js: -------------------------------------------------------------------------------- 1 | Test.UI.extend({ 2 | Browser: new JS.Class({ 3 | getOptions: function() { 4 | var qs = (location.search || '').replace(/^\?/, ''), 5 | pairs = qs.split('&'), 6 | options = {}, 7 | parts, key, value; 8 | 9 | for (var i = 0, n = pairs.length; i < n; i++) { 10 | parts = pairs[i].split('='); 11 | key = decodeURIComponent(parts[0]); 12 | value = decodeURIComponent(parts[1]); 13 | 14 | if (/\[\]$/.test(parts[0])) { 15 | key = key.replace(/\[\]$/, ''); 16 | if (!(options[key] instanceof Array)) options[key] = []; 17 | options[key].push(value); 18 | } else { 19 | options[key] = value; 20 | } 21 | } 22 | 23 | if (options.test) 24 | options.test = [].concat(options.test); 25 | else 26 | options.test = []; 27 | 28 | return options; 29 | }, 30 | 31 | getReporters: function(options) { 32 | var reporters = [], 33 | R = Test.Reporters, 34 | reg = R._registry, 35 | browser = new R.Browser(options), 36 | reporter; 37 | 38 | reporters.push(new R.Coverage()); 39 | reporters.push(browser); 40 | 41 | for (var name in reg) { 42 | reporter = reg[name] && reg[name].create && reg[name].create(options, browser); 43 | if (reporter) reporters.push(reporter); 44 | } 45 | 46 | return reporters; 47 | } 48 | }) 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /source/test/reporters/json.js: -------------------------------------------------------------------------------- 1 | Test.Reporters.extend({ 2 | JSON: new JS.Class({ 3 | include: Console, 4 | 5 | _log: function(eventName, data) { 6 | if (!JS.ENV.JSON) return; 7 | this.puts(JSON.stringify({jstest: [eventName, data]})); 8 | }, 9 | 10 | extend: { 11 | create: function() { 12 | if (!JS.ENV.navigator) return; 13 | if (/\bPhantomJS\b/.test(navigator.userAgent)) return new this(); 14 | }, 15 | 16 | Reader: new JS.Class({ 17 | initialize: function(reporter) { 18 | this._reporter = new Test.Reporters.Composite([reporter]); 19 | }, 20 | 21 | read: function(message) { 22 | if (!JS.ENV.JSON) return false; 23 | try { 24 | var data = JSON.parse(message), 25 | payload = data.jstest, 26 | method = payload[0], 27 | event = payload[1]; 28 | 29 | this._reporter[method](event); 30 | return true; 31 | } 32 | catch (e) { 33 | return false; 34 | } 35 | } 36 | }) 37 | } 38 | }) 39 | }); 40 | 41 | (function() { 42 | var methods = Test.Reporters.METHODS, 43 | n = methods.length; 44 | 45 | while (n--) 46 | (function(i) { 47 | var method = methods[i]; 48 | Test.Reporters.JSON.define(method, function(event) { 49 | this._log(method, event); 50 | }); 51 | })(n); 52 | })(); 53 | 54 | Test.Reporters.register('json', Test.Reporters.JSON); 55 | 56 | -------------------------------------------------------------------------------- /site/src/pages/hooks.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Metaprogramming hooks 3 | 4 | Ruby defines a few hook methods that you can use to detect when a class is 5 | subclassed or when a module is mixed in. These hooks are called @inherited()@, 6 | @included()@ and @extended()@. 7 | 8 | If a class has a class method called @inherited()@ it will be called whenever 9 | you create a subclass of it: 10 | 11 |
var ChildDetector = new JS.Class({
12 |       extend: {
13 |           inherited: function(klass) {
14 |               // Do stuff with child class
15 |           }
16 |       }
17 |   });
18 | 19 | The hook receives the new child class as an argument. Note that @class@ is a 20 | reserved word in JavaScript and should not be used as a variable name. The 21 | child class will have all its methods in place when @inherited()@ gets called, 22 | so you can use them within your callback function. 23 | 24 | In the same vein, if you @include()@ a module that has a singleton method 25 | called @included@, that method will be called. This effectively allows you to 26 | redefine the meaning of @include@ for individual modules. 27 | 28 | The @extended()@ hook works in much the same way as @included()@, except that 29 | it will be called when the module is used to @extend()@ an object. 30 | 31 |
// This will call MyMod.extended(obj)
32 |   obj.extend(MyMod);
33 | 34 | Again, you can use this to redefine how @extend()@ works with individual 35 | modules, so they can change the behaviour of the objects they extend. 36 | -------------------------------------------------------------------------------- /site/src/pages/binding.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Method binding 3 | 4 | Ruby has method objects (@Method@ and @UnboundMethod@) for passing references 5 | to an object's methods, so you can call a method without having a reference to 6 | the object. JavaScript has functions and treats them as first-class objects, 7 | so you can get a reference to a method and call it later: 8 | 9 |
var rex = new Dog('Rex');
10 |   var spk = rex.speak;    // a reference, we are not calling the method
11 |   spk('biscuits');
12 |       // -> "MY NAME IS AND I LIKE BISCUITS!"
13 | 14 | Where did Rex's name go? The thing is, we've not called @spk@ through the 15 | object @rex@, so @this@ inside the function no longer refers to the right 16 | thing. JS.Class gives each object a @method()@ method, that returns a method 17 | by name, bound to its source object. This method is simply a JavaScript 18 | function that you can call on its own and maintain the binding of @this@: 19 | 20 |
var speak = rex.method('speak');
21 |   speak('biscuits');
22 |       // -> "MY NAME IS REX AND I LIKE BISCUITS!"
23 | 24 | You can also do this with class methods, since classes are objects too: 25 | 26 |
var User = new JS.Class({
27 |       extend: {
28 |           create: function(name) {
29 |               return new this(name);
30 |           }
31 |       },
32 |       initialize: function(name) {
33 |           this.username = name;
34 |       }
35 |   });
36 | 
37 |   var u = User.method('create');
38 |   u('James')    // -> {username: 'James'}
39 | 40 | -------------------------------------------------------------------------------- /source/test/context/suite.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var suite = Test.Unit.TestCase.suite; 3 | 4 | Test.Unit.TestCase.extend({ 5 | // Tweaks to standard method so we don't get superclass methods and we don't 6 | // get weird default tests 7 | suite: function(filter) { 8 | return suite.call(this, filter, false, false); 9 | } 10 | }); 11 | })(); 12 | 13 | Test.Unit.TestSuite.include({ 14 | run: function(result, continuation, callback, context) { 15 | if (this._metadata.fullName) 16 | callback.call(context, this.klass.STARTED, this); 17 | 18 | var withIvars = function(ivarsFromCallback) { 19 | this.forEach(function(test, resume) { 20 | if (ivarsFromCallback && test.setValuesFromCallbacks) 21 | test.setValuesFromCallbacks(ivarsFromCallback); 22 | 23 | test.run(result, resume, callback, context); 24 | 25 | }, function() { 26 | var afterCallbacks = function() { 27 | if (this._metadata.fullName) 28 | callback.call(context, this.klass.FINISHED, this); 29 | 30 | continuation.call(context); 31 | }; 32 | if (ivarsFromCallback && first.runAllCallbacks) 33 | first.runAllCallbacks('after', afterCallbacks, this); 34 | else 35 | afterCallbacks.call(this); 36 | 37 | }, this); 38 | }; 39 | 40 | var first = this._tests[0], ivarsFromCallback = null; 41 | 42 | if (first && first.runAllCallbacks) 43 | first.runAllCallbacks('before', withIvars, this); 44 | else 45 | withIvars.call(this, null); 46 | } 47 | }); 48 | 49 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'httparty' 3 | require 'nokogiri' 4 | require 'set' 5 | 6 | def mdc_url(type) 7 | 'http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/' + type 8 | end 9 | 10 | def document(url) 11 | puts "Fetching #{url} ..." 12 | Nokogiri.parse(HTTParty.get(url)) 13 | rescue 14 | document(url) 15 | end 16 | 17 | MDC_URLS = %w(Array Boolean Date EvalError Error Function Math 18 | Number Object RangeError ReferenceError RegExp 19 | String SyntaxError TypeError URIError). 20 | map(&method(:mdc_url)) 21 | 22 | ELEMENT_URL = 'http://developer.mozilla.org/en/DOM/element' 23 | EVENT_URL = 'http://developer.mozilla.org/en/DOM/event' 24 | STYLE_URL = 'http://developer.mozilla.org/en/DOM/CSS' 25 | 26 | class MethodSet < SortedSet 27 | def add_method(link) 28 | name = link.text.strip.gsub(/^event\./, '') 29 | add(name) if name =~ /^[a-z][a-zA-Z0-9\_\$]*$/ 30 | end 31 | 32 | def import(url, selector) 33 | document(url).search(selector).each(&method(:add_method)) 34 | end 35 | end 36 | 37 | namespace :import do 38 | task :method_chain do 39 | methods = MethodSet.new 40 | 41 | MDC_URLS.each { |url| methods.import(url, 'dt a') } 42 | 43 | methods.import(ELEMENT_URL, 'td code a:first-child') 44 | 45 | document(ELEMENT_URL).search('#pageText>p').last.search('a').map do |link| 46 | methods.import(link[:href], 'td code a:first-child') 47 | end 48 | 49 | methods.import(EVENT_URL, 'dt a') 50 | methods.import(STYLE_URL, 'li a') 51 | 52 | p methods.entries 53 | end 54 | end 55 | 56 | -------------------------------------------------------------------------------- /site/src/pages/platforms.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Supported platforms 3 | 4 | One of the main goals of @jsclass@ is to help you write programs that work 5 | across a wide variety of JavaScript platforms. It does this by abstracting 6 | some platform-specific details such as "module loading":/packages.html, and by 7 | providing cross-platform "testing tools":http://jstest.jcoglan.com. 8 | 9 | @jsclass@ currently runs without modification on all the following 10 | environments: 11 | 12 | h3. Web browsers 13 | 14 | * "Chrome":http://www.google.com/chrome 15 | * "Firefox":http://www.getfirefox.com/ 16 | * "Internet Explorer":http://www.microsoft.com/windows/internet-explorer/default.aspx 17 | * "Opera":http://www.opera.com/ 18 | * "Safari":http://www.apple.com/safari/ 19 | 20 | h3. Headless DOM environments 21 | 22 | * "PhantomJS":http://www.phantomjs.org/ (both in the browser and the scripting 23 | runtime) 24 | 25 | h3. Server-side platforms 26 | 27 | * "Node.js":http://nodejs.org/ 28 | * "Narwhal":http://narwhaljs.org/ 29 | * "RingoJS":http://ringojs.org/ 30 | 31 | h3. Database shells 32 | 33 | * "MongoDB":http://www.mongodb.org/ 34 | 35 | h3. GUI frameworks 36 | 37 | * "Mozilla XULRunner":https://developer.mozilla.org/en/xulrunner 38 | * "Adobe AIR":http://www.adobe.com/products/air/ 39 | 40 | h3. Shell environments 41 | 42 | * "V8 shell":http://code.google.com/p/v8/ 43 | * "Mozilla Rhino":http://www.mozilla.org/rhino/ 44 | * "Mozilla SpiderMonkey":http://www.mozilla.org/js/spidermonkey/ 45 | * "Windows Script Host":http://msdn.microsoft.com/en-us/library/9bbdkx3k(VS.85).aspx 46 | 47 | -------------------------------------------------------------------------------- /source/constant_scope.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var ConstantScope = new JS.Module('ConstantScope', { 12 | extend: { 13 | included: function(base) { 14 | base.__consts__ = new JS.Module(); 15 | base.extend(this.ClassMethods); 16 | base.__eigen__().extend(this.ClassMethods); 17 | 18 | base.include(base.__consts__); 19 | base.extend(base.__consts__); 20 | 21 | base.include(this.extract(base.__fns__)); 22 | base.extend(this.extract(base.__eigen__().__fns__)); 23 | }, 24 | 25 | ClassMethods: new JS.Module({ 26 | define: function(name, callable) { 27 | var constants = this.__consts__ || this.__tgt__.__consts__; 28 | 29 | if (/^[A-Z]/.test(name)) 30 | constants.define(name, callable); 31 | else 32 | this.callSuper(); 33 | 34 | if (JS.isType(callable, JS.Module)) { 35 | callable.include(ConstantScope); 36 | callable.__consts__.include(constants); 37 | } 38 | } 39 | }), 40 | 41 | extract: function(methods, base) { 42 | var constants = {}, key, object; 43 | for (key in methods) { 44 | if (!/^[A-Z]/.test(key)) continue; 45 | 46 | object = methods[key]; 47 | constants[key] = object; 48 | delete methods[key]; 49 | } 50 | return constants; 51 | } 52 | } 53 | }); 54 | 55 | exports.ConstantScope = ConstantScope; 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /test/specs/singleton_spec.js: -------------------------------------------------------------------------------- 1 | JS.ENV.SingletonSpec = JS.Test.describe(JS.Singleton, function() { with(this) { 2 | before(function() { with(this) { 3 | this.mixin = new JS.Module() 4 | this.parent = new JS.Class() 5 | }}) 6 | 7 | describe("with no ancestors", function() { with(this) { 8 | before(function() { with(this) { 9 | this.singleton = new JS.Singleton({ foo: function() { return "foo" } }) 10 | }}) 11 | 12 | it("creates an object that inherits from Kernel", function() { with(this) { 13 | assertEqual( [JS.Kernel, singleton.klass, singleton.__eigen__()], 14 | singleton.__eigen__().ancestors() ) 15 | }}) 16 | 17 | it("creates an object with the right methods", function() { with(this) { 18 | assertEqual( "foo", singleton.foo() ) 19 | }}) 20 | }}) 21 | 22 | describe("with a parent class", function() { with(this) { 23 | before(function() { with(this) { 24 | this.singleton = new JS.Singleton(parent) 25 | }}) 26 | 27 | it("creates an object that inherits from the parent", function() { with(this) { 28 | assertEqual( [JS.Kernel, parent, singleton.klass, singleton.__eigen__()], 29 | singleton.__eigen__().ancestors() ) 30 | }}) 31 | }}) 32 | 33 | describe("with a mixin", function() { with(this) { 34 | before(function() { with(this) { 35 | this.singleton = new JS.Singleton({ include: mixin }) 36 | }}) 37 | 38 | it("creates an object that inherits from the mixin", function() { with(this) { 39 | assertEqual( [JS.Kernel, mixin, singleton.klass, singleton.__eigen__()], 40 | singleton.__eigen__().ancestors() ) 41 | }}) 42 | }}) 43 | }}) 44 | 45 | -------------------------------------------------------------------------------- /test/examples/benchmarks.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var $ = (typeof this.global === 'object') ? this.global : this 3 | $.JSCLASS_PATH = 'build/min/' 4 | })() 5 | 6 | if (typeof require === 'function') 7 | var JS = require('../../' + JSCLASS_PATH + 'loader') 8 | else 9 | load(JSCLASS_PATH + 'loader.js') 10 | 11 | 12 | JS.require('JS.Module', 'JS.Class', 'JS.Benchmark', 'JS.Set', 'JS.OrderedSet', 'JS.SortedSet', 13 | function(Module, Class, bm, Set, OrderedSet, SortedSet) { 14 | 15 | var sets = [SortedSet, OrderedSet, Set], 16 | i = sets.length 17 | 18 | while (i--) { 19 | bm.measure(sets[i] + ' creation', 10, { 20 | test: function() { 21 | var set = new sets[i](), n = 1000 22 | while (n--) set.add(n) 23 | } 24 | }) 25 | } 26 | 27 | bm.measure('Class creation', 300, { 28 | test: function() { 29 | new Class({ 30 | method1: function() {}, 31 | method1: function() {}, 32 | method1: function() {} 33 | }) 34 | } 35 | }) 36 | 37 | bm.measure('Module#ancestors', 5000, { 38 | setup: function() { 39 | var included = new Module({ include: new Module({ include: new Module() }) }) 40 | this.module = new Module() 41 | this.module.include(included) 42 | }, 43 | test: function() { 44 | this.module.ancestors() 45 | } 46 | }) 47 | 48 | bm.measure('Module#ancestors (cached)', 5000, { 49 | setup: function() { 50 | var included = new Module({ include: new Module({ include: new Module() }) }) 51 | this.module = new Module() 52 | this.module.include(included) 53 | this.module.ancestors() 54 | }, 55 | test: function() { 56 | this.module.ancestors() 57 | } 58 | }) 59 | }) 60 | 61 | -------------------------------------------------------------------------------- /source/dom/event.js: -------------------------------------------------------------------------------- 1 | DOM.Event = { 2 | _registry: [], 3 | 4 | on: function(element, eventName, callback, context) { 5 | if (element === undefined) return; 6 | 7 | if (element !== DOM.ENV && 8 | element.nodeType !== DOM.ELEMENT_NODE && 9 | element.nodeType !== DOM.DOCUMENT_NODE) 10 | return; 11 | 12 | var wrapped = function() { callback.call(context, element) }; 13 | 14 | if (element.addEventListener) 15 | element.addEventListener(eventName, wrapped, false); 16 | else if (element.attachEvent) 17 | element.attachEvent('on' + eventName, wrapped); 18 | 19 | this._registry.push({ 20 | _element: element, 21 | _type: eventName, 22 | _callback: callback, 23 | _context: context, 24 | _handler: wrapped 25 | }); 26 | }, 27 | 28 | detach: function(element, eventName, callback, context) { 29 | var i = this._registry.length, register; 30 | while (i--) { 31 | register = this._registry[i]; 32 | 33 | if ((element && element !== register._element) || 34 | (eventName && eventName !== register._type) || 35 | (callback && callback !== register._callback) || 36 | (context && context !== register._context)) 37 | continue; 38 | 39 | if (register._element.removeEventListener) 40 | register._element.removeEventListener(register._type, register._handler, false); 41 | else if (register._element.detachEvent) 42 | register._element.detachEvent('on' + register._type, register._handler); 43 | 44 | this._registry.splice(i,1); 45 | register = null; 46 | } 47 | } 48 | }; 49 | 50 | DOM.Event.on(DOM.ENV, 'unload', DOM.Event.detach, DOM.Event); 51 | 52 | -------------------------------------------------------------------------------- /source/test/reporters/tap.js: -------------------------------------------------------------------------------- 1 | Test.Reporters.extend({ 2 | TAP: new JS.Class({ 3 | extend: { 4 | HOSTNAME: 'testling', 5 | 6 | create: function(options) { 7 | if (!JS.ENV.location) return; 8 | var parts = location.hostname.split('.'); 9 | if (JS.indexOf(parts, this.HOSTNAME) >= 0) return new this(options); 10 | } 11 | }, 12 | 13 | include: Console, 14 | 15 | startSuite: function(event) { 16 | this._testId = 0; 17 | this.puts('1..' + event.size); 18 | }, 19 | 20 | startContext: function(event) {}, 21 | 22 | startTest: function(event) { 23 | this._testPassed = true; 24 | this._faults = []; 25 | }, 26 | 27 | addFault: function(event) { 28 | this._testPassed = false; 29 | this._faults.push(event); 30 | }, 31 | 32 | endTest: function(event) { 33 | var line = this._testPassed ? 'ok' : 'not ok'; 34 | line += ' ' + ++this._testId + ' - ' + this._format(event.fullName); 35 | this.puts(line); 36 | 37 | var fault, message, parts, j, m; 38 | for (var i = 0, n = this._faults.length; i < n; i++) { 39 | fault = this._faults[i]; 40 | var message = fault.error.message; 41 | if (fault.error.backtrace) message += '\n' + fault.error.backtrace; 42 | parts = message.split(/[\r\n]/); 43 | for (j = 0, m = parts.length; j < m; j++) 44 | this.puts(' ' + parts[j]); 45 | } 46 | }, 47 | 48 | endContext: function(event) {}, 49 | 50 | update: function(event) {}, 51 | 52 | endSuite: function(event) {}, 53 | 54 | _format: function(string) { 55 | return string.replace(/[\s\t\r\n]+/g, ' '); 56 | } 57 | }) 58 | }); 59 | 60 | Test.Reporters.register('tap', Test.Reporters.TAP); 61 | 62 | -------------------------------------------------------------------------------- /test/specs/test/test_spec_helpers.js: -------------------------------------------------------------------------------- 1 | JS.ENV.TestSpecHelpers = new JS.Module({ 2 | suite: function(tests) { 3 | return new JS.Class("TestedSuite", JS.Test.Unit.TestCase, tests).suite() 4 | }, 5 | 6 | createTestEnvironment: function() { 7 | this.result = new JS.Test.Unit.TestResult() 8 | this.faults = [] 9 | this.result.addListener(JS.Test.Unit.TestResult.FAULT, this.faults.push, this.faults) 10 | }, 11 | 12 | runTests: function(tests, resume) { 13 | if (tests) this.testcase = this.suite(tests) 14 | JS.Test.showStack = false 15 | this.testcase.run(this.result, function() { 16 | if (resume) resume() 17 | JS.Test.showStack = true 18 | }, function() {}) 19 | }, 20 | 21 | assertTestResult: function(runs, assertions, failures, errors) { with(this) { 22 | __wrapAssertion__(function() { with(this) { 23 | assertEqual( runs, result.runCount(), "Incorrect run count" ) 24 | assertEqual( assertions, result.assertionCount(), "Incorrect assertion count" ) 25 | assertEqual( failures, result.failureCount(), "Incorrect failure count" ) 26 | assertEqual( errors, result.errorCount(), "Incorrect error count" ) 27 | 28 | assertEqual( failures + errors, faults.length ) 29 | }}) 30 | }}, 31 | 32 | assertMessage: function(index, message) { with(this) { 33 | if (typeof index === "string") { 34 | message = index 35 | index = 1 36 | } 37 | 38 | var f = faults[index-1], 39 | t = f.testMetadata(), 40 | e = f.errorMetadata(); 41 | 42 | assertEqual( message, (e.type === "failure" ? "Failure" : "Error") + ":\n" + 43 | t.shortName + "(" + t.context.pop() + "):\n" + 44 | e.message ) 45 | }} 46 | }) 47 | 48 | -------------------------------------------------------------------------------- /source/package/dsl.js: -------------------------------------------------------------------------------- 1 | var DSL = { 2 | __FILE__: function() { 3 | return Package.loader.__FILE__(); 4 | }, 5 | 6 | pkg: function(name, path) { 7 | var pkg = path 8 | ? Package._getByPath(path) 9 | : Package._getByName(name); 10 | pkg.provides(name); 11 | return pkg; 12 | }, 13 | 14 | file: function(filename) { 15 | var files = [], i = arguments.length; 16 | while (i--) files[i] = resolve(arguments[i]); 17 | return Package._getByPath.apply(Package, files); 18 | }, 19 | 20 | load: function(path, fireCallbacks) { 21 | Package.loader.loadFile(path, fireCallbacks); 22 | }, 23 | 24 | autoload: function(pattern, options) { 25 | Package._autoload(pattern, options); 26 | } 27 | }; 28 | 29 | DSL.files = DSL.file; 30 | DSL.loader = DSL.file; 31 | 32 | var packages = function(declaration) { 33 | declaration.call(DSL); 34 | }; 35 | 36 | var parseLoadArgs = function(args) { 37 | var files = [], i = 0; 38 | 39 | while (typeof args[i] === 'string'){ 40 | files.push(args[i]); 41 | i += 1; 42 | } 43 | 44 | return {files: files, callback: args[i], context: args[i+1]}; 45 | }; 46 | 47 | exports.load = function(path, callback) { 48 | var args = parseLoadArgs(arguments), 49 | n = args.files.length; 50 | 51 | var loadNext = function(index) { 52 | if (index === n) return args.callback.call(args.context); 53 | Package.loader.loadFile(args.files[index], function() { 54 | loadNext(index + 1); 55 | }); 56 | }; 57 | loadNext(0); 58 | }; 59 | 60 | exports.require = function() { 61 | var args = parseLoadArgs(arguments); 62 | 63 | Package.when({complete: args.files}, function(objects) { 64 | if (!args.callback) return; 65 | args.callback.apply(args.context, objects && objects.complete); 66 | }); 67 | 68 | return this; 69 | }; 70 | 71 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | JS.ENV.CWD = (typeof CWD === 'undefined') ? '.' : CWD 2 | 3 | JS.cache = false 4 | if (JS.ENV.JS_DEBUG) JS.debug = true 5 | 6 | JS.packages(function() { with(this) { 7 | autoload(/^(.*)Spec$/, {from: CWD + '/test/specs', require: 'JS.$1'}) 8 | 9 | pkg('Test.UnitSpec').requires('JS.Set', 'JS.Observable') 10 | pkg('ClassSpec').requires('ModuleSpec') 11 | 12 | file(CWD + '/test/specs/test/test_spec_helpers.js').provides('TestSpecHelpers') 13 | 14 | pkg('Test.UnitSpec').requires('TestSpecHelpers') 15 | pkg('Test.MockingSpec').requires('TestSpecHelpers') 16 | }}) 17 | 18 | JS.require('JS', 'JS.Test', function(js, Test) { 19 | js.extend(JS, js) 20 | JS.Test = Test 21 | 22 | var specs = [ 'Test.UnitSpec', 23 | 'Test.ContextSpec', 24 | 'Test.MockingSpec', 25 | 'Test.FakeClockSpec', 26 | 'Test.AsyncStepsSpec', 27 | 'ModuleSpec', 28 | 'ClassSpec', 29 | 'MethodSpec', 30 | 'KernelSpec', 31 | 'SingletonSpec', 32 | 'InterfaceSpec', 33 | 'CommandSpec', 34 | 'ComparableSpec', 35 | 'ConsoleSpec', 36 | 'ConstantScopeSpec', 37 | 'DecoratorSpec', 38 | 'EnumerableSpec', 39 | 'ForwardableSpec', 40 | 'HashSpec', 41 | 'LinkedListSpec', 42 | 'MethodChainSpec', 43 | 'DeferrableSpec', 44 | 'ObservableSpec', 45 | 'PackageSpec', 46 | 'ProxySpec', 47 | 'RangeSpec', 48 | 'SetSpec', 49 | 'StateSpec', 50 | 'TSortSpec' ] 51 | 52 | specs = Test.filter(specs, 'Spec') 53 | specs.push(function() { Test.autorun() }) 54 | JS.require.apply(JS, specs) 55 | }) 56 | 57 | -------------------------------------------------------------------------------- /source/test/unit/test_result.js: -------------------------------------------------------------------------------- 1 | Test.Unit.extend({ 2 | TestResult: new JS.Class({ 3 | include: Test.Unit.Observable, 4 | 5 | extend: { 6 | CHANGED: 'Test.Unit.TestResult.CHANGED', 7 | FAULT: 'Test.Unit.TestResult.FAULT' 8 | }, 9 | 10 | initialize: function() { 11 | this._runCount = this._assertionCount = 0; 12 | this._failures = []; 13 | this._errors = []; 14 | }, 15 | 16 | addRun: function() { 17 | this._runCount += 1; 18 | this.notifyListeners(this.klass.CHANGED, this); 19 | }, 20 | 21 | addFailure: function(failure) { 22 | this._failures.push(failure); 23 | this.notifyListeners(this.klass.FAULT, failure); 24 | this.notifyListeners(this.klass.CHANGED, this); 25 | }, 26 | 27 | addError: function(error) { 28 | this._errors.push(error); 29 | this.notifyListeners(this.klass.FAULT, error); 30 | this.notifyListeners(this.klass.CHANGED, this); 31 | }, 32 | 33 | addAssertion: function() { 34 | this._assertionCount += 1; 35 | this.notifyListeners(this.klass.CHANGED, this); 36 | }, 37 | 38 | passed: function() { 39 | return this._failures.length === 0 && this._errors.length === 0; 40 | }, 41 | 42 | runCount: function() { 43 | return this._runCount; 44 | }, 45 | 46 | assertionCount: function() { 47 | return this._assertionCount; 48 | }, 49 | 50 | failureCount: function() { 51 | return this._failures.length; 52 | }, 53 | 54 | errorCount: function() { 55 | return this._errors.length; 56 | }, 57 | 58 | metadata: function() { 59 | return { 60 | passed: this.passed(), 61 | tests: this.runCount(), 62 | assertions: this.assertionCount(), 63 | failures: this.failureCount(), 64 | errors: this.errorCount() 65 | }; 66 | } 67 | }) 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | JS.Class: Ruby-style JavaScript 2 | http://jsclass.jcoglan.com 3 | Copyright (c) 2007-2013 James Coglan and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | Parts of the Software build on techniques from the following open-source 16 | projects: 17 | 18 | * The Prototype framework, (c) 2005-2010 Sam Stephenson (MIT license) 19 | * Alex Arnell's Inheritance library, (c) 2006 Alex Arnell (MIT license) 20 | * Base, (c) 2006-2010 Dean Edwards (MIT license) 21 | 22 | The Software contains direct translations to JavaScript of these open-source 23 | Ruby libraries: 24 | 25 | * Ruby standard library modules, (c) Yukihiro Matsumoto and contributors (Ruby license) 26 | * Test::Unit, (c) 2000-2003 Nathaniel Talbott (Ruby license) 27 | * Context, (c) 2008 Jeremy McAnally (MIT license) 28 | * EventMachine::Deferrable, (c) 2006-07 Francis Cianfrocca (Ruby license) 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 36 | THE SOFTWARE. 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/specs/method_spec.js: -------------------------------------------------------------------------------- 1 | JS.ENV.MethodSpec = JS.Test.describe(JS.Method, function() { with(this) { 2 | before(function() { with(this) { 3 | this.callable = function(a,b) { return "something" } 4 | this.theModule = new JS.Module({ im_a_method: callable }) 5 | this.theMethod = theModule.instanceMethod("im_a_method") 6 | }}) 7 | 8 | it("should be bootstrapped properly", function() { with(this) { 9 | assertKindOf( JS.Class, JS.Method ) 10 | assertKindOf( JS.Module, JS.Method ) 11 | assertKindOf( JS.Kernel, JS.Method ) 12 | assertEqual( "Method", JS.Method.displayName ) 13 | }}) 14 | 15 | describe("#module", function() { with(this) { 16 | it("refers to the module hosting the method", function() { with(this) { 17 | assertEqual( theModule, theMethod.module ) 18 | }}) 19 | }}) 20 | 21 | describe("#name", function() { with(this) { 22 | it("returns the name of the method", function() { with(this) { 23 | assertEqual( "im_a_method", theMethod.name ) 24 | }}) 25 | }}) 26 | 27 | describe("#callable", function() { with(this) { 28 | it("refers to the JavaScript function the method represents", function() { with(this) { 29 | assertEqual( callable, theMethod.callable ) 30 | }}) 31 | }}) 32 | 33 | describe("#arity", function() { with(this) { 34 | it("gives the number of arguments the method accepts", function() { with(this) { 35 | assertEqual( 2, theMethod.arity ) 36 | }}) 37 | }}) 38 | 39 | describe("#contains", function() { with(this) { 40 | it("returns true if the method's source includes the word", function() { with(this) { 41 | assert( theMethod.contains("return") ) 42 | assert( theMethod.contains("something") ) 43 | }}) 44 | 45 | it("returns false if the method's source does not include the word", function() { with(this) { 46 | assert( !theMethod.contains("nothing") ) 47 | }}) 48 | }}) 49 | }}) 50 | 51 | -------------------------------------------------------------------------------- /source/test/context/shared_behavior.js: -------------------------------------------------------------------------------- 1 | Test.Context.extend({ 2 | SharedBehavior: new JS.Class(JS.Module, { 3 | extend: { 4 | createFromBehavior: function(beh) { 5 | var mod = new this(); 6 | mod._behavior = beh; 7 | return mod; 8 | }, 9 | 10 | moduleName: function(name) { 11 | return name.toLowerCase() 12 | .replace(/[\s:',\.~;!#=\(\)&]+/g, '_') 13 | .replace(/\/(.?)/g, function(m,a) { return '.' + a.toUpperCase() }) 14 | .replace(/(?:^|_)(.)/g, function(m,a) { return a.toUpperCase() }); 15 | } 16 | }, 17 | 18 | included: function(arg) { 19 | this._behavior.call(arg); 20 | } 21 | }) 22 | }); 23 | 24 | Test.Unit.TestCase.extend({ 25 | shared: function(name, block) { 26 | name = Test.Context.SharedBehavior.moduleName(name); 27 | JS.ENV[name] = Test.Context.SharedBehavior.createFromBehavior(block); 28 | }, 29 | 30 | use: function(sharedName) { 31 | if (JS.isType(sharedName, Test.Context.SharedBehavior) || 32 | JS.isType(sharedName, JS.Module)) 33 | this.include(sharedName); 34 | 35 | else if (JS.isType(sharedName, 'string')) { 36 | var name = Test.Context.SharedBehavior.moduleName(sharedName), 37 | beh = JS.ENV[name]; 38 | 39 | if (!beh) throw new Error('Could not find example group named "' + sharedName + '"'); 40 | this.include(beh); 41 | } 42 | } 43 | }); 44 | 45 | (function() { 46 | var alias = function(method, aliases) { 47 | var extension = {}; 48 | for (var i = 0, n = aliases.length; i < n; i++) 49 | extension[aliases[i]] = Test.Unit.TestCase[method]; 50 | Test.Unit.TestCase.extend(extension); 51 | }; 52 | 53 | alias('shared', ['sharedBehavior', 'shareAs', 'shareBehaviorAs', 'sharedExamplesFor']); 54 | alias('use', ['uses', 'itShouldBehaveLike', 'behavesLike', 'usesExamplesFrom']); 55 | })(); 56 | 57 | -------------------------------------------------------------------------------- /source/console/browser_color.js: -------------------------------------------------------------------------------- 1 | Console.extend({ 2 | BrowserColor: new JS.Class(Console.Browser, { 3 | COLORS: { 4 | green: 'limegreen' 5 | }, 6 | 7 | __queue__: [], 8 | __state__: {}, 9 | 10 | format: function(type, name) { 11 | name = name.replace(/^bg/, ''); 12 | 13 | var state = JS.extend({}, this.__state__), 14 | color = this.COLORS[name] || name, 15 | no = /^no/.test(name); 16 | 17 | if (type === 'reset') 18 | state = {}; 19 | else if (no) 20 | delete state[type]; 21 | else if (type === 'weight') 22 | state.weight = 'font-weight: ' + name; 23 | else if (type === 'style') 24 | state.style = 'font-style: ' + name; 25 | else if (type === 'underline') 26 | state.underline = 'text-decoration: underline'; 27 | else if (type === 'color') 28 | state.color = 'color: ' + color; 29 | else if (type === 'background') 30 | state.background = 'background-color: ' + color; 31 | else 32 | state = null; 33 | 34 | if (state) { 35 | this.__state__ = state; 36 | this.__queue__.push(state); 37 | } 38 | }, 39 | 40 | print: function(string) { 41 | this.__queue__.push(string) 42 | }, 43 | 44 | puts: function(string) { 45 | this.print(string); 46 | var buffer = '', formats = [], item; 47 | while (item = this.__queue__.shift()) { 48 | if (typeof item === 'string') { 49 | buffer += '%c' + item; 50 | formats.push(this._serialize(this.__state__)); 51 | } else { 52 | this.__state__ = item; 53 | } 54 | } 55 | console.log.apply(console, [buffer].concat(formats)); 56 | }, 57 | 58 | _serialize: function(state) { 59 | var rules = []; 60 | for (var key in state) rules.push(state[key]); 61 | return rules.join('; '); 62 | } 63 | }) 64 | }); 65 | 66 | -------------------------------------------------------------------------------- /test/examples/tracing.js: -------------------------------------------------------------------------------- 1 | if (this.ActiveXObject) load = function(path) { 2 | var fso = new ActiveXObject('Scripting.FileSystemObject'), file, runner; 3 | try { 4 | file = fso.OpenTextFile(path); 5 | runner = function() { eval(file.ReadAll()) }; 6 | runner(); 7 | } finally { 8 | try { if (file) file.Close() } catch (e) {} 9 | } 10 | }; 11 | 12 | (function() { 13 | var $ = (typeof this.global === 'object') ? this.global : this 14 | $.JSCLASS_PATH = 'build/src/' 15 | })() 16 | 17 | if (typeof require === 'function') { 18 | var JS = require('../../' + JSCLASS_PATH + 'loader') 19 | } else { 20 | load(JSCLASS_PATH + 'loader.js') 21 | } 22 | 23 | Foo = {} 24 | 25 | JS.require('JS.Class', 'JS.Method', 'JS.Console', 'JS.Hash', 'JS.OrderedHash', 'JS.TSort', 26 | function(Class, Method, Console, Hash, OrderedHash, TSort) { 27 | Tasks = new Class({ 28 | include: TSort, 29 | 30 | initialize: function(table) { 31 | this.table = table; 32 | }, 33 | 34 | tsortEachNode: function(block, context) { 35 | for (var task in this.table) { 36 | if (this.table.hasOwnProperty(task)) 37 | block.call(context, task); 38 | } 39 | }, 40 | 41 | tsortEachChild: function(task, block, context) { 42 | var tasks = this.table[task]; 43 | for (var i = 0, n = tasks.length; i < n; i++) 44 | block.call(context, tasks[i]); 45 | } 46 | }) 47 | 48 | var tasks = new Tasks({ 49 | 'eat breakfast': ['serve'], 50 | 'serve': ['cook'], 51 | 'cook': ['buy bacon', 'buy eggs'], 52 | 'buy bacon': [], 53 | 'buy eggs': [] 54 | }) 55 | 56 | var hash = new OrderedHash(['foo', 4, 'bar', 5]) 57 | 58 | Method.tracing([Hash, TSort], function() { 59 | tasks.tsort() 60 | hash.hasKey('foo') 61 | Console.puts(Console.nameOf(Foo)) 62 | hash.select(function() { throw new Error('fail') }) 63 | }) 64 | 65 | hash.hasKey('something') 66 | }) 67 | 68 | -------------------------------------------------------------------------------- /source/core/kernel.js: -------------------------------------------------------------------------------- 1 | JS.Kernel = new JS.Module('Kernel', { 2 | __eigen__: function() { 3 | if (this.__meta__) return this.__meta__; 4 | var name = this.toString() + '.'; 5 | this.__meta__ = new JS.Module(name, null, {_target: this}); 6 | return this.__meta__.include(this.klass, {_resolve: false}); 7 | }, 8 | 9 | equals: function(other) { 10 | return this === other; 11 | }, 12 | 13 | extend: function(module, options) { 14 | var resolve = (options || {})._resolve; 15 | this.__eigen__().include(module, {_extended: this, _resolve: resolve}); 16 | return this; 17 | }, 18 | 19 | hash: function() { 20 | return JS.Kernel.hashFor(this); 21 | }, 22 | 23 | isA: function(module) { 24 | return (typeof module === 'function' && this instanceof module) || 25 | this.__eigen__().includes(module); 26 | }, 27 | 28 | method: function(name) { 29 | var cache = this.__mct__ = this.__mct__ || {}, 30 | value = cache[name], 31 | field = this[name]; 32 | 33 | if (typeof field !== 'function') return field; 34 | if (value && field === value._value) return value._bound; 35 | 36 | var bound = JS.bind(field, this); 37 | cache[name] = {_value: field, _bound: bound}; 38 | return bound; 39 | }, 40 | 41 | methods: function() { 42 | return this.__eigen__().instanceMethods(); 43 | }, 44 | 45 | tap: function(block, context) { 46 | block.call(context, this); 47 | return this; 48 | }, 49 | 50 | toString: function() { 51 | if (this.displayName) return this.displayName; 52 | var name = this.klass.displayName || this.klass.toString(); 53 | return '#<' + name + ':' + this.hash() + '>'; 54 | } 55 | }); 56 | 57 | (function() { 58 | var id = 1; 59 | 60 | JS.Kernel.hashFor = function(object) { 61 | if (object.__hash__ !== undefined) return object.__hash__; 62 | object.__hash__ = (new JS.Date().getTime() + id).toString(16); 63 | id += 1; 64 | return object.__hash__; 65 | }; 66 | })(); 67 | 68 | -------------------------------------------------------------------------------- /source/decorator.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var Decorator = new JS.Class('Decorator', { 12 | initialize: function(decoree, methods) { 13 | var decorator = new JS.Class(), 14 | delegators = {}, 15 | method, func; 16 | 17 | for (method in decoree.prototype) { 18 | func = decoree.prototype[method]; 19 | if (typeof func === 'function' && func !== decoree) func = this.klass.delegate(method); 20 | delegators[method] = func; 21 | } 22 | 23 | decorator.include(new JS.Module(delegators), {_resolve: false}); 24 | decorator.include(this.klass.InstanceMethods, {_resolve: false}); 25 | decorator.include(methods); 26 | return decorator; 27 | }, 28 | 29 | extend: { 30 | delegate: function(name) { 31 | return function() { 32 | return this.component[name].apply(this.component, arguments); 33 | }; 34 | }, 35 | 36 | InstanceMethods: new JS.Module({ 37 | initialize: function(component) { 38 | this.component = component; 39 | this.klass = this.constructor = component.klass; 40 | var method, func; 41 | for (method in component) { 42 | if (this[method]) continue; 43 | func = component[method]; 44 | if (typeof func === 'function') func = Decorator.delegate(method); 45 | this[method] = func; 46 | } 47 | }, 48 | 49 | extend: function(source) { 50 | this.component.extend(source); 51 | var method, func; 52 | for (method in source) { 53 | func = source[method]; 54 | if (typeof func === 'function') func = Decorator.delegate(method); 55 | this[method] = func; 56 | } 57 | } 58 | }) 59 | } 60 | }); 61 | 62 | exports.Decorator = Decorator; 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /site/src/pages/debugging.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Debugging support 3 | 4 | The 2.1 release introduced support for "WebKit's @displayName@":http://www.alertdebugging.com/2009/04/29/building-a-better-javascript-profiler-with-webkit/ 5 | property for profiling and debugging JavaScript. Essentially, it is a 6 | workaround for the fact that JavaScript objects and functions do not have 7 | names directly attached to them, and can be referenced by any number of 8 | variables, thus making objects and functions basically anonymous. 9 | 10 | WebKit's profiler and debugger improves the situation by using the 11 | @displayName@ assigned to a @Function@ object if one has been assigned. 12 | JS.Class generates display names for methods and inner classes, so long as you 13 | specify a name for the outermost class. Class (and module) names are optional 14 | and are specified using the first argument to the @Class@ and @Module@ 15 | constructors. For example: 16 | 17 |
Foo = new JS.Module('Foo', {
18 |       sleep: function() { /* ... */ },
19 | 
20 |       extend: {
21 |           eatFood: function() { /* ... */ },
22 | 
23 |           InnerClass: new JS.Class({
24 |               haveVisions: function() { /* ... */ }
25 |           })
26 |       }
27 |   });
28 | 29 | The name does not have to be the same as the variable you assign the module to, 30 | although it will probably be more helpful if the names _are_ the same. The 31 | name given is not used for variable assignment, though. 32 | 33 | Given the above definition, we now find @displayName@ set on the methods and 34 | the inner class: 35 | 36 |
Foo.instanceMethod('sleep').displayName
37 |   // -> "Foo#sleep"
38 | 
39 |   Foo.eatFood.displayName
40 |   // -> "Foo.eatFood"
41 | 
42 |   Foo.InnerClass.displayName
43 |   // -> "Foo.InnerClass"
44 | 
45 |   Foo.InnerClass.instanceMethod('haveVisions').displayName
46 |   // -> "Foo.InnerClass#haveVisions"
47 | 48 | Further debugging support is provided by the "@StackTrace@":/stacktrace.html module. 49 | 50 | -------------------------------------------------------------------------------- /source/observable.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var Observable = new JS.Module('Observable', { 12 | extend: { 13 | DEFAULT_METHOD: 'update' 14 | }, 15 | 16 | addObserver: function(observer, context) { 17 | (this.__observers__ = this.__observers__ || []).push({_block: observer, _context: context}); 18 | }, 19 | 20 | removeObserver: function(observer, context) { 21 | this.__observers__ = this.__observers__ || []; 22 | context = context; 23 | var i = this.countObservers(); 24 | while (i--) { 25 | if (this.__observers__[i]._block === observer && this.__observers__[i]._context === context) { 26 | this.__observers__.splice(i,1); 27 | return; 28 | } 29 | } 30 | }, 31 | 32 | removeObservers: function() { 33 | this.__observers__ = []; 34 | }, 35 | 36 | countObservers: function() { 37 | return (this.__observers__ = this.__observers__ || []).length; 38 | }, 39 | 40 | notifyObservers: function() { 41 | if (!this.isChanged()) return; 42 | var i = this.countObservers(), observer, block, context; 43 | while (i--) { 44 | observer = this.__observers__[i]; 45 | block = observer._block; 46 | context = observer._context; 47 | if (typeof block === 'function') block.apply(context, arguments); 48 | else block[context || Observable.DEFAULT_METHOD].apply(block, arguments); 49 | } 50 | }, 51 | 52 | setChanged: function(state) { 53 | this.__changed__ = !(state === false); 54 | }, 55 | 56 | isChanged: function() { 57 | if (this.__changed__ === undefined) this.__changed__ = true; 58 | return !!this.__changed__; 59 | } 60 | }); 61 | 62 | Observable.alias({ 63 | subscribe: 'addObserver', 64 | unsubscribe: 'removeObserver' 65 | }, true); 66 | 67 | exports.Observable = Observable; 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /site/src/pages/packages.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Package management 3 | 4 | @jsclass@ comes with a package manager that makes it really easy to load 5 | libraries into your application on demand on all @jsclass@'s "supported 6 | platforms":/platforms.html. You can tell it which file each module in your 7 | application lives in, and what other modules it depends on, and it takes care 8 | of resolving dependencies and loading your code for you. This means your 9 | application code only needs to specify which objects it needs, rather than 10 | which scripts to download in order to run. 11 | 12 | For example, say I want to do something with the "@Console@":/console.html 13 | module. I just need to @require@ it, and supply a function to run once all the 14 | requisite code has loaded: 15 | 16 |
JS.require('JS.Console', function(Console) {
17 |       Console.puts('Hello, world!');
18 |   });
19 | 20 | The package system only loads what is needed to get the objects you want, and 21 | it makes sure that each file is only downloaded once. Where possible, files 22 | are downloaded in parallel to improve performance, but it makes sure 23 | interdependent scripts run in the right order. 24 | 25 | It is designed to be able to load any code from any domain, including from 26 | libraries that have their own package systems. It does not require the files 27 | it loads to follow any particular package format, but it can load files that 28 | export global variables or a CommonJS module. It expects the arguments to 29 | @JS.require()@ to either be the names of global objects, or exported 30 | properties of a CommonJS module. Whichever format the loaded files use, 31 | @JS.require()@ will yield the referenced objects as parameters to the callback 32 | function. 33 | 34 | * "Basic usage examples":/packages/introduction.html 35 | * "Pattern-based loading with @autoload@":/packages/autoload.html 36 | * "Custom loader functions":/packages/customloaders.html 37 | 38 | You can use the "@jsbuild@ command-line 39 | tool":http://github.com/jcoglan/jsbuild to bundle your packages for 40 | production. 41 | 42 | -------------------------------------------------------------------------------- /site/src/pages/packages/autoload.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Short-hand setup using @autoload@ 3 | 4 | As your application grows you may find that your package configuration becomes 5 | repetitive. For example, you may have a set of test scripts that mirror the 6 | set of classes in your application: 7 | 8 |
JS.packages(function() { with(this) {
 9 | 
10 |       file('tests/widget_spec.js')
11 |           .provides('WidgetSpec')
12 |           .requires('MyApp.Widget');
13 | 
14 |       file('tests/blog_post_spec.js')
15 |           .provides('BlogPostSpec')
16 |           .requires('MyApp.BlogPost');
17 | 
18 |       file('tests/users/profile_spec.js')
19 |           .provides('Users.ProfileSpec')
20 |           .requires('MyApp.Users.Profile');
21 |   });
22 | 23 | If you run into this situation you can use the @autoload()@ function to set up 24 | packages for objects whose name matches a certain pattern. For example you 25 | could compress the above configuration like this: 26 | 27 |
JS.packages(function() { with(this) {
28 |       autoload(/^(.*)Spec$/, {from: 'tests', require: 'MyApp.$1'});
29 |   });
30 | 31 | @autoload()@ expects three parameters. The first is a regex that is used to 32 | match package names. The @from@ option is a directory path where packages with 33 | that name pattern live, for example this rule would make the package loader 34 | look in @tests/users/profile_spec.js@ to find the @Users.ProfileSpec@ module. 35 | The @require@ option lets you specify an object the package depends on, using 36 | match results from the regex. The above rule would mean that 37 | @Users.ProfileSpec@ has a dependency on @MyApp.Users.Profile@. 38 | 39 | If you @require()@ a package that doesn't have an explicit configuration, the 40 | autoloader will try to figure out where to load it from by matching its name 41 | against the set of patterns it knows about. A naming convention is adopted for 42 | converting object names to paths: dots convert to path separators, and 43 | camelcase names are converted to underscored style. Thus @Users.ProfileSpec@ 44 | becomes @users/profile_spec.js@. 45 | -------------------------------------------------------------------------------- /source/test/mocking/dsl.js: -------------------------------------------------------------------------------- 1 | Test.Mocking.Stub.include({ 2 | given: function() { 3 | var matcher = new Test.Mocking.Parameters(arguments, this._expected); 4 | this._argMatchers.push(matcher); 5 | this._currentMatcher = matcher; 6 | return this; 7 | }, 8 | 9 | raises: function(exception) { 10 | this._currentMatcher._exception = exception; 11 | return this; 12 | }, 13 | 14 | returns: function() { 15 | this._currentMatcher.returns(arguments); 16 | return this; 17 | }, 18 | 19 | yields: function() { 20 | this._currentMatcher.yields(arguments); 21 | return this; 22 | }, 23 | 24 | atLeast: function(n) { 25 | this._currentMatcher.setMinimum(n); 26 | return this; 27 | }, 28 | 29 | atMost: function(n) { 30 | this._currentMatcher.setMaximum(n); 31 | return this; 32 | }, 33 | 34 | exactly: function(n) { 35 | this._currentMatcher.setExpected(n); 36 | return this; 37 | } 38 | }); 39 | 40 | Test.Mocking.Stub.alias({ 41 | raising: 'raises', 42 | returning: 'returns', 43 | yielding: 'yields' 44 | }); 45 | 46 | Test.Mocking.extend({ 47 | DSL: new JS.Module({ 48 | stub: function() { 49 | return Test.Mocking.stub.apply(Test.Mocking, arguments); 50 | }, 51 | 52 | expect: function() { 53 | var stub = Test.Mocking.stub.apply(Test.Mocking, arguments); 54 | stub.expected(); 55 | this.addAssertion(); 56 | return stub; 57 | }, 58 | 59 | anything: function() { 60 | return new Test.Mocking.Anything(); 61 | }, 62 | 63 | anyArgs: function() { 64 | return new Test.Mocking.AnyArgs(); 65 | }, 66 | 67 | instanceOf: function(type) { 68 | return new Test.Mocking.InstanceOf(type); 69 | }, 70 | 71 | match: function(type) { 72 | return new Test.Mocking.Matcher(type); 73 | }, 74 | 75 | arrayIncluding: function() { 76 | return new Test.Mocking.ArrayIncluding(arguments); 77 | }, 78 | 79 | objectIncluding: function(elements) { 80 | return new Test.Mocking.ObjectIncluding(elements); 81 | } 82 | }) 83 | }); 84 | 85 | Test.Unit.TestCase.include(Test.Mocking.DSL); 86 | Test.Unit.mocking = Test.Mocking; 87 | 88 | -------------------------------------------------------------------------------- /source/test/reporters/error.js: -------------------------------------------------------------------------------- 1 | Test.Reporters.extend({ 2 | Error: new JS.Class({ 3 | include: Console, 4 | 5 | NAMES: { 6 | failure: 'Failure', 7 | error: 'Error' 8 | }, 9 | 10 | startSuite: function(event) { 11 | this._faults = []; 12 | this._start = event.timestamp; 13 | 14 | this.consoleFormat('bold'); 15 | this.puts('Loaded suite: ' + event.children.join(', ')); 16 | this.reset(); 17 | this.puts(''); 18 | }, 19 | 20 | startContext: function(event) {}, 21 | 22 | startTest: function(event) {}, 23 | 24 | addFault: function(event) { 25 | this._faults.push(event); 26 | this._printFault(this._faults.length, event); 27 | }, 28 | 29 | update: function(event) {}, 30 | 31 | endTest: function(event) {}, 32 | 33 | endContext: function(event) {}, 34 | 35 | endSuite: function(event) { 36 | this._printSummary(event); 37 | }, 38 | 39 | _printFault: function(index, fault) { 40 | this.consoleFormat('bold', 'red'); 41 | this.puts(index + ') ' + this.NAMES[fault.error.type] + ': ' + fault.test.fullName); 42 | this.reset(); 43 | this.puts(fault.error.message); 44 | if (fault.error.backtrace) { 45 | this.grey(); 46 | this.puts(fault.error.backtrace); 47 | } 48 | this.reset(); 49 | this.puts(''); 50 | }, 51 | 52 | _printSummary: function(event) { 53 | var runtime = (event.timestamp - this._start) / 1000; 54 | this.reset(); 55 | this.puts('Finished in ' + runtime + ' seconds'); 56 | 57 | var color = event.passed ? 'green' : 'red'; 58 | this.consoleFormat(color); 59 | this.puts(this._plural(event.tests, 'test') + ', ' + 60 | this._plural(event.assertions, 'assertion') + ', ' + 61 | this._plural(event.failures, 'failure') + ', ' + 62 | this._plural(event.errors, 'error')); 63 | this.reset(); 64 | this.puts(''); 65 | }, 66 | 67 | _plural: function(number, noun) { 68 | return number + ' ' + noun + (number === 1 ? '' : 's'); 69 | } 70 | }) 71 | }); 72 | 73 | Test.Reporters.register('error', Test.Reporters.Error); 74 | 75 | -------------------------------------------------------------------------------- /source/core/utils.js: -------------------------------------------------------------------------------- 1 | var JS = {ENV: global}; 2 | 3 | JS.END_WITHOUT_DOT = /([^\.])$/; 4 | 5 | JS.array = function(enumerable) { 6 | var array = [], i = enumerable.length; 7 | while (i--) array[i] = enumerable[i]; 8 | return array; 9 | }; 10 | 11 | JS.bind = function(method, object) { 12 | return function() { 13 | return method.apply(object, arguments); 14 | }; 15 | }; 16 | 17 | JS.Date = JS.ENV.Date; 18 | 19 | JS.extend = function(destination, source, overwrite) { 20 | if (!destination || !source) return destination; 21 | for (var field in source) { 22 | if (destination[field] === source[field]) continue; 23 | if (overwrite === false && destination.hasOwnProperty(field)) continue; 24 | destination[field] = source[field]; 25 | } 26 | return destination; 27 | }; 28 | 29 | JS.indexOf = function(list, item) { 30 | if (list.indexOf) return list.indexOf(item); 31 | var i = list.length; 32 | while (i--) { 33 | if (list[i] === item) return i; 34 | } 35 | return -1; 36 | }; 37 | 38 | JS.isType = function(object, type) { 39 | if (typeof type === 'string') 40 | return typeof object === type; 41 | 42 | if (object === null || object === undefined) 43 | return false; 44 | 45 | return (typeof type === 'function' && object instanceof type) || 46 | (object.isA && object.isA(type)) || 47 | object.constructor === type; 48 | }; 49 | 50 | JS.makeBridge = function(parent) { 51 | var bridge = function() {}; 52 | bridge.prototype = parent.prototype; 53 | return new bridge(); 54 | }; 55 | 56 | JS.makeClass = function(parent) { 57 | parent = parent || Object; 58 | 59 | var constructor = function() { 60 | return this.initialize 61 | ? this.initialize.apply(this, arguments) || this 62 | : this; 63 | }; 64 | constructor.prototype = JS.makeBridge(parent); 65 | 66 | constructor.superclass = parent; 67 | 68 | constructor.subclasses = []; 69 | if (parent.subclasses) parent.subclasses.push(constructor); 70 | 71 | return constructor; 72 | }; 73 | 74 | JS.match = function(category, object) { 75 | if (object === undefined) return false; 76 | return typeof category.test === 'function' 77 | ? category.test(object) 78 | : category.match(object); 79 | }; 80 | 81 | -------------------------------------------------------------------------------- /site/src/pages/license.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. License 3 | 4 | Copyright © 2007–2013 James Coglan and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | Parts of the Software build on techniques from the following open-source 17 | projects: 18 | 19 | * "Prototype":http://prototypejs.org/, © 2005–2010 Sam Stephenson 20 | (MIT license) 21 | * Alex Arnell's "Inheritance":http://www.twologic.com/projects/inheritance/ 22 | library, © 2006 Alex Arnell (MIT license) 23 | * "Base":http://dean.edwards.name/weblog/2006/03/base/, © 2006–2010 24 | Dean Edwards (MIT license) 25 | 26 | The Software contains direct translations to JavaScript of these open-source 27 | Ruby libraries: 28 | 29 | * "Ruby standard library":http://www.ruby-lang.org/en/ modules, © 30 | Yukihiro Matsumoto and contributors (Ruby license) 31 | * "Test::Unit":http://test-unit.rubyforge.org/, © 2000–2003 32 | Nathaniel Talbott (Ruby license) 33 | * "Context":http://github.com/jm/context, © 2008 Jeremy McAnally (MIT 34 | license) 35 | * "EventMachine::Deferrable":http://rubyeventmachine.com/, © 36 | 2006–07 Francis Cianfrocca (Ruby license) 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | 46 | -------------------------------------------------------------------------------- /source/test/unit/assertion_message.js: -------------------------------------------------------------------------------- 1 | Test.Unit.extend({ 2 | AssertionMessage: new JS.Class({ 3 | extend: { 4 | Literal: new JS.Class({ 5 | initialize: function(value) { 6 | this._value = value; 7 | this.toString = this.inspect; 8 | }, 9 | 10 | inspect: function() { 11 | return this._value.toString(); 12 | } 13 | }), 14 | 15 | literal: function(value) { 16 | return new this.Literal(value); 17 | }, 18 | 19 | Template: new JS.Class({ 20 | extend: { 21 | create: function(string) { 22 | var parts = string ? string.match(/\(\?\)|(?=[^\\])\?|(?:(?!\(\?\))(?:\\\?|[^\?]))+/g) : []; 23 | return new this(parts); 24 | } 25 | }, 26 | 27 | initialize: function(parts) { 28 | this._parts = new Enumerable.Collection(parts); 29 | this.count = this._parts.findAll(function(e) { return e === '?' || e === '(?)' }).length; 30 | }, 31 | 32 | result: function(parameters) { 33 | if (parameters.length !== this.count) throw 'The number of parameters does not match the number of substitutions'; 34 | var params = JS.array(parameters); 35 | return this._parts.collect(function(e) { 36 | if (e === '(?)') return params.shift().replace(/^\[/, '(').replace(/\]$/, ')'); 37 | if (e === '?') return params.shift(); 38 | return e.replace(/\\\?/g, '?'); 39 | }).join(''); 40 | } 41 | }) 42 | }, 43 | 44 | initialize: function(head, template, parameters) { 45 | this._head = head; 46 | this._templateString = template; 47 | this._parameters = new Enumerable.Collection(parameters); 48 | }, 49 | 50 | template: function() { 51 | return this._template = this._template || this.klass.Template.create(this._templateString); 52 | }, 53 | 54 | toString: function() { 55 | var messageParts = [], head, tail; 56 | if (this._head) messageParts.push(this._head); 57 | tail = this.template().result(this._parameters.collect(function(e) { 58 | return Console.convert(e); 59 | }, this)); 60 | if (tail !== '') messageParts.push(tail); 61 | return messageParts.join('\n'); 62 | } 63 | }) 64 | }); 65 | 66 | -------------------------------------------------------------------------------- /source/proxy.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var Proxy = new JS.Module('Proxy', { 12 | extend: { 13 | Virtual: new JS.Class({ 14 | initialize: function(klass) { 15 | var bridge = function() {}, 16 | proxy = new JS.Class(), 17 | delegators = {}, 18 | method, func; 19 | 20 | bridge.prototype = klass.prototype; 21 | 22 | for (method in klass.prototype) { 23 | func = klass.prototype[method]; 24 | if (typeof func === 'function' && func !== klass) func = this.klass.forward(method); 25 | delegators[method] = func; 26 | } 27 | 28 | proxy.include({ 29 | initialize: function() { 30 | var args = arguments, 31 | subject = null; 32 | 33 | this.__getSubject__ = function() { 34 | subject = new bridge; 35 | klass.apply(subject, args); 36 | return (this.__getSubject__ = function() { return subject; })(); 37 | }; 38 | }, 39 | klass: klass, 40 | constructor: klass 41 | }, {_resolve: false}); 42 | 43 | proxy.include(new JS.Module(delegators), {_resolve: false}); 44 | proxy.include(this.klass.InstanceMethods); 45 | return proxy; 46 | }, 47 | 48 | extend: { 49 | forward: function(name) { 50 | return function() { 51 | var subject = this.__getSubject__(); 52 | return subject[name].apply(subject, arguments); 53 | }; 54 | }, 55 | 56 | InstanceMethods: new JS.Module({ 57 | extend: function(source) { 58 | this.__getSubject__().extend(source); 59 | var method, func; 60 | for (method in source) { 61 | func = source[method]; 62 | if (typeof func === 'function') func = Proxy.Virtual.forward(method); 63 | this[method] = func; 64 | } 65 | } 66 | }) 67 | } 68 | }) 69 | } 70 | }); 71 | 72 | exports.Proxy = Proxy; 73 | }); 74 | 75 | -------------------------------------------------------------------------------- /test/specs/console_spec.js: -------------------------------------------------------------------------------- 1 | JS.require('JS.Console', function(Console) { 2 | 3 | JS.ENV.ConsoleSpec = JS.Test.describe(Console, function() { with(this) { 4 | describe("convert", function() { with(this) { 5 | it("strigifies undefined", function() { with(this) { 6 | assertEqual( "undefined", Console.convert(undefined) ) 7 | }}) 8 | 9 | it("strigifies null", function() { with(this) { 10 | assertEqual( "null", Console.convert(null) ) 11 | }}) 12 | 13 | it("strigifies booleans", function() { with(this) { 14 | assertEqual( "true", Console.convert(true) ) 15 | assertEqual( "false", Console.convert(false) ) 16 | }}) 17 | 18 | it("strigifies numbers", function() { with(this) { 19 | assertEqual( "0", Console.convert(0) ) 20 | assertEqual( "5", Console.convert(5) ) 21 | }}) 22 | 23 | it("stringifies strings", function() { with(this) { 24 | assertEqual( '""', Console.convert("") ) 25 | assertEqual( '"hi"', Console.convert("hi") ) 26 | }}) 27 | 28 | it("strigifies arrays", function() { with(this) { 29 | assertEqual( "[]", Console.convert([]) ) 30 | assertEqual( "[ 1, 2, 3 ]", Console.convert([1,2,3]) ) 31 | }}) 32 | 33 | it("strigifies circular arrays", function() { with(this) { 34 | var a = [1,2] 35 | a.push(a) 36 | assertEqual( "[ 1, 2, #circular ]", Console.convert(a) ) 37 | }}) 38 | 39 | it("strigifies objects", function() { with(this) { 40 | assertEqual( "{}", Console.convert({}) ) 41 | assertEqual( "{ \"foo\": \"bar\" }", Console.convert({foo: "bar"}) ) 42 | }}) 43 | 44 | it("strigifies recursive objects", function() { with(this) { 45 | assertEqual( "{ \"foo\": { \"bar\": [ 1, 2, 3 ] } }", Console.convert({foo: {bar: [1,2,3]}}) ) 46 | }}) 47 | 48 | it("strigifies circular objects", function() { with(this) { 49 | var o = {foo: "bar"} 50 | o.bar = o 51 | assertEqual( "{ \"bar\": #circular, \"foo\": \"bar\" }", Console.convert(o) ) 52 | }}) 53 | 54 | it("strigifies DOM nodes", function() { with(this) { 55 | var node = { 56 | nodeType: 0, 57 | toString: function() { return "[object HTMLFormElement]" } 58 | } 59 | assertEqual( "[object HTMLFormElement]", Console.convert(node) ) 60 | }}) 61 | }}) 62 | }}) 63 | 64 | }) 65 | 66 | -------------------------------------------------------------------------------- /test/specs/interface_spec.js: -------------------------------------------------------------------------------- 1 | JS.ENV.InterfaceSpec = JS.Test.describe(JS.Interface, function() { with(this) { 2 | before(function() { with(this) { 3 | this.face = new JS.Interface(["foo", "bar"]) 4 | }}) 5 | 6 | describe("#test", function() { with(this) { 7 | it("returns true iff the object implements all the methods", function() { with(this) { 8 | assert( face.test({foo: function() {}, bar: function() {}}) ) 9 | }}) 10 | 11 | it("returns false iff one of the names does not map to a function", function() { with(this) { 12 | assert( !face.test({foo: function() {}, bar: true}) ) 13 | assert( !face.test({foo: true, bar: function() {}}) ) 14 | }}) 15 | 16 | it("returns false iff one of the names is missing", function() { with(this) { 17 | assert( !face.test({foo: function() {}}) ) 18 | assert( !face.test({bar: function() {}}) ) 19 | }}) 20 | }}) 21 | 22 | describe(".ensure", function() { with(this) { 23 | it("passes iff the object implements all the methods", function() { with(this) { 24 | assertNothingThrown(function() { 25 | face.test({foo: function() {}, bar: function() {}}) 26 | }) 27 | }}) 28 | 29 | it("throws an error iff one of the names does not map to a function", function() { with(this) { 30 | assertThrows(Error, function() { 31 | JS.Interface.ensure({foo: function() {}, bar: true}, face) 32 | }) 33 | assertThrows(Error, function() { 34 | JS.Interface.ensure({foo: true, bar: function() {}}, face) 35 | }) 36 | }}) 37 | 38 | it("throws an error iff one of the names is missing", function() { with(this) { 39 | assertThrows(Error, function() { 40 | JS.Interface.ensure({foo: function() {}}, face) 41 | }) 42 | assertThrows(Error, function() { 43 | JS.Interface.ensure({bar: function() {}}, face) 44 | }) 45 | }}) 46 | 47 | it("throws an error iff the object does not fully implement one of the interfaces", function() { with(this) { 48 | assertThrows(Error, function() { 49 | JS.Interface.ensure({foo: function() {}}, new JS.Interface(["foo"]), new JS.Interface(["bar"])) 50 | }) 51 | assertThrows(Error, function() { 52 | JS.Interface.ensure({bar: function() {}}, new JS.Interface(["foo"]), new JS.Interface(["bar"])) 53 | }) 54 | }}) 55 | }}) 56 | }}) 57 | 58 | -------------------------------------------------------------------------------- /site/src/pages/forwardable.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Forwardable 3 | 4 | What was it the "Gang of Four":http://en.wikipedia.org/wiki/Design_Patterns 5 | said? _Prefer delegation_. Delegation is the act of getting the receiver of a 6 | method call to respond by simply passing that call along to some other object. 7 | @JS.Forwardable@, a port of Ruby's "@Forwardable@ module":http://ruby-doc.org/core/classes/Forwardable.html, 8 | allows you to easily define instance methods that do just that. 9 | 10 | Let's say you have a class that wraps a collection of objects, which it stores 11 | as an array in one of its instance properties. You might implement some 12 | methods for manipulating the array through the class' own interface: 13 | 14 |
var RecordCollection = new JS.Class({
15 |       initialize: function() {
16 |           this.records = [];
17 |       },
18 |       push: function(record) {
19 |           return this.records.push(record);
20 |       },
21 |       shift: function() {
22 |          return this.records.shift();
23 |       }
24 |   });
25 | 26 | Instead, you can @extend@ the class with @JS.Forwardable@, then use 27 | @defineDelegators()@ to create the methods. @defineDelegators@ takes the name 28 | of the instance property to delegate calls to as the first argument, and the 29 | names of the delegated methods as the other arguments: 30 | 31 |
var RecordCollection = new JS.Class({
32 |       extend: JS.Forwardable,
33 |       initialize: function() {
34 |           this.records = [];
35 |       }
36 |   });
37 |   RecordCollection.defineDelegators('records', 'push', 'shift');
38 | 
39 |   var recs = new RecordCollection();
40 |   recs.push('The White Stripes - Icky Thump');
41 |   recs.push('Battles - Mirrored');
42 | 
43 |   recs.shift()    // -> "The White Stripes - Icky Thump"
44 | 45 | If you need to define extra class methods, you need to change the notation 46 | slightly: 47 | 48 |
var RecordCollection = new JS.Class({
49 |       extend: [JS.Forwardable, {
50 |           // class methods go here
51 |       }],
52 |       initialize: function() {
53 |           this.records = [];
54 |       }
55 |   });
56 | 57 | If you want to give the delegating method a different name, use the 58 | @defineDelegator()@ (singular) method instead: 59 | 60 |
// Add an instance method called 'add' that calls records.push
61 |   RecordCollection.defineDelegator('records', 'push', 'add');
62 | -------------------------------------------------------------------------------- /source/test/mocking/matchers.js: -------------------------------------------------------------------------------- 1 | Test.Mocking.extend({ 2 | Anything: new JS.Class({ 3 | equals: function() { return true }, 4 | toString: function() { return 'anything' } 5 | }), 6 | 7 | AnyArgs: new JS.Class({ 8 | equals: function() { return Enumerable.ALL_EQUAL }, 9 | toString: function() { return '*arguments' } 10 | }), 11 | 12 | ArrayIncluding: new JS.Class({ 13 | initialize: function(elements) { 14 | this._elements = Array.prototype.slice.call(elements); 15 | }, 16 | 17 | equals: function(array) { 18 | if (!JS.isType(array, Array)) return false; 19 | var i = this._elements.length, j; 20 | loop: while (i--) { 21 | j = array.length; 22 | while (j--) { 23 | if (Enumerable.areEqual(this._elements[i], array[j])) 24 | continue loop; 25 | } 26 | return false; 27 | } 28 | return true; 29 | }, 30 | 31 | toString: function() { 32 | var name = Console.convert(this._elements); 33 | return 'arrayIncluding(' + name + ')'; 34 | } 35 | }), 36 | 37 | ObjectIncluding: new JS.Class({ 38 | initialize: function(elements) { 39 | this._elements = elements; 40 | }, 41 | 42 | equals: function(object) { 43 | if (!JS.isType(object, Object)) return false; 44 | for (var key in this._elements) { 45 | if (!Enumerable.areEqual(this._elements[key], object[key])) 46 | return false; 47 | } 48 | return true; 49 | }, 50 | 51 | toString: function() { 52 | var name = Console.convert(this._elements); 53 | return 'objectIncluding(' + name + ')'; 54 | } 55 | }), 56 | 57 | InstanceOf: new JS.Class({ 58 | initialize: function(type) { 59 | this._type = type; 60 | }, 61 | 62 | equals: function(object) { 63 | return JS.isType(object, this._type); 64 | }, 65 | 66 | toString: function() { 67 | var name = Console.convert(this._type), 68 | an = /^[aeiou]/i.test(name) ? 'an' : 'a'; 69 | return an + '(' + name + ')'; 70 | } 71 | }), 72 | 73 | Matcher: new JS.Class({ 74 | initialize: function(type) { 75 | this._type = type; 76 | }, 77 | 78 | equals: function(object) { 79 | return JS.match(this._type, object); 80 | }, 81 | 82 | toString: function() { 83 | var name = Console.convert(this._type); 84 | return 'matching(' + name + ')'; 85 | } 86 | }) 87 | }); 88 | 89 | -------------------------------------------------------------------------------- /source/deferrable.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var Deferrable = new JS.Module('Deferrable', { 12 | extend: { 13 | Timeout: new JS.Class(Error) 14 | }, 15 | 16 | callback: function(block, context) { 17 | if (this.__deferredStatus__ === 'success') 18 | return block.apply(context, this.__deferredValue__); 19 | 20 | if (this.__deferredStatus__ === 'failure') 21 | return; 22 | 23 | this.__callbacks__ = this.__callbacks__ || []; 24 | this.__callbacks__.push([block, context]); 25 | }, 26 | 27 | errback: function(block, context) { 28 | if (this.__deferredStatus__ === 'failure') 29 | return block.apply(context, this.__deferredValue__); 30 | 31 | if (this.__deferredStatus__ === 'success') 32 | return; 33 | 34 | this.__errbacks__ = this.__errbacks__ || []; 35 | this.__errbacks__.push([block, context]); 36 | }, 37 | 38 | timeout: function(milliseconds) { 39 | this.cancelTimeout(); 40 | var self = this, error = new Deferrable.Timeout(); 41 | this.__timeout__ = JS.ENV.setTimeout(function() { self.fail(error) }, milliseconds); 42 | }, 43 | 44 | cancelTimeout: function() { 45 | if (!this.__timeout__) return; 46 | JS.ENV.clearTimeout(this.__timeout__); 47 | delete this.__timeout__; 48 | }, 49 | 50 | setDeferredStatus: function(status, args) { 51 | this.__deferredStatus__ = status; 52 | this.__deferredValue__ = args; 53 | 54 | this.cancelTimeout(); 55 | 56 | switch (status) { 57 | case 'success': 58 | if (!this.__callbacks__) return; 59 | var callback; 60 | while (callback = this.__callbacks__.pop()) 61 | callback[0].apply(callback[1], args); 62 | break; 63 | 64 | case 'failure': 65 | if (!this.__errbacks__) return; 66 | var errback; 67 | while (errback = this.__errbacks__.pop()) 68 | errback[0].apply(errback[1], args); 69 | break; 70 | } 71 | }, 72 | 73 | succeed: function() { 74 | return this.setDeferredStatus('success', arguments); 75 | }, 76 | 77 | fail: function() { 78 | return this.setDeferredStatus('failure', arguments); 79 | } 80 | }); 81 | 82 | exports.Deferrable = Deferrable; 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /source/benchmark.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS, 4 | 5 | Console = js.Console || require('./console').Console; 6 | 7 | if (E) exports.JS = exports; 8 | factory(js, Console, E ? exports : js); 9 | 10 | })(function(JS, Console, exports) { 11 | 'use strict'; 12 | 13 | var Benchmark = new JS.Module('Benchmark', { 14 | include: Console, 15 | N: 5, 16 | 17 | measure: function(name, runs, functions) { 18 | var envs = [], env, 19 | times = [], 20 | block = functions.test; 21 | 22 | var i = runs * Benchmark.N; 23 | while (i--) { 24 | env = {}; 25 | if (functions.setup) functions.setup.call(env); 26 | envs.push(env); 27 | } 28 | 29 | var n = Benchmark.N, start, end; 30 | while (n--) { 31 | i = runs; 32 | start = new JS.Date().getTime(); 33 | while (i--) block.call(envs.pop()); 34 | end = new JS.Date().getTime(); 35 | times.push(end - start); 36 | } 37 | this.printResult(name, times); 38 | }, 39 | 40 | printResult: function(name, times) { 41 | var average = this.average(times); 42 | this.reset(); 43 | this.print(' '); 44 | this.consoleFormat('bgblack', 'white'); 45 | this.print('BENCHMARK'); 46 | this.reset(); 47 | this.print(' [' + this.format(average) + ']'); 48 | this.consoleFormat('cyan'); 49 | this.puts(' ' + name); 50 | this.reset(); 51 | }, 52 | 53 | format: function(average) { 54 | var error = (average.value === 0) ? 0 : 100 * average.error / average.value; 55 | return Math.round(average.value) + 56 | 'ms \u00B1 ' + Math.round(error) + '%'; 57 | }, 58 | 59 | average: function(list) { 60 | return { value: this.mean(list), error: this.stddev(list) }; 61 | }, 62 | 63 | mean: function(list, mapper) { 64 | var values = [], 65 | mapper = mapper || function(x) { return x }, 66 | n = list.length, 67 | sum = 0; 68 | 69 | while (n--) values.push(mapper(list[n])); 70 | 71 | n = values.length; 72 | while (n--) sum += values[n]; 73 | return sum / values.length; 74 | }, 75 | 76 | stddev: function(list) { 77 | var square = function(x) { return x*x }; 78 | return Math.sqrt(this.mean(list, square) - square(this.mean(list))); 79 | } 80 | }); 81 | 82 | Benchmark.extend(Benchmark); 83 | 84 | exports.Benchmark = Benchmark; 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /site/src/pages/proxies.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Proxy 3 | 4 | In general, a proxy is an object that controls access to another object 5 | (referred to as the _real subject_). It has exactly the same interface as the 6 | real subject and does not modify the subject's behaviour. All it does is 7 | forward method calls onto the subject and it returns the results of such calls. 8 | Proxies are often used to restrict access to a subject, to provide a local 9 | interface to a remote object (such as a web service API), or to allow the 10 | instantiation of memory-intensive objects on demand. 11 | 12 | h3. Virtual proxies 13 | 14 | A virtual proxy is an object that acts as a stand-in for its real subject. The 15 | subject is not initialized until it is really needed, allowing for 'lazy 16 | instantiation' of objects that are expensive to create. 17 | 18 | JS.Class provides a module called @JS.Proxy.Virtual@. This allows you create 19 | proxies for classes without needing to write all the forwarding and 20 | instantiation methods yourself. @JS.Proxy.Virtual@ inspects the proxied class 21 | and automatically creates proxies for all its instance methods. This saves you 22 | time and reduces code duplication. 23 | 24 | Consider the following example: we have a @Dog@ class, which keeps track of 25 | how many times it has been instantiated through its class property @instances@. 26 | We then create a @DogProxy@ class from @Dog@, and instantiate it. At this 27 | point we see that @Dog.instances == 0@. Then we call @rex.bark()@, which 28 | instantiates @rex@'s subject and calls @bark()@ on it. @Dog.instances@ now 29 | equals @1@. Calling further methods on @rex@ will not create any more @Dog@ 30 | instances. 31 | 32 |
var Dog = new JS.Class({
33 |       extend: {
34 |           instances: 0
35 |       },
36 |       initialize: function(name) {
37 |           this.name = name;
38 |           this.klass.instances++;
39 |       },
40 |       bark: function() {
41 |           return this.name + ' says WOOF!';
42 |       }
43 |   });
44 | 
45 |   var DogProxy = new JS.Proxy.Virtual(Dog);
46 | 
47 |   var rex = new DogProxy('Rex');
48 |   Dog.instances   // -> 0
49 | 
50 |   rex.bark()  // -> "Rex says WOOF!"
51 |   Dog.instances   // -> 1
52 | 53 | This pattern is particularly suited to creating parts of a UI that are 54 | initially hidden - you can create proxies for them on page load, but they 55 | won't add any of their HTML to the page until you choose to show them. 56 | -------------------------------------------------------------------------------- /site/src/pages/packages/customloaders.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Custom loader functions 3 | 4 | Some libraries, such as the "Google Ajax 5 | APIs":http://code.google.com/apis/ajax/, have their own systems for loading 6 | code on demand that involve more than simply knowing the path to a script 7 | file. Our package system allows you to specify packages that use a loader 8 | function rather than a path to load themselves; the function should take a 9 | callback and call it when the library in question is done loading. For 10 | example, here's how you'd incorporate Google Maps into your library: 11 | 12 |
JS.packages(function() { with(this) {
13 |       file('http://www.google.com/jsapi?key=MY_GOOGLE_KEY')
14 |           .provides('google.load');
15 | 
16 |       loader(function(cb) { google.load('maps', '2.x', {callback: cb}) })
17 |           .provides('GMap2', 'GClientGeocoder')
18 |           .requires('google.load');
19 |   }});
20 | 21 | The callback (@cb@) is a function generated by the package system that 22 | continues to load and run dependent code once the custom loader has finished 23 | its work. If you don't call @cb@ (or pass it to a function that will call it 24 | for you as above), code that depends on this library will not run. 25 | 26 | @JS.packages@ also provides post-load setup hooks that let you run some code 27 | after a file loads. For example, a strategy for loading 28 | "YUI3":http://developer.yahoo.com/yui/3/ might involve loading the seed file, 29 | creating a new global instance of the library, then using YUI's own loader 30 | functions to load further modules. Some sample code: 31 | 32 |
JS.packages(function() { with(this) {
33 |       file('http://yui.yahooapis.com/3.0.0pr2/build/yui/yui-min.js')
34 |           .setup(function() { window.yui3 = YUI() })
35 |           .provides('YUI', 'yui3');
36 | 
37 |       loader(function(cb) { yui3.use('node', cb) })
38 |           .provides('yui3.Node')
39 |           .requires('yui3');
40 |   }});
41 | 42 | Loader functions can also be used to generate library objects that are 43 | expensive to create without necessarily loading code from external files. Just 44 | remember to call @cb@ yourself when the generated object is ready: 45 | 46 |
JS.packages(function() { with(this) {
47 |       loader(function(cb) {
48 |           window.ChocolateFactory = new WonkaVenture();
49 |           // Perform other expensive setup operations
50 |           cb();
51 |       })
52 |       .provides('ChocolateFactory');
53 |   }});
54 | -------------------------------------------------------------------------------- /site/src/pages/singletonmethods.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Singleton methods 3 | 4 | In Ruby, singleton methods are methods attached to a single object rather than 5 | defined in a class. If you add a singleton method to a Ruby object, @super@ 6 | refers to the method from the object's class definition. @jsclass@ allows the 7 | same behaviour. Recall our @Animal@ class: 8 | 9 |
var Animal = new Class({
10 |       initialize: function(name) {
11 |           this.name = name;
12 |       },
13 |       speak: function(things) {
14 |           return 'My name is ' + this.name + ' and I like ' + things;
15 |       }
16 |   });
17 | 18 | We can create a new animal and @extend@ it with singleton methods. All 19 | @Class@-derived objects have an @extend()@ method for doing this: 20 | 21 |
var cow = new Animal('Daisy');
22 | 
23 |   cow.extend({
24 |       speak: function(stuff) {
25 |           return 'Mooo! ' + this.callSuper();
26 |       },
27 |       getName: function() {
28 |           return this.name;
29 |       }
30 |   });
31 | 
32 |   cow.getName()   // -> "Daisy"
33 | 
34 |   cow.speak('grass')
35 |       // -> "Mooo! My name is Daisy and I like grass"
36 | 37 | h3. Modules as object extensions 38 | 39 | As well as passing simple objects into the @extend()@ method, you can use 40 | modules. The receiving object will then gain all the methods of the module. If 41 | we extend using "@Observable@":/observable.html, for example: 42 | 43 |
cow.extend(Observable);
44 | 
45 |   cow.addObserver(function() {
46 |       alert('This cow is observable!');
47 |   });
48 | 
49 |   cow.notifyObservers();
50 | 51 | This alerts @"This cow is observable!"@, as expected. Using modules to extend 52 | objects has some interesting inheritance consequences, which are more 53 | thoroughly exposed in "the inheritance article":/inheritance.html. In short, 54 | all singleton methods are stored in a module attached to the object - this is 55 | known as an eigenclass or metaclass in Ruby circles. By using a module to 56 | extend an object, the module is mixed into the eigenclass, making it part of 57 | the inheritance tree. So we can override @notifyObservers()@, for example, to 58 | duplicate every observer added to an object. @callSuper()@ calls out to the 59 | module we used to extend the object. 60 | 61 |
cow.extend({
62 |       notifyObservers: function() {
63 |           this.callSuper();
64 |           this.callSuper();
65 |       }
66 |   });
67 | 
68 |   // alerts "This cow is observable!" twice
69 |   cow.notifyObservers();
70 | 71 | -------------------------------------------------------------------------------- /source/dom/builder.js: -------------------------------------------------------------------------------- 1 | DOM.Builder = new JS.Class('DOM.Builder', { 2 | extend: { 3 | addElement: function(name) { 4 | this.define(name, function() { 5 | return this.makeElement(name, arguments); 6 | }); 7 | DOM[name] = function() { 8 | return new DOM.Builder().makeElement(name, arguments); 9 | }; 10 | }, 11 | 12 | addElements: function(list) { 13 | var i = list.length; 14 | while (i--) this.addElement(list[i]); 15 | } 16 | }, 17 | 18 | initialize: function(parent) { 19 | this._parentNode = parent; 20 | }, 21 | 22 | makeElement: function(name, children) { 23 | var element, child, attribute; 24 | if ( document.createElementNS ) { 25 | // That makes possible to mix HTML within SVG or XUL. 26 | element = document.createElementNS('http://www.w3.org/1999/xhtml', name); 27 | } else { 28 | element = document.createElement(name); 29 | } 30 | for (var i = 0, n = children.length; i < n; i++) { 31 | child = children[i]; 32 | if (typeof child === 'function') { 33 | child(new this.klass(element)); 34 | } else if (JS.isType(child, 'string')) { 35 | element.appendChild(document.createTextNode(child)); 36 | } else { 37 | for (attribute in child) 38 | element[attribute] = child[attribute]; 39 | } 40 | } 41 | if (this._parentNode) this._parentNode.appendChild(element); 42 | return element; 43 | }, 44 | 45 | concat: function(text) { 46 | if (!this._parentNode) return; 47 | this._parentNode.appendChild(document.createTextNode(text)); 48 | } 49 | }); 50 | 51 | DOM.Builder.addElements([ 52 | 'a', 'abbr', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 53 | 'base', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 54 | 'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 55 | 'details', 'device', 'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 56 | 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 57 | 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 58 | 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'map', 'mark', 59 | 'marquee', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 60 | 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 61 | 'rt', 'ruby', 'samp', 'script', 'section', 'select', 'small', 'source', 62 | 'span', 'strong', 'style', 'sub', 'sup', 'summary', 'table', 'tbody', 'td', 63 | 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'ul', 64 | 'var', 'video', 'wbr' 65 | ]); 66 | 67 | -------------------------------------------------------------------------------- /site/src/pages/classmethods.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Class methods 3 | 4 | In Ruby, modules and classes are just another type of object; they are objects 5 | that are responsible for storing methods and making new objects. Class methods 6 | are methods attached to (and called on) an individual class object, rather 7 | than its instances. They are really just a special case of "singleton methods":/singletonmethods.html. 8 | To add class methods when defining a class, wrap them up in an @extend@ block: 9 | 10 |
var User = new JS.Class({
11 |       extend: {
12 |           find: function(id) {
13 |               // Return a User with id
14 |           },
15 |           create: function(name) {
16 |               return new this(name);
17 |           }
18 |       },
19 |       initialize: function(name) {
20 |           this.username = name;
21 |       }
22 |   });
23 | 24 | We could equally add the methods after the class was created: 25 | 26 |
var User = new JS.Class({
27 |       initialize: function(name) {
28 |           this.username = name;
29 |       }
30 |   });
31 | 
32 |   User.extend({
33 |       find: function(id) {
34 |           // Return a User with id
35 |       },
36 |       create: function(name) {
37 |           return new this(name);
38 |       }
39 |   });
40 | 41 | These two syntaxes apply equally to creating and extending "@Modules@":/modules.html. 42 | Within a class method, the keyword @this@ refers to the class itself - see the 43 | @User.create()@ method: 44 | 45 |
var james = User.create('James');
46 |   james.username    // -> 'James'
47 |   james.klass       // -> User
48 | 49 | When you create a subclass, it will inherit any class methods of its parent, 50 | and you can use @callSuper()@ too: 51 | 52 |
var LoudUser = new JS.Class(User, {
53 |       extend: {
54 |           create: function(name) {
55 |               return this.callSuper(name.toUpperCase());
56 |           }
57 |       }
58 |   });
59 | 
60 |   var me = LoudUser.create('James');
61 |   me.username   // -> 'JAMES'
62 |   me.klass      // -> LoudUser
63 | 
64 |   var you = LoudUser.find(24)   // inherited from User
65 | 66 | Note how @this@, even in @callSuper@ methods, always refers to the same thing 67 | as in the original method call. We get back a @LoudUser@, not a @User@. 68 | 69 | Note that class methods are not the same as Java's static methods. If you want 70 | to call a class method from an instance of the class, you need to get a 71 | reference to the class through the object's @klass@ property. 72 | 73 |
User.define('copy', function() {
74 |       return this.klass.create(this.username);
75 |   });
76 | 77 | -------------------------------------------------------------------------------- /test/specs/forwardable_spec.js: -------------------------------------------------------------------------------- 1 | JS.require('JS.Forwardable', function(Forwardable) { 2 | 3 | JS.ENV.ForwardableSpec = JS.Test.describe(Forwardable, function() { with(this) { 4 | define("Subject", new JS.Class({ 5 | initialize: function() { 6 | this.name = "something"; 7 | }, 8 | setName: function(name) { 9 | this.name = name; 10 | }, 11 | getName: function() { 12 | return this.name; 13 | }, 14 | multiply: function(a,b) { 15 | return a*b; 16 | } 17 | })) 18 | 19 | define("forwardableClass", function() { with(this) { 20 | return new JS.Class({extend: Forwardable, 21 | initialize: function() { 22 | this.subject = new Subject() 23 | } 24 | }) 25 | }}) 26 | 27 | before(function() { with(this) { 28 | this.forwarderClass = forwardableClass() 29 | this.forwarder = new forwarderClass() 30 | }}) 31 | 32 | describe("#defineDelegator", function() { with(this) { 33 | it("defines a forwarding method", function() { with(this) { 34 | forwarderClass.defineDelegator("subject", "getName") 35 | assertRespondTo( forwarder, "getName" ) 36 | assertEqual( "something", forwarder.getName() ) 37 | }}) 38 | 39 | it("passes arguments through when forwarding calls", function() { with(this) { 40 | forwarderClass.defineDelegator("subject", "multiply") 41 | assertEqual( 20, forwarder.multiply(4,5) ) 42 | }}) 43 | 44 | it("uses the named property as the forwarding target", function() { with(this) { 45 | forwarder.target = {getName: function() { return "target name" }} 46 | forwarderClass.defineDelegator("target", "getName") 47 | assertEqual( "target name", forwarder.getName() ) 48 | }}) 49 | 50 | it("defines a forwarding method under a different name", function() { with(this) { 51 | forwarderClass.defineDelegator("subject", "getName", "subjectName") 52 | assertRespondTo( forwarder, "subjectName" ) 53 | assertEqual( "something", forwarder.subjectName() ) 54 | }}) 55 | 56 | it("defines forwarding methods for mutators", function() { with(this) { 57 | forwarderClass.defineDelegator("subject", "getName") 58 | forwarderClass.defineDelegator("subject", "setName") 59 | forwarder.setName("nothing") 60 | assertEqual( "nothing", forwarder.getName() ) 61 | }}) 62 | }}) 63 | 64 | describe("#defineDelegators", function() { with(this) { 65 | it("defines multiple forwarding methods", function() { with(this) { 66 | forwarderClass.defineDelegators("subject", "getName", "setName") 67 | forwarder.setName("nothing") 68 | assertEqual( "nothing", forwarder.getName() ) 69 | }}) 70 | }}) 71 | }}) 72 | 73 | }) 74 | 75 | -------------------------------------------------------------------------------- /site/src/pages/comparable.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Comparable 3 | 4 | @JS.Comparable@ is a module that helps you manipulate objects that can be 5 | ordered relative to each other. It is designed to work exactly like 6 | "@Comparable@":http://ruby-doc.org/core/classes/Comparable.html 7 | in Ruby, only without the nice method names like @"<="@ that Ruby allows. To 8 | use it, your class must define an instance method called @compareTo@, that 9 | tells it how to compare itself to other objects. As an example, let's create a 10 | class to represent to-do list items. These can be ordered according to their 11 | priority. 12 | 13 |
var TodoItem = new JS.Class({
14 |       include: JS.Comparable,
15 | 
16 |       initialize: function(task, priority) {
17 |           this.task = task;
18 |           this.priority = priority;
19 |       },
20 | 
21 |       // Must return -1 if this object is 'less than' other,
22 |       // +1 if it is 'greater than' other, or 0 if they are equal
23 |       compareTo: function(other) {
24 |           if (this.priority < other.priority) return -1;
25 |           if (this.priority > other.priority) return 1;
26 |           return 0;
27 |       }
28 |   });
29 | 30 | @TodoItem@ now has the following instance methods: 31 | 32 | * @lt(other)@ - returns @true@ iff the receiver is less than @other@ 33 | * @lte(other)@ - returns @true@ iff the receiver is less than or equal to@other@ 34 | * @gt(other)@ - returns @true@ iff the receiver is greater than @other@ 35 | * @gte(other)@ - returns @true@ iff the receiver is greater than or equal to @other@ 36 | * @eq(other)@ - returns @true@ iff the receiver is equal to @other@ 37 | * @between(a,b)@ - returns @true@ iff the receiver is between @a@ and @b@ inclusive 38 | * @compareTo(other)@ - returns @-1@/@0@/@1@ to indicate result of comparison 39 | 40 | @TodoItem@ also has a class method called @compare@ that you should use for 41 | sorting. 42 | 43 | Let's create some items and see how they behave: 44 | 45 |
var items = [
46 |       new TodoItem('Go to work', 5),
47 |       new TodoItem('Buy milk', 3),
48 |       new TodoItem('Pay the rent', 2),
49 |       new TodoItem('Write code', 1),
50 |       new TodoItem('Head down the pub', 4)
51 |   ];
52 | 
53 |   items[2].lt(items[1])   // -> true
54 |   items[0].gte(items[3])  // -> true
55 |   items[4].eq(items[4])   // -> true
56 |   items[1].between(items[3], items[4])  // -> true
57 | 
58 |   items.sort(TodoItem.compare)
59 |       // -> [
60 |       //        {task: 'Write code', ...},
61 |       //        {task: 'Pay the rent', ...},
62 |       //        {task: 'Buy milk', ...},
63 |       //        {task: 'Head down the pub', ...},
64 |       //        {task: 'Go to work', ...}
65 |       //    ]
66 | 67 | -------------------------------------------------------------------------------- /site/src/pages/tsort.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. TSort 3 | 4 | @JS.TSort@ is a JavaScript version of Ruby's @TSort@ module, which provides 5 | "topological sort":http://en.wikipedia.org/wiki/Topological_sorting capability 6 | to data structures. The canonical example of this is determining how a 7 | set of dependent tasks should be sorted such that each task comes after those 8 | it depends on in the list. One way to represent this information may be as 9 | a task table: 10 | 11 |
var tasks = new Tasks({
12 |       'eat breakfast': ['serve'],
13 |       'serve':         ['cook'],
14 |       'cook':          ['buy bacon', 'buy eggs'],
15 |       'buy bacon':     [],
16 |       'buy eggs':      []
17 |   });
18 | 19 | (This example is borrowed from the excellent "Getting to know the Ruby 20 | standard library":http://endofline.wordpress.com/2010/12/22/ruby-standard-library-tsort/ 21 | blog.) 22 | 23 | This table represents each task involved in serving breakfast. The tasks are 24 | the keys in the table, and each task maps to the list of tasks which must be 25 | done immediately before it. We want to to sort all our tasks into a linear 26 | list so we can process them in the correct order, and @TSort@ lets us do that. 27 | 28 | To use @TSort@, our @Tasks@ class must implement two methods. @tsortEachNode@ 29 | must take a callback function and context and yield each task in the set to 30 | the callback. @tsortEachChild@ must accept an individual task and yield each 31 | of its direct children. We can implement this simply: 32 | 33 |
var Tasks = new JS.Class({
34 |       include: JS.TSort,
35 | 
36 |       initialize: function(table) {
37 |           this.table = table;
38 |       },
39 | 
40 |       tsortEachNode: function(block, context) {
41 |           for (var task in this.table) {
42 |               if (this.table.hasOwnProperty(task))
43 |                   block.call(context, task);
44 |           }
45 |       },
46 | 
47 |       tsortEachChild: function(task, block, context) {
48 |           var tasks = this.table[task];
49 |           for (var i = 0, n = tasks.length; i < n; i++)
50 |               block.call(context, tasks[i]);
51 |       }
52 |   });
53 | 54 | Once we've told @TSort@ how to traverse our list of tasks, it can sort the 55 | dependent tasks out for us: 56 | 57 |
tasks.tsort()
58 |   // -> ['buy bacon', 'buy eggs', 'cook', 'serve', 'eat breakfast']
59 | 60 | We now have a flat list of the tasks and can iterate over them in order, 61 | knowing that each task will have all its dependencies run before it in the 62 | list. 63 | 64 | Note that @TSort@ sorts based on the how objects are related to each other 65 | in a graph, not by how they compare using "comparison":/comparable.html 66 | methods. 67 | -------------------------------------------------------------------------------- /source/tsort.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS, 4 | 5 | Hash = js.Hash || require('./hash').Hash; 6 | 7 | if (E) exports.JS = exports; 8 | factory(js, Hash, E ? exports : js); 9 | 10 | })(function(JS, Hash, exports) { 11 | 'use strict'; 12 | 13 | var TSort = new JS.Module('TSort', { 14 | extend: { 15 | Cyclic: new JS.Class(Error) 16 | }, 17 | 18 | tsort: function() { 19 | var result = []; 20 | this.tsortEach(result.push, result); 21 | return result; 22 | }, 23 | 24 | tsortEach: function(block, context) { 25 | this.eachStronglyConnectedComponent(function(component) { 26 | if (component.length === 1) 27 | block.call(context, component[0]); 28 | else 29 | throw new TSort.Cyclic('topological sort failed: ' + component.toString()); 30 | }); 31 | }, 32 | 33 | stronglyConnectedComponents: function() { 34 | var result = []; 35 | this.eachStronglyConnectedComponent(result.push, result); 36 | return result; 37 | }, 38 | 39 | eachStronglyConnectedComponent: function(block, context) { 40 | var idMap = new Hash(), 41 | stack = []; 42 | 43 | this.tsortEachNode(function(node) { 44 | if (idMap.hasKey(node)) return; 45 | this.eachStronglyConnectedComponentFrom(node, idMap, stack, function(child) { 46 | block.call(context, child); 47 | }); 48 | }, this); 49 | }, 50 | 51 | eachStronglyConnectedComponentFrom: function(node, idMap, stack, block, context) { 52 | var nodeId = idMap.size, 53 | stackLength = stack.length, 54 | minimumId = nodeId, 55 | component, i; 56 | 57 | idMap.store(node, nodeId); 58 | stack.push(node); 59 | 60 | this.tsortEachChild(node, function(child) { 61 | if (idMap.hasKey(child)) { 62 | var childId = idMap.get(child); 63 | if (child !== undefined && childId < minimumId) minimumId = childId; 64 | } else { 65 | var subMinimumId = this.eachStronglyConnectedComponentFrom(child, idMap, stack, block, context); 66 | if (subMinimumId < minimumId) minimumId = subMinimumId; 67 | } 68 | }, this); 69 | 70 | if (nodeId === minimumId) { 71 | component = stack.splice(stackLength, stack.length - stackLength); 72 | i = component.length; 73 | while (i--) idMap.store(component[i], undefined); 74 | block.call(context, component); 75 | } 76 | 77 | return minimumId; 78 | }, 79 | 80 | tsortEachNode: function() { 81 | throw new JS.NotImplementedError('tsortEachNode'); 82 | }, 83 | 84 | tsortEachChild: function() { 85 | throw new JS.NotImplementedError('tsortEachChild'); 86 | } 87 | }); 88 | 89 | exports.TSort = TSort; 90 | }); 91 | 92 | -------------------------------------------------------------------------------- /site/src/pages/decorator.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Decorator 3 | 4 | The @JS.Decorator@ module gives you a means of implementing "the decorator pattern":http://en.wikipedia.org/wiki/Decorator_pattern 5 | with minimal boilerplate and code duplication. When creating a decorator class, 6 | you only need to define methods that differ in some way from the methods in 7 | the decorated object (the _component_). This means you don't have to write 8 | lots of forwarding methods by hand, which saves you time, filesize, and 9 | reduces code duplication. 10 | 11 | Let's take a quick example: 12 | 13 |
// Basic Bike class. Bikes cost $10 per gear.
14 | 
15 |   var Bike = new JS.Class({
16 |       initialize: function(model, gears) {
17 |           this.model = model;
18 |           this.gears = gears;
19 |       },
20 |       getModel: function() {
21 |           return this.model;
22 |       },
23 |       getPrice: function() {
24 |           return 10 * this.gears;
25 |       },
26 |       applyBrakes: function(force) {
27 |           // slow the bike down...
28 |       }
29 |   });
30 | 
31 |   // Disk brake decorator. Disk brakes add to the price,
32 |   // and make the bike's brakes more powerful.
33 | 
34 |   var DiskBrakeDecorator = new JS.Decorator(Bike, {
35 |       getPrice: function() {
36 |           return this.component.getPrice() + 50;
37 |       },
38 |       applyBrakes: function(force) {
39 |           this.component.applyBrakes(8 * force);
40 |       }
41 |   });
42 | 43 | @DiskBrakeDecorator@ gets versions of all @Bike@'s instance methods that 44 | forward the method call onto the component and return the result. e.g., 45 | @DiskBrakeDecorator@'s @getModel()@ method looks like: 46 | 47 |
getModel: function() {
48 |       return this.component.getModel();
49 |   };
50 | 51 | Any methods you don't redefine in the decorator class will look similar to 52 | this. Let's try our new classes out: 53 | 54 |
var bike = new Bike('Specialized Rock Hopper', 21);
55 |   bike.getPrice()   // -> 210
56 | 
57 |   bike = new DiskBrakeDecorator(bike);
58 |   bike.getPrice()   // -> 260
59 |   bike.getModel()   // -> "Specialized Rock Hopper"
60 | 61 | Within your decorator methods, use @this.component@ to refer to the decorated 62 | object. If a decorator defines new methods, they will be passed through by any 63 | other decorators you wrap an object with. 64 | 65 |
var HornDecorator = new JS.Decorator(Bike, {
66 |       beepHorn: function(noise) {
67 |           return noise.toUpperCase();
68 |       }
69 |   });
70 | 
71 |   var bike = new Bike('Specialized Rock Hopper', 21);
72 | 
73 |   // Let's wrap a HornDecorator with a DiskBrakeDecorator
74 |   bike = new HornDecorator(bike);
75 |   bike = new DiskBrakeDecorator(bike);
76 | 
77 |   bike.beepHorn('beep!')    // -> "BEEP!"
78 | -------------------------------------------------------------------------------- /source/console/base.js: -------------------------------------------------------------------------------- 1 | Console.extend({ 2 | Base: new JS.Class({ 3 | __buffer__: '', 4 | __format__: '', 5 | 6 | backtraceFilter: function() { 7 | if (typeof version === 'function' && version() > 100) { 8 | return /.*/; 9 | } else { 10 | return null; 11 | } 12 | }, 13 | 14 | coloring: function() { 15 | return !this.envvar(Console.NO_COLOR); 16 | }, 17 | 18 | echo: function(string) { 19 | if (typeof console !== 'undefined') return console.log(string); 20 | if (typeof print === 'function') return print(string); 21 | }, 22 | 23 | envvar: function(name) { 24 | return null; 25 | }, 26 | 27 | exit: function(status) { 28 | if (typeof system === 'object' && system.exit) system.exit(status); 29 | if (typeof quit === 'function') quit(status); 30 | }, 31 | 32 | format: function(type, name, args) { 33 | if (!this.coloring()) return; 34 | var escape = Console.ESCAPE_CODES[type][name]; 35 | 36 | for (var i = 0, n = args.length; i < n; i++) 37 | escape = escape.replace('%' + (i+1), args[i]); 38 | 39 | this.__format__ += Console.escape(escape); 40 | }, 41 | 42 | flushFormat: function() { 43 | var format = this.__format__; 44 | this.__format__ = ''; 45 | return format; 46 | }, 47 | 48 | getDimensions: function() { 49 | var width = this.envvar('COLUMNS') || Console.DEFAULT_WIDTH, 50 | height = this.envvar('ROWS') || Console.DEFAULT_HEIGHT; 51 | 52 | return [parseInt(width, 10), parseInt(height, 10)]; 53 | }, 54 | 55 | print: function(string) { 56 | var coloring = this.coloring(), 57 | width = this.getDimensions()[0], 58 | esc = Console.escape, 59 | length, prefix, line; 60 | 61 | while (string.length > 0) { 62 | length = this.__buffer__.length; 63 | prefix = (length > 0 && coloring) ? esc('1F') + esc((length + 1) + 'G') : ''; 64 | line = string.substr(0, width - length); 65 | 66 | this.__buffer__ += line; 67 | 68 | if (coloring) this.echo(prefix + this.flushFormat() + line); 69 | 70 | if (this.__buffer__.length === width) { 71 | if (!coloring) this.echo(this.__buffer__); 72 | this.__buffer__ = ''; 73 | } 74 | string = string.substr(width - length); 75 | } 76 | }, 77 | 78 | puts: function(string) { 79 | var coloring = this.coloring(), 80 | esc = Console.escape, 81 | length = this.__buffer__.length, 82 | prefix = (length > 0 && coloring) ? esc('1F') + esc((length + 1) + 'G') : this.__buffer__; 83 | 84 | this.echo(prefix + this.flushFormat() + string); 85 | this.__buffer__ = ''; 86 | } 87 | }) 88 | }); 89 | 90 | -------------------------------------------------------------------------------- /source/test/unit/test_suite.js: -------------------------------------------------------------------------------- 1 | Test.Unit.extend({ 2 | TestSuite: new JS.Class({ 3 | include: Enumerable, 4 | 5 | extend: { 6 | STARTED: 'Test.Unit.TestSuite.STARTED', 7 | FINISHED: 'Test.Unit.TestSuite.FINISHED', 8 | 9 | forEach: function(tests, block, continuation, context) { 10 | var looping = false, 11 | pinged = false, 12 | n = tests.length, 13 | i = -1, 14 | breakTime = new JS.Date().getTime(), 15 | setTimeout = Test.FakeClock.REAL.setTimeout; 16 | 17 | var ping = function() { 18 | pinged = true; 19 | var time = new JS.Date().getTime(); 20 | 21 | if (Console.BROWSER && (time - breakTime) > 1000) { 22 | breakTime = time; 23 | looping = false; 24 | setTimeout(iterate, 0); 25 | } 26 | else if (!looping) { 27 | looping = true; 28 | while (looping) iterate(); 29 | } 30 | }; 31 | 32 | var iterate = function() { 33 | i += 1; 34 | if (i === n) { 35 | looping = false; 36 | return continuation && continuation.call(context); 37 | } 38 | pinged = false; 39 | block.call(context, tests[i], ping); 40 | if (!pinged) looping = false; 41 | }; 42 | 43 | ping(); 44 | } 45 | }, 46 | 47 | initialize: function(metadata, tests) { 48 | this._metadata = metadata; 49 | this._tests = tests; 50 | }, 51 | 52 | forEach: function(block, continuation, context) { 53 | this.klass.forEach(this._tests, block, continuation, context); 54 | }, 55 | 56 | run: function(result, continuation, callback, context) { 57 | if (this._metadata.fullName) 58 | callback.call(context, this.klass.STARTED, this); 59 | 60 | this.forEach(function(test, resume) { 61 | test.run(result, resume, callback, context) 62 | 63 | }, function() { 64 | if (this._metadata.fullName) 65 | callback.call(context, this.klass.FINISHED, this); 66 | 67 | continuation.call(context); 68 | 69 | }, this); 70 | }, 71 | 72 | size: function() { 73 | if (this._size !== undefined) return this._size; 74 | var totalSize = 0, i = this._tests.length; 75 | while (i--) totalSize += this._tests[i].size(); 76 | return this._size = totalSize; 77 | }, 78 | 79 | empty: function() { 80 | return this._tests.length === 0; 81 | }, 82 | 83 | metadata: function(root) { 84 | var data = JS.extend({size: this.size()}, this._metadata); 85 | if (root) { 86 | delete data.fullName; 87 | delete data.shortName; 88 | delete data.context; 89 | } 90 | return data; 91 | } 92 | }) 93 | }); 94 | 95 | -------------------------------------------------------------------------------- /test/specs/proxy_spec.js: -------------------------------------------------------------------------------- 1 | JS.require('JS.Proxy', function(Proxy) { 2 | 3 | JS.ENV.ProxySpec = JS.Test.describe(Proxy, function() { with(this) { 4 | describe(Proxy.Virtual, function() { with(this) { 5 | before(function() { with(this) { 6 | this.instances = 0 7 | 8 | this.Subject = new JS.Class({ 9 | initialize: function(name, age) { 10 | instances += 1 11 | this.name = name 12 | this.age = age 13 | }, 14 | 15 | setName: function(a, b) { 16 | this.name = a + " " + b 17 | }, 18 | 19 | getName: function() { return this.name }, 20 | 21 | getAge: function() { return this.age } 22 | }) 23 | 24 | this.Proxy = new Proxy.Virtual(Subject) 25 | this.proxyInstance = new Proxy("jcoglan", 26) 26 | }}) 27 | 28 | it("creates classes", function() { with(this) { 29 | assertKindOf( JS.Class, Proxy ) 30 | }}) 31 | 32 | it("does not instantiate the wrapped class immediately", function() { with(this) { 33 | assertEqual( 0, instances ) 34 | }}) 35 | 36 | it("instantiates the wrapped class when a method is called", function() { with(this) { 37 | proxyInstance.getName() 38 | assertEqual( 1, instances ) 39 | }}) 40 | 41 | it("instantiates the wrapped class once per proxy instance", function() { with(this) { 42 | proxyInstance.getName() 43 | proxyInstance.getName() 44 | assertEqual( 1, instances ) 45 | new Proxy("bart", 10).getName() 46 | assertEqual( 2, instances ) 47 | }}) 48 | 49 | it("passes constructor arguments down to the subject", function() { with(this) { 50 | assertEqual( "jcoglan", proxyInstance.getName() ) 51 | assertEqual( 26, proxyInstance.getAge() ) 52 | }}) 53 | 54 | it("passes method arguments down to the subject", function() { with(this) { 55 | proxyInstance.setName("some", "words") 56 | assertEqual( "some words", proxyInstance.getName() ) 57 | }}) 58 | 59 | describe("a singleton method", function() { with(this) { 60 | before(function() { with(this) { 61 | proxyInstance.extend({ 62 | newMethod: function() { return this.name.toUpperCase() } 63 | }) 64 | }}) 65 | 66 | it("can access the subject's instance variables", function() { with(this) { 67 | assertEqual( "JCOGLAN", proxyInstance.newMethod() ) 68 | }}) 69 | }}) 70 | 71 | describe("a singleton method that calls super", function() { with(this) { 72 | before(function() { with(this) { 73 | proxyInstance.extend({ 74 | getAge: function() { return this.callSuper() * 2 } 75 | }) 76 | }}) 77 | 78 | it("calls the subject's implementation of the method", function() { with(this) { 79 | assertEqual( 52, proxyInstance.getAge() ) 80 | }}) 81 | }}) 82 | }}) 83 | }}) 84 | 85 | }) 86 | 87 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # JS.Class - Ruby-style JavaScript 2 | 3 | [http://jsclass.jcoglan.com](http://jsclass.jcoglan.com) 4 | 5 | JS.Class is a JavaScript library for building object-oriented programs using 6 | Ruby idioms. It implements Ruby's core object/module/class system in JavaScript, 7 | as well as several standard Ruby libraries and various other extensions. 8 | 9 | 10 | ## Development 11 | 12 | To hack on JS.Class you'll need to be able to build it and run the tests. You 13 | need Node.js and Ruby installed, then run: 14 | 15 | cd path/to/js.class 16 | npm install 17 | bundle install 18 | bundle exec jake 19 | 20 | This will build the project and create files in the `build` directory. 21 | 22 | 23 | ### Running the tests 24 | 25 | To run the tests, you need to run several tasks. Make sure all the target server 26 | platforms work: 27 | 28 | $ JS=(v8 node phantomjs spidermonkey rhino narwhal ringo mongo) 29 | $ for js in "${JS[@]}"; do echo "$js" ; $js test/console.js ; echo $? ; done 30 | 31 | Some interpreters will skip the tests that use asynchronous APIs. 32 | 33 | Using Node.js, check that all the output formats work: 34 | 35 | $ FMT=(dot error json progress spec tap xml) 36 | $ for fmt in "${FMT[@]}" ; do node test/console.js -f $fmt ; done 37 | 38 | Check the tests work in the PhantomJS browser. All the output formats should 39 | work here too -- change the format by editing `test/phantom.js`. 40 | 41 | $ phantomjs test/phantom.js 42 | 43 | Run the test suite in as many web browsers as you can: 44 | 45 | $ open test/browser.html 46 | 47 | For desktop application platforms, run it in XULRunner and AIR: 48 | 49 | $ xulrunner -app test/xulenv/application.ini 50 | $ adl test/airenv/app.xml 51 | 52 | Finally, JS.Class supports several distributed test environments, including 53 | Buster.JS, Karma, Testem, Testling CI and TestSwarm. Here's how to test on 54 | each platform; some of these require running commands in multiple terminals and 55 | I've numbered the prompts to indicate this. 56 | 57 | #### Buster.JS 58 | 59 | 1 $ python -m SimpleHTTPServer 8000 60 | 2 $ ./node_modules/.bin/buster-server 61 | 3 $ open http://localhost:1111 62 | 4 $ ./node_modules/.bin/buster-test 63 | 64 | #### Karma 65 | 66 | 1 $ python -m SimpleHTTPServer 8000 67 | 2 $ ./node_modules/.bin/testacular start test/testacular.js 68 | 3 $ open http://localhost:8080 69 | 4 $ ./node_modules/.bin/testacular run 70 | 71 | #### Testem 72 | 73 | 1 $ ./node_modules/.bin/testem -f test/testem.json 74 | 2 $ open http://localhost:7357 75 | 76 | Tests should run automatically in all connected browsers when you edit files. 77 | 78 | #### TestSwarm 79 | 80 | TestSwarm integration requires a hosted server and is tested during the release 81 | process. 82 | 83 | 84 | ## License 85 | 86 | Distributed under the MIT license. 87 | Copyright (c) 2007-2013 James Coglan 88 | 89 | -------------------------------------------------------------------------------- /site/src/pages/modifyingmodules.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Modifying existing classes and modules 3 | 4 | Just like in Ruby, classes and modules are open for modification at any time, 5 | so you can add and change methods in existing classes. All classes and modules 6 | in @jsclass@ have some special methods for modifying the methods they contain. 7 | The following is written to apply to modifying classes, but it applies equally 8 | to modifying modules -- classes are just modules that can be instantiated. 9 | 10 | The first of these methods is called @define()@. This adds a single named 11 | method to the class/module. If you're modifying a class, the method instantly 12 | becomes available in instances of the class, and in its subclasses. 13 | 14 |
Animal.define('sleep', function() {
15 |       return this.name + ' is sleeping';
16 |   });
17 | 
18 |   rex.sleep()
19 |   // -> "Rex is sleeping"
20 | 21 | Note that if the class already has a method with the same name as the one 22 | you're defining, the old method will be overwritten. Methods in parent classes 23 | and in mixins can be accessed using @callSuper()@. 24 | 25 | If you want to create aliases for methods that already exist, use the @alias()@ 26 | method. The new alias goes on the left-hand-side, the existing method name on 27 | the right. 28 | 29 |
Animal.alias({
30 |       talk:   'speak',
31 |       rest:   'sleep'
32 |   });
33 | 34 | The other option available to you is to use @include()@. This method has a 35 | couple of roles; if you supply a "@Module@":/modules.html, the module is mixed 36 | into the class and becomes part of its inheritance tree. If you supply any 37 | other type of object, the methods from the object are copied into the class 38 | itself, overwriting any pre-existing methods with similar names. 39 | 40 |
// Replace Dog's speak method  (#1)
41 |   Dog.include({
42 |       speak: function(stuff) {
43 |           return this.callSuper('lots of ' + stuff) + '!';
44 |       }
45 |   });
46 | 
47 |   rex.speak('cats')
48 |   // -> "My name is Rex and I like lots of cats!"
49 | 
50 |   // Mix in a module, altering the class's ancestry
51 |   // callSuper() in Dog#speak will now call this method
52 |   var Speaker = new Module({
53 |       speak: function(stuff) {
54 |           return 'I can talk about ' + stuff + '!';
55 |       }
56 |   });
57 | 
58 |   Dog.include(Speaker);
59 |   rex.speak('cats')
60 |   // -> "I can talk about lots of cats!!"
61 | 62 | Notice how including @Speaker@ does not overwrite the @speak@ method in the 63 | @Dog@ class (marked @#1@ above). That method is defined in @Dog@ and will 64 | persist until overwritten directly in the @Dog@ class. @Speaker@ merely 65 | injects another method called @speak@ into the inheritance chain for the @Dog@ 66 | class. For more information, refer to the "explanation of Ruby's method lookup 67 | algorithm":/inheritance.html. 68 | -------------------------------------------------------------------------------- /site/src/pages/stacktrace.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. StackTrace 3 | 4 | @JS.StackTrace@ is a module you can use to inspect what an application is 5 | doing internally while it runs. It provides an interface for monitoring method 6 | calls, which you can use to build monitoring and debugging tools. 7 | 8 | The @StackTrace@ module supports the "@Observable@":/observable.html interface 9 | for monitoring what the stack is doing: 10 | 11 |
JS.StackTrace.addObserver(monitor);
12 | 13 | @monitor@ should be an object that responds to the @update()@ method. This 14 | method takes two arguments: an event name, and an event object. So, the 15 | object should look something like this: 16 | 17 |
monitor = {
18 |       update: function(event, data) {
19 |           if (event === 'call') // ...
20 |       }
21 |   };
22 | 23 | There are three types of event, which tell you when a function is called, when 24 | a function returns, and when an error is thrown. 25 | 26 | h3. @call@ event 27 | 28 | The @call@ event fires when a function is called. The @data@ object in this 29 | case represents the data surrounding the method call. It has the following 30 | properties: 31 | 32 | * @object@ - The object receiving the method call 33 | * @method@ - The @Method@ object for the current method call 34 | * @env@ - The @Class@ or @Module@ where the method is being executed 35 | * @args@ - An @Array@ of the arguments to the method call 36 | * @leaf@ - Boolean indicating whether the call is a leaf; it's a leaf if no 37 | other method calls are logged while it is running. This is always @true@ 38 | when a method is first called. 39 | 40 | h3. @return@ event 41 | 42 | The @return@ event fires when a function returns. The @data@ object is the 43 | same object that's passed to the @call@ event, with one extra property: 44 | 45 | * @result@ - The return value of the method call 46 | 47 | h3. @error@ event 48 | 49 | This event fires when an exception is thrown. The @data@ object is just the 50 | error that was raised. 51 | 52 | h3. Enabling tracing 53 | 54 | Since tracing incurs a performance cost, JS.Class does not trace anything 55 | by default. When you want to trace a module or class, you pass a list of the 56 | modules you want to trace to @Method.trace()@, and use @Method.untrace()@ to 57 | stop tracing them. 58 | 59 |
JS.Method.trace([JS.Hash, JS.Range]);
60 | 61 | h3. Call stack logging 62 | 63 | There is a logger you can use to print the call stack to the "@Console@":/console.html. 64 | To use it, just pass a list of modules to trace and a function to @JS.Method.tracing()@. 65 | This enables tracing for the given modules, runs the function, then disables 66 | tracing again. 67 | 68 |
JS.Method.tracing([JS.Hash], function() {
69 |       var hash = new JS.OrderedHash(['foo', 4, 'bar', 5]);
70 |       hash.hasKey('foo');
71 |   });
72 | 73 | !/images/tracing.png! 74 | -------------------------------------------------------------------------------- /source/test/async_steps.js: -------------------------------------------------------------------------------- 1 | Test.extend({ 2 | AsyncSteps: new JS.Class(JS.Module, { 3 | define: function(name, method) { 4 | this.callSuper(name, function() { 5 | var args = [name, method].concat(JS.array(arguments)); 6 | this.__enqueue__(args); 7 | }); 8 | }, 9 | 10 | included: function(klass) { 11 | klass.include(Test.AsyncSteps.Sync); 12 | if (!klass.includes(Test.Context)) return; 13 | 14 | klass.extend({ 15 | it: function(name, opts, block) { 16 | if (typeof opts === 'function') { 17 | block = opts; 18 | opts = {}; 19 | } 20 | this.callSuper(name, opts, function(resume) { 21 | this.exec(block, function(error) { 22 | Test.Unit.TestCase.processError(this, error); 23 | this.sync(resume); 24 | }); 25 | }); 26 | } 27 | }); 28 | }, 29 | 30 | extend: { 31 | Sync: new JS.Module({ 32 | __enqueue__: function(args) { 33 | this.__stepQueue__ = this.__stepQueue__ || []; 34 | this.__stepQueue__.push(args); 35 | if (this.__runningSteps__) return; 36 | this.__runningSteps__ = true; 37 | 38 | var setTimeout = Test.FakeClock.REAL.setTimeout; 39 | setTimeout(this.method('__runNextStep__'), 1); 40 | }, 41 | 42 | __runNextStep__: function(error) { 43 | if (error !== undefined) return this.addError(error); 44 | 45 | var step = this.__stepQueue__.shift(), n; 46 | 47 | if (!step) { 48 | this.__runningSteps__ = false; 49 | if (!this.__stepCallbacks__) return; 50 | 51 | n = this.__stepCallbacks__.length; 52 | while (n--) this.__stepCallbacks__.shift().call(this); 53 | 54 | return; 55 | } 56 | 57 | var methodName = step.shift(), 58 | method = step.shift(), 59 | parameters = step.slice(), 60 | block = function() { method.apply(this, parameters) }; 61 | 62 | parameters[method.length - 1] = this.method('__runNextStep__'); 63 | if (!this.exec) return block.call(this); 64 | this.exec(block, function() {}, this.method('__endSteps__')); 65 | }, 66 | 67 | __endSteps__: function(error) { 68 | Test.Unit.TestCase.processError(this, error); 69 | this.__stepQueue__ = []; 70 | this.__runNextStep__(); 71 | }, 72 | 73 | addError: function() { 74 | this.callSuper(); 75 | this.__endSteps__(); 76 | }, 77 | 78 | sync: function(callback) { 79 | if (!this.__runningSteps__) return callback.call(this); 80 | this.__stepCallbacks__ = this.__stepCallbacks__ || []; 81 | this.__stepCallbacks__.push(callback); 82 | } 83 | }) 84 | } 85 | }), 86 | 87 | asyncSteps: function(methods) { 88 | return new this.AsyncSteps(methods); 89 | } 90 | }); 91 | 92 | -------------------------------------------------------------------------------- /site/src/pages/kernel.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. The Kernel module 3 | 4 | Ruby's @Kernel@ module defines the methods common to all objects. Similarly, 5 | the @JS.Kernel@ module defines methods shared by all objects (including class 6 | and module objects). Every object created using @JS.Class@ has these methods, 7 | though they may be overridden depending on the object's type. 8 | 9 | h3. @object.__eigen__()@ 10 | 11 | Returns the @object@'s "metamodule", a module used to store any "singleton methods":/singletonmethods.html 12 | attached to the object. Calling @__eigen__()@ on a class or module returns a 13 | module used to store its class methods. 14 | 15 | h3. @object.enumFor(methodName, *args)@ 16 | 17 | Returns an @Enumerator@ (see "@Enumerable@":/enumerable.html) for the object 18 | using the given @methodName@ and optional arguments. For example, the 19 | @Enumerator@ generated by @Enumerable#forEachCons@ is generated as follows: 20 | 21 |
forEachCons: function(n, block, context) {
22 |       if (!block) return this.enumFor('forEachCons', n);
23 |       // forEachCons implementation details ...
24 |   }
25 | 26 | h3. @object.equals(other)@ 27 | 28 | Returns @true@ iff @object@ and @other@ are the same object. This can be 29 | overridden to provide more meaningful equality tests. If you want to use an 30 | object as a key in a "@Hash@":/hash.html you must also override @Kernel#hash()@ 31 | (see below). 32 | 33 | h3. @object.extend(module)@ 34 | 35 | Adds the methods from @module@ as "singleton methods":/singletonmethods.html 36 | to @object@. 37 | 38 | h3. @object.hash()@ 39 | 40 | Returns a hashcode for the object, which is used by "@Hash@":/hash.html when 41 | storing keys.The default implementation returns a unique hexadecimal number 42 | for each object. If two objects are equal according to the @equals()@ method, 43 | they must both return the same hashcode otherwise they will not work correctly 44 | as keys in a @Hash@. 45 | 46 | h3. @object.isA(type)@ 47 | 48 | Returns @true@ iff @object@ is an instance of the given @type@, which should 49 | be a class or module. If @type@ is anywhere in @object@'s inheritance 50 | hierarchy this method will return @true@. 51 | 52 | h3. @object.method(name)@ 53 | 54 | Returns a copy of the method with the given name from @object@, as a 55 | standalone function bound to @object@'s instance variables. See "method binding":/binding.html. 56 | 57 | h3. @object.tap(block, context)@ 58 | 59 | Calls the function @block@ in the given (optional) @context@, passing @object@ 60 | as a parameter to @block@, and returns @object@. Useful for inspecting 61 | intermediate values in a long method chain. For example: 62 | 63 |
list                 .tap(function(x) { console.log("original: ", x) })
64 |     .toArray()         .tap(function(x) { console.log("array: ", x) })
65 |     .select(condition) .tap(function(x) { console.log("evens: ", x) })
66 |     .map(square)       .tap(function(x) { console.log("squares: ", x) });
67 | 68 | -------------------------------------------------------------------------------- /test/examples/async.js: -------------------------------------------------------------------------------- 1 | JSCLASS_PATH = "build/min/" 2 | var JS = require("../../" + JSCLASS_PATH + "loader") 3 | 4 | JS.require("JS.Test", "JS.MethodChain", function(Test, MC) { 5 | 6 | Test.describe("Asynchronous testing", function() { with(this) { 7 | describe("with a simple test", function() { with(this) { 8 | it("allows async assertions", function(resume) { with(this) { 9 | setTimeout(function() { 10 | resume(function() { assert(false) }) 11 | }, 1000) 12 | }}) 13 | }}) 14 | 15 | describe("with nested resume blocks", function() { with(this) { 16 | define("wait", function(resume, block) { with(this) { 17 | setTimeout(function() { resume(block) }, 1000) 18 | }}) 19 | 20 | it("keeps running until you use a resume block with no continuation", function(resume) { with(this) { 21 | var startTime = new Date().getTime(); 22 | 23 | wait(resume, function(resume) { 24 | assert(true) 25 | wait(resume, function(resume) { 26 | assert(true) 27 | wait(resume, function() { 28 | var endTime = new Date().getTime(); 29 | assertInDelta( 4, endTime - startTime, 0.1 ) 30 | }) 31 | }) 32 | }) 33 | }}) 34 | }}) 35 | 36 | describe("with an async before block", function() { with(this) { 37 | before(function(resume) { with(this) { 38 | var self = this 39 | setTimeout(function() { 40 | self.value = 2 41 | resume() 42 | }, 1000); 43 | }}) 44 | 45 | it("waits for the before block to resume", function() { with(this) { 46 | assertEqual( 2, value ) 47 | }}) 48 | 49 | describe("with another nested block", function() { with(this) { 50 | before(function(resume) { with(this) { 51 | var self = this 52 | setTimeout(function() { 53 | self.value *= 4 54 | resume() 55 | }, 500) 56 | }}) 57 | 58 | it("runs both before blocks sequentially", function() { with(this) { 59 | assertEqual( 80, this.value ) 60 | }}) 61 | }}) 62 | }}) 63 | 64 | describe("with an async before all block", function() { with(this) { 65 | before("all", function(resume) { with(this) { 66 | var self = this 67 | setTimeout(function() { 68 | self.value = 20 69 | resume() 70 | }, 1000); 71 | }}) 72 | 73 | it("waits for the before all block to resume", function() { with(this) { 74 | assertEqual( 2, value ) 75 | }}) 76 | 77 | describe("with another nested all block", function() { with(this) { 78 | before("all", function(resume) { with(this) { 79 | var self = this 80 | setTimeout(function() { 81 | self.value *= 4 82 | resume() 83 | }, 500) 84 | }}) 85 | 86 | it("runs both before all blocks sequentially", function() { with(this) { 87 | assertEqual( 8, value ) 88 | }}) 89 | }}) 90 | }}) 91 | }}) 92 | 93 | Test.autorun() 94 | }) 95 | 96 | -------------------------------------------------------------------------------- /test/specs/decorator_spec.js: -------------------------------------------------------------------------------- 1 | JS.require('JS.Decorator', function(Decorator) { 2 | 3 | JS.ENV.DecoratorSpec = JS.Test.describe(Decorator, function() { with(this) { 4 | var Bicycle = new JS.Class({ 5 | initialize: function(model, gears) { 6 | this.model = model; 7 | this.gears = gears; 8 | }, 9 | getModel: function() { 10 | return this.model; 11 | }, 12 | getPrice: function() { 13 | return 10 * this.gears; 14 | } 15 | }); 16 | 17 | var HeadlightDecorator = new Decorator(Bicycle, { 18 | getPrice: function() { 19 | return 5 + this.component.getPrice(); 20 | } 21 | }); 22 | 23 | var PedalsDecorator = new Decorator(Bicycle, { 24 | getPrice: function() { 25 | return 24 + this.component.getPrice(); 26 | }, 27 | rotatePedals: function() { 28 | return 'Turning the pedals'; 29 | } 30 | }); 31 | 32 | define("Bicycle", Bicycle) 33 | define("HeadlightDecorator", HeadlightDecorator) 34 | define("PedalsDecorator", PedalsDecorator) 35 | 36 | before(function() { with(this) { 37 | this.bicycle = new Bicycle("Trek", 24) 38 | this.withHeadlights = new HeadlightDecorator(bicycle) 39 | this.withPedals = new PedalsDecorator(bicycle) 40 | this.withBoth = new HeadlightDecorator(withPedals) 41 | }}) 42 | 43 | it("creates classes", function() { with(this) { 44 | assertKindOf( JS.Class, HeadlightDecorator ) 45 | }}) 46 | 47 | it("generates objects of the decorated type", function() { with(this) { 48 | assertKindOf( Bicycle, withHeadlights ) 49 | assertKindOf( Bicycle, withBoth ) 50 | }}) 51 | 52 | it("generates the same API of the decorated class", function() { with(this) { 53 | assertRespondTo( withHeadlights, "getModel" ) 54 | assertRespondTo( withHeadlights, "getPrice" ) 55 | }}) 56 | 57 | it("adds methods specified in the decorating class", function() { with(this) { 58 | assertRespondTo( withPedals, "rotatePedals" ) 59 | assertEqual( "Turning the pedals", withPedals.rotatePedals() ) 60 | }}) 61 | 62 | it("passes undefined method calls down to the component", function() { with(this) { 63 | assertEqual( "Trek", withHeadlights.getModel() ) 64 | assertEqual( "Trek", withPedals.getModel() ) 65 | }}) 66 | 67 | it("allows decorators to call down to the decoree using this.component", function() { with(this) { 68 | assertEqual( 240, bicycle.getPrice() ) 69 | assertEqual( 245, withHeadlights.getPrice() ) 70 | assertEqual( 264, withPedals.getPrice() ) 71 | }}) 72 | 73 | it("allows decorators to be composed", function() { with(this) { 74 | assertEqual( 269, withBoth.getPrice() ) 75 | }}) 76 | 77 | it("allows decorators to wrap any object", function() { with(this) { 78 | var subject = { 79 | getPrice: function() { return 50 }, 80 | getSizes: function() { return ['S', 'M', 'L', 'XL'] } 81 | } 82 | var decorated = new PedalsDecorator(subject) 83 | assertEqual( 74, decorated.getPrice() ) 84 | assertEqual( decorated.getSizes(), subject.getSizes() ) 85 | }}) 86 | }}) 87 | 88 | }) 89 | 90 | -------------------------------------------------------------------------------- /test/specs/tsort_spec.js: -------------------------------------------------------------------------------- 1 | JS.require('JS.TSort', 'JS.Hash', function(TSort, Hash) { 2 | 3 | JS.ENV.TSortSpec = JS.Test.describe(TSort, function() { with(this) { 4 | before(function() { with(this) { 5 | this.Hash = new JS.Class(Hash, { 6 | include: TSort, 7 | 8 | tsortEachNode: function(block, context) { 9 | return this.forEachKey(block, context) 10 | }, 11 | 12 | tsortEachChild: function(node, block, context) { 13 | var list = this.fetch(node); 14 | for (var i = 0, n = list.length; i < n; i++) 15 | block.call(context, list[i]); 16 | } 17 | }) 18 | }}) 19 | 20 | describe("with primitive data", function() { with(this) { 21 | describe("with no cycles", function() { with(this) { 22 | before(function() { with(this) { 23 | this.hash = new Hash([ 1, [2,3], 24 | 2, [3], 25 | 3, [], 26 | 4, [] ]) 27 | }}) 28 | 29 | it("sorts the elements topologically", function() { with(this) { 30 | assertEqual( [3,2,1,4], hash.tsort() ) 31 | }}) 32 | 33 | it("identifies strongly connected nodes", function() { with(this) { 34 | assertEqual( [[3],[2],[1],[4]], hash.stronglyConnectedComponents() ) 35 | }}) 36 | }}) 37 | 38 | describe("when there are cycles", function() { with(this) { 39 | before(function() { with(this) { 40 | this.hash = new Hash([ 1, [2,3,4], 41 | 2, [3], 42 | 3, [2], 43 | 4, [1] ]) 44 | }}) 45 | 46 | it("raises an error", function() { with(this) { 47 | assertThrows(TSort.Cyclic, function() { hash.tsort() }) 48 | }}) 49 | 50 | it("identifies strongly connected nodes", function() { with(this) { 51 | assertEqual( [[2,3],[1,4]], hash.stronglyConnectedComponents() ) 52 | }}) 53 | }}) 54 | }}) 55 | 56 | describe("with object data", function() { with(this) { 57 | include(JS.Test.Helpers) 58 | 59 | before(function() { with(this) { 60 | this.TodoItem = new JS.Class("TodoItem", { 61 | initialize: function(priority) { 62 | this.priority = priority 63 | }, 64 | equals: function(other) { 65 | return this.priority == other.priority 66 | }, 67 | hash: function() { 68 | return this.priority 69 | } 70 | }) 71 | }}) 72 | 73 | describe("with no cycles", function() { with(this) { 74 | before(function() { with(this) { 75 | this.hash = new Hash([ new TodoItem(1), [new TodoItem(2),new TodoItem(3)], 76 | new TodoItem(2), [new TodoItem(3)], 77 | new TodoItem(3), [], 78 | new TodoItem(4), [] ]) 79 | }}) 80 | 81 | it("sorts the elements topologically", function() { with(this) { 82 | assertEqual( [3,2,1,4], map(hash.tsort(), "priority") ) 83 | }}) 84 | 85 | it("identifies strongly connected nodes", function() { with(this) { 86 | assertEqual( [[new TodoItem(3)],[new TodoItem(2)],[new TodoItem(1)],[new TodoItem(4)]], 87 | hash.stronglyConnectedComponents() ) 88 | }}) 89 | }}) 90 | }}) 91 | }}) 92 | 93 | }) 94 | 95 | -------------------------------------------------------------------------------- /source/package/loaders/browser.js: -------------------------------------------------------------------------------- 1 | Package.BrowserLoader = { 2 | HOST_REGEX: /^(https?\:)?\/\/[^\/]+/i, 3 | 4 | usable: function() { 5 | return !!Package._getObject('window.document.getElementsByTagName') && 6 | typeof phantom === 'undefined'; 7 | }, 8 | 9 | __FILE__: function() { 10 | var scripts = document.getElementsByTagName('script'), 11 | src = scripts[scripts.length - 1].src, 12 | url = window.location.href; 13 | 14 | if (/^\w+\:\/+/.test(src)) return src; 15 | if (/^\//.test(src)) return window.location.origin + src; 16 | return url.replace(/[^\/]*$/g, '') + src; 17 | }, 18 | 19 | cacheBust: function(path) { 20 | if (exports.cache !== false) return path; 21 | var token = new JS.Date().getTime(); 22 | return path + (/\?/.test(path) ? '&' : '?') + token; 23 | }, 24 | 25 | fetch: function(path) { 26 | var originalPath = path; 27 | path = this.cacheBust(path); 28 | 29 | this.HOST = this.HOST || this.HOST_REGEX.exec(window.location.href); 30 | var host = this.HOST_REGEX.exec(path); 31 | 32 | if (!this.HOST || (host && host[0] !== this.HOST[0])) return null; 33 | Package.log('[FETCH] ' + path); 34 | 35 | var source = new Package.Deferred(), 36 | self = this, 37 | xhr = window.ActiveXObject 38 | ? new ActiveXObject('Microsoft.XMLHTTP') 39 | : new XMLHttpRequest(); 40 | 41 | xhr.open('GET', path, true); 42 | xhr.onreadystatechange = function() { 43 | if (xhr.readyState !== 4) return; 44 | xhr.onreadystatechange = self._K; 45 | source.succeed(xhr.responseText + '\n//@ sourceURL=' + originalPath); 46 | xhr = null; 47 | }; 48 | xhr.send(null); 49 | return source; 50 | }, 51 | 52 | loadFile: function(path, fireCallbacks, source) { 53 | if (!source) path = this.cacheBust(path); 54 | 55 | var self = this, 56 | head = document.getElementsByTagName('head')[0], 57 | script = document.createElement('script'); 58 | 59 | script.type = 'text/javascript'; 60 | 61 | if (source) 62 | return source.callback(function(code) { 63 | Package.log('[EXEC] ' + path); 64 | var execute = new Function('code', 'eval(code)'); 65 | execute(code); 66 | fireCallbacks(); 67 | }); 68 | 69 | Package.log('[LOAD] ' + path); 70 | script.src = path; 71 | 72 | script.onload = script.onreadystatechange = function() { 73 | var state = script.readyState, status = script.status; 74 | if ( !state || state === 'loaded' || state === 'complete' || 75 | (state === 4 && status === 200) ) { 76 | fireCallbacks(); 77 | script.onload = script.onreadystatechange = self._K; 78 | head = null; 79 | script = null; 80 | } 81 | }; 82 | head.appendChild(script); 83 | }, 84 | 85 | loadStyle: function(path) { 86 | var link = document.createElement('link'); 87 | link.rel = 'stylesheet'; 88 | link.type = 'text/css'; 89 | link.href = this.cacheBust(path); 90 | 91 | document.getElementsByTagName('head')[0].appendChild(link); 92 | }, 93 | 94 | _K: function() {} 95 | }; 96 | 97 | -------------------------------------------------------------------------------- /site/src/pages/introduction.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Getting started with @jsclass@ 3 | 4 | You can use @jsclass@ on any of the "supported platforms":/platforms.html 5 | without any custom configuration. To start using @jsclass@ in your project, 6 | you'll need to download it using the link on the left. 7 | 8 | If you're using @jsclass@ on Node and do not need to support other 9 | environments, you can "install it with @npm@":/platforms/node.html and avoid a 10 | lot of the boilerplate shown below. 11 | 12 | The download contains two directories, @src@ and @min@. Both contain the same 13 | files; @src@ contains the @jsclass@ source code, and @min@ contains a minified 14 | version suitable for production use on the web. Pick which version you want to 15 | use, and copy it into your project. You can then load @jsclass@ into your 16 | pages as follows. 17 | 18 |

19 |   
20 | 21 | On server-side platforms, you need to set the global variable @JSCLASS_PATH@ 22 | like this: 23 | 24 |
(function() {
25 |       var $ = (typeof global === 'object') ? global : this;
26 |       $.JSCLASS_PATH = 'path/to/jsclass/src';
27 |   })();
28 | 29 | Then use the platform's module loading API to load the @jsclass@ package 30 | loader: 31 | 32 |
// On CommonJS:
33 |   var JS = require('./' + JSCLASS_PATH + '/loader');
34 | 
35 |   // On other platforms, this creates JS as a global:
36 |   load(JSCLASS_PATH + '/loader.js');
37 | 38 | @JSCLASS_PATH@ tells the @loader@ script where you're storing the @jsclass@ 39 | library, and will be interpreted relative to the current working directory. 40 | @loader.js@ is a script that knows how to load packages on any platform, while 41 | @loader-browser.js@ only contains code for loading packages in the browser, 42 | and is thus a little smaller. 43 | 44 | This may look like quite a lot of boilerplate, but once you've done this you 45 | can use a single mechanism to load @jsclass@ components (and any other 46 | components you make) on any platform your code needs to run on. That mechanism 47 | is the @JS.require()@ function. 48 | 49 | All components that are part of the @jsclass@ library can be loaded using the 50 | @JS.require()@ function, passing in the name of the object(s) you want to use 51 | and a callback to run once they're ready. 52 | 53 |
JS.require('JS.Hash', 'JS.Observable', function(Hash, Observable) {
54 |       // ...
55 |   });
56 | 57 | The @JS.require()@ function is aware of dependencies and will load everything 58 | you need to use the objects you want. One some platforms, package loading is 59 | asynchronous, and you should be aware of that when structuring your code. If 60 | an object you want to use is already loaded, @JS.require()@ will not reload 61 | it. 62 | 63 | You're now ready to start using @jsclass@. Dive into the reference 64 | documentation linked on the left of this page, and find out how you can "use 65 | the @jsclass@ package system":/packages.html to manage dependencies in your 66 | own projects. 67 | 68 | -------------------------------------------------------------------------------- /site/src/pages/console.haml: -------------------------------------------------------------------------------- 1 | :textile 2 | h2. Console 3 | 4 | Most JavaScript environments have some sort of console that allows running 5 | programs to print output, log messages and the like. In many web browsers, 6 | this capability is provided by the @window.console@ object, and in server-side 7 | environments the program may write output to the terminal. @JS.Console@ 8 | provides a single interface to all these facilities, using the most 9 | appropriate device wherever it is being used. 10 | 11 | When writing output to a terminal, @JS.Console@ also supports many ANSI 12 | formatting commands for changing the color and other attributes of printed 13 | text. 14 | 15 | The main two output methods are as follows: 16 | 17 | * @JS.Console.puts(string)@ - prints @string@ to the console with a line break 18 | afterward, so that subsequent messages appear on the next line. 19 | * @JS.Console.print(string)@ - prints @string@ without applying line breaks, 20 | allowing multiple @print()@ calls to write output to the same line if the 21 | console supports this. In environments that don't support this, the output 22 | is buffered until the next @puts()@ call. 23 | 24 | The output devices used are as follows: 25 | 26 | * In web browsers that support @window.console@, the development console is 27 | used to log output. 28 | * In web browsers with no @console@ object, the @alert()@ function is used. 29 | * In Adobe AIR applications running under @adl@, the @runtime.trace()@ 30 | function is used to write to the terminal. 31 | * In programs running in a terminal, output is sent to @STDOUT@. 32 | 33 | In addition, the following formatting commands are available for formatting 34 | output in environments that support ANSI escape codes for formatting text. 35 | All commands are methods of @JS.Console@, for example you can switch to bold 36 | text by calling @JS.Console.bold()@. Text printed after the command has been 37 | issued will have the formatting applied to it. In environments that do not 38 | support formatting these methods have no effect. 39 | 40 | * @reset()@ resets all formatting attributes 41 | * @bold()@, @normal()@ set the weight of the text 42 | * @underline()@, @noline()@ apply and remove underlining 43 | * @blink()@, @noblink()@ apply and remove blinking text 44 | * @black()@, @red()@, @green()@, @yellow()@, @blue()@, @magenta()@, @cyan()@, 45 | @white()@, @nocolor()@ set the current text color 46 | * Color methods can be prefixed with @bg@, for example @bgyellow()@, to set 47 | the current background color 48 | 49 | Multiple formatting instructions can be combined using this method: 50 | 51 |
JS.Console.consoleFormat('bold', 'red');
52 | 53 | The @consoleFormat()@ method takes a list of formatting instructions as input. 54 | It calls @reset()@ and then applies the given formats. 55 | 56 | Finally note that @JS.Console@ can be used as a mixin so all the above methods 57 | appear in the current class: 58 | 59 |
Logger = new JS.Class({
60 |       include: JS.Console,
61 | 
62 |       info: function(message) {
63 |           this.bgblack();
64 |           this.white();
65 |           this.print('INFO');
66 |           this.reset();
67 |           this.puts(' ' + message);
68 |       }
69 |   });
70 | -------------------------------------------------------------------------------- /source/state.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS; 4 | 5 | if (E) exports.JS = exports; 6 | factory(js, E ? exports : js); 7 | 8 | })(function(JS, exports) { 9 | 'use strict'; 10 | 11 | var State = new JS.Module('State', { 12 | __getState__: function(state) { 13 | if (typeof state === 'object') return state; 14 | if (typeof state === 'string') return (this.states || {})[state]; 15 | return {}; 16 | }, 17 | 18 | setState: function(state) { 19 | this.__state__ = this.__getState__(state); 20 | State.addMethods(this.__state__, this.klass); 21 | }, 22 | 23 | inState: function() { 24 | var i = arguments.length; 25 | while (i--) { 26 | if (this.__state__ === this.__getState__(arguments[i])) return true; 27 | } 28 | return false; 29 | }, 30 | 31 | extend: { 32 | ClassMethods: new JS.Module({ 33 | states: function(block) { 34 | this.define('states', State.buildCollection(this, block)); 35 | } 36 | }), 37 | 38 | included: function(klass) { 39 | klass.extend(this.ClassMethods); 40 | }, 41 | 42 | stub: function() { return this; }, 43 | 44 | buildStubs: function(stubs, collection, states) { 45 | var state, method; 46 | for (state in states) { 47 | collection[state] = {}; 48 | for (method in states[state]) stubs[method] = this.stub; 49 | } 50 | }, 51 | 52 | findStates: function(collections, name) { 53 | var i = collections.length, results = []; 54 | while (i--) { 55 | if (collections[i].hasOwnProperty(name)) 56 | results.push(collections[i][name]); 57 | } 58 | return results; 59 | }, 60 | 61 | buildCollection: function(module, states) { 62 | var stubs = {}, 63 | collection = {}, 64 | superstates = module.lookup('states'), 65 | state, klass, methods, name, mixins, i, n; 66 | 67 | this.buildStubs(stubs, collection, states); 68 | 69 | for (i = 0, n = superstates.length; i < n; i++) 70 | this.buildStubs(stubs, collection, superstates[i]); 71 | 72 | for (state in collection) { 73 | klass = new JS.Class(states[state]); 74 | mixins = this.findStates(superstates, state); 75 | 76 | i = mixins.length; 77 | while (i--) { 78 | if (mixins[i]) klass.include(mixins[i].klass); 79 | } 80 | 81 | methods = {}; 82 | for (name in stubs) { 83 | if (!klass.prototype[name]) methods[name] = stubs[name]; 84 | } 85 | klass.include(methods); 86 | collection[state] = new klass; 87 | } 88 | if (module.__tgt__) this.addMethods(stubs, module.__tgt__.klass); 89 | return collection; 90 | }, 91 | 92 | addMethods: function(state, klass) { 93 | if (!klass) return; 94 | 95 | var methods = {}, 96 | proto = klass.prototype, 97 | method; 98 | 99 | for (method in state) { 100 | if (proto[method]) continue; 101 | klass.define(method, this.wrapped(method)); 102 | } 103 | }, 104 | 105 | wrapped: function(method) { 106 | return function() { 107 | var func = (this.__state__ || {})[method]; 108 | return func ? func.apply(this, arguments) : this; 109 | }; 110 | } 111 | } 112 | }); 113 | 114 | exports.State = State; 115 | }); 116 | 117 | -------------------------------------------------------------------------------- /source/test/coverage.js: -------------------------------------------------------------------------------- 1 | Test.extend({ 2 | Coverage: new JS.Class({ 3 | initialize: function(module) { 4 | this._module = module; 5 | this._methods = new Hash([]); 6 | 7 | var storeMethods = function(module) { 8 | var methods = module.instanceMethods(false), 9 | i = methods.length; 10 | while (i--) this._methods.store(module.instanceMethod(methods[i]), 0); 11 | }; 12 | storeMethods.call(this, module); 13 | storeMethods.call(this, module.__eigen__()); 14 | }, 15 | 16 | attach: function() { 17 | var module = this._module; 18 | StackTrace.addObserver(this); 19 | JS.Method.trace([module, module.__eigen__()]); 20 | }, 21 | 22 | detach: function() { 23 | var module = this._module; 24 | JS.Method.untrace([module, module.__eigen__()]); 25 | StackTrace.removeObserver(this); 26 | }, 27 | 28 | update: function(event, frame) { 29 | if (event !== 'call') return; 30 | var pair = this._methods.assoc(frame.method); 31 | if (pair) pair.setValue(pair.value + 1); 32 | }, 33 | 34 | report: function() { 35 | var methods = this._methods.entries().sort(function(a,b) { 36 | return b.value - a.value; 37 | }); 38 | var covered = this._methods.all(function(pair) { return pair.value > 0 }); 39 | 40 | this.printTable(methods, function(row, i) { 41 | if (row[1] === 0) return ['bgred', 'white']; 42 | return (i % 2 === 0) ? ['bold'] : []; 43 | }); 44 | return covered; 45 | }, 46 | 47 | printTable: function(table, formatter) { 48 | var widths = [], 49 | table = [['Method', 'Calls']].concat(table), 50 | C = Console, 51 | i = table.length, 52 | j, string; 53 | 54 | while (i--) { 55 | j = table[i].length; 56 | while (j--) { 57 | widths[j] = widths[j] || 0; 58 | string = (table[i][j] === undefined ? '' : table[i][j]).toString(); 59 | widths[j] = Math.max(string.length, widths[j]); 60 | } 61 | } 62 | 63 | var divider = '+', j = widths.length; 64 | while (j--) divider = '+' + this.repeat('-', widths[j] + 2) + divider; 65 | divider = ' ' + divider; 66 | C.reset(); 67 | C.puts(); 68 | C.puts(divider); 69 | 70 | var printRow = function(row, format) { 71 | var data = table[row]; 72 | C.reset(); 73 | C.print(' '); 74 | for (var i = 0, n = data.length; i < n; i++) { 75 | C.reset(); 76 | C.print('|'); 77 | C.consoleFormat.apply(C, format); 78 | C.print(' ' + this.pad(data[i], widths[i]) + ' '); 79 | } 80 | C.reset(); 81 | C.puts('|'); 82 | }; 83 | printRow.call(this, 0, ['bold']); 84 | C.reset(); 85 | C.puts(divider); 86 | 87 | for (var i = 1, n = table.length; i < n; i++) { 88 | var format = formatter ? formatter(table[i], i) : []; 89 | printRow.call(this, i, format); 90 | } 91 | C.reset(); 92 | C.puts(divider); 93 | }, 94 | 95 | pad: function(string, width) { 96 | string = (string === undefined ? '' : string).toString(); 97 | return string + this.repeat(' ', width - string.length); 98 | }, 99 | 100 | repeat: function(string, n) { 101 | var result = ''; 102 | while (n--) result += string; 103 | return result; 104 | } 105 | }) 106 | }); 107 | -------------------------------------------------------------------------------- /test/specs/comparable_spec.js: -------------------------------------------------------------------------------- 1 | JS.require('JS.Comparable', function(Comparable) { 2 | 3 | JS.ENV.ComparableSpec = JS.Test.describe(Comparable, function() { with(this) { 4 | include(JS.Test.Helpers) 5 | 6 | define("TodoItem", new JS.Class({ 7 | include: Comparable, 8 | initialize: function(position, task) { 9 | this.position = position; 10 | this.task = task || ""; 11 | }, 12 | compareTo: function(other) { 13 | if (this.position < other.position) 14 | return -1; 15 | else if (this.position > other.position) 16 | return 1; 17 | else 18 | return 0; 19 | } 20 | })) 21 | 22 | describe("sorting", function() { with(this) { 23 | before(function() { with(this) { 24 | this.todos = map([8,2,7,5,3,7,6], function(id) { return new TodoItem(id) }) 25 | }}) 26 | 27 | it("uses the #compareTo method to sort", function() { with(this) { 28 | todos.sort(TodoItem.compare) 29 | assertEqual( [2,3,5,6,7,7,8], map(todos, 'position') ) 30 | }}) 31 | }}) 32 | 33 | describe("#lt", function() { with(this) { 34 | it("returns true if A < B", function() { with(this) { 35 | assert( new TodoItem(1).lt(new TodoItem(2)) ) 36 | }}) 37 | 38 | it("returns false if A = B", function() { with(this) { 39 | assert( !new TodoItem(2).lt(new TodoItem(2)) ) 40 | }}) 41 | 42 | it("returns false if A > B", function() { with(this) { 43 | assert( !new TodoItem(3).lt(new TodoItem(2)) ) 44 | }}) 45 | }}) 46 | 47 | describe("#lte", function() { with(this) { 48 | it("returns true if A < B", function() { with(this) { 49 | assert( new TodoItem(1).lte(new TodoItem(2)) ) 50 | }}) 51 | 52 | it("returns true if A = B", function() { with(this) { 53 | assert( new TodoItem(2).lte(new TodoItem(2)) ) 54 | }}) 55 | 56 | it("returns false if A > B", function() { with(this) { 57 | assert( !new TodoItem(3).lte(new TodoItem(2)) ) 58 | }}) 59 | }}) 60 | 61 | describe("#eq", function() { with(this) { 62 | it("returns false if A < B", function() { with(this) { 63 | assert( !new TodoItem(1).eq(new TodoItem(2)) ) 64 | }}) 65 | 66 | it("returns true if A = B", function() { with(this) { 67 | assert( new TodoItem(2).eq(new TodoItem(2)) ) 68 | }}) 69 | 70 | it("returns false if A > B", function() { with(this) { 71 | assert( !new TodoItem(3).eq(new TodoItem(2)) ) 72 | }}) 73 | }}) 74 | 75 | describe("#gt", function() { with(this) { 76 | it("returns false if A < B", function() { with(this) { 77 | assert( !new TodoItem(1).gt(new TodoItem(2)) ) 78 | }}) 79 | 80 | it("returns false if A = B", function() { with(this) { 81 | assert( !new TodoItem(2).gt(new TodoItem(2)) ) 82 | }}) 83 | 84 | it("returns true if A > B", function() { with(this) { 85 | assert( new TodoItem(3).gt(new TodoItem(2)) ) 86 | }}) 87 | }}) 88 | 89 | describe("#gte", function() { with(this) { 90 | it("returns false if A < B", function() { with(this) { 91 | assert( !new TodoItem(1).gte(new TodoItem(2)) ) 92 | }}) 93 | 94 | it("returns true if A = B", function() { with(this) { 95 | assert( new TodoItem(2).gte(new TodoItem(2)) ) 96 | }}) 97 | 98 | it("returns true if A > B", function() { with(this) { 99 | assert( new TodoItem(3).gte(new TodoItem(2)) ) 100 | }}) 101 | }}) 102 | }}) 103 | 104 | }) 105 | 106 | -------------------------------------------------------------------------------- /source/command.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | var E = (typeof exports === 'object'), 3 | js = (typeof JS === 'undefined') ? require('./core') : JS, 4 | 5 | Enumerable = js.Enumerable || require('./enumerable').Enumerable, 6 | Observable = js.Observable || require('./observable').Observable; 7 | 8 | if (E) exports.JS = exports; 9 | factory(js, Enumerable, Observable, E ? exports : js); 10 | 11 | })(function(JS, Enumerable, Observable, exports) { 12 | 'use strict'; 13 | 14 | var Command = new JS.Class('Command', { 15 | initialize: function(functions) { 16 | if (typeof functions === 'function') 17 | functions = {execute: functions}; 18 | this._functions = functions; 19 | this._stack = this._functions.stack || null; 20 | }, 21 | 22 | execute: function(push) { 23 | if (this._stack) this._stack._restart(); 24 | var exec = this._functions.execute; 25 | if (exec) exec.apply(this); 26 | if (this._stack && push !== false) this._stack.push(this); 27 | }, 28 | 29 | undo: function() { 30 | var exec = this._functions.undo; 31 | if (exec) exec.apply(this); 32 | }, 33 | 34 | extend: { 35 | Stack: new JS.Class({ 36 | include: [Observable || {}, Enumerable || {}], 37 | 38 | initialize: function(options) { 39 | options = options || {}; 40 | this._redo = options.redo || null; 41 | this.clear(); 42 | }, 43 | 44 | forEach: function(block, context) { 45 | if (!block) return this.enumFor('forEach'); 46 | block = Enumerable.toFn(block); 47 | 48 | for (var i = 0, n = this._stack.length; i < n; i++) { 49 | if (this._stack[i] !== undefined) 50 | block.call(context, this._stack[i], i); 51 | } 52 | return this; 53 | }, 54 | 55 | clear: function() { 56 | this._stack = []; 57 | this.length = this.pointer = 0; 58 | }, 59 | 60 | _restart: function() { 61 | if (this.pointer === 0 && this._redo && this._redo.execute) 62 | this._redo.execute(); 63 | }, 64 | 65 | push: function(command) { 66 | this._stack.splice(this.pointer, this.length); 67 | this._stack.push(command); 68 | this.length = this.pointer = this._stack.length; 69 | if (this.notifyObservers) this.notifyObservers(this); 70 | }, 71 | 72 | stepTo: function(position) { 73 | if (position < 0 || position > this.length) return; 74 | var i, n; 75 | 76 | switch (true) { 77 | case position > this.pointer : 78 | for (i = this.pointer, n = position; i < n; i++) 79 | this._stack[i].execute(false); 80 | break; 81 | 82 | case position < this.pointer : 83 | if (this._redo && this._redo.execute) { 84 | this._redo.execute(); 85 | for (i = 0, n = position; i < n; i++) 86 | this._stack[i].execute(false); 87 | } else { 88 | for (i = 0, n = this.pointer - position; i < n; i++) 89 | this._stack[this.pointer - i - 1].undo(); 90 | } 91 | break; 92 | } 93 | this.pointer = position; 94 | if (this.notifyObservers) this.notifyObservers(this); 95 | }, 96 | 97 | undo: function() { 98 | this.stepTo(this.pointer - 1); 99 | }, 100 | 101 | redo: function() { 102 | this.stepTo(this.pointer + 1); 103 | } 104 | }) 105 | } 106 | }); 107 | 108 | exports.Command = Command; 109 | }); 110 | 111 | -------------------------------------------------------------------------------- /source/test/context/life_cycle.js: -------------------------------------------------------------------------------- 1 | Test.Context.LifeCycle = new JS.Module({ 2 | extend: { 3 | included: function(base) { 4 | base.extend(this.ClassMethods); 5 | 6 | base.before_all_callbacks = []; 7 | base.before_each_callbacks = []; 8 | base.after_all_callbacks = []; 9 | base.after_each_callbacks = []; 10 | base.before_should_callbacks = {}; 11 | 12 | base.extend({ 13 | inherited: function(child) { 14 | this.callSuper(); 15 | child.before_all_callbacks = []; 16 | child.before_each_callbacks = []; 17 | child.after_all_callbacks = []; 18 | child.after_each_callbacks = []; 19 | child.before_should_callbacks = {}; 20 | } 21 | }); 22 | }, 23 | 24 | ClassMethods: new JS.Module({ 25 | before: function(period, block) { 26 | if ((typeof period === 'function') || !block) { 27 | block = period; 28 | period = 'each'; 29 | } 30 | 31 | this['before_' + (period + '_') + 'callbacks'].push(block); 32 | }, 33 | 34 | after: function(period, block) { 35 | if ((typeof period === 'function') || !block) { 36 | block = period; 37 | period = 'each'; 38 | } 39 | 40 | this['after_' + (period + '_') + 'callbacks'].push(block); 41 | }, 42 | 43 | gatherCallbacks: function(callbackType, period) { 44 | var outerCallbacks = (typeof this.superclass.gatherCallbacks === 'function') 45 | ? this.superclass.gatherCallbacks(callbackType, period) 46 | : []; 47 | 48 | var mine = this[callbackType + '_' + (period + '_') + 'callbacks']; 49 | 50 | return (callbackType === 'before') 51 | ? outerCallbacks.concat(mine) 52 | : mine.concat(outerCallbacks); 53 | } 54 | }) 55 | }, 56 | 57 | setup: function(resume) { 58 | if (this.klass.before_should_callbacks[this._methodName]) 59 | this.klass.before_should_callbacks[this._methodName].call(this); 60 | 61 | this.runCallbacks('before', 'each', resume); 62 | }, 63 | 64 | teardown: function(resume) { 65 | this.runCallbacks('after', 'each', resume); 66 | }, 67 | 68 | runCallbacks: function(callbackType, period, continuation) { 69 | var callbacks = this.klass.gatherCallbacks(callbackType, period); 70 | 71 | Test.Unit.TestSuite.forEach(callbacks, function(callback, resume) { 72 | this.exec(callback, resume, continuation); 73 | }, continuation, this); 74 | }, 75 | 76 | runAllCallbacks: function(callbackType, continuation, context) { 77 | var previousIvars = this.instanceVariables(); 78 | this.runCallbacks(callbackType, 'all', function() { 79 | 80 | var ivars = this.instanceVariables().inject({}, function(hash, ivar) { 81 | if (previousIvars.member(ivar)) return hash; 82 | hash[ivar] = this[ivar]; 83 | return hash; 84 | }, this); 85 | 86 | if (continuation) continuation.call(context, ivars); 87 | }); 88 | }, 89 | 90 | setValuesFromCallbacks: function(values) { 91 | for (var key in values) 92 | this[key] = values[key]; 93 | }, 94 | 95 | instanceVariables: function() { 96 | var ivars = []; 97 | for (var key in this) { 98 | if (this.hasOwnProperty(key)) ivars.push(key); 99 | } 100 | return new Enumerable.Collection(ivars); 101 | } 102 | }); 103 | 104 | (function() { 105 | var m = Test.Context.LifeCycle.ClassMethods.method('instanceMethod'); 106 | 107 | Test.Context.LifeCycle.ClassMethods.include({ 108 | setup: m('before'), 109 | teardown: m('after') 110 | }); 111 | })(); 112 | 113 | --------------------------------------------------------------------------------