1048 lines
39 KiB
JavaScript
1048 lines
39 KiB
JavaScript
'use strict';
|
|
|
|
/*@
|
|
Ox.doc <f> Generates documentation for annotated JavaScript
|
|
(source) -> <[o]> Array of doc objects
|
|
(file, callback) -> <u> undefined
|
|
(files, callback) -> <u> undefined
|
|
source <s> JavaScript source code
|
|
file <s> JavaScript file
|
|
files <[s]> Array of JavaScript files
|
|
callback <f> 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 <s|u> Class of the item
|
|
default <s|u> Default value of the item
|
|
description <s|u> 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 <s> 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 <n> Line number
|
|
name <s> 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 <s|u> Section in the file
|
|
source <[o]> Source code (array of tokens)
|
|
column <n> Column
|
|
line <n> Line
|
|
type <s> Type (see Ox.tokenize for a list of types)
|
|
value <s> Value
|
|
returns <[o]> Return values (array of doc objects)
|
|
Present if the `type` of the item is `"function"`.
|
|
summary <s> One-line summary, with some Markdown
|
|
See Ox.parseMarkdown for details
|
|
tests <[o]|u> Tests (array of test objects)
|
|
expected <s> Expected result
|
|
statement <s> Statement
|
|
types <[s]> Types of the item
|
|
<script>
|
|
Ox.test.doc = Ox.doc([
|
|
'//@ My.FOO <n> Magic constant',
|
|
'My.FOO = 23;',
|
|
'',
|
|
'/*@',
|
|
'My.foo <f> Returns an items\'s bar per baz',
|
|
' Bar per baz is a good indicator of an item\'s foo-ness.',
|
|
' (item) -> <n> Bar per baz, or NaN',
|
|
' item <o> Any item',
|
|
' > My.foo({bar: 1, baz: 10})',
|
|
' 0.1',
|
|
' > My.foo({})',
|
|
' NaN',
|
|
'@*' + '/',
|
|
'My.foo = function(item) {',
|
|
' return item.bar / item.baz;',
|
|
'};'
|
|
].join('\n'));
|
|
</script>
|
|
> 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*<script>s*\n[\w\W]+\n\s*<\/script>s*)/g,
|
|
signature: /^(\(.*?\))\s+\->(.*)/,
|
|
singleline: /^\/\/@\s*(.*?)\s*$/,
|
|
test: /\n(\s*> .+\n.+?)/g,
|
|
},
|
|
types = {
|
|
a: 'array', b: 'boolean', d: 'date', e: 'error', f: 'function',
|
|
g: 'arguments', h: 'htmlelement', 'l': 'nodelist', n: 'number',
|
|
o: 'object', r: 'regexp', s: 'string', u: 'undefined',
|
|
'w': 'window', '*': 'any', '+': 'other', '!': 'event'
|
|
};
|
|
function addInheritedProperties(items) {
|
|
var constructors = getConstructors(items), instances = {}, nodes = {};
|
|
function hasProperty(item, property) {
|
|
var properties = item.properties || [],
|
|
inheritedproperties = item.inheritedproperties
|
|
? item.inheritedproperties.map(function(v) {
|
|
return v.properties;
|
|
})
|
|
: [];
|
|
return Ox.contains(
|
|
properties.concat(inheritedproperties).map(function(property) {
|
|
return property.name;
|
|
}),
|
|
property.name
|
|
);
|
|
}
|
|
var foo = {};
|
|
constructors.forEach(function(constructor) {
|
|
var instance = Ox.last(constructor.returns);
|
|
instances[constructor.name] = instance;
|
|
nodes[constructor.name] = instance['class'];
|
|
});
|
|
Ox.forEach(getChains(nodes), function(chain, childName) {
|
|
var child = instances[childName];
|
|
chain.forEach(function(parentName) {
|
|
var parent = instances[parentName]
|
|
|| Ox.last(items[Ox.indexOf(items, function(item) {
|
|
return item.name == parentName;
|
|
})].returns);
|
|
['properties', 'events'].forEach(function(key) {
|
|
parent[key] && parent[key].forEach(function(value) {
|
|
var key_ = 'inherited' + key;
|
|
if (key == 'events' || !hasProperty(child, value)) {
|
|
if (!child[key_]) {
|
|
child[key_] = [];
|
|
}
|
|
if (!child[key_].some(function(v) {
|
|
return v.name == parentName;
|
|
})) {
|
|
child[key_].push(
|
|
Ox.extend({name: parentName}, key, [])
|
|
);
|
|
}
|
|
Ox.last(child[key_])[key].push(value);
|
|
}
|
|
})
|
|
});
|
|
});
|
|
});
|
|
return items;
|
|
}
|
|
function decodeLinebreaks(match, submatch) {
|
|
return (submatch || match).replace(/\u21A9/g, '\n');
|
|
}
|
|
function encodeLinebreaks(match, submatch) {
|
|
return '\n' + (submatch || match).replace(/\n/g, '\u21A9');
|
|
}
|
|
function getChains(nodes) {
|
|
var chains = {}, sorted = [], visited = [];
|
|
function visit(name, stack) {
|
|
stack = stack || [];
|
|
if (Ox.contains(stack, name)) {
|
|
throw new Error(
|
|
'Circular dependency: ' + name + ' <-> ' + Ox.last(stack)
|
|
);
|
|
}
|
|
if (!Ox.contains(visited, name)) {
|
|
visited.push(name);
|
|
stack.push(name)
|
|
Ox.forEach(nodes, function(parent, name_) {
|
|
parent == name && visit(name_, stack);
|
|
});
|
|
sorted.unshift(name);
|
|
}
|
|
}
|
|
Ox.forEach(nodes, function(parent, name) {
|
|
visit(name);
|
|
});
|
|
sorted.forEach(function(name) {
|
|
chains[name] = [nodes[name]].concat(chains[nodes[name]] || [])
|
|
});
|
|
return chains;
|
|
}
|
|
function getConstructors(items) {
|
|
var constructors = [];
|
|
items.forEach(function(item) {
|
|
if (item.returns) {
|
|
Ox.forEach(item.returns, function(v) {
|
|
if (v['class']) {
|
|
constructors.push(item);
|
|
Ox.Break();
|
|
}
|
|
});
|
|
}
|
|
['arguments', 'properties', 'returns'].forEach(function(key) {
|
|
if (item[key]) {
|
|
constructors.concat(getConstructors(item[key]));
|
|
}
|
|
});
|
|
});
|
|
return constructors;
|
|
}
|
|
function getIndent(string) {
|
|
var indent = -1;
|
|
while (string[++indent] == ' ') {}
|
|
return indent;
|
|
}
|
|
function parseItem(string) {
|
|
var matches = re.item.exec(string);
|
|
// to tell a variable with default value, like
|
|
// name <string|'<a href="...">foo</a>'> summary
|
|
// from a line of description with tags, like
|
|
// some <a href="...">description</a> text
|
|
// we need to check if there is either no forward slash
|
|
// or if the second last char is a single or double quote
|
|
return matches && (
|
|
matches[2].indexOf('/') == -1 ||
|
|
'\'"'.indexOf(matches[2].slice(-2, -1)) > -1
|
|
) ? Ox.extend(
|
|
parseName(matches[1]),
|
|
parseTypes(matches[2]),
|
|
{summary: matches[3].trim()}
|
|
) : null;
|
|
}
|
|
function parseName(string) {
|
|
var matches = re.signature.exec(string);
|
|
return matches
|
|
? {signature: matches[1], name: matches[2].trim()}
|
|
: {name: string};
|
|
}
|
|
function parseNode(node) {
|
|
var item = parseItem(node.line), order = [];
|
|
item.name = item.name.replace(/^\./, '');
|
|
node.nodes && node.nodes.forEach(function(node) {
|
|
var key, line = node.line, subitem;
|
|
if (!/^#/.test(node.line)) {
|
|
if (/^<script>/.test(line)) {
|
|
item.tests = [parseScript(line)];
|
|
} else if (/^>/.test(line)) {
|
|
item.tests = item.tests || [];
|
|
item.tests.push(parseTest(line));
|
|
} else if ((subitem = parseItem(line))) {
|
|
if (subitem.signature) {
|
|
item.returns = item.returns || [];
|
|
item.returns.push(parseNode(node));
|
|
order.push('returns');
|
|
} else if (subitem.types[0] == 'event') {
|
|
item.events = item.events || [];
|
|
item.events.push(parseNode(node));
|
|
order.push('events');
|
|
} else {
|
|
key = item.types[0] == 'function'
|
|
&& !/^\./.test(subitem.name)
|
|
? 'arguments' : 'properties';
|
|
item[key] = item[key] || [];
|
|
item[key].push(parseNode(node));
|
|
order.push(key);
|
|
}
|
|
} else {
|
|
item.description = item.description
|
|
? item.description + ' ' + line : line
|
|
}
|
|
}
|
|
});
|
|
item.summary = Ox.parseMarkdown(item.summary);
|
|
if (item.description) {
|
|
item.description = Ox.parseMarkdown(item.description)
|
|
}
|
|
if (item.types[0] == 'function') {
|
|
item.order = Ox.unique(order);
|
|
}
|
|
return item;
|
|
}
|
|
function parseScript(string) {
|
|
// remove script tags and extra indentation
|
|
var lines = decodeLinebreaks(string).split('\n'),
|
|
indent = getIndent(lines[1]);
|
|
return {
|
|
statement: lines.slice(1, -1).map(function(line, i) {
|
|
return line.slice(indent);
|
|
}).join('\n')
|
|
};
|
|
}
|
|
function parseSource(source, file) {
|
|
var blocks = [],
|
|
items = [],
|
|
section = '',
|
|
tokens = [];
|
|
Ox.tokenize(source).forEach(function(token) {
|
|
var match;
|
|
if (token.type == 'comment' && (
|
|
match = re.multiline.exec(token.value)
|
|
|| re.singleline.exec(token.value)
|
|
)) {
|
|
blocks.push(match[1]);
|
|
tokens.push([]);
|
|
} else if (tokens.length) {
|
|
tokens[tokens.length - 1].push(token);
|
|
}
|
|
});
|
|
blocks.forEach(function(block, i) {
|
|
var item, lastItem, lastToken,
|
|
lines = block
|
|
.replace(re.script, encodeLinebreaks)
|
|
.replace(re.test, encodeLinebreaks)
|
|
.split('\n'),
|
|
parent, tree = parseTree(lines);
|
|
if (re.item.test(tree.line)) {
|
|
// parse the tree's root node
|
|
item = parseNode(tree);
|
|
item.file = file || '';
|
|
if (section) {
|
|
item.section = section;
|
|
}
|
|
if (/^[A-Z]/.test(item.name)) {
|
|
// main item
|
|
// remove leading linebreaks and whitespace
|
|
item.source = parseTokens(tokens[i]);
|
|
item.line = item.source[0].line;
|
|
items.push(item);
|
|
} else {
|
|
// property of an item
|
|
lastItem = Ox.last(items);
|
|
parent = lastItem.types[0] == 'function'
|
|
&& lastItem.returns
|
|
&& lastItem.returns[0].types[0] == 'object'
|
|
? lastItem.returns[0] : lastItem;
|
|
parent.properties = parent.properties || [];
|
|
parent.properties.push(item);
|
|
if (
|
|
parent.order && !Ox.contains(parent.order, 'properties')
|
|
) {
|
|
parent.order.push('properties');
|
|
}
|
|
// add a linebreak, and the property's source without
|
|
// leading whitespace, to the last item's source
|
|
lastToken = Ox.last(lastItem.source);
|
|
lastItem.source = lastItem.source.concat(
|
|
{
|
|
column: lastToken.column + lastToken.value.length,
|
|
line: lastToken.line,
|
|
type: 'linebreak',
|
|
value: '\n'
|
|
},
|
|
parseTokens(tokens[i], true)
|
|
);
|
|
}
|
|
} else {
|
|
section = tree.line.split(' ')[0]
|
|
}
|
|
});
|
|
return items;
|
|
}
|
|
function parseTest(string) {
|
|
// fixme: we cannot properly handle tests where a string contains '\n '
|
|
var lines = decodeLinebreaks(string).split('\n ');
|
|
return {
|
|
statement: lines[0].slice(2),
|
|
expected: lines[1].trim()
|
|
};
|
|
}
|
|
function parseTokens(tokens, includeLeading) {
|
|
// removes leading and trailing linebreaks and whitespace
|
|
var start = 0, stop = tokens.length,
|
|
types = ['linebreak', 'whitespace'];
|
|
if (includeLeading) {
|
|
// for properties, whose tokens belong to the main item, we only
|
|
// remove leading whitespace
|
|
while (tokens[start].type == 'whitespace') {
|
|
start++;
|
|
}
|
|
} else {
|
|
// remove leading linebreaks and whitespace
|
|
while (types.indexOf(tokens[start].type) > -1) {
|
|
start++;
|
|
}
|
|
// but keep leading whitespace in the first significant line
|
|
while (start && tokens[start - 1].type == 'whitespace') {
|
|
start--;
|
|
}
|
|
}
|
|
// remove trailing linebreaks and whitespace
|
|
while (stop > start && types.indexOf(tokens[stop - 1].type) > -1) {
|
|
stop--;
|
|
}
|
|
return tokens.slice(start, stop);
|
|
}
|
|
function parseTree(lines) {
|
|
// parses indented lines into a tree structure, like
|
|
// {line: "...", nodes: [{line: "...", nodes: [...]}]}
|
|
var branches = [],
|
|
indent,
|
|
node = {
|
|
// chop the root line
|
|
line: lines.shift().trim()
|
|
};
|
|
if (lines.length) {
|
|
indent = getIndent(lines[0]);
|
|
lines.forEach(function(line) {
|
|
if (getIndent(line) == indent) {
|
|
// line is a child,
|
|
// make it the root line of a new branch
|
|
branches.push([line]);
|
|
} else {
|
|
// line is a descendant of the last child,
|
|
// add it to the last branch
|
|
branches[branches.length - 1].push(line);
|
|
}
|
|
});
|
|
node.nodes = branches.map(function(lines) {
|
|
return parseTree(lines);
|
|
});
|
|
}
|
|
return node;
|
|
}
|
|
function parseTypes(string) {
|
|
// returns {types: [""]}
|
|
// or {types: [""], default: ""}
|
|
// or {types: [""], class: ""}
|
|
var array,
|
|
isArray,
|
|
ret = {types: []},
|
|
type;
|
|
// only split by ':' if there is no default string value
|
|
if ('\'"'.indexOf(string.slice(-2, -1)) == -1) {
|
|
array = string.split(':');
|
|
string = array[0];
|
|
if (array.length == 2) {
|
|
ret['class'] = array[1];
|
|
}
|
|
}
|
|
string.split('|').forEach(function(string) {
|
|
var unwrapped = unwrap(string);
|
|
if (unwrapped in types) {
|
|
ret.types.push(wrap(types[unwrapped]))
|
|
} else if (
|
|
(type = Ox.filter(Ox.values(types), function(type) {
|
|
return Ox.startsWith(type, unwrapped);
|
|
})).length
|
|
) {
|
|
ret.types.push(wrap(type[0]));
|
|
} else {
|
|
ret['default'] = string;
|
|
}
|
|
});
|
|
function unwrap(string) {
|
|
return (isArray = /^\[.+\]$/.test(string))
|
|
? string.slice(1, -1) : string;
|
|
}
|
|
function wrap(string) {
|
|
return isArray ? '[' + string + 's' + ']' : string;
|
|
}
|
|
return ret;
|
|
}
|
|
return function(argument, callback) {
|
|
var counter = 0, items = [], ret;
|
|
if (arguments.length == 1) {
|
|
// source
|
|
ret = addInheritedProperties(parseSource(argument));
|
|
} else {
|
|
// file(s)
|
|
argument = Ox.makeArray(argument);
|
|
argument.forEach(function(file) {
|
|
Ox.get(file, function(source) {
|
|
items = items.concat(
|
|
parseSource(source, file.split('?')[0])
|
|
);
|
|
if (++counter == argument.length) {
|
|
callback(addInheritedProperties(items));
|
|
}
|
|
});
|
|
})
|
|
}
|
|
return ret;
|
|
}
|
|
}());
|
|
|
|
/*@
|
|
Ox.identify <f> Returns the type of a JavaScript identifier
|
|
(string) -> <s> Type
|
|
Type can be `constant`, `identifier`, `keyword`, `method`, `object` or
|
|
`property`.
|
|
> Ox.identify('foo')
|
|
'identifier'
|
|
> Ox.identify('break')
|
|
'keyword'
|
|
> Ox.identify('window')
|
|
'object'
|
|
@*/
|
|
Ox.identify = (function() {
|
|
// see https://developer.mozilla.org/en/JavaScript/Reference
|
|
var identifiers = {
|
|
constant: [
|
|
// Math
|
|
'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'PI', 'SQRT1_2', 'SQRT2',
|
|
// Number
|
|
'MAX_VALUE', 'MIN_VALUE', 'NEGATIVE_INFINITY', 'POSITIVE_INFINITY'
|
|
],
|
|
keyword: [
|
|
'break',
|
|
'case', 'catch', 'class', 'const', 'continue',
|
|
'debugger', 'default', 'delete', 'do',
|
|
'else', 'enum', 'export', 'extends',
|
|
'false', 'finally', 'for', 'function',
|
|
'if', 'implements', 'import', 'in', 'instanceof', 'interface',
|
|
'let', 'module',
|
|
'new', 'null',
|
|
'package', 'private', 'protected', 'public',
|
|
'return',
|
|
'super', 'switch', 'static',
|
|
'this', 'throw', 'true', 'try', 'typeof',
|
|
'var', 'void',
|
|
'yield',
|
|
'while', 'with'
|
|
],
|
|
method: [
|
|
// Array
|
|
'concat',
|
|
'every',
|
|
'filter', 'forEach',
|
|
'join',
|
|
'lastIndexOf',
|
|
'indexOf', 'isArray',
|
|
'map',
|
|
'pop', 'push',
|
|
'reduce', 'reduceRight', 'reverse',
|
|
'shift', 'slice', 'some', 'sort', 'splice',
|
|
'unshift',
|
|
// Date
|
|
'getDate', 'getDay', 'getFullYear', 'getHours',
|
|
'getMilliseconds', 'getMinutes', 'getMonth', 'getSeconds',
|
|
'getTime', 'getTimezoneOffset',
|
|
'getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours',
|
|
'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds',
|
|
'now',
|
|
'parse',
|
|
'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
|
|
'setMinutes', 'setMonth', 'setSeconds', 'setTime',
|
|
'setUTCDate', 'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds',
|
|
'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
|
|
'toDateString', 'toJSON', 'toLocaleDateString', 'toLocaleString',
|
|
'toLocaleTimeString', 'toTimeString', 'toUTCString',
|
|
'UTC',
|
|
// Function
|
|
'apply', 'bind', 'call', 'isGenerator',
|
|
// JSON
|
|
'parse', 'stringify',
|
|
// Math
|
|
'abs', 'acos', 'asin', 'atan', 'atan2',
|
|
'ceil', 'cos',
|
|
'exp',
|
|
'floor',
|
|
'log',
|
|
'max', 'min',
|
|
'pow',
|
|
'random', 'round',
|
|
'sin', 'sqrt',
|
|
'tan',
|
|
// Number
|
|
'toExponential', 'toFixed', 'toLocaleString', 'toPrecision',
|
|
// Object
|
|
'create',
|
|
'defineProperty', 'defineProperties',
|
|
'freeze',
|
|
'getOwnPropertyDescriptor', 'getOwnPropertyNames', 'getPrototypeOf',
|
|
'hasOwnProperty',
|
|
'isExtensible', 'isFrozen', 'isPrototypeOf', 'isSealed',
|
|
'keys',
|
|
'preventExtensions', 'propertyIsEnumerable',
|
|
'seal',
|
|
'toLocaleString', 'toString',
|
|
'valueOf',
|
|
// RegExp
|
|
'exec', 'test',
|
|
// String
|
|
'charAt', 'charCodeAt', 'concat',
|
|
'fromCharCode',
|
|
'indexOf',
|
|
'lastIndexOf', 'localeCompare',
|
|
'match',
|
|
'replace',
|
|
'search', 'slice', 'split', 'substr', 'substring',
|
|
'toLocaleLowerCase', 'toLocaleUpperCase',
|
|
'toLowerCase', 'toUpperCase', 'trim',
|
|
// Window
|
|
'addEventListener', 'alert', 'atob',
|
|
'blur', 'btoa',
|
|
'clearInterval', 'clearTimeout', 'close', 'confirm',
|
|
'dispatchEvent',
|
|
'escape',
|
|
'find', 'focus',
|
|
'getComputedStyle', 'getSelection',
|
|
'moveBy', 'moveTo',
|
|
'open',
|
|
'postMessage', 'print', 'prompt',
|
|
'removeEventListener', 'resizeBy', 'resizeTo',
|
|
'scroll', 'scrollBy', 'scrollTo',
|
|
'setCursor', 'setInterval', 'setTimeout', 'stop',
|
|
'unescape'
|
|
],
|
|
object: [
|
|
'Array',
|
|
'Boolean',
|
|
'Date', 'decodeURI', 'decodeURIComponent',
|
|
'encodeURI', 'encodeURIComponent', 'Error', 'eval', 'EvalError',
|
|
'Function',
|
|
'Infinity', 'isFinite', 'isNaN',
|
|
'JSON',
|
|
'Math',
|
|
'NaN', 'Number',
|
|
'Object',
|
|
'parseFloat', 'parseInt',
|
|
'RangeError', 'ReferenceError', 'RegExp',
|
|
'String', 'SyntaxError',
|
|
'TypeError',
|
|
'undefined', 'URIError',
|
|
'window'
|
|
],
|
|
property: [
|
|
// Function
|
|
'constructor', 'length', 'prototype',
|
|
// RegExp
|
|
'global', 'ignoreCase', 'lastIndex', 'multiline', 'source',
|
|
// Window
|
|
'applicationCache',
|
|
'closed', 'console', 'content', 'crypto',
|
|
'defaultStatus', 'document',
|
|
'frameElement', 'frames',
|
|
'history',
|
|
'innerHeight', 'innerWidth',
|
|
'length', 'location', 'locationbar', 'localStorage',
|
|
'menubar',
|
|
'name', 'navigator',
|
|
'opener', 'outerHeight', 'outerWidth',
|
|
'pageXOffset', 'pageYOffset', 'parent', 'personalbar',
|
|
'screen', 'screenX', 'screenY', 'scrollbars', 'scrollX', 'scrollY',
|
|
'self', 'sessionStorage', 'status', 'statusbar',
|
|
'toolbar', 'top'
|
|
]
|
|
};
|
|
return function(identifier) {
|
|
var ret;
|
|
if (identifiers.keyword.indexOf(identifier) > -1) {
|
|
// fast track for keywords (used in Ox.tokenize)
|
|
ret = 'keyword';
|
|
} else {
|
|
ret = 'identifier';
|
|
Ox.forEach(identifiers, function(words, type) {
|
|
if (words.indexOf(identifier) > -1) {
|
|
ret = type;
|
|
Ox.Break();
|
|
}
|
|
});
|
|
}
|
|
return ret;
|
|
};
|
|
}());
|
|
|
|
/*@
|
|
Ox.minify <f> Minifies JavaScript
|
|
(source) -> <s> Minified JavaScript
|
|
(file, callback) -> <u> undefined
|
|
source <s> JavaScript source
|
|
file <s> JavaScript file
|
|
callback <f> Callback function
|
|
> Ox.minify('for (a in b)\n{\t\tc = void 0;\n}')
|
|
'for(a in b)\n{c=void 0;}'
|
|
> Ox.minify('return a; return 0; return "";')
|
|
'return a;return 0;return"";'
|
|
> Ox.minify('return\na;\nreturn\n0;\nreturn\n"";')
|
|
'return\na;return\n0;return\n"";'
|
|
@*/
|
|
Ox.minify = function() {
|
|
// see https://github.com/douglascrockford/JSMin/blob/master/README
|
|
// and http://inimino.org/~inimino/blog/javascript_semicolons
|
|
if (arguments.length == 1) {
|
|
return minify(arguments[0]);
|
|
} else {
|
|
Ox.get(arguments[0], function(source) {
|
|
arguments[1](minify(source));
|
|
});
|
|
}
|
|
function minify(source) {
|
|
var tokens = Ox.tokenize(source),
|
|
length = tokens.length,
|
|
ret = '';
|
|
tokens.forEach(function(token, i) {
|
|
var next, nextToken, prevToken;
|
|
if (['linebreak', 'whitespace'].indexOf(token.type) > -1) {
|
|
prevToken = i == 0 ? null : tokens[i - 1];
|
|
next = i + 1;
|
|
while (
|
|
next < length && ['comment', 'linebreak', 'whitespace']
|
|
.indexOf(tokens[next].type) > -1
|
|
) {
|
|
next++;
|
|
}
|
|
nextToken = next == length ? null : tokens[next];
|
|
}
|
|
if (token.type == 'linebreak') {
|
|
// replace a linebreak between two tokens that are identifiers
|
|
// or numbers or strings or unary operators or grouping
|
|
// operators with a single newline, otherwise remove it
|
|
if (
|
|
prevToken && nextToken && (
|
|
['identifier', 'number', 'string'].indexOf(prevToken.type) > -1
|
|
|| ['++', '--', ')', ']', '}'].indexOf(prevToken.value) > -1
|
|
) && (
|
|
['identifier', 'number', 'string'].indexOf(nextToken.type) > -1
|
|
|| ['+', '-', '++', '--', '~', '!', '(', '[', '{'].indexOf(nextToken.value) > -1
|
|
)
|
|
) {
|
|
ret += '\n';
|
|
}
|
|
} else if (token.type == 'whitespace') {
|
|
// replace whitespace between two tokens that are identifiers or
|
|
// numbers, or between a token that ends with "+" or "-" and one
|
|
// that begins with "+" or "-", with a single space, otherwise
|
|
// remove it
|
|
if (
|
|
prevToken && nextToken && ((
|
|
['identifier', 'number'].indexOf(prevToken.type) > -1
|
|
&& ['identifier', 'number'].indexOf(nextToken.type) > -1
|
|
) || (
|
|
['+', '-', '++', '--'].indexOf(prevToken.value) > -1
|
|
&& ['+', '-', '++', '--'].indexOf(nextToken.value) > -1
|
|
))
|
|
) {
|
|
ret += ' ';
|
|
}
|
|
} else if (token.type != 'comment') {
|
|
// remove comments and leave all other tokens untouched
|
|
ret += token.value;
|
|
}
|
|
});
|
|
return ret;
|
|
}
|
|
};
|
|
|
|
/*@
|
|
Ox.test <f> Takes JavaScript, runs inline tests, returns results
|
|
(source, callback) -> <[o]> Array of results
|
|
(file, callback) -> <u> undefined
|
|
(files, callback) -> <u> undefined
|
|
(doc, callback) -> <u> undefined
|
|
(docs, callback) -> <u> undefined
|
|
source <s> JavaScript source
|
|
file <s> JavaScript file
|
|
files <[s]> Array of JavaScript files
|
|
doc <o> Documentation object (as returned by Ox.doc)
|
|
docs <[o]> Array of documentation objects (as returned by Ox.doc)
|
|
callback <f> Callback function
|
|
results <[o]> Array of results
|
|
actual <s> Actual result
|
|
expected <s> Expected result
|
|
name <s> Item name
|
|
section <s|u> Section in the file
|
|
statement <s> Test statement
|
|
passed <b> True if actual result and expected result are equal
|
|
.data <o> undocumented
|
|
<script>
|
|
Ox.test.foo = function(item) {
|
|
return item.bar / item.baz;
|
|
};
|
|
Ox.test.source = [
|
|
'/*@',
|
|
'Ox.test.foo <f> Returns an items\'s bar per baz',
|
|
' Bar per baz is a good indicator of an item\'s foo-ness.',
|
|
' (item) -> <n> Bar per baz, or NaN',
|
|
' item <o> Any item',
|
|
' > Ox.test.foo({bar: 1, baz: 10})',
|
|
' 0.1',
|
|
' > Ox.test.foo({})',
|
|
' NaN',
|
|
'@*' + '/',
|
|
'Ox.test.foo = function(item) {',
|
|
' return item.bar / item.baz;',
|
|
'};'
|
|
].join('\n');
|
|
</script>
|
|
> 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 <f> Tokenizes JavaScript
|
|
(source) -> <[o]> Array of tokens
|
|
column <n> Column of the token
|
|
line <n> Line of the token
|
|
type <s> Type of the token
|
|
Type can be `"comment"`, `"error"`, `"identifier"`, `"linebreak"`,
|
|
`"number"`, `"operator"`, `"regexp"`, `"string"` or `"whitespace"`
|
|
value <s> Value of the token
|
|
source <s> JavaScript source code
|
|
> Ox.tokenize('// comment\nvar foo = bar / baz;').length
|
|
14
|
|
> Ox.tokenize('return /foo/g;')[2].value.length
|
|
6
|
|
> Ox.tokenize('[0xFF, 1e1, 1e+1, 1e-1, 1e+1+1]').length
|
|
17
|
|
@*/
|
|
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 + '.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) {
|
|
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;
|
|
};
|
|
|
|
}());
|