UNPKG

vuepress-plugin-autodoc

Version:
462 lines (382 loc) 11.4 kB
'use strict'; var child_process = require('child_process'); // Token class /** * class Token **/ /** * new Token(type, tag, nesting) * * Create new token and fill passed properties. **/ function Token(type, tag, nesting) { /** * Token#type -> String * * Type of the token (string, e.g. "paragraph_open") **/ this.type = type; /** * Token#tag -> String * * html tag name, e.g. "p" **/ this.tag = tag; /** * Token#attrs -> Array * * Html attributes. Format: `[ [ name1, value1 ], [ name2, value2 ] ]` **/ this.attrs = null; /** * Token#map -> Array * * Source map info. Format: `[ line_begin, line_end ]` **/ this.map = null; /** * Token#nesting -> Number * * Level change (number in {-1, 0, 1} set), where: * * - `1` means the tag is opening * - `0` means the tag is self-closing * - `-1` means the tag is closing **/ this.nesting = nesting; /** * Token#level -> Number * * nesting level, the same as `state.level` **/ this.level = 0; /** * Token#children -> Array * * An array of child nodes (inline and img tokens) **/ this.children = null; /** * Token#content -> String * * In a case of self-closing tag (code, html, fence, etc.), * it has contents of this tag. **/ this.content = ''; /** * Token#markup -> String * * '*' or '_' for emphasis, fence string for fence, etc. **/ this.markup = ''; /** * Token#info -> String * * fence infostring **/ this.info = ''; /** * Token#meta -> Object * * A place for plugins to store an arbitrary data **/ this.meta = null; /** * Token#block -> Boolean * * True for block-level tokens, false for inline tokens. * Used in renderer to calculate line breaks **/ this.block = false; /** * Token#hidden -> Boolean * * If it's true, ignore this element when rendering. Used for tight lists * to hide paragraphs. **/ this.hidden = false; } /** * Token.attrIndex(name) -> Number * * Search attribute index by name. **/ Token.prototype.attrIndex = function attrIndex(name) { var attrs, i, len; if (!this.attrs) { return -1; } attrs = this.attrs; for (i = 0, len = attrs.length; i < len; i++) { if (attrs[i][0] === name) { return i; } } return -1; }; /** * Token.attrPush(attrData) * * Add `[ name, value ]` attribute to list. Init attrs if necessary **/ Token.prototype.attrPush = function attrPush(attrData) { if (this.attrs) { this.attrs.push(attrData); } else { this.attrs = [ attrData ]; } }; /** * Token.attrSet(name, value) * * Set `name` attribute to `value`. Override old value if exists. **/ Token.prototype.attrSet = function attrSet(name, value) { var idx = this.attrIndex(name), attrData = [ name, value ]; if (idx < 0) { this.attrPush(attrData); } else { this.attrs[idx] = attrData; } }; /** * Token.attrGet(name) * * Get the value of attribute `name`, or null if it does not exist. **/ Token.prototype.attrGet = function attrGet(name) { var idx = this.attrIndex(name), value = null; if (idx >= 0) { value = this.attrs[idx][1]; } return value; }; /** * Token.attrJoin(name, value) * * Join value to existing attribute via space. Or create new attribute if not * exists. Useful to operate with token classes. **/ Token.prototype.attrJoin = function attrJoin(name, value) { var idx = this.attrIndex(name); if (idx < 0) { this.attrPush([ name, value ]); } else { this.attrs[idx][1] = this.attrs[idx][1] + ' ' + value; } }; var token = Token; var CSS = ".badge {\n display: inline-block;\n font-size: 20px;\n font-family: monospace;\n height: 28px;\n line-height: 28px;\n border-radius: 3px;\n padding: 0 6px;\n color: #fff;\n background-color: #42b983;\n}\n.scoped .badge,\nblockquote .badge {\n font-size: 15px;\n height: 23px;\n line-height: 23px;\n}\n.badge.warning {\n background-color: #e7c000;\n}\n.badge.error {\n background-color: #da5961;\n}\n.badge.tip {\n background-color: #42b983;\n}\nblockquote.scoped.warning {\n border-color: #e7c000;\n}\nblockquote.scoped.error {\n border-color: #da5961;\n}\nblockquote.scoped.tip {\n border-color: #42b983;\n}\n"; /** * Main entry point for module */ /** * Return badge class for specific object type. * * @param type - Object type to document. */ function badgeClass(type) { const data = { class: 'tip', function: 'error', const: 'warning', member: 'warning' }; return type in data ? data[type] : 'tip'; } /** * Generate html formatting for single parameter. * * @param param - Object with param data structure. */ function formatParam(param) { const name = param.name ? `<code>${param.name}</code>` : ''; const type = param.type ? ' (' + param.type.names.map(x => `<em>${x}</em>`).join(', ') + ')' : ''; const desc = param.description ? ` - ${param.description}` : ''; return `${name}${type}${desc}`; } /** * Generate formatted html for single component. * * @param data - Structured data to generate html from. */ function html(data, nested) { nested = nested || false; const result = []; let cls = badgeClass(data.type); let call = `${data.name}`; if (['class', 'function'].includes(data.type)) { call += `(`; if (data.params) { call += data.params.map(x => x.name).join(', '); } call += `)`; } // header const htag = nested ? 'h4' : 'h3'; const anchor = data.name.toLowerCase(); result.push(`<${htag} id="${anchor}">`); result.push(` <a href="#${anchor}" class="header-anchor">#</a>`); result.push(` <span class="badge ${cls}" style="vertical-align: top;">${data.type}</span>`); result.push(` <code>${call}</code>`); result.push(`</${htag}>`); // description if (data.description) { result.push(`<blockquote><p>${data.description}</p></blockquote>`); } // parameters if (data.params && data.params.length > 0) { result.push(`<blockquote>`); result.push(`<p><strong>Parameters</strong></p>`); result.push(`<ul>`); data.params.forEach(param => { const parsed = formatParam(param); result.push(`<li>${parsed}</li>`); }); result.push(`</ul>`); result.push(`</blockquote>`); } // returns if (data.returns && data.returns.length > 0) { result.push(`<blockquote>`); result.push(`<p><strong>Returns</strong></p>`); result.push(`<ul>`); data.returns.forEach(param => { const parsed = formatParam(param); result.push(`<li>${parsed}</li>`); }); result.push(`</ul>`); result.push(`</blockquote>`); } // nested if (data.nested) { Object.keys(data.nested).forEach(key => { cls = badgeClass(data.nested[key].type); result.push(`<blockquote class="scoped ${cls}">`); result.push(html(data.nested[key], true)); result.push(`</blockquote>`); }); } return result.join('\n'); } /** * Use jsdoc cli to parse specified files. * * @param path - Path to file. */ function explain(path) { const proc = child_process.execSync(`jsdoc --explain ${path}`); return JSON.parse(proc.toString()); } /** * Read file with jsdoc and return data structure * for formatting results. This method will automatically * nest configuration for related modules (classes with methods, etc...) * * @param {String} path - Path to file. */ function read(path) { const data = explain(path).filter(item => item.comment); const parsed = {}; data.forEach(item => { // construct data from item const obj = { name: item.name, type: item.kind, description: item.classdesc || item.description, line: item.meta.lineno, path: item.meta.path, filename: item.meta.filename, returns: item.returns, params: item.params, nested: {} }; // handle constructor methods if (obj.name in parsed) { parsed[obj.name].type = item.kind ? item.kind : obj.type; parsed[obj.name].params = item.params ? item.params : obj.params; parsed[obj.name].returns = item.returns ? item.returns : obj.returns; parsed[obj.name].description = obj.description ? obj.description : item.description; // handle nesting } else if (item.memberof in parsed) { parsed[item.memberof].nested[obj.name] = obj; // save new base object } else { parsed[item.name] = obj; } }); return parsed; } /** * Markdown-it plugin for automatic code documentation * using JSDoc3 conventions. * @param md - Markdown object to extend. * @param options - Options for plugin. */ function autodoc(md, options) { options = options || {}; const cache = {}; const regex = options.regex || /\/autodoc\s+(.+)$/; let css = options.css || CSS; css = `\n\n<style>\n${css}\n</style>\n\n`; let documented = false; // add markdown-it rule for plugin md.core.ruler.push('autodoc', state => { state.tokens.forEach((token, idx) => { // process inline tokens const match = token.content.match(regex); if (token.type === 'inline' && match) { let [path, ...modules] = match[1].trim().split(/[ ,;]/); // read data into cache if (!(path in cache)) { cache[path] = read(path); } // figure out modules to document const data = cache[path]; if (!modules.length) { modules = Object.keys(data); } // render html for doc documented = true; token.content = modules.map(key => { if (!(key in data)) { throw new Error(`Autodoc: could not find export \`${key}\` in file \`${path}\``); } return html(data[key]); }).join('\n'); token.type = 'html_inline'; token.markdown = modules.join(', '); token.children = null; // hide adjacent header items (used for sidebar) if (idx - 4 >= 0) { if (state.tokens[idx - 4].type === 'heading_open' && state.tokens[idx - 3].type === 'inline' && state.tokens[idx - 2].type === 'heading_close') { let hide = false; state.tokens[idx - 3].children.forEach(({ content }) => { if (modules.includes(content)) { hide = true; } }); if (hide) { state.tokens[idx - 3].children = []; state.tokens[idx - 4].hidden = true; state.tokens[idx - 2].hidden = true; } } } // hide outer paragraph tokens if (state.tokens[idx - 1].type === 'paragraph_open') { state.tokens[idx - 1].hidden = true; } if (state.tokens[idx + 1].type === 'paragraph_close') { state.tokens[idx + 1].hidden = true; } } }); // add extra to ken for autodoc css if (documented) { const style = new token('html_inline', '', 0); style.content = css; style.children = null; state.tokens.push(style); } }); } // exports var index = (options => { options = options || {}; return { name: 'vuepress-autodoc', extendMarkdown: md => { md.set({ breaks: true }); md.use(autodoc, { css: options.css || CSS }); } }; }); module.exports = index;