/** * All parsing/transformation code goes here. All code here should be sync to ease testing. */ var DOM = require('./dom.js').DOM; var htmlEscape = require('./dom.js').htmlEscape; var Example = require('./example.js').Example; var NEW_LINE = /\n\r?/; var globalID = 0; var fs = require('fs'); var fspath = require('path'); var shell = require('shelljs'); var gruntUtil = require('../../lib/grunt/utils.js'); var errorsJson; var marked = require('marked'); marked.setOptions({ gfm: true, tables: true }); var lookupMinerrMsg = function (doc) { var code, namespace; if (errorsJson === undefined) { errorsJson = require('../../build/errors.json').errors; } namespace = doc.getMinerrNamespace(); code = doc.getMinerrCode(); if (namespace === undefined) { return errorsJson[code]; } return errorsJson[namespace][code]; }; exports.trim = trim; exports.metadata = metadata; exports.scenarios = scenarios; exports.merge = merge; exports.Doc = Doc; exports.ngVersions = function() { var versions = [], regex = /^v([1-9]\d*(?:\.\d+\S+)+)$/; //only fetch >= 1.0.0 versions shell.exec('git tag', {silent: true}).output.split(/\s*\n\s*/) .forEach(function(line) { var matches = regex.exec(line); if(matches && matches.length > 0) { versions.push(matches[1]); } }); versions.push(exports.ngCurrentVersion().full); return versions; }; exports.ngCurrentVersion = function() { return gruntUtil.getVersion(); }; var BOOLEAN_ATTR = {}; ['multiple', 'selected', 'checked', 'disabled', 'readOnly', 'required'].forEach(function(value) { BOOLEAN_ATTR[value] = true; }); ////////////////////////////////////////////////////////// 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 || []; this.events = this.events || []; this.links = this.links || []; } Doc.METADATA_IGNORE = (function() { var words = fs.readFileSync(__dirname + '/ignore.words', 'utf8'); return words.toString().split(/[,\s\n\r]+/gm); })(); Doc.prototype = { keywords: function keywords() { var keywords = {}; var words = []; Doc.METADATA_IGNORE.forEach(function(ignore){ keywords[ignore] = true; }); function extractWords(text) { var tokens = text.toLowerCase().split(/[\.\s,`'"#]+/mg); tokens.forEach(function(key){ var match = key.match(/^((ng:|[\$_a-z])[\w\-_]+)/); if (match){ key = match[1]; if (!keywords[key]) { keywords[key] = true; words.push(key); } } }); } extractWords(this.text); this.properties.forEach(function(prop) { extractWords(prop.text || prop.description || ''); }); this.methods.forEach(function(method) { extractWords(method.text || method.description || ''); }); if (this.ngdoc === 'error') { words.push(this.getMinerrNamespace()); words.push(this.getMinerrCode()); } words.sort(); return words.join(' '); }, getMinerrNamespace: function () { if (this.ngdoc !== 'error') { throw new Error('Tried to get the minErr namespace, but @ngdoc ' + this.ngdoc + ' was supplied. It should be @ngdoc error'); } return this.name.split(':')[0]; }, getMinerrCode: function () { if (this.ngdoc !== 'error') { throw new Error('Tried to get the minErr error code, but @ngdoc ' + this.ngdoc + ' was supplied. It should be @ngdoc error'); } return this.name.split(':')[1]; }, /** * Converts relative urls (without section) into absolute * Absolute url means url with section * * @example * - if the link is inside any api doc: * angular.widget -> api/angular.widget * * - if the link is inside any guid doc: * intro -> guide/intro * * @param {string} url Absolute or relative url * @returns {string} Absolute url */ convertUrlToAbsolute: function(url) { if (url.substr(-1) == '/') return url + 'index'; if (url.match(/\//)) return url; return this.section + '/' + url; }, markdown: function(text) { if (!text) return text; var self = this, IS_URL = /^(https?:\/\/|ftps?:\/\/|mailto:|\.|\/)/, IS_ANGULAR = /^(api\/)?(angular|ng|AUTO)\./, IS_HASH = /^#/, parts = trim(text).split(/(<pre.*?>[\s\S]*?<\/pre>|<doc:example(\S*).*?>[\s\S]*?<\/doc:example>|<example[^>]*>[\s\S]*?<\/example>)/), seq = 0, placeholderMap = {}; function placeholder(text) { var id = 'REPLACEME' + (seq++); placeholderMap[id] = text; return id; } function extractInlineDocCode(text, tag) { if(tag == 'all') { //use a greedy operator to match the last </docs> tag regex = /\/\/<docs.*?>([.\s\S]+)\/\/<\/docs>/im; } else { //use a non-greedy operator to match the next </docs> tag regex = new RegExp("\/\/<docs\\s*tag=\"" + tag + "\".*?>([.\\s\\S]+?)\/\/<\/docs>","im"); } var matches = regex.exec(text.toString()); return matches && matches.length > 1 ? matches[1] : ""; } parts.forEach(function(text, i) { parts[i] = (text || ''). replace(/<example(?:\s+module="([^"]*)")?(?:\s+deps="([^"]*)")?(\s+animations="true")?>([\s\S]*?)<\/example>/gmi, function(_, module, deps, animations, content) { var example = new Example(self.scenarios); if(animations) { example.enableAnimations(); example.addDeps('angular-animate.js'); } example.setModule(module); example.addDeps(deps); content.replace(/<file\s+name="([^"]*)"\s*>([\s\S]*?)<\/file>/gmi, function(_, name, content) { example.addSource(name, content); }); content.replace(/<file\s+src="([^"]+)"(?:\s+tag="([^"]+)")?(?:\s+name="([^"]+)")?\s*\/?>/gmi, function(_, file, tag, name) { if(fs.existsSync(file)) { var content = fs.readFileSync(file, 'utf8'); if(content && content.length > 0) { if(tag && tag.length > 0) { content = extractInlineDocCode(content, tag); } name = name && name.length > 0 ? name : fspath.basename(file); example.addSource(name, content); } } return ''; }) return placeholder(example.toHtml()); }). replace(/(?:\*\s+)?<file.+?src="([^"]+)"(?:\s+tag="([^"]+)")?\s*\/?>/i, function(_, file, tag) { if(fs.existsSync(file)) { var content = fs.readFileSync(file, 'utf8'); if(tag && tag.length > 0) { content = extractInlineDocCode(content, tag); } return content; } }). replace(/^<doc:example(\s+[^>]*)?>([\s\S]*)<\/doc:example>/mi, function(_, attrs, content) { var html, script, scenario, example = new Example(self.scenarios); example.setModule((attrs||'module=""').match(/^\s*module=["'](.*)["']\s*$/)[1]); content. replace(/<doc:source(\s+[^>]*)?>([\s\S]*)<\/doc:source>/mi, function(_, attrs, content) { example.addSource('index.html', content. replace(/<script>([\s\S]*)<\/script>/mi, function(_, script) { example.addSource('script.js', script); return ''; }). replace(/<style>([\s\S]*)<\/style>/mi, function(_, style) { example.addSource('style.css', style); return ''; }) ); }). replace(/(<doc:scenario>)([\s\S]*)(<\/doc:scenario>)/mi, function(_, before, content){ example.addSource('scenario.js', content); }); return placeholder(example.toHtml()); }). replace(/^<pre(.*?)>([\s\S]*?)<\/pre>/mi, function(_, attrs, content){ return placeholder( '<pre'+attrs+' class="prettyprint linenums">' + content.replace(/</g, '<').replace(/>/g, '>') + '</pre>'); }). replace(/<div([^>]*)><\/div>/, '<div$1>\n<\/div>'). replace(/{@link\s+([^\s}]+)\s*([^}]*?)\s*}/g, function(_all, url, title){ var isFullUrl = url.match(IS_URL), isAngular = url.match(IS_ANGULAR), isHash = url.match(IS_HASH), absUrl = isHash ? url : (isFullUrl ? url : self.convertUrlToAbsolute(url)); if (!isFullUrl) self.links.push(absUrl); return '<a href="' + absUrl + '">' + (isAngular ? '<code>' : '') + (title || url).replace(/^#/g, '').replace(/\n/g, ' ') + (isAngular ? '</code>' : '') + '</a>'; }). replace(/{@type\s+(\S+)(?:\s+(\S+))?}/g, function(_, type, url) { url = url || '#'; return '<a href="' + url + '" class="' + self.prepare_type_hint_class_name(type) + '">' + type + '</a>'; }); }); text = parts.join(''); function prepareClassName(text) { return text.toLowerCase().replace(/[_\W]+/g, '-'); }; var pageClassName, suffix = '-page'; if(this.name) { var split = this.name.match(/^\s*(.+?)\s*:\s*(.+)/); if(split && split.length > 1) { var before = prepareClassName(split[1]); var after = prepareClassName(split[2]); pageClassName = before + suffix + ' ' + before + '-' + after + suffix; } } pageClassName = pageClassName || prepareClassName(this.name || 'docs') + suffix; text = '<div class="' + pageClassName + '">' + marked(text) + '</div>'; text = text.replace(/(?:<p>)?(REPLACEME\d+)(?:<\/p>)?/g, function(_, id) { return placeholderMap[id]; }); //!annotate CONTENT //!annotate="REGEX" CONTENT //!annotate="REGEX" TITLE|CONTENT text = text.replace(/\n?\/\/!annotate\s*(?:=\s*['"](.+?)['"])?\s+(.+?)\n\s*(.+?\n)/img, function(_, pattern, content, line) { var pattern = new RegExp(pattern || '.+'); var title, text, split = content.split(/\|/); if(split.length > 1) { text = split[1]; title = split[0]; } else { title = 'Info'; text = content; } return "\n" + line.replace(pattern, function(match) { return '<div class="nocode nocode-content" data-popover ' + 'data-content="' + text + '" ' + 'data-title="' + title + '">' + match + '</div>'; }); } ); //!details /path/to/local/docs/file.html //!details="REGEX" /path/to/local/docs/file.html text = text.replace(/\/\/!details\s*(?:=\s*['"](.+?)['"])?\s+(.+?)\n\s*(.+?\n)/img, function(_, pattern, url, line) { url = '/notes/' + url; var pattern = new RegExp(pattern || '.+'); return line.replace(pattern, function(match) { return '<div class="nocode nocode-content" data-foldout data-url="' + url + '">' + match + '</div>'; }); } ); return text; }, 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().trim(); this.id = this.id || // if we have an id just use it (this.ngdoc === 'error' ? this.name : '') || (((this.file||'').match(/.*(\/|\\)([^(\/|\\)]*)\.ngdoc/)||{})[2]) || // 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')), match; if (atName == 'param') { match = text.match(/^\{([^}]+)\}\s+(([^\s=]+)|\[(\S+)=([^\]]+)\])\s+(.*)/); // 1 1 23 3 4 4 5 5 2 6 6 if (!match) { throw new Error("Not a valid 'param' format: " + text + ' (found in: ' + self.file + ':' + self.line + ')'); } var optional = (match[1].slice(-1) === '='); var param = { name: match[4] || match[3], description:self.markdown(text.replace(match[0], match[6])), type: optional ? match[1].substring(0, match[1].length-1) : match[1], optional: optional, 'default':match[5] }; self.param.push(param); } else if (atName == 'returns' || atName == 'return') { match = text.match(/^\{([^}]+)\}\s+(.*)/); if (!match) { throw new Error("Not a valid 'returns' format: " + text + ' (found in: ' + self.file + ':' + self.line + ')'); } self.returns = { type: match[1], description: self.markdown(text.replace(match[0], match[2])) }; } else if(atName == 'requires') { match = text.match(/^([^\s]*)\s*([\S\s]*)/); self.requires.push({ name: match[1], text: self.markdown(match[2]) }); } else if(atName == 'property') { match = text.match(/^\{(\S+)\}\s+(\S+)(\s+(.*))?/); if (!match) { throw new Error("Not a valid 'property' format: " + text + ' (found in: ' + self.file + ':' + self.line + ')'); } var property = new Doc({ type: match[1], name: match[2], shortName: match[2], description: self.markdown(text.replace(match[0], match[4])) }); self.properties.push(property); } else if(atName == 'eventType') { match = text.match(/^([^\s]*)\s+on\s+([\S\s]*)/); self.type = match[1]; self.target = match[2]; } else { self[atName] = text; } } } }, html: function() { var dom = new DOM(), self = this, minerrMsg; if (this.section === 'api') { dom.tag('a', { href: 'http://github.com/angular/angular.js/tree/v' + gruntUtil.getVersion().cdn + '/' + self.file + '#L' + self.line, class: 'view-source btn btn-action' }, function(dom) { dom.tag('i', {class:'icon-zoom-in'}, ' '); dom.text(' View source'); }); } dom.tag('a', { href: 'http://github.com/angular/angular.js/edit/master/' + self.file, class: 'improve-docs btn btn-primary' }, function(dom) { dom.tag('i', {class:'icon-edit'}, ' '); dom.text(' Improve this doc'); }); dom.h(title(this), function() { notice('deprecated', 'Deprecated API', self.deprecated); if (self.ngdoc === 'error') { minerrMsg = lookupMinerrMsg(self); dom.tag('pre', { class:'minerr-errmsg', 'error-display': minerrMsg.replace(/"/g, '"') }, minerrMsg); } if (self.ngdoc != 'overview') { dom.h('Description', self.description, dom.html); } dom.h('Dependencies', self.requires, function(require){ dom.tag('code', function() { dom.tag('a', {href: 'api/ng.' + require.name}, require.name); }); dom.html(require.text); }); (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); }); } }, prepare_type_hint_class_name : function(type) { var typeClass = type.toLowerCase().match(/^[-\w]+/) || []; typeClass = typeClass[0] ? typeClass[0] : 'object'; return 'label type-hint type-hint-' + typeClass; }, html_usage_parameters: function(dom) { var self = this; var params = this.param ? this.param : []; if(this.animations) { dom.h('Animations', this.animations, function(animations){ dom.html('<ul>'); var animations = animations.split("\n"); animations.forEach(function(ani) { dom.html('<li>'); dom.text(ani); dom.html('</li>'); }); dom.html('</ul>'); }); dom.html('<a href="api/ngAnimate.$animate">Click here</a> to learn more about the steps involved in the animation.'); } if(params.length > 0) { dom.html('<h2 id="parameters">Parameters</h2>'); dom.html('<table class="variables-matrix table table-bordered table-striped">'); dom.html('<thead>'); dom.html('<tr>'); dom.html('<th>Param</th>'); dom.html('<th>Type</th>'); dom.html('<th>Details</th>'); dom.html('</tr>'); dom.html('</thead>'); dom.html('<tbody>'); for(var i=0;i<params.length;i++) { param = params[i]; var name = param.name; var types = param.type; if(types[0]=='(') { types = types.substr(1); } var limit = types.length - 1; if(types.charAt(limit) == ')' && types.charAt(limit-1) != '(') { types = types.substr(0,limit); } types = types.split(/\|(?![\(\)\w\|\s]+>)/); var description = param.description; if (param.optional) { name += ' <div><em>(optional)</em></div>'; } dom.html('<tr>'); dom.html('<td>' + name + '</td>'); dom.html('<td>'); for(var j=0;j<types.length;j++) { var type = types[j]; dom.html('<a href="" class="' + self.prepare_type_hint_class_name(type) + '">'); dom.text(type); dom.html('</a>'); } dom.html('</td>'); dom.html('<td>' + description + '</td>'); dom.html('</tr>'); }; dom.html('</tbody>'); dom.html('</table>'); } }, html_usage_returns: function(dom) { var self = this; if (self.returns) { dom.html('<h2 id="returns">Returns</h2>'); dom.html('<table class="variables-matrix">'); dom.html('<tr>'); dom.html('<td>'); dom.html('<a href="" class="' + self.prepare_type_hint_class_name(self.returns.type) + '">'); dom.text(self.returns.type); dom.html('</a>'); dom.html('</td>'); dom.html('<td>'); dom.html(self.returns.description); dom.html('</td>'); dom.html('</tr>'); dom.html('</table>'); } }, 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; var name = self.name.match(/^angular(\.mock)?\.(\w+)$/) ? self.name : self.name.split(/\./).pop() dom.h('Usage', function() { dom.code(function() { dom.text(name); dom.text('('); self.parameters(dom, ', '); dom.text(');'); }); self.html_usage_parameters(dom); self.html_usage_this(dom); self.html_usage_returns(dom); }); this.method_properties_events(dom); }, html_usage_property: function(dom){ var self = this; dom.h('Usage', function() { dom.code(function() { dom.text(self.name); }); self.html_usage_returns(dom); }); }, html_usage_directive: function(dom){ var self = this; dom.h('Usage', function() { var restrict = self.restrict || 'AC'; if (restrict.match(/E/)) { dom.html('<p>'); dom.text('This directive can be used as custom element, but be aware of '); dom.tag('a', {href:'guide/ie'}, 'IE restrictions'); dom.text('.'); dom.html('</p>'); } if (self.usage) { dom.tag('pre', function() { dom.tag('code', function() { dom.text(self.usage); }); }); } else { if (restrict.match(/E/)) { dom.text('as element:'); dom.code(function() { dom.text('<'); dom.text(dashCase(self.shortName)); renderParams('\n ', '="', '"'); dom.text('>\n</'); dom.text(dashCase(self.shortName)); dom.text('>'); }); } if (restrict.match(/A/)) { var element = self.element || 'ANY'; dom.text('as attribute'); dom.code(function() { dom.text('<' + element + ' '); dom.text(dashCase(self.shortName)); renderParams('\n ', '="', '"', true); dom.text('>\n ...\n'); dom.text('</' + element + '>'); }); } if (restrict.match(/C/)) { dom.text('as class'); var element = self.element || 'ANY'; dom.code(function() { dom.text('<' + element + ' class="'); dom.text(dashCase(self.shortName)); renderParams(' ', ': ', ';', true); dom.text('">\n ...\n'); dom.text('</' + element + '>'); }); } } self.html_usage_directiveInfo(dom); self.html_usage_parameters(dom); }); self.method_properties_events(dom); function renderParams(prefix, infix, suffix, skipSelf) { (self.param||[]).forEach(function(param) { var skip = skipSelf && (param.name == self.shortName || param.name.indexOf(self.shortName + '|') == 0); if (!skip) { dom.text(prefix); dom.text(param.optional ? '[' : ''); var parts = param.name.split('|'); dom.text(parts[skipSelf ? 0 : 1] || parts[0]); } if (BOOLEAN_ATTR[param.name]) { dom.text(param.optional ? ']' : ''); } else { dom.text(BOOLEAN_ATTR[param.name] ? '' : infix ); dom.text(('{' + param.type + '}').replace(/^\{\'(.*)\'\}$/, '$1')); dom.text(suffix); dom.text(param.optional && !skip ? ']' : ''); } }); } }, html_usage_filter: function(dom){ var self = this; dom.h('Usage', function() { dom.h('In HTML Template Binding', function() { dom.tag('code', function() { if (self.usage) { dom.text(self.usage); } else { 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('$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_inputType: function(dom){ var self = this; dom.h('Usage', function() { dom.code(function() { dom.text('<input type="' + self.shortName + '"'); (self.param||[]).forEach(function(param){ dom.text('\n '); dom.text(param.optional ? ' [' : ' '); dom.text(dashCase(param.name)); dom.text(BOOLEAN_ATTR[param.name] ? '' : '="{' + param.type + '}"'); dom.text(param.optional ? ']' : ''); }); dom.text('>'); }); self.html_usage_parameters(dom); }); }, html_usage_directiveInfo: function(dom) { var self = this; var list = []; if (self.scope !== undefined) { list.push('This directive creates new scope.'); } if (self.priority !== undefined) { list.push('This directive executes at priority level ' + self.priority + '.'); } if (list.length) { dom.h('Directive info', function() { dom.ul(list); }); } }, html_usage_overview: function(dom){ dom.html(this.description); }, html_usage_error: function (dom) { dom.html(); }, html_usage_interface: function(dom){ var self = this; 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); }); } this.method_properties_events(dom); }, html_usage_service: function(dom) { this.html_usage_interface(dom) }, html_usage_object: function(dom) { this.html_usage_interface(dom) }, method_properties_events: function(dom) { var self = this; if (self.methods.length) { dom.div({class:'member method'}, function(){ dom.h('Methods', self.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); self.html_usage_this(dom); method.html_usage_returns(dom); dom.h('Example', method.example, dom.html); }); }); }); } if (self.properties.length) { dom.div({class:'member property'}, function(){ dom.h('Properties', self.properties, function(property){ dom.h(property.shortName, function() { dom.html(property.description); if (!property.html_usage_returns) { console.log(property); } property.html_usage_returns(dom); dom.h('Example', property.example, dom.html); }); }); }); } if (self.events.length) { dom.div({class:'member event'}, function(){ dom.h('Events', self.events, function(event){ dom.h(event.shortName, event, function() { dom.html(event.description); if (event.type == 'listen') { dom.tag('div', {class:'inline'}, function() { dom.h('Listen on:', event.target); }); } else { dom.tag('div', {class:'inline'}, function() { dom.h('Type:', event.type); }); dom.tag('div', {class:'inline'}, function() { dom.h('Target:', event.target); }); } event.html_usage_parameters(dom); self.html_usage_this(dom); dom.h('Example', event.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; }); } }; ////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////// var GLOBALS = /^angular\.([^\.]+)$/, MODULE = /^((?:(?!^angular\.)[^\.])+)$/, MODULE_MOCK = /^angular\.mock\.([^\.]+)$/, MODULE_DIRECTIVE = /^((?:(?!^angular\.)[^\.])+)\.directive:([^\.]+)$/, MODULE_DIRECTIVE_INPUT = /^((?:(?!^angular\.)[^\.])+)\.directive:input\.([^\.]+)$/, MODULE_FILTER = /^((?:(?!^angular\.)[^\.])+)\.filter:([^\.]+)$/, MODULE_SERVICE = /^((?:(?!^angular\.)[^\.])+)\.([^\.]+?)(Provider)?$/, MODULE_TYPE = /^((?:(?!^angular\.)[^\.])+)\..+\.([A-Z][^\.]+)$/; function title(doc) { if (!doc.name) return doc.name; var match, text = doc.name; var makeTitle = function (name, type, componentType, component) { // Makes title markup. // makeTitle('Foo', 'directive', 'module', 'ng') -> // Foo is a directive in module ng return function () { this.tag('code', name); this.tag('div', function () { this.tag('span', {class: 'hint'}, function () { if (type && component) { this.text(type + ' in ' + componentType + ' '); this.tag('code', component); } }); }); }; }; if (doc.ngdoc === 'error') { return makeTitle(doc.fullName, 'error', 'component', doc.getMinerrNamespace()); } else if (text == 'angular.Module') { return makeTitle('Module', 'Type', 'module', 'ng'); } else if (match = text.match(GLOBALS)) { return makeTitle('angular.' + match[1], 'API', 'module', 'ng'); } else if (match = text.match(MODULE)) { return makeTitle('', '', 'module', match[1]); } else if (match = text.match(MODULE_MOCK)) { return makeTitle('angular.mock.' + match[1], 'API', 'module', 'ng'); } else if (match = text.match(MODULE_DIRECTIVE)) { return makeTitle(match[2], 'directive', 'module', match[1]); } else if (match = text.match(MODULE_DIRECTIVE_INPUT)) { return makeTitle('input [' + match[2] + ']', 'directive', 'module', match[1]); } else if (match = text.match(MODULE_FILTER)) { return makeTitle(match[2], 'filter', 'module', match[1]); } else if (match = text.match(MODULE_SERVICE)) { return makeTitle(match[2] + (match[3] || ''), 'service', 'module', match[1]); } else if (match = text.match(MODULE_TYPE)) { return makeTitle(match[2], 'type', 'module', match[1]); } return text; } function scenarios(docs){ var specs = []; specs.push('describe("angular+jqlite", function() {'); appendSpecs('index-nocache.html#!/'); specs.push('});'); specs.push(''); specs.push(''); specs.push('describe("angular+jquery", function() {'); appendSpecs('index-jq-nocache.html#!/'); specs.push('});'); return specs.join('\n'); function appendSpecs(urlPrefix) { docs.forEach(function(doc){ specs.push(' describe("' + doc.section + '/' + doc.id + '", function() {'); specs.push(' beforeEach(function() {'); specs.push(' browser().navigateTo("' + urlPrefix + doc.section + '/' + doc.id + '");'); specs.push(' });'); specs.push(' '); doc.scenarios.forEach(function(scenario){ specs.push(indentCode(trim(scenario), 4)); specs.push(''); }); specs.push('});'); specs.push(''); }); } } ////////////////////////////////////////////////////////// function metadata(docs){ var pages = []; docs.forEach(function(doc){ var path = (doc.name || '').split(/(\.|\:\s*)/); for ( var i = 1; i < path.length; i++) { path.splice(i, 1); } var shortName = path.pop().trim(); if (path.pop() == 'input') { shortName = 'input [' + shortName + ']'; } pages.push({ section: doc.section, id: doc.id, name: title(doc), shortName: shortName, type: doc.ngdoc, keywords:doc.keywords() }); }); pages.sort(keywordSort); return pages; } var KEYWORD_PRIORITY = { '.index': 1, '.overview': 1, '.bootstrap': 2, '.mvc': 3, '.scopes': 4, '.compiler': 5, '.templates': 6, '.services': 7, '.di': 8, '.unit-testing': 9, '.dev_guide': 9, '.dev_guide.overview': 1, '.dev_guide.bootstrap': 2, '.dev_guide.bootstrap.auto_bootstrap': 1, '.dev_guide.bootstrap.manual_bootstrap': 2, '.dev_guide.mvc': 3, '.dev_guide.mvc.understanding_model': 1, '.dev_guide.mvc.understanding_controller': 2, '.dev_guide.mvc.understanding_view': 3, '.dev_guide.scopes': 4, '.dev_guide.scopes.understanding_scopes': 1, '.dev_guide.scopes.internals': 2, '.dev_guide.compiler': 5, '.dev_guide.templates': 6, '.dev_guide.services': 7, '.dev_guide.di': 8, '.dev_guide.unit-testing': 9 }; 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 (doc.section + '/' + mangled.join('.')).toLowerCase(); } 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 indentCode(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 byFullId = {}; docs.forEach(function(doc) { byFullId[doc.section + '/' + doc.id] = doc; }); for(var i = 0; i < docs.length;) { var doc = docs[i]; // check links - do they exist ? doc.links.forEach(function(link) { // convert #id to path#id if (link[0] == '#') { link = doc.section + '/' + doc.id.split('#').shift() + link; } link = link.split('#').shift(); if (!byFullId[link]) { console.log('WARNING: In ' + doc.section + '/' + doc.id + ', non existing link: "' + link + '"'); } }); // merge into parents if (findParent(doc, 'method') || findParent(doc, 'property') || findParent(doc, 'event')) { docs.splice(i, 1); } else { i++; } } function findParent(doc, name) { var parentName = doc[name + 'Of']; if (!parentName) return false; var parent = byFullId['api/' + 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]; }; } var DASH_CASE_REGEXP = /[A-Z]/g; function dashCase(name){ return name.replace(DASH_CASE_REGEXP, function(letter, pos) { return (pos ? '-' : '') + letter.toLowerCase(); }); }