'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". description Multi-line description with optional markup See Ox.sanitizeHTML for details events <[o]|u> Events (array of doc objects) Present if the item fires any events file File name 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) 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 tests <[o]> Tests (array of test objects) expected Expected result statement Statement type Type 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: /^(.+?) <(.+?)> (.+?)$/, 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, isAsync = regexp.test(test.statement); if (isAsync) { // Add a pending test Ox.test.data[id].tests[test.statement] = { name: item.name, section: item.section }; // Patch the test statement test.statement = test.statement.replace( regexp, "$1'" + test.statement.replace(/'/g, "\\'") + "', " ); } if (test.expected || test.statement.match(/Ox\.test\./)) { // Eval the statement, unless it's a script tag that doesn't // add a property to Ox.test Ox.Log('TEST', test.statement); actual = eval(test.statement); } if (!isAsync && test.expected) { Ox.test.data[id].results.push({ actual: JSON.stringify(actual), expected: test.expected, name: item.name, section: item.section, statement: test.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); } } 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], result = 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]; Ox.Break(); } }); Ox.test.data[id].results.push(Ox.extend(test, { actual: result, expected: expected, statement: statement, passed: Ox.isEqual(result, 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); } } }; 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/g;')[2].value.length 6 @*/ // FIXME: numbers (hex, exp, etc.) 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) { type = 'number'; while ((number + '.').indexOf(source[++cursor]) > -1) {} } 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) { 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; }; }());