UNPKG

panino

Version:

API documentation generator with a strict grammar and testing tools

750 lines (651 loc) 18.2 kB
var StringScanner = require("StringScanner"); var _ = require('underscore'); require('colors'); /* Parses doc-comment into array of @tags For each @tag it produces Hash like the following: { "tagname" : cfg/property/type/extends/..., "doc" : "Some documentation for this tag", ...@tag specific stuff like :name, :type, and so on... } When doc-comment begins with comment, not preceded by @tag, then the comment will be placed into Hash with "tagname" : "default". Unrecognized @tags are left as is into documentation as if they were normal text. */ var ident_pattern = /[$\w-]+/; var ident_pattern_with_dot = /[$\w-]+\.[$\w-]+/; var ident_chain_pattern = /[$\w-]+(\.[$\w-]+)*/; var tags = []; var current_tag = {}; var input; exports.parse = function(content, customTags) { tags = []; input = new StringScanner((purify(content))); parse_loop(input, customTags); // The parsing process can leave whitespace at the ends of // doc-strings, here we get rid of it. Additionally, null all empty docs _.each(tags, function(tag) { tag["doc"] = strip(tag["doc"]); tag["doc"] = tag["doc"] || ""; }); // Get rid of empty default tag if (_.first(tags) && _.first(tags)["tagname"] == "default" && !_.first(tags)["doc"]) tags.shift(); return tags; }; // Extracts content inside /** ... */ function purify(input) { var result = [], indent = null, codeBlock = false; // We can have two types of lines: // - those beginning with * // - and those without it input.split("\n").forEach(function(line) { // chomp ! line = line.replace(/(\n|\r)+$/, ''); if (!codeBlock && /^\s*\*\s*```/.test(line)) codeBlock = true; else if (codeBlock && /^\s*\*\s*```/.test(line)) codeBlock = false; var m; if ( ( m = line.match(/^\s*\*\s?(.*)$/)) ) { // When comment contains *-lines, switch indent-trimming off indent = 0; if (codeBlock) result.push(m[1]); else result.push(m[1].replace(/^\s+/,"")); } else if (/^\s*$/.test(line)) { // pass-through empty lines result.push(line); } else if (indent === undefined && (m = line.match(/^(\s*)(.*?$)/) ) ) { // When indent not measured, measure it and remember indent = m[1]; result.push(m[2]); } else { // Trim away indent if available //result.push(line.replace(new RegExp("^\s{0," + indent + "}"), "")); result.push(line.replace(/^\s+/,"")); } }); return result.join("\n"); } function add_tag(tag) { current_tag = {"tagname": tag, "doc": ""}; tags.push(current_tag); } // at tags goes here function parse_loop(input, customTags) { add_tag("default"); i = 0; while(!input.eos()) { if (look(/@class\b/)) at_class(); else if (look(/@extends?\b/)) at_extends() else if (look(/@inherits?\b/)) at_inherits() else if (look(/@mixins?\b/)) class_list_at_tag(/@mixins?/, "mixins"); else if (look(/@alternateClassNames?\b/)) class_list_at_tag(/@alternateClassNames?/, "alternateClassNames"); else if (look(/@uses\b/)) class_list_at_tag(/@uses/, "uses"); else if (look(/@requires\b/)) class_list_at_tag(/@requires/, "requires"); else if (look(/@singleton\b/)) boolean_at_tag(/@singleton/, "singleton"); else if (look(/@event\b/)) at_event(); else if (look(/@method\b/)) at_method(); else if (look(/@constructor\b/)) at_method(true); else if (look(/@attribute\b/)) at_attribute(); else if (look(/@binding\b/)) at_binding(); else if (look(/@param\b/)) at_param(); else if (look(/@returns?\b/)) at_return(); else if (look(/@cfg\b/)) at_cfg(); else if (look(/@property\b/)) at_property(); else if (look(/@type\s/)) at_type(); else if (look(/@xtype\b/)) at_xtype(/@xtype/, "widget"); else if (look(/@ftype\b/)) at_xtype(/@ftype/, "feature"); else if (look(/@ptype\b/)) at_xtype(/@ptype/, "plugin"); else if (look(/@member\b/)) at_member(); else if (look(/@inherit[dD]oc\b/)) at_inheritdoc(); else if (look(/@alias/)) at_alias(); else if (look(/@see/)) at_see(); else if (look(/@todo/)) at_todo(); else if (look(/@link/)) at_link(); else if (look(/@var\b/)) at_var(); else if (look(/@throws?\b/)) at_throws(); else if (look(/@enum\b/)) at_enum(); else if (look(/@override\b/)) at_override(); else if (look(/@cancelable\b/)) at_cancelable(); else if (look(/@author\b/)) at_author(); else if (look(/@version\b/)) at_version(); else if (look(/@since\b/)) at_since(); else if (look(/@deprecated\b/)) at_deprecated(); else if (look(/@related\b/)) at_related(); else if (look(/@private\b/)) boolean_at_tag(/@private/, "private"); else if (look(/@ignore\b/)) boolean_at_tag(/@ignore/, "ignore"); else if (look(/@experimental\b/)) boolean_at_tag(/@experimental/, "experimental"); else if (look(/@chainable\b/)) boolean_at_tag(/@chainable/, "chainable"); else if (look(/@inheritable\b/)) boolean_at_tag(/@inheritable/, "inheritable"); else if (look(/@accessor\b/)) boolean_at_tag(/@accessor/, "accessor"); else if (look(/@evented\b/)) boolean_at_tag(/@evented/, "evented"); else if (look(/@bubbles\b/)) boolean_at_tag(/@bubbles/, "bubbles"); else if (look(/@read\-?only\b/)) boolean_at_tag(/@read\-?only/, "readonly"); else if (look(/@default_private\b/)) boolean_at_tag(/@default_private/, "default_private"); else if (look(/@/)) { match(/@/); var tagName = look(/\w+/); var found = _.find(customTags, function(tag) { return RegExp(tag).test(tagName); }); if (found) { match(found); add_tag(tagName); skip_horiz_white(); if (look(/.*/).length > 0) current_tag["doc"] = strip(match(/.*/)); // no text means it's a boolean } else { // basically, "@" has to be the first dang thing after the * // this resolves false positives, like [@read] var currPos = input.pointer(); input.setPointer(currPos - (tagName.length + 3)); var startingLine = input.peek(tagName.length + 3); if (!/.@/.test(startingLine)) { console.warn("Warning".yellow + ": I found @" + tagName + ", but I'm not sure what it ought to do."); } input.setPointer(currPos); current_tag["doc"] += "@"; } } else if (look(/[^@]/)) { current_tag["doc"] += match(/[^@]+/); } } } // matches @class name ... function at_class() { match(/@class/); add_tag("class"); maybe_ident_chain("name"); skip_white(); } // matches @extends name ... function at_extends() { match(/@extends?/); add_tag("extends"); maybe_ident_chain("extends"); skip_white(); } // matches @inherits name ... function at_inherits() { match(/@inherits?/); add_tag("inherits"); skip_horiz_white(); current_tag["doc"] = strip(match(/.*/)); skip_white(); } // matches @<tagname> classname1 classname2 ... function class_list_at_tag(regex, tagname) { match(regex); add_tag(tagname); skip_horiz_white(); current_tag[tagname] = class_list(); skip_white(); } // matches @event name ... function at_event() { match(/@event/); add_tag("event"); maybe_name(); skip_white(); } // matches @method name ... // also matches @constructor function at_method(isConstructor) { if (isConstructor) { match(/@constructor\b/); add_tag("method"); add_tag("Constructor"); skip_white(); } else { match(/@method/); add_tag("method"); maybe_name(); skip_white(); } } // matches @param {type} [name] (optional) ... function at_param() { match(/@param/); add_tag("param"); maybe_type(); maybe_name_with_default(); maybe_optional(); skip_white(); } // matches @return/@returns {type} [ return.name ] ... function at_return() { match(/@returns?/); add_tag("return"); maybe_type(); skip_white(); if (look(/return\.\w/)) current_tag["name"] = ident_chain; else current_tag["name"] = "return"; skip_white(); } // matches @attribute {type} [name] (optional) ... function at_attribute() { match(/@attribute/); add_tag("attribute"); maybe_type(); maybe_name_with_default(); maybe_optional(); skip_white(); } // matches @binding [name] ... function at_binding() { match(/@binding/); add_tag("binding"); maybe_name_with_default(); skip_white(); } // matches @cfg {type} name ... function at_cfg() { match(/@cfg/); add_tag("cfg"); maybe_type(); maybe_name_with_default(); maybe_required(); skip_white(); } // matches @property {type} name ... // // ext-doc doesn't support {type} and name for @property - name is // inferred from source and @type is required to specify type, // jsdoc-toolkit on the other hand follows the sensible route, and // so do we. function at_property() { match(/@property/) add_tag("property"); maybe_type(); maybe_name_with_default(); skip_white(); } // matches @var {type} $name ... function at_var() { match(/@var/) add_tag("css_var"); maybe_type(); maybe_name_with_default(); skip_white(); } // matches @throws {type} ... function at_throws() { match(/@throws?/) add_tag("throws"); maybe_type(); skip_white(); } // matches @enum {type} name ... function at_enum() { match(/@enum/) add_tag("class"); current_tag["enum"] = true; maybe_type(); maybe_name_with_default(); skip_white(); } // matches @override name ... function at_override() { match(/@override/) add_tag("override"); maybe_ident_chain("class"); skip_white(); } // matches @cancelable words ... function at_cancelable() { match(/@cancelable/) add_tag("cancelable"); current_tag["doc"] = strip(match(/.*/)); skip_white(); } // matches @author words ... function at_author() { match(/@author/); add_tag("author"); skip_horiz_white(); current_tag["doc"] = strip(match(/.*/)); skip_white(); } // matches @version words ... function at_version() { match(/@version/); add_tag("version"); skip_horiz_white(); current_tag["doc"] = strip(match(/.*/)); skip_white(); } // matches @since words ... function at_since() { match(/@since/); add_tag("since"); skip_horiz_white(); current_tag["doc"] = strip(match(/.*/)); skip_white(); } // matches @deprecated; @deprecated x.x.x; @deprecated words ... function at_deprecated() { match(/@deprecated/); add_tag("deprecated"); skip_horiz_white(); current_tag["doc"] = strip(match(/.*/)); skip_white(); } // matches @related <ident-chain> function at_related() { match(/@related/) add_tag("related"); skip_horiz_white(); current_tag["name"] = ident_chain(); skip_white(); } // matches @type {type} or @type type // // The presence of @type implies that we are dealing with property. // ext-doc allows type name to be either inside curly braces or // without them at all. function at_type() { match(/@type/); add_tag("type"); skip_horiz_white(); if (look(/\{/)) { var tdf = typedef(); current_tag["type"] = tdf["type"]; current_tag["optional"] = tdf["optional"] ? true : false; } else if (look(/\S/)) { current_tag["type"] = match(/\S+/); } skip_white(); } // matches @member name ... function at_member() { match(/@member/); add_tag("member"); maybe_ident_chain("member"); skip_white(); } // matches @xtype/ptype/ftype/... name function at_xtype(tag, namespace) { match(tag); add_tag("alias"); skip_horiz_white(); current_tag["name"] = namespace + "." + (ident_chain() || ""); skip_white(); } // matches @alias <ident-chain> function at_alias() { match(/@alias/) add_tag("alias"); skip_horiz_white(); current_tag["name"] = ident_chain(); skip_white(); } // matches @see <ident-chain> // @see https?://blahblah... function at_see() { match(/@see/) add_tag("see"); skip_horiz_white(); current_tag["name"] = ident_chain(); skip_white(); } // matches @todo words ... function at_todo() { match(/@todo/) add_tag("todo"); current_tag["doc"] = strip(match(/.*/)); skip_white(); } // matches @link [<ident-chain> some text...] or // @link https?://blahblah some text... function at_link() { match(/\{@link/) skip_horiz_white(); var linkText = strip(match(/.*?\}/)).replace(/\s*@link\s*/, "").slice(0,-1); if (/^https?/.test(linkText)) { var anchor = linkText.split(/\s+/); var link = anchor.shift(); current_tag["doc"] = current_tag["doc"].slice(0,-1) + "[" + anchor.join(" ") + "]" + "(" + link + ")"; } else { // sugar this into panino style [[ ]] link block current_tag["doc"] = current_tag["doc"].slice(0,-1) + "[[" + linkText + "]] "; } skip_white(); } // matches @inheritdoc class.name#static-type-member function at_inheritdoc() { match(/@inherit[dD]oc|@alias/); add_tag("inheritdoc"); skip_horiz_white(); /* original JSDuck code if (look(ident_chain_pattern)) current_tag["cls"] = ident_chain(); if (look(/#\w/)) { match(/#/); if (look(/static-/)) { current_tag["static"] = true; match(/static-/); } if (look(/(cfg|property|method|attribute|event|css_var|css_mixin)-/)) { current_tag["type"] = ident(); match(/-/); } current_tag["member"] = ident(); } else { }*/ current_tag["src"] = strip(match(/.*/)); skip_white(); } // Used to match @private, @ignore, @hide, ... function boolean_at_tag(regex, propname) { match(regex); add_tag(propname); skip_white(); } // matches {type} if possible and sets it on @current_tag // Also checks for {optionality=} in type definition. function maybe_type() { skip_horiz_white(); if (look(/\{/)) { var tdf = typedef(); current_tag["type"] = tdf["type"]; current_tag["optional"] = tdf["optional"] ? true : false; } } // matches: <ident-chain> | "[" <ident-chain> [ "=" <default-value> ] "]" function maybe_name_with_default() { skip_horiz_white(); if (look(/\[/)) { match(/\[/); maybe_ident_chain("name"); skip_horiz_white(); if (look(/=/)) { match(/=/); skip_horiz_white(); current_tag["default"] = default_value(); } skip_horiz_white(); match(/\]/); current_tag["optional"] = true; } else { maybe_ident_chain("name"); } } // matches: "(optional)" function maybe_optional() { skip_horiz_white(); if (look(/\(optional\)/i)) { match(/\(optional\)/i); current_tag["optional"] = true; } } // matches: "(required)" function maybe_required() { skip_horiz_white if (look(/\(required\)/i)) { match(/\(required\)/i) current_tag["optional"] = false } } // matches identifier name if possible and sets it on @current_tag function maybe_name() { skip_horiz_white(); if (look(ident_pattern_with_dot)) { current_tag["name"] = match(ident_pattern_with_dot); } else if (look(ident_pattern)) { current_tag["name"] = match(ident_pattern); } } // matches ident.chain if possible and sets it on @current_tag function maybe_ident_chain(propname) { skip_horiz_white(); if (look(ident_chain_pattern)) { current_tag[propname] = ident_chain(); } } // Attempts to allow balanced braces in default value. // When the nested parsing doesn't finish at closing "]", // roll back to beginning and simply grab anything up to closing "]". function default_value() { start_pos = input.pointer(); value = parse_balanced(/\[/, /\]/, /[^\[\]]*/); if (look(/\]/)) { return value; } else { input.setPointer(start_pos); return match(/[^\]]*/); } } // matches {...=} and returns text inside brackets function typedef() { match(/\{/); name = parse_balanced(/\{/, /\}/, /[^{}]*/); if (/=$/.test(name)) { name = name.substring(0, name.length - 1); optional = true; } else { optional = null; } match(/\}/); return {"type" : name, "optional": optional}; } // Helper method to parse a string up to a closing brace, // balancing opening-closing braces in between. // // @param re_open The beginning brace regex // @param re_close The closing brace regex // @param re_rest Regex to match text without any braces function parse_balanced(re_open, re_close, re_rest) { result = match(re_rest); while (look(re_open)) { result += match(re_open); result += parse_balanced(re_open, re_close, re_rest); result += match(re_close); result += match(re_rest); } return result; } // matches <ident_chain> <ident_chain> ... until line end function class_list() { skip_horiz_white(); classes = []; while (look(ident_chain_pattern)) { classes.push(ident_chain()); skip_horiz_white(); } return classes; } // matches chained.identifier.name and returns it function ident_chain() { return input.scan(ident_chain_pattern); } // matches identifier and returns its name function ident() { return input.scan(/\w+/); } function look(re) { return input.check(re); } function match(re) { return input.scan(re); } function skip_white() { return input.scan(/\s+/); } // skips horizontal whitespace (tabs and spaces) function skip_horiz_white() { return input.scan(/[ \t]+/); } function strip(str) { return str.replace(/^\s+|\s+$/g, ""); }