/** * Generate Markdown from your Javadoc, PHPDoc or JSDoc comments * * Usage: Create a new instance of JavadocToMarkdown and then * call either fromJavadoc(), fromPHPDoc() or fromJSDoc() * * @constructor */ var JavadocToMarkdown = function () { "use strict"; var self = this; /** * Generates Markdown documentation from code on a more abstract level * * @param {string} code the code that contains doc comments * @param {number} headingsLevel the headings level to use as the base (1-6) * @param {function} fnAddTagsMarkdown the function that processes doc tags and generates the Markdown documentation * @returns {string} the Markdown documentation */ function fromDoc(code, headingsLevel, fnAddTagsMarkdown) { var i, out, sections; // get all documentation sections from code sections = getSections(code); // initialize a string buffer out = []; out.push("#".repeat(headingsLevel)+" Documentation"); for (i = 0; i < sections.length; i++) { out.push(fromSection(sections[i], headingsLevel, fnAddTagsMarkdown)); } // return the contents of the string buffer and add a trailing newline return out.join("")+"\n"; } /** * Generates Markdown documentation from a statically typed language's doc comments * * @param {string} code the code that contains doc comments * @param {number} headingsLevel the headings level to use as the base (1-6) * @returns {string} the Markdown documentation */ this.fromStaticTypesDoc = function(code, headingsLevel) { return fromDoc(code, headingsLevel, function(tag, assocBuffer) { var tokens; switch (tag.key) { case "abstract": addToBuffer(assocBuffer, "Abstract", tag.value); break; case "access": addToBuffer(assocBuffer, "Access", tag.value); break; case "author": addToBuffer(assocBuffer, "Author", tag.value); break; case "constructor": addToBuffer(assocBuffer, "Constructor", null); break; case "copyright": addToBuffer(assocBuffer, "Copyright", tag.value); break; case "deprec": case "deprecated": addToBuffer(assocBuffer, "Deprecated", null); break; case "example": addToBuffer(assocBuffer, "Example", tag.value); break; case "exception": case "throws": tokens = tag.value.tokenize(/\s+/g, 2); addToBuffer(assocBuffer, "Exceptions", "`"+tokens[0]+"` — "+tokens[1]); break; case "exports": addToBuffer(assocBuffer, "Exports", tag.value); break; case "license": addToBuffer(assocBuffer, "License", tag.value); break; case "link": addToBuffer(assocBuffer, "Link", tag.value); break; case "name": addToBuffer(assocBuffer, "Alias", tag.value); break; case "package": addToBuffer(assocBuffer, "Package", tag.value); break; case "param": tokens = tag.value.tokenize(/\s+/g, 2); addToBuffer(assocBuffer, "Parameters", "`"+tokens[0]+"` — "+tokens[1]); break; case "private": addToBuffer(assocBuffer, "Private", null); break; case "return": case "returns": addToBuffer(assocBuffer, "Returns", tag.value); break; case "see": addToBuffer(assocBuffer, "See also", tag.value); break; case "since": addToBuffer(assocBuffer, "Since", tag.value); break; case "static": addToBuffer(assocBuffer, "Static", tag.value); break; case "subpackage": addToBuffer(assocBuffer, "Sub-package", tag.value); break; case "this": addToBuffer(assocBuffer, "This", "`"+tag.value+"`"); break; case "todo": addToBuffer(assocBuffer, "To-do", tag.value); break; case "version": addToBuffer(assocBuffer, "Version", tag.value); break; default: break; } }); }; /** * Generates Markdown documentation from a dynamically typed language's doc comments * * @param {string} code the code that contains doc comments * @param {number} headingsLevel the headings level to use as the base (1-6) * @param {function} fnFormatType the function that formats a type information (single argument) * @param {function} fnFormatTypeAndName the function that formats type and name information (two arguments) * @returns {string} the Markdown documentation */ this.fromDynamicTypesDoc = function(code, headingsLevel, fnFormatType, fnFormatTypeAndName) { return fromDoc(code, headingsLevel, function(tag, assocBuffer) { var tokens; switch (tag.key) { case "abstract": addToBuffer(assocBuffer, "Abstract", tag.value); break; case "access": addToBuffer(assocBuffer, "Access", tag.value); break; case "author": addToBuffer(assocBuffer, "Author", tag.value); break; case "constructor": addToBuffer(assocBuffer, "Constructor", null); break; case "copyright": addToBuffer(assocBuffer, "Copyright", tag.value); break; case "deprec": case "deprecated": addToBuffer(assocBuffer, "Deprecated", null); break; case "example": addToBuffer(assocBuffer, "Example", tag.value); break; case "exception": case "throws": tokens = tag.value.tokenize(/\s+/g, 2); addToBuffer(assocBuffer, "Exceptions", fnFormatType(tokens[0])+" — "+tokens[1]); break; case "exports": addToBuffer(assocBuffer, "Exports", tag.value); break; case "license": addToBuffer(assocBuffer, "License", tag.value); break; case "link": addToBuffer(assocBuffer, "Link", tag.value); break; case "name": addToBuffer(assocBuffer, "Alias", tag.value); break; case "package": addToBuffer(assocBuffer, "Package", tag.value); break; case "param": tokens = tag.value.tokenize(/\s+/g, 3); addToBuffer(assocBuffer, "Parameters", fnFormatTypeAndName(tokens[0], tokens[1])+" — "+tokens[2]); break; case "private": addToBuffer(assocBuffer, "Private", null); break; case "return": case "returns": tokens = tag.value.tokenize(/\s+/g, 2); addToBuffer(assocBuffer, "Returns", fnFormatType(tokens[0])+" — "+tokens[1]); break; case "see": addToBuffer(assocBuffer, "See also", tag.value); break; case "since": addToBuffer(assocBuffer, "Since", tag.value); break; case "static": addToBuffer(assocBuffer, "Static", tag.value); break; case "subpackage": addToBuffer(assocBuffer, "Sub-package", tag.value); break; case "this": addToBuffer(assocBuffer, "This", "`"+tag.value+"`"); break; case "todo": addToBuffer(assocBuffer, "To-do", tag.value); break; case "var": tokens = tag.value.tokenize(/\s+/g, 2); addToBuffer(assocBuffer, "Type", fnFormatType(tokens[0])+" — "+tokens[1]); break; case "version": addToBuffer(assocBuffer, "Version", tag.value); break; default: break; } }); }; /** * Generates Markdown documentation from Javadoc comments * * @param {string} code the code that contains doc comments * @param {number} headingsLevel the headings level to use as the base (1-6) * @returns {string} the Markdown documentation */ this.fromJavadoc = function(code, headingsLevel) { return self.fromStaticTypesDoc(code, headingsLevel); }; /** * Generates Markdown documentation from PHPDoc comments * * @param {string} code the code that contains doc comments * @param {number} headingsLevel the headings level to use as the base (1-6) * @returns {string} the Markdown documentation */ this.fromPHPDoc = function(code, headingsLevel) { return self.fromDynamicTypesDoc( code, headingsLevel, function (type) { return "`"+type+"`"; }, function (type, name) { // if we have a valid name (and type) if (/^\$([a-zA-Z0-9_$]+)$/.test(name)) { return "`"+name+"` — `"+type+"`"; } // if it seems we only have a name else { // return the name that was, wrongly, in the position of the type return "`"+type+"`"; } } ); }; /** * Generates Markdown documentation from JSDoc comments * * @param {string} code the code that contains doc comments * @param {number} headingsLevel the headings level to use as the base (1-6) * @returns {string} the Markdown documentation */ this.fromJSDoc = function(code, headingsLevel) { return self.fromDynamicTypesDoc( code, headingsLevel, function (type) { return "`"+type.substr(1, type.length-2)+"`"; }, function (type, name) { // if we have a valid type (and name) if (/^\{([^{}]+)\}$/.test(type)) { return "`"+name+"` — `"+type.substr(1, type.length-2)+"`"; } // if it seems we only have a name else { // return the name that was, wrongly, in the position of the type return "`"+type+"`"; } } ); }; /** * Generates Markdown documentation from a given section * * The function processes units of documentation, a line of code with accompanying doc comment * * @param {object} section the section that consists of code line and doc comment * @param {number} headingsLevel the headings level to use as the base (1-6) * @param {function} fnAddTagsMarkdown the function that processes doc tags and generates the Markdown documentation * @returns {string} the Markdown documentation */ function fromSection(section, headingsLevel, fnAddTagsMarkdown) { var assocBuffer, description, field, out, p, t, tags; // initialize a string buffer out = []; // first get the field that we want to describe field = getFieldDeclaration(section.line); // if there is no field to describe if (!field) { // do not return any documentation return ""; } out.push("\n\n"); out.push("#".repeat(headingsLevel+1)+" `"+field+"`"); // split the doc comment into main description and tag section var docCommentParts = section.doc.split(/^(?:\t| )*?\*(?:\t| )*?(?=@)/m); // get the main description (which may be an empty string) var rawMainDescription = docCommentParts.shift(); // get the tag section (which may be an empty array) var rawTags = docCommentParts; description = getDocDescription(rawMainDescription); if (description.length) { out.push("\n\n"); out.push(description); } tags = getDocTags(rawTags); if (tags.length) { out.push("\n"); assocBuffer = {}; for (t = 0; t < tags.length; t++) { fnAddTagsMarkdown(tags[t], assocBuffer); } for (p in assocBuffer) { if (assocBuffer.hasOwnProperty(p)) { out.push(fromTagGroup(p, assocBuffer[p])); } } } // return the contents of the string buffer return out.join(""); } function fromTagGroup(name, entries) { var i, out; // initialize a string buffer out = []; out.push("\n"); if (entries.length === 1 && entries[0] === null) { out.push(" * **"+name+"**"); } else { out.push(" * **"+name+":**"); if (entries.length > 1) { for (i = 0; i < entries.length; i++) { out.push("\n"); out.push(" * "+entries[i]); } } else if (entries.length === 1) { out.push(" "+entries[0]); } } // return the contents of the string buffer return out.join(""); } function getSections(code) { var docLine, fieldDeclaration, m, out, regex; regex = /\/\*\*([^]*?)\*\/([^{;/]+)/gm; out = []; while ((m = regex.exec(code)) !== null) { if (m.index === regex.lastIndex) { regex.lastIndex++; } if (typeof m[1] === "string" && m[1] !== null) { if (typeof m[2] === "string" && m[2] !== null) { fieldDeclaration = m[2].trim(); docLine = m[1]; // if the source code line is an import statement if (/^import\s+/.test(fieldDeclaration)) { // ignore this piece continue; } // if this is a single line comment if (docLine.indexOf("*") === -1) { // prepend an asterisk to achieve the normal line structure docLine = "*"+docLine; } // interpret empty lines as if they contained a p-tag docLine = docLine.replace(/\*[ ]*$/gm, "*

"); out.push({ "line": fieldDeclaration, "doc": docLine }); } } } return out; } function getFieldDeclaration(line) { var regex = /^([^\{;]+)(.*?)$/gm; var m; while ((m = regex.exec(line)) !== null) { if (m.index === regex.lastIndex) { regex.lastIndex++; } if (typeof m[1] === "string" && m[1] !== null) { return cleanSingleLine(m[1]); } } return ""; } function replaceHTMLWithMarkdown(html) { return html.replace(/<\s*?code\s*?>(.*?)<\s*?\/\s*?code\s*?>/g, "`$1`"); } function getDocDescription(docLines) { var regex = /^(\t| )*?\*(\t| )+(.*?)$/gm; var m; var out = []; while ((m = regex.exec(docLines)) !== null) { if (m.index === regex.lastIndex) { regex.lastIndex++; } if (typeof m[3] === "string" && m[3] !== null) { m[3] = cleanLine(m[3]); m[3] = replaceHTMLWithMarkdown(m[3]); out.push(m[3]); } } return cleanLine(out.join(" ").replace(/<(\/)?p>/gi, "\n\n")); } function getDocTags(docLines) { var regex = /^(?:\t| )*?@([a-zA-Z]+)([\s\S]*)/; var m; var out = []; for (var i = 0; i < docLines.length; i++) { m = regex.exec(docLines[i]); if (m !== null) { if (typeof m[1] === "string" && m[1] !== null) { if (typeof m[2] === "string" && m[2] !== null) { // trim leading and trailing space in the tag value m[2] = m[2].trim(); // format multi-line tag values correctly m[2] = m[2].split(/[\r\n]{1,2}(?:\t| )*?\*(?:\t| )*/).join("\n\n "); // add the key and value for this tag to the output out.push({ "key": cleanSingleLine(m[1]), "value": m[2] }); } } } } return out; } function cleanLine(line) { // trim leading and trailing spaces line = line.trim(); // clear spaces before and after line breaks and tabs line = line.replace(/ *([\n\r\t]) */gm, "$1"); // make consecutive spaces one line = line.replace(/[ ]{2,}/g, " "); return line; } function cleanSingleLine(line) { // perform normal line cleaning line = cleanLine(line); // replace line breaks and tabs with spaces line = line.replace(/(\n|\r|\t)/g, " "); return line; } function addToBuffer(buffer, key, value) { if (typeof buffer[key] === "undefined" || buffer[key] === null) { buffer[key] = []; } buffer[key].push(value); } String.prototype.tokenize = function(splitByRegex, limit) { var counter, i, m, start, tokens; tokens = []; counter = 1; start = 0; while ((m = splitByRegex.exec(this)) !== null) { if (m.index === splitByRegex.lastIndex) { splitByRegex.lastIndex++; } if (counter < limit) { tokens.push(this.substring(start, m.index)); start = m.index + m[0].length; } counter++; } // add the remainder as a single part tokens.push(this.substring(start)); // fill the array to match the limit if necessary for (i = tokens.length; i < limit; i++) { tokens.push(""); } return tokens; }; String.prototype.repeat = function(count) { return new Array(count + 1).join(this); }; };