vuepress-plugin-autodoc
Version:
Automatic Code Documentation for VuePress
462 lines (382 loc) • 11.4 kB
JavaScript
;
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;