'use strict'; /*@ Ox.doc Generates documentation for annotated JavaScript (source) -> <[o]> Array of doc objects (file, callback) -> undefined (files, callback) -> undefined source JavaScript source code file JavaScript file files <[s]> Array of JavaScript files callback Callback function doc <[o]> Array of doc objects arguments <[o]|u> Arguments (array of doc objects) Present if the `type` of the item is `"function"`. class Class of the item default Default value of the item description Multi-line description with some Markdown See Ox.parseMarkdown for details events <[o]|u> Events (array of doc objects) Present if the item fires any events file File name inheritedevents <[o]|u> Inherited events (array of doc objects) Present if the item has a class, and any item in its inheritance chain fires events inheritedproperties <[o]|u> Inherited properties (array of doc objects) Present if the item has a class, and any item in its inheritance chain has (unshadowed) properties line Line number name Name of the item order <[s]> Order of returns, arguments, properties Present if the type of the item is "function" properties <[o]|u> Properties (array of doc objects) May be present if the `type` of the item is `"event"`, `"function"` or `"object"`. section Section in the file source <[o]> Source code (array of tokens) column Column line Line type Type (see Ox.tokenize for a list of types) value Value returns <[o]> Return values (array of doc objects) Present if the `type` of the item is `"function"`. summary One-line summary, with some Markdown See Ox.parseMarkdown for details tests <[o]|u> Tests (array of test objects) expected Expected result statement Statement types <[s]> Types of the item > Ox.test.doc[0].name 'My.FOO' > Ox.test.doc[0].types ['number'] > Ox.test.doc[0].summary 'Magic constant' > Ox.test.doc[1].description 'Bar per baz is a good indicator of an item\'s foo-ness.' > Ox.test.doc[1].returns[0].types ['number'] > Ox.test.doc[1].returns[0].summary 'Bar per baz, or NaN' > Ox.test.doc[1].tests[1] {expected: 'NaN', statement: 'My.foo({})'} @*/ Ox.doc = (function() { var re = { item: /^(.+?)\s+<(.+?)>\s+(.+?)$/, multiline: /^\/\*\@.*?\n([\w\W]+)\n.*?\@?\*\/$/, script: /\n(\s* > Ox.test(Ox.test.source, function(r) { Ox.test(r[0].passed, true); }) undefined @*/ Ox.test = function(argument, callback) { // Ansynchronous functions can be tested by calling Ox.test(actual, // expected) in the callback. If Ox.test is called inside a test statement // (unless at the beginning of the statement, which is a test for Ox.test), // the call to Ox.test is patched by inserting the test statement string as // the first argument of the Ox.test call, and Ox.test will branch when // called with three arguments. function runTests(items) { var id = Ox.uid(), regexp = /(.+Ox\.test\()/, results = []; // We have to create a globally accessible object so that synchronous // and asynchronous tests can read, write and return the same data. Ox.test.data[id] = { callback: callback, done: false, results: results, tests: {} }; items.forEach(function(item) { item.tests && item.tests.some(function(test) { return test.expected; }) && item.tests.forEach(function(test) { var actual, statement = test.statement, isAsync = regexp.test(statement); if (isAsync) { // Add a pending test Ox.test.data[id].tests[test.statement] = { expected: test.expected, name: item.name, section: item.section }; // Patch the test statement statement = statement.replace( regexp, "$1'" + statement.replace(/'/g, "\\'") + "', " ); } Ox.Log('TEST', statement); actual = eval(statement); if (!isAsync && test.expected) { Ox.test.data[id].results.push({ actual: stringifyResult(actual), expected: test.expected, name: item.name, section: item.section, statement: statement, passed: Ox.isEqual( actual, eval('(' + test.expected + ')') ) }); } }); }); Ox.test.data[id].done = true; if (Ox.isEmpty(Ox.test.data[id].tests)) { callback(Ox.test.data[id].results); delete Ox.test.data[id]; } } function stringifyResult(result) { return Ox.isEqual(result, -0) ? '-0' : Ox.isNaN(result) ? 'NaN' : Ox.isUndefined(result) ? 'undefined' : JSON.stringify(result); } if (arguments.length == 2) { if (Ox.typeOf(argument) == 'string' && Ox.contains(argument, '\n')) { // source code runTests(Ox.doc(argument)); } else { argument = Ox.makeArray(argument); if (Ox.typeOf(argument[0]) == 'string') { // files Ox.doc(argument, runTests); } else { // doc objects runTests(argument); } } } else { var statement = arguments[0], actual = arguments[1], expected = arguments[2], id, test; Ox.forEach(Ox.test.data, function(v, k) { if (v.tests[statement]) { id = k; test = v.tests[statement]; return false; // break } }); Ox.test.data[id].results.push(Ox.extend(test, { actual: stringifyResult(actual), statement: statement, passed: Ox.isEqual(actual, expected) })); delete Ox.test.data[id].tests[statement]; if (Ox.test.data[id].done && Ox.isEmpty(Ox.test.data[id].tests)) { Ox.test.data[id].callback(Ox.test.data[id].results); delete Ox.test.data[id]; } } }; Ox.test.data = {}; /*@ Ox.tokenize Tokenizes JavaScript (source) -> <[o]> Array of tokens column Column of the token line Line of the token type Type of the token Type can be `'comment'`, `'error'`, `'identifier'`, `'linebreak'`, `'number'`, `'operator'`, `'regexp'`, `'string'` or `'whitespace'` value Value of the token source JavaScript source code > Ox.tokenize('// comment\nvar foo = bar / baz;').length 14 > Ox.tokenize('return /foo/gi;')[2].value.length 7 > Ox.tokenize('[.1, 0xFF, 1e1, 1e+1, 1e-1, 1e+1+1]').length 20 @*/ Ox.tokenize = (function() { // see https://github.com/mozilla/narcissus/blob/master/lib/lexer.js var comment = ['//', '/*'], identifier = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_', linebreak = '\n\r', number = '0123456789', operator = [ // arithmetic '+', '-', '*', '/', '%', '++', '--', // assignment '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', // bitwise '&', '|', '^', '~', '<<', '>>', '>>>', // comparison '==', '!=', '===', '!==', '>', '>=', '<', '<=', // conditional '?', ':', // grouping '(', ')', '[', ']', '{', '}', // logical '&&', '||', '!', // other '.', ',', ';' ], regexp = 'abcdefghijklmnopqrstuvwxyz', string = '\'"', whitespace = ' \t'; function isRegExp(tokens) { // Returns true if the current token is the beginning of a RegExp, as // opposed to the beginning of an operator var i = tokens.length - 1, isRegExp, token // Scan back to the previous significant token, or to the beginning of // the source while (i >= 0 && [ 'comment', 'linebreak', 'whitespace' ].indexOf(tokens[i].type) > -1) { i--; } if (i == -1) { // Source begins with a forward slash isRegExp = true; } else { token = tokens[i]; isRegExp = ( token.type == 'identifier' && Ox.identify(token.value) == 'keyword' && ['false', 'null', 'true'].indexOf(token.value) == -1 ) || ( token.type == 'operator' && ['++', '--', ')', ']', '}'].indexOf(token.value) == -1 ); } return isRegExp; } return function(source) { var char, column = 1, cursor = 0, delimiter, length = source.length, line = 1, lines, next, tokens = [], start, type, value; source = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); while (cursor < length) { start = cursor; char = source[cursor]; if (comment.indexOf(delimiter = char + source[cursor + 1]) > -1) { type = 'comment'; ++cursor; while (char = source[++cursor]) { if (delimiter == '//' && char == '\n') { break; } else if (delimiter == '/*' && char + source[cursor + 1] == '*/') { cursor += 2; break; } } } else if (identifier.indexOf(char) > -1) { type = 'identifier'; while ((identifier + number).indexOf(source[++cursor]) > -1) {} } else if (linebreak.indexOf(char) > -1) { type = 'linebreak'; while (linebreak.indexOf(source[++cursor]) > -1) {} } else if ( number.indexOf(char) > -1 || char == '.' && number.indexOf(source[cursor + 1]) > -1 ) { type = 'number'; while ((number + '.abcdefxABCDEFX+-').indexOf(source[++cursor]) > -1) { if ( source[cursor - 1] != 'e' && source[cursor - 1] != 'E' && (source[cursor] == '+' || source[cursor] == '-') ) { break; } } } else if (char == '/' && isRegExp(tokens)) { type = 'regexp'; while ((char = source[++cursor]) != '/' && cursor < length) { char == '\\' && ++cursor; } while (regexp.indexOf(source[++cursor]) > -1) {} } else if (operator.indexOf(char) > -1) { // has to be tested after number and regexp type = 'operator'; while (operator.indexOf(char += source[++cursor]) > -1 && cursor < length) {} } else if (string.indexOf(delimiter = char) > -1) { type = 'string'; while ((char = source[++cursor]) != delimiter && cursor < length) { char == '\\' && ++cursor; } ++cursor; } else if (whitespace.indexOf(char) > -1) { type = 'whitespace'; while (whitespace.indexOf(source[++cursor]) > -1) {} } else { type = 'error'; ++cursor; } value = source.slice(start, cursor); if ( type == 'error' && tokens.length && tokens[tokens.length - 1].type == 'error' ) { tokens[tokens.length - 1].value += value; } else { tokens.push( {column: column, line: line, type: type, value: value} ); } if (type == 'comment') { lines = value.split('\n'); column = lines[lines.length - 1].length; line += lines.length - 1; } else if (type == 'linebreak') { column = 1; line += value.length; } else { column += value.length; } } return tokens; }; }());