documentation
Version:
a documentation generator
677 lines (633 loc) • 16.8 kB
JavaScript
import doctrine from 'doctrine-temporary-fork';
import parseMarkdown from './remark-parse.js';
/**
* Flatteners: these methods simplify the structure of JSDoc comments
* into a flat object structure, parsing markdown and extracting
* information where appropriate.
* @private
*/
const flatteners = {
abstract: flattenBoolean,
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
access(result, tag) {
// doctrine ensures that tag.access is valid
result.access = tag.access;
},
alias: flattenName,
arg: synonym('param'),
argument: synonym('param'),
async: flattenBoolean,
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
augments(result, tag) {
// Google variation of augments/extends tag:
// uses type with brackets instead of name.
// https://github.com/google/closure-library/issues/746
if (!tag.name && tag.type && tag.type.name) {
tag.name = tag.type.name;
}
if (!tag.name) {
console.error('@extends from complex types is not supported yet'); // eslint-disable-line no-console
return;
}
result.augments.push(tag);
},
author: flattenDescription,
borrows: todo,
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
callback(result, tag) {
result.kind = 'typedef';
if (tag.description) {
result.name = tag.description;
}
result.type = {
type: 'NameExpression',
name: 'Function'
};
},
class: flattenKindShorthand,
classdesc: flattenMarkdownDescription,
const: synonym('constant'),
constant: flattenKindShorthand,
constructor: synonym('class'),
constructs: todo,
copyright: flattenMarkdownDescription,
default: todo,
defaultvalue: synonym('default'),
deprecated(result, tag) {
const description = tag.description || 'This is deprecated.';
result.deprecated = parseMarkdown(description);
},
flattenMarkdownDescription,
desc: synonym('description'),
description: flattenMarkdownDescription,
emits: synonym('fires'),
enum(result, tag) {
result.kind = 'enum';
result.type = tag.type;
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
event(result, tag) {
result.kind = 'event';
if (tag.description) {
result.name = tag.description;
}
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
example(result, tag) {
if (!tag.description) {
result.errors.push({
message: '@example without code',
commentLineNumber: tag.lineNumber
});
return;
}
const example = {
description: tag.description
};
if (tag.caption) {
example.caption = parseMarkdown(tag.caption);
}
result.examples.push(example);
},
exception: synonym('throws'),
exports: todo,
extends: synonym('augments'),
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
external(result, tag) {
result.kind = 'external';
if (tag.description) {
result.name = tag.description;
}
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
file(result, tag) {
result.kind = 'file';
if (tag.description) {
result.description = parseMarkdown(tag.description);
}
},
fileoverview: synonym('file'),
fires: todo,
func: synonym('function'),
function: flattenKindShorthand,
generator: flattenBoolean,
/**
* Parse tag
* @private
* @param {Object} result target comment
* @returns {undefined} has side-effects
*/
global(result) {
result.scope = 'global';
},
hideconstructor: flattenBoolean,
host: synonym('external'),
ignore: flattenBoolean,
implements(result, tag) {
// Match @extends/@augments above.
if (!tag.name && tag.type && tag.type.name) {
tag.name = tag.type.name;
}
result.implements.push(tag);
},
inheritdoc: todo,
/**
* Parse tag
* @private
* @param {Object} result target comment
* @returns {undefined} has side-effects
*/
inner(result) {
result.scope = 'inner';
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @returns {undefined} has side-effects
*/
instance(result) {
result.scope = 'instance';
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
interface(result, tag) {
result.kind = 'interface';
if (tag.description) {
result.name = tag.description;
}
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
kind(result, tag) {
// doctrine ensures that tag.kind is valid
result.kind = tag.kind;
},
lends: flattenDescription,
license: flattenDescription,
listens: todo,
member: flattenKindShorthand,
memberof: flattenDescription,
method: synonym('function'),
mixes: todo,
mixin: flattenKindShorthand,
module: flattenKindShorthand,
name: flattenName,
namespace: flattenKindShorthand,
override: flattenBoolean,
overview: synonym('file'),
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
param(result, tag) {
const param = {
title: 'param',
name: tag.name,
lineNumber: tag.lineNumber // TODO: remove
};
if (tag.description) {
param.description = parseMarkdown(tag.description);
}
if (tag.type) {
param.type = tag.type;
}
if (tag.default) {
param.default = tag.default;
if (param.type && param.type.type === 'OptionalType') {
param.type = param.type.expression;
}
}
result.params.push(param);
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @returns {undefined} has side-effects
*/
private(result) {
result.access = 'private';
},
prop: synonym('property'),
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
property(result, tag) {
const property = {
title: 'property',
name: tag.name,
lineNumber: tag.lineNumber // TODO: remove
};
if (tag.description) {
property.description = parseMarkdown(tag.description);
}
if (tag.type) {
property.type = tag.type;
}
result.properties.push(property);
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @returns {undefined} has side-effects
*/
protected(result) {
result.access = 'protected';
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @returns {undefined} has side-effects
*/
public(result) {
result.access = 'public';
},
readonly: flattenBoolean,
requires: todo,
return: synonym('returns'),
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
returns(result, tag) {
const returns = {
description: parseMarkdown(tag.description),
title: 'returns'
};
if (tag.type) {
returns.type = tag.type;
}
result.returns.push(returns);
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
see(result, tag) {
const sees = {
description: parseMarkdown(tag.description),
title: 'sees'
};
if (tag.type) {
sees.type = tag.type;
}
result.sees.push(sees);
},
since: flattenDescription,
/**
* Parse tag
* @private
* @param {Object} result target comment
* @returns {undefined} has side-effects
*/
static(result) {
result.scope = 'static';
},
summary: flattenMarkdownDescription,
this: todo,
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
throws(result, tag) {
const throws = {};
if (tag.description) {
throws.description = parseMarkdown(tag.description);
}
if (tag.type) {
throws.type = tag.type;
}
result.throws.push(throws);
},
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
todo(result, tag) {
result.todos.push(parseMarkdown(tag.description));
},
tutorial: todo,
type(result, tag) {
result.type = tag.type;
},
typedef: flattenKindShorthand,
var: synonym('member'),
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
variation(result, tag) {
result.variation = tag.variation;
},
version: flattenDescription,
virtual: synonym('abstract'),
yield: synonym('yields'),
/**
* Parse tag
* @private
* @param {Object} result target comment
* @param {Object} tag the tag
* @returns {undefined} has side-effects
*/
yields(result, tag) {
const yields = {
description: parseMarkdown(tag.description),
title: 'yields'
};
if (tag.type) {
yields.type = tag.type;
}
result.yields.push(yields);
}
};
/**
* A no-op function for unsupported tags
* @returns {undefined} does nothing
*/
function todo() {}
/**
* Generate a function that curries a destination key for a flattener
* @private
* @param {string} key the eventual destination key
* @returns {Function} a flattener that remembers that key
*/
function synonym(key) {
return function (result, tag) {
const fun = flatteners[key];
fun.apply(null, [result, tag, key].slice(0, fun.length));
};
}
/**
* Treat the existence of a tag as a sign to mark `key` as true in the result
* @private
* @param {Object} result the documentation object
* @param {Object} tag the tag object, with a name property
* @param {string} key destination on the result
* @returns {undefined} operates with side-effects
*/
function flattenBoolean(result, tag, key) {
result[key] = true;
}
/**
* Flatten a usable-once name tag into a key
* @private
* @param {Object} result the documentation object
* @param {Object} tag the tag object, with a name property
* @param {string} key destination on the result
* @returns {undefined} operates with side-effects
*/
function flattenName(result, tag, key) {
result[key] = tag.name;
}
/**
* Flatten a usable-once description tag into a key
* @private
* @param {Object} result the documentation object
* @param {Object} tag the tag object, with a description property
* @param {string} key destination on the result
* @returns {undefined} operates with side-effects
*/
function flattenDescription(result, tag, key) {
result[key] = tag.description;
}
/**
* Flatten a usable-once description tag into a key and parse it as Markdown
* @private
* @param {Object} result the documentation object
* @param {Object} tag the tag object, with a description property
* @param {string} key destination on the result
* @returns {undefined} operates with side-effects
*/
function flattenMarkdownDescription(result, tag, key) {
result[key] = parseMarkdown(tag.description);
}
/**
* Parse [kind shorthand](http://usejsdoc.org/tags-kind.html) into
* both name and type tags, like `@class [<type> <name>]`
*
* @param {Object} result comment
* @param {Object} tag parsed tag
* @param {string} key tag
* @returns {undefined} operates through side effects
* @private
*/
function flattenKindShorthand(result, tag, key) {
result.kind = key;
if (tag.name) {
result.name = tag.name;
}
if (tag.type) {
result.type = tag.type;
}
}
/**
* Parse a comment with doctrine, decorate the result with file position and code
* context, handle parsing errors, and fix up various infelicities in the structure
* outputted by doctrine.
*
* The following tags are treated as synonyms for a canonical tag:
*
* * `@virtual` ⇢ `@abstract`
* * `@extends` ⇢ `@augments`
* * `@constructor` ⇢ `@class`
* * `@const` ⇢ `@constant`
* * `@defaultvalue` ⇢ `@default`
* * `@desc` ⇢ `@description`
* * `@host` ⇢ `@external`
* * `@fileoverview`, `@overview` ⇢ `@file`
* * `@emits` ⇢ `@fires`
* * `@func`, `@method` ⇢ `@function`
* * `@var` ⇢ `@member`
* * `@arg`, `@argument` ⇢ `@param`
* * `@prop` ⇢ `@property`
* * `@return` ⇢ `@returns`
* * `@exception` ⇢ `@throws`
* * `@linkcode`, `@linkplain` ⇢ `@link`
*
* The following tags are assumed to be singletons, and are flattened
* to a top-level property on the result whose value is extracted from
* the tag:
*
* * `@name`
* * `@memberof`
* * `@classdesc`
* * `@kind`
* * `@class`
* * `@constant`
* * `@event`
* * `@external`
* * `@file`
* * `@function`
* * `@member`
* * `@mixin`
* * `@module`
* * `@namespace`
* * `@typedef`
* * `@access`
* * `@lends`
* * `@description`
* * `@summary`
* * `@copyright`
* * `@deprecated`
*
* The following tags are flattened to a top-level array-valued property:
*
* * `@param` (to `params` property)
* * `@property` (to `properties` property)
* * `@returns` (to `returns` property)
* * `@augments` (to `augments` property)
* * `@example` (to `examples` property)
* * `@throws` (to `throws` property)
* * `@see` (to `sees` property)
* * `@todo` (to `todos` property)
*
* The `@global`, `@static`, `@instance`, and `@inner` tags are flattened
* to a `scope` property whose value is `"global"`, `"static"`, `"instance"`,
* or `"inner"`.
*
* The `@access`, `@public`, `@protected`, and `@private` tags are flattened
* to an `access` property whose value is `"protected"` or `"private"`.
* The assumed default value is `"public"`, so `@access public` or `@public`
* tags result in no `access` property.
*
* @param {string} comment input to be parsed
* @param {Object} loc location of the input
* @param {Object} context code context of the input
* @returns {Comment} an object conforming to the
* [documentation schema](https://github.com/documentationjs/api-json)
*/
export default function parseJSDoc(comment, loc, context) {
const result = doctrine.parse(comment, {
// have doctrine itself remove the comment asterisks from content
unwrap: true,
// enable parsing of optional parameters in brackets, JSDoc3 style
sloppy: true,
// `recoverable: true` is the only way to get error information out
recoverable: true,
// include line numbers
lineNumbers: true
});
result.loc = loc;
result.context = context;
result.augments = [];
result.errors = [];
result.examples = [];
result.implements = [];
result.params = [];
result.properties = [];
result.returns = [];
result.sees = [];
result.throws = [];
result.todos = [];
result.yields = [];
if (result.description) {
result.description = parseMarkdown(result.description);
}
// Reject parameter tags without a parameter name
result.tags.filter(function (tag) {
if (tag.title === 'param' && tag.name === undefined) {
result.errors.push({
message: 'A @param tag without a parameter name was rejected'
});
return false;
}
return true;
});
result.tags.forEach(function (tag) {
if (tag.errors) {
for (let j = 0; j < tag.errors.length; j++) {
result.errors.push({ message: tag.errors[j] });
}
} else if (flatteners[tag.title]) {
flatteners[tag.title](result, tag, tag.title);
} else {
result.errors.push({
message: 'unknown tag @' + tag.title,
commentLineNumber: tag.lineNumber
});
}
});
// Using the @name tag, or any other tag that sets the name of a comment,
// disconnects the comment from its surrounding code.
if (context && result.name) {
delete context.ast;
}
return result;
}