'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 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 summary One-line summary usage <[o]> Usage (array of doc objects) Present if the type of the item is "function". 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].usage[0].types ['number'] > Ox.test.doc[1].usage[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) { Ox.print('Ox.test', argument, '----------------------------------') function runTests(items) { var id = Ox.uid(), regexp = /(.+Ox\.test\()/, results = []; 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) { Ox.test.data[id].tests[test.statement] = { name: item.name, section: item.section }; Ox.Log('TEST', 'XXX', test.statement); test.statement = test.statement.replace( regexp, "$1'" + test.statement.replace(/'/g, "\\'") + "', " ); } if (test.expected || test.statement.match(/Ox\.test\./)) { // don't eval script tags without assignment to Ox.test.foo 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')) { runTests(Ox.doc(argument)) } else { argument = Ox.makeArray(argument); if (Ox.typeOf(argument[0]) == 'string') { Ox.doc(argument, runTests); } else { 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; }; }());