angular.js/docs/src/ngdoc.js
Igor Minar d600c608e3 markdown pre-processor should strip all the extra indentation
- split trim into trim and indent
- merged my indentation code with trim
- cleaned up some small issues
2011-02-07 23:56:51 -08:00

663 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* All parsing/transformation code goes here. All code here should be sync to ease testing.
*/
var Showdown = require('showdown').Showdown;
var DOM = require('dom.js').DOM;
var htmlEscape = require('dom.js').htmlEscape;
var NEW_LINE = /\n\r?/;
exports.trim = trim;
exports.metadata = metadata;
exports.scenarios = scenarios;
exports.merge = merge;
exports.Doc = Doc;
//////////////////////////////////////////////////////////
function Doc(text, file, line) {
if (typeof text == 'object') {
for ( var key in text) {
this[key] = text[key];
}
} else {
this.text = text;
this.file = file;
this.line = line;
}
this.scenarios = this.scenarios || [];
this.requires = this.requires || [];
this.param = this.param || [];
this.properties = this.properties || [];
this.methods = this.methods || [];
}
Doc.METADATA_IGNORE = (function(){
var words = require('fs').readFileSync(__dirname + '/ignore.words', 'utf8');
return words.toString().split(/[,\s\n\r]+/gm);
})();
Doc.prototype = {
keywords: function keywords(){
var keywords = {};
Doc.METADATA_IGNORE.forEach(function(ignore){ keywords[ignore] = true; });
var words = [];
var tokens = this.text.toLowerCase().split(/[,\.\`\'\"\s]+/mg);
tokens.forEach(function(key){
var match = key.match(/^(([\$\_a-z]|ng\:)[\w\_\-]{2,})/);
if (match){
key = match[1];
if (!keywords[key]) {
keywords[key] = true;
words.push(key);
}
}
});
words.sort();
return words.join(' ');
},
markdown: function (text) {
var self = this;
var IS_URL = /^(https?:\/\/|ftps?:\/\/|mailto:|\.|\/)/;
var IS_ANGULAR = /^angular\./;
if (!text) return text;
text = trim(text);
var parts = text.split(/(<pre>[\s\S]*?<\/pre>|<doc:example>[\s\S]*?<\/doc:example>)/);
parts.forEach(function(text, i){
if (text.match(/^<pre>/)) {
text = text.replace(/^<pre>([\s\S]*)<\/pre>/mi, function(_, content){
return '<div ng:non-bindable><pre class="brush: js; html-script: true;">' +
content.replace(/</g, '&lt;').replace(/>/g, '&gt;') +
'</pre></div>';
});
} else if (text.match(/^<doc:example>/)) {
text = text.replace(/(<doc:source>)([\s\S]*)(<\/doc:source>)/mi,
function(_, before, content, after){
return before + htmlEscape(content) + after;
});
text = text.replace(/(<doc:scenario>)([\s\S]*)(<\/doc:scenario>)/mi,
function(_, before, content, after){
self.scenarios.push(content);
return before + htmlEscape(content) + after;
});
} else {
text = text.replace(/<angular\/>/gm, '<tt>&lt;angular/&gt;</tt>');
text = text.replace(/{@link ([^\s}]+)((\s|\n)+(.+?))?\s*}/gm,
function(_all, url, _2, _3, title){
return '<a href="' + (url.match(IS_URL) ? '' : '#!') + url + '">'
+ (url.match(IS_ANGULAR) ? '<code>' : '')
+ (title || url)
+ (url.match(IS_ANGULAR) ? '</code>' : '')
+ '</a>';
});
text = new Showdown.converter().makeHtml(text);
}
parts[i] = text;
});
return parts.join('');
},
parse: function(){
var atName;
var atText;
var match;
var self = this;
self.text.split(NEW_LINE).forEach(function(line){
if (match = line.match(/^\s*@(\w+)(\s+(.*))?/)) {
// we found @name ...
// if we have existing name
flush();
atName = match[1];
atText = [];
if(match[3]) atText.push(match[3]);
} else {
if (atName) {
atText.push(line);
}
}
});
flush();
this.shortName = (this.name || '').split(/[\.#]/).pop();
this.id = this.id // if we have an id just use it
|| (((this.file||'').match(/.*\/([^\/]*)\.ngdoc/)||{})[1]) // try to extract it from file name
|| this.name; // default to name
this.description = this.markdown(this.description);
this.example = this.markdown(this.example);
this['this'] = this.markdown(this['this']);
return this;
function flush(){
if (atName) {
var text = trim(atText.join('\n'));
if (atName == 'param') {
var match = text.match(/^{([^}=]+)(=)?}\s+(([^\s=]+)|\[(\S+)=([^\]]+)\])\s+(.*)/);
// 1 12 2 34 4 5 5 6 6 3 7 7
if (!match) {
throw new Error("Not a valid 'param' format: " + text);
}
var param = {
name: match[5] || match[4],
description:self.markdown(text.replace(match[0], match[7])),
type: match[1],
optional: !!match[2],
'default':match[6]
};
self.param.push(param);
} else if (atName == 'returns') {
var match = text.match(/^{([^}=]+)}\s+(.*)/);
if (!match) {
throw new Error("Not a valid 'returns' format: " + text);
}
self.returns = {
type: match[1],
description: self.markdown(text.replace(match[0], match[2]))
};
} else if(atName == 'requires') {
self.requires.push(text);
} else if(atName == 'property') {
var match = text.match(/^{(\S+)}\s+(\S+)(\s+(.*))?/);
if (!match) {
throw new Error("Not a valid 'property' format: " + text);
}
var property = {
type: match[1],
name: match[2],
description: self.markdown(text.replace(match[0], match[4]))
};
self.properties.push(property);
} else {
self[atName] = text;
}
}
}
},
html: function(){
var dom = new DOM(),
self = this;
dom.h(this.name, function(){
notice('workInProgress', 'Work in Progress',
'This page is currently being revised. It might be incomplete or contain inaccuracies.');
notice('deprecated', 'Deprecated API', self.deprecated);
(self['html_usage_' + self.ngdoc] || function(){
throw new Error("Don't know how to format @ngdoc: " + self.ngdoc);
}).call(self, dom);
dom.h('Example', self.example, dom.html);
});
return dom.toString();
//////////////////////////
function notice(name, legend, msg){
if (self[name] == undefined) return;
dom.tag('fieldset', {'class':name}, function(dom){
dom.tag('legend', legend);
dom.text(msg);
});
}
},
html_usage_parameters: function(dom) {
dom.h('Parameters', this.param, function(param){
dom.tag('code', function(){
dom.text(param.name);
if (param.optional) {
dom.tag('i', function(){
dom.text('(optional');
if(param['default']) {
dom.text('=' + param['default']);
}
dom.text(')');
});
}
dom.text(' {');
dom.text(param.type);
dom.text('} ');
});
dom.html(param.description);
});
},
html_usage_returns: function(dom) {
var self = this;
if (self.returns) {
dom.h('Returns', function(){
dom.tag('code', '{' + self.returns.type + '}');
dom.text(' ');
dom.html(self.returns.description);
});
}
},
html_usage_this: function(dom) {
var self = this;
if (self['this']) {
dom.h(function(dom){
dom.html("Method's <code>this</code>");
}, function(dom){
dom.html(self['this']);
});
}
},
html_usage_function: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.code(function(){
dom.text(self.name);
dom.text('(');
self.parameters(dom, ', ');
dom.text(');');
});
self.html_usage_parameters(dom);
self.html_usage_this(dom);
self.html_usage_returns(dom);
});
},
html_usage_directive: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.tag('pre', {'class':"brush: js; html-script: true;"}, function(){
dom.text('<' + self.element + ' ');
dom.text(self.shortName);
if (self.param.length) {
dom.text('="' + self.param[0].name + '"');
}
dom.text('>\n ...\n');
dom.text('</' + self.element + '>');
});
self.html_usage_parameters(dom);
});
},
html_usage_filter: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.tag('code', function(){
dom.text('{{ ');
dom.text(self.shortName);
dom.text('_expression | ');
dom.text(self.shortName);
self.parameters(dom, ':', true);
dom.text(' }}');
});
});
dom.h('In JavaScript', function(){
dom.tag('code', function(){
dom.text('angular.filter.');
dom.text(self.shortName);
dom.text('(');
self.parameters(dom, ', ');
dom.text(')');
});
});
self.html_usage_parameters(dom);
self.html_usage_this(dom);
self.html_usage_returns(dom);
});
},
html_usage_formatter: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
if (self.inputType=='select')
dom.text('<select name="bindExpression"');
else
dom.text('<input type="text" name="bindExpression"');
dom.text(' ng:format="');
dom.text(self.shortName);
self.parameters(dom, ':', false, true);
dom.text('">');
});
});
dom.h('In JavaScript', function(){
dom.code(function(){
dom.text('var userInputString = angular.formatter.');
dom.text(self.shortName);
dom.text('.format(modelValue');
self.parameters(dom, ', ', false, true);
dom.text(');');
dom.text('\n');
dom.text('var modelValue = angular.formatter.');
dom.text(self.shortName);
dom.text('.parse(userInputString');
self.parameters(dom, ', ', false, true);
dom.text(');');
});
});
self.html_usage_parameters(dom);
self.html_usage_this(dom);
self.html_usage_returns(dom);
});
},
html_usage_validator: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
dom.text('<input type="text" ng:validate="');
dom.text(self.shortName);
self.parameters(dom, ':', true);
dom.text('"/>');
});
});
dom.h('In JavaScript', function(){
dom.code(function(){
dom.text('angular.validator.');
dom.text(self.shortName);
dom.text('(');
self.parameters(dom, ', ');
dom.text(')');
});
});
self.html_usage_parameters(dom);
self.html_usage_this(dom);
self.html_usage_returns(dom);
});
},
html_usage_widget: function(dom){
var self = this;
dom.h('Description', self.description, dom.html);
dom.h('Dependencies', self.requires);
dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){
dom.code(function(){
if (self.shortName.match(/^@/)) {
dom.text('<');
dom.text(self.element);
dom.text(' ');
dom.text(self.shortName.substring(1));
if (self.param.length) {
dom.text('="');
dom.text(self.param[0].name);
dom.text('"');
}
dom.text('>\n ...\n</');
dom.text(self.element);
dom.text('>');
} else {
dom.text('<');
dom.text(self.shortName);
(self.param||[]).forEach(function(param){
if (param.optional) {
dom.text(' [' + param.name + '="..."]');
} else {
dom.text(' ' + param.name + '="..."');
}
});
dom.text('></');
dom.text(self.shortName);
dom.text('>');
}
});
});
self.html_usage_parameters(dom);
self.html_usage_returns(dom);
});
},
html_usage_overview: function(dom){
dom.html(this.description);
},
html_usage_service: function(dom){
var self = this;
dom.h('Description', this.description, dom.html);
dom.h('Dependencies', this.requires);
if (this.param.length) {
dom.h('Usage', function(){
dom.code(function(){
dom.text(self.name.split('.').pop());
dom.text('(');
self.parameters(dom, ', ');
dom.text(');');
});
self.html_usage_parameters(dom);
self.html_usage_this(dom);
self.html_usage_returns(dom);
});
}
dom.h('Methods', this.methods, function(method){
var signature = (method.param || []).map(property('name'));
dom.h(method.shortName + '(' + signature.join(', ') + ')', method, function(){
dom.html(method.description);
method.html_usage_parameters(dom);
dom.h('Example', method.example, dom.html);
});
});
dom.h('Properties', this.properties, function(property){
dom.h(property.name, function(){
dom.html(property.description);
dom.h('Example', property.example, dom.html);
});
});
},
parameters: function(dom, separator, skipFirst, prefix) {
var sep = prefix ? separator : '';
(this.param||[]).forEach(function(param, i){
if (!(skipFirst && i==0)) {
if (param.optional) {
dom.text('[' + sep + param.name + ']');
} else {
dom.text(sep + param.name);
}
}
sep = separator;
});
}
};
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
function scenarios(docs){
var specs = [];
docs.forEach(function(doc){
specs.push('describe("' + doc.id + '", function(){');
specs.push(' beforeEach(function(){');
specs.push(' browser().navigateTo("index.html#!' + doc.id + '");');
specs.push(' });');
specs.push('');
doc.scenarios.forEach(function(scenario){
specs.push(indent(trim(scenario), 2));
specs.push('');
});
specs.push('});');
specs.push('');
if (doc.scenario) {
}
});
return specs.join('\n');
}
//////////////////////////////////////////////////////////
function metadata(docs){
var words = [];
docs.forEach(function(doc){
var path = (doc.name || '').split(/(\.|\:\s+)/);
for ( var i = 1; i < path.length; i++) {
path.splice(i, 1);
}
var depth = path.length - 1;
var shortName = path.pop();
words.push({
id: doc.id,
name: doc.name,
depth: depth,
shortName: shortName,
type: doc.ngdoc,
keywords:doc.keywords()
});
});
words.sort(keywordSort);
return words;
}
var KEYWORD_PRIORITY = {
'.started': 1,
'.guide': 2,
'.guide.overview': 1,
'.angular': 7,
'.angular.Array': 7,
'.angular.Object': 7,
'.angular.directive': 7,
'.angular.filter': 7,
'.angular.formatter': 7,
'.angular.scope': 7,
'.angular.service': 7,
'.angular.validator': 7,
'.angular.widget': 7
};
function keywordSort(a, b){
function mangleName(doc) {
var path = doc.id.split(/\./);
var mangled = [];
var partialName = '';
path.forEach(function(name){
partialName += '.' + name;
mangled.push(KEYWORD_PRIORITY[partialName] || 5);
mangled.push(name);
});
return mangled.join('.');
}
var nameA = mangleName(a);
var nameB = mangleName(b);
return nameA < nameB ? -1 : (nameA > nameB ? 1 : 0);
}
//////////////////////////////////////////////////////////
function trim(text) {
var MAX_INDENT = 9999;
var empty = RegExp.prototype.test.bind(/^\s*$/);
var lines = text.split('\n');
var minIndent = MAX_INDENT;
var indentRegExp;
var ignoreLine = (lines[0][0] != ' ' && lines.length > 1);
// ignore first line if it has no indentation and there is more than one line
lines.forEach(function(line){
if (ignoreLine) {
ignoreLine = false;
return;
}
var indent = line.match(/^\s*/)[0].length;
if (indent > 0 || minIndent == MAX_INDENT) {
minIndent = Math.min(minIndent, indent);
}
});
indentRegExp = new RegExp('^\\s{0,' + minIndent + '}');
for ( var i = 0; i < lines.length; i++) {
lines[i] = lines[i].replace(indentRegExp, '');
}
// remove leading lines
while (empty(lines[0])) {
lines.shift();
}
// remove trailing
while (empty(lines[lines.length - 1])) {
lines.pop();
}
return lines.join('\n');
}
function indent(text, spaceCount) {
var lines = text.split('\n'),
indent = '',
fixedLines = [];
while(spaceCount--) indent += ' ';
lines.forEach(function(line) {
fixedLines.push(indent + line);
});
return fixedLines.join('\n');
}
//////////////////////////////////////////////////////////
function merge(docs){
var byName = {};
docs.forEach(function(doc){
byName[doc.name] = doc;
});
for(var i=0; i<docs.length;) {
if (findParent(docs[i], 'method') ||
findParent(docs[i], 'property')) {
docs.splice(i, 1);
} else {
i++;
}
}
function findParent(doc, name){
var parentName = doc[name+'Of'];
if (!parentName) return false;
var parent = byName[parentName];
if (!parent)
throw new Error("No parent named '" + parentName + "' for '" +
doc.name + "' in @" + name + "Of.");
var listName = (name + 's').replace(/ys$/, 'ies');
var list = parent[listName] = (parent[listName] || []);
list.push(doc);
list.sort(orderByName);
return true;
}
function orderByName(a, b){
return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0);
}
}
//////////////////////////////////////////////////////////
function property(name) {
return function(value){
return value[name];
};
}