UNPKG

javadoc

Version:

Unopinionated, simple and powerful documentation generator.

366 lines (354 loc) 16.4 kB
const fs = require("fs-extra"); const glob = require("glob"); const path = require("path"); const { EOL } = require("os"); const capitalizeString = (s) => { return s.substr(0, 1).toUpperCase() + s.substr(1); }; /** * ----------------- * * ### `Javadoc` * @type *Class. Function.* * @description Master class of the `javadoc` package. */ class Javadoc { /** * * ------------------- * * ### `Javadoc.REGEX_PATTERNS` * @type *Static property. Object.* * @description Regular expression patterns used by the class. */ static get REGEX_PATTERNS() { return { JAVADOC_COMMENT: /\/\*\*[\t ]*((\r?\n)([\t ]*\*[\t ]*).*)*/g, JAVADOC_ENTRY: /^\/\*\*[\t ]?/g, JAVADOC_LINE_ENTRY: /^\r?\n[\t ]*\*[\t ]?/g, JAVADOC_PROPERTY: /^[\t ]*\@[^\n\r\t ]*/g, JAVADOC_VALUE: /^.*/g, } } /** * * ------------------------- * * ### `Javadoc.DEFAULT_GLOB_OPTIONS` * @type *Static property. Object.* * @property * @property - `cwd`. Defaults to `process.cwd()`. Allowed options at [glob package](https://www.npmjs.com/package/glob#options) * @property - `dot`. Defaults to `true`. Allowed options at [glob package](https://www.npmjs.com/package/glob#options) * @description Default options used by the `glob` package. */ static get DEFAULT_GLOB_OPTIONS() { return { cwd: process.cwd(), dot: true, } } /** * * --------------------------- * * ### `Javadoc.DEFAULT_OPTIONS` * @type *Static property. Object.* * @property * @property - `include:Array<String>`. Defaults to `["** /*.js"]`. Allowed rules at [glob package](https://www.npmjs.com/package/glob). * @property - `exclude:Array<String>`. Defaults to `["** /node_modules/**.js"]`. Allowed rules at [glob package](https://www.npmjs.com/package/glob). * @property - `format:String`. Defaults to `"markdown"`. Allowed values: * - `"md"` * - `"json"` * @property - `output:String`. Defaults to `undefined`. File into which dump the results. **Required value**. * @description General options of the `Javadoc.generate` main method. */ static get DEFAULT_OPTIONS() { return { include: ["**/*.js"], exclude: ["**/node_modules/**"], format: "markdown", output: undefined } } /** * * ------------------------ * * ### `Javadoc.findFiles` * @type *Static method. Function.* * @parameter * @parameter - `parameterIncludes:Array<String>`. Replaces `DEFAULT_OPTIONS.include` as value. * @parameter - `parameterExcludes:Array<String>`. Replaces `DEFAULT_OPTIONS.exclude` as value. * @parameter - `parameterOptions:Object`. Overrides `DEFAULT_GLOB_OPTIONS` as object. * @returns files:Promise<Array<String>>. Asynchronously, returns an array of matched files (as Strings). * @description Finds files based on glob patterns ([more info](https://www.npmjs.com/package/glob#usage)) included, excluded and glob options ([more info](https://www.npmjs.com/package/glob#options)). */ static findFiles(parameterIncludes, parameterExcludes = [], parameterOptions = {}) { return new Promise((success, failure) => { const options = Object.assign({}, this.DEFAULT_GLOB_OPTIONS, parameterOptions); const excludes = [].concat(parameterExcludes).map(f => { return path.resolve(options.cwd, f); }); options.ignore = excludes; const includez = [].concat(parameterIncludes).map(f => { return path.resolve(options.cwd, f); }); console.log("[javadoc] Patterns:"); includez.forEach(includezItem => { console.log("[javadoc] - (include) " + includezItem); }); excludes.forEach(excluded => { console.log("[javadoc] - (exclude) " + excluded); }); // @TODO: match all included glob patterns in a loop... const files = []; let counterIncluded = 0; const finalize = () => { counterIncluded++; if (counterIncluded >= includez.length) { return success(files); } } if (includez.length === 0) { return finalize(); } for (let includeIndex = 0; includeIndex < includez.length; includeIndex++) { const includePattern = includez[includeIndex]; glob(includePattern, options, (error, filesMatched) => { if (error) { return finalize(); } for (let indexMatched = 0; indexMatched < filesMatched.length; indexMatched++) { const fileMatched = filesMatched[indexMatched]; if (files.indexOf(fileMatched) === -1) { files.push(fileMatched); } } return finalize(); }); } }); } /** * * ------------------------------- * * ### `Javadoc.findCommentsInFile` * @type *Static method. Function.* * @parameter `file:String`. File into which look for the javadoc-comments. * @returns `matches:Promise<Array<Object>>`. Asynchronously, returns a list of matched javadoc-comments (as Objects) found in the passed file. * @description From a file, it returns (asynchronously, by a Promise) javadoc-comments represented as Objects. */ static findCommentsInFile(file) { return new Promise((success, failure) => { return fs.readFile(file, "utf8", (error, contents) => { if (error) { return failure(error); } const matches = this.findCommentsInString(contents); return success(matches); }); }); } /** * * -------------------------------- * * ### `Javadoc.findCommentsInString` * @type *Static method. Function.* * @parameter `text:String`. Text into which look for the javadoc-comments. * @returns `totalMatches:Array<Object>`. List of javadoc-comments (as Objects) found in the passed String. * @description Finds javadoc-comments (as Objects) from a String. */ static findCommentsInString(text) { const totalMatches = []; const { JAVADOC_COMMENT, JAVADOC_ENTRY, JAVADOC_LINE_ENTRY, JAVADOC_PROPERTY, JAVADOC_VALUE, } = this.REGEX_PATTERNS; // 1. Get all javadoc-comment matches. const allMatches = text.match(JAVADOC_COMMENT); if (allMatches === null) { return totalMatches; } // 2. For each javadoc-comment... for (let indexMatch = 0; indexMatch < allMatches.length; indexMatch++) { const matches = {}; let lastProperty = "_"; let wasStarted = false; let token = allMatches[indexMatch]; // 3. Remove the entry token = token.replace(JAVADOC_ENTRY, ""); // 4. For each line of the javadoc-comment... starts the extraction. Extraction: while (JAVADOC_LINE_ENTRY.test(token)) { // 5. Remove the line entry. token = token.replace(JAVADOC_LINE_ENTRY, ""); // 6. Get the current property of the line (description by default) const isPropertyLine = JAVADOC_PROPERTY.test(token); const property = isPropertyLine ? token.match(JAVADOC_PROPERTY)[0] : lastProperty; // 7. Remove the property. token = token.replace(JAVADOC_PROPERTY, ""); // 8. Get the current value. Also, replace "* /" for "*/" (in order to escape closed comments). const value = token.match(JAVADOC_VALUE)[0].replace(/\* \//g, "*/"); // 9. If it is the last line of the javadoc, get out of the extraction of that javadoc-comment. if (value === "/") { continue Extraction; } // 10. Remove the value. token = token.replace(JAVADOC_VALUE, ""); // 11. If the property did not exist, create it. if (!(property in matches)) { matches[property] = []; } // 12. Add the value to that property. if ((!isPropertyLine) && wasStarted) { // a. Inline if it is a new-property-line matches[property][matches[property].length - 1] += EOL + value; } else { // b. Otherwise as a new entry matches[property].push(value); } // 13. Assign the last property to the current property. lastProperty = property; wasStarted = true; // 14. Go for the next javadoc-comment line extraction. } // 15. Once all the lines were extracted, push the data for this javadoc-comment. totalMatches.push(matches); } return totalMatches; } /** * * ----------------------------------- * * ### `Javadoc.formatJsonToMarkdown` * @type *Static method. Function.* * @parameter `fileComments:Array<Object>`. List of javadoc-comments (as Object) to convert to markdown format. * @returns `formatted:String`. Markdown code from passed javadoc-comments. * @description Generates `markdown` code from a list of javadoc-comments. */ static formatJsonToMarkdown(filesAndComments) { let formatted = ""; for (let indexFiles = 0; indexFiles < filesAndComments.length; indexFiles++) { const fileComments = filesAndComments[indexFiles]; for (let indexComments = 0; indexComments < fileComments.length; indexComments++) { const fileComment = fileComments[indexComments]; Object.keys(fileComment).forEach(property => { const value = fileComment[property].join(EOL + EOL + EOL); if (property !== "_") { const propertyTransformed = capitalizeString(property.replace(/^@/g, "").replace(/\-[a-z]/g, (match) => { return " " + match.substr(1).toUpperCase(); })); formatted += `**${propertyTransformed}**`; } if (value) { formatted += (property.startsWith("@") ? ": " : "") + value + "" + EOL + EOL + EOL; } else { formatted += "." + EOL + EOL + EOL; } }); } } return formatted; } /** * * ----------------------------------- * * ### `Javadoc.generate` * @type *Static method. Function.* * @parameter `parameters:Object`. Overrides `Javadoc.DEFAULT_OPTIONS` as Object. To see more about properties, go to that static property of Javadoc class. * @returns `output:Promise<String|Array<Object>>`. Depending on `parameters.format` (`"json"`|`"markdown"`), it can return an Array or a String. * @description Finds included files, dismisses excluded files, extracts javadoc-comments, formats them adecuately, and dumps the results into output file, while returning the output asynchronously (as a Promise). */ static generate(parameters = {}) { return new Promise((success, failure) => { const options = Object.assign({}, this.DEFAULT_OPTIONS, parameters); console.log("[javadoc] Start extracting javadoc comments."); return this.findFiles(options.include, options.exclude).then(files => { console.log(`[javadoc] Total of files matched: ${files.length}.`); let counter = 0; const output = { success: [], error: [] }; if (files.length === 0) { return success([]); } const endExecution = (result) => { console.log("[javadoc] Final javadoc report per file:"); console.log(`[javadoc] - ${output.success.length} success(es).`); console.log(`[javadoc] - ${output.error.length} error(s).`); if (output.error.length === 0) { console.log("[javadoc] Successfully finished."); } return success(result); } const finalize = () => { counter++; if (counter >= files.length) { if (["md", "markdown"].indexOf(options.format) !== -1) { const formatted = this.formatJsonToMarkdown(output.success); if (options.output) { const outputFile = path.resolve(options.output); try { fs.outputFileSync(outputFile, formatted, "utf8"); console.log(`[javadoc] File output (markdown) written successfully at ${path.basename(outputFile)}.`); } catch (error) { console.log("[javadoc] Error writing the (markdown) output.", error); } } else { console.log("[javadoc] CAUTION: No output file was specified."); } return endExecution(formatted); } else { if (options.output) { const outputFile = path.resolve(options.output); try { fs.outputFileSync(outputFile, JSON.stringify(output, null, 2), "utf8"); console.log(`[javadoc] File output (json) written successfully at ${path.basename(outputFile)}.`); } catch (error) { console.log("[javadoc] Error writing the (json) output.", error); } } else { console.log("[javadoc] CAUTION: No output file was specified."); } return endExecution(output); } } } for (let index = 0; index < files.length; index++) { const file = files[index]; ((file) => { return this.findCommentsInFile(file).then(comments => { console.log(`[javadoc] File ${path.basename(file)} has ${comments.length} javadoc comments.`); Agregar_fichero_en_nodo_de_comentario: { comments = comments.map(comment => { comment.file = file; return comment; }) } if (comments.length) { output.success.push(comments); } return finalize(); }).catch(error => { console.log(`[javadoc] Error finding comments on file: ${path.basename(file)}.`); output.error.push(error); return finalize(); }); })(file); } }).catch(error => { console.log("[javadoc] Failed on finding files.", error); return failure(error); }); }); } } module.exports = Javadoc;