UNPKG

panino

Version:

API documentation generator with a strict grammar and testing tools

208 lines (175 loc) 5.8 kB
var _ = require('underscore'); require("colors"); // All possible node types in Esprima-created abstract syntax tree // // Each node type maps to list of properties of that node into // which we can recurse for further parsing. var NODE_TYPES = { "Program" : ["body"], "BlockStatement" : ["body"], "BreakStatement" : [], "ContinueStatement" : [], "DoWhileStatement" : ["body", "test"], "DebuggerStatement" : [], "EmptyStatement" : [], "ExpressionStatement" : ["expression"], "ForStatement" : ["init", "test", "update", "body"], "ForInStatement" : ["left", "right", "body"], "IfStatement" : ["test", "consequent", "alternate"], "LabeledStatement" : ["body"], "ReturnStatement" : ["argument"], "SwitchStatement" : ["discriminant", "cases"], "SwitchCase" : ["test", "consequent"], "ThrowStatement" : ["argument"], "TryStatement" : ["block", "handlers", "finalizer"], "CatchClause" : ["param", "body"], "WhileStatement" : ["test", "body"], "WithStatement" : ["object", "body"], "FunctionDeclaration" : ["id", "params", "body"], "VariableDeclaration" : ["declarations"], "VariableDeclarator" : ["id", "init"], "AssignmentExpression" : ["left", "right"], "ArrayExpression" : ["elements"], "BinaryExpression" : ["left", "right"], "CallExpression" : ["callee", "arguments"], "ConditionalExpression" : ["test", "consequent", "alternate"], "FunctionExpression" : ["body"], "LogicalExpression" : ["left", "right"], "MemberExpression" : ["object", "property"], "NewExpression" : ["callee", "arguments"], "ObjectExpression" : ["properties"], "Property" : ["key", "value"], "SequenceExpression" : ["expressions"], "ThisExpression" : [], "UnaryExpression" : ["argument"], "UpdateExpression" : ["argument"], "Identifier" : [], "Literal" : [] } var start_index = 0, start_linenr = 1; exports.parse = function(ast, source) { ast["comments"] = merge_comments(ast["comments"], source); return locate_comments(ast, source); }; function merge_comments(original_comments, source) { var result = [], i = 0, comment = original_comments[0]; while (comment) { i++; var next_comment = original_comments[i]; if (next_comment && mergeable(comment, next_comment, source)) { // Merge next comment to current one comment["value"] += "\n" + next_comment["value"]; comment["range"][1] = next_comment["range"][1]; } else { // Create a link and continue with next comment comment["next"] = next_comment; result.push(comment); comment = next_comment; } } return result; } /* Two comments can be merged if they are both line-comments and they are separated only by whitespace (only one newline at the end of the first comment is allowed) */ function mergeable(c1, c2, source) { if (c1["type"] == "Line" && c2["type"] == "Line") { var x = /^(\r\n|\n|\r)?[ \t]*$/.test(source.slice(c1["range"][1], c2["range"][0])); return x; } else return false; } function locate_comments(ast, source) { return ast["comments"].map(function(comment) { // Detect comment type and strip * at the beginning of doc-comment var value = comment["value"]; var type = ""; if (comment["type"] == "Block" && /^\*/.test(value)) { type = "doc_comment"; value = value.slice(1, value.length); } else { type = "plain_comment"; } return {"comment": value, "code": stuff_after(comment, ast), "linenr": line_number(comment["range"][0], source), "type": type} }); } // Given index inside input string, returns the corresponding line number function line_number(index, source) { // To speed things up, remember the index until which we counted, // then next time just begin counting from there. This way we // only count each line once. var i = start_index; var count = 0; while (i < index) { if (source[i] === "\n") { count++; } i++; } start_linenr = count + start_linenr; start_index = index; return start_linenr; } // Sees if there is some code following the comment. // Returns the code found. But if the comment is instead // followed by another comment, returns an empty string. function stuff_after(comment, ast) { var code = code_after(comment["range"], ast); if (code && comment["next"]) return code["range"][0] < comment["next"]["range"][0] ? code : ""; else return code; } // Looks for code following the given range. // // The second argument is the parent node within which we perform // our search. function code_after(range, parent) { // Look through all child nodes of parent... var children = child_nodes(parent); for (var i = 0; i < children.length; i++) { if (less(range, children[i]["range"])) { // If node is after our range, then that's it. There could // be comments in our way, but that's taken care of in // #stuff_after method. return children[i]; } else if (within(range, children[i]["range"])) { // Our range is within the node --> recurse return code_after(range, children[i]) } } return; } // True if range A is less than range B function less(a, b) { return a[1] < b[0]; } // True if range A is greater than range B function greater(a, b) { return a[0] > b[1]; } // True if range A is within range B function within(a, b) { return b[0] < a[0] && a[1] < b[1]; } // Returns array of child nodes of given node function child_nodes(node) { var properties = NODE_TYPES[node["type"]]; if (properties === undefined) { console.error("FATAL".red + ": Unknown node type: " + node["type"]); console.trace(); process.exit(1); } var x = properties.map(function(p) { return node[p]; }); return _.flatten(_.compact(x)); }