'use strict'; var doctrine = require('doctrine'); var remark = require('remark'); var inlineTokenizer = require('./inline_tokenizer'); function parseMarkdown(string) { return remark.use(inlineTokenizer).parse(string); } var flatteners = { 'abstract': flattenBoolean, 'access': function (result, tag) { result.access = tag.access; }, 'alias': flattenName, 'arg': synonym('param'), 'argument': synonym('param'), 'augments': function (result, tag) { if (!result.augments) { result.augments = []; } result.augments.push(tag); }, 'author': flattenDescription, 'borrows': todo, 'callback': function (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': flattenMarkdownDescription, 'desc': synonym('description'), 'description': flattenMarkdownDescription, 'emits': synonym('fires'), 'enum': todo, 'event': function (result, tag) { result.kind = 'event'; if (tag.description) { result.name = tag.description; } }, 'example': function (result, tag) { if (!tag.description) { result.errors.push({ message: '@example without code', commentLineNumber: tag.lineNumber }); return; } if (!result.examples) { result.examples = []; } var example = { description: tag.description }; if (tag.caption) { example.caption = parseMarkdown(tag.caption); } result.examples.push(example); }, 'exception': synonym('throws'), 'exports': todo, 'extends': synonym('augments'), 'external': function (result, tag) { result.kind = 'external'; if (tag.description) { result.name = tag.description; } }, 'file': function (result, tag) { result.kind = 'file'; if (tag.description) { result.description = parseMarkdown(tag.description); } }, 'fileoverview': synonym('file'), 'fires': todo, 'func': synonym('function'), 'function': flattenKindShorthand, 'global': function (result) { result.scope = 'global'; }, 'host': synonym('external'), 'ignore': flattenBoolean, 'implements': todo, 'inheritdoc': todo, 'inner': function (result) { result.scope = 'inner'; }, 'instance': function (result) { result.scope = 'instance'; }, 'interface': function (result, tag) { result.interface = true; if (tag.description) { result.name = tag.description; } }, 'kind': function (result, tag) { 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'), 'param': function (result, tag) { if (!result.params) { result.params = []; } var 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; } result.params.push(param); }, 'private': function (result) { result.access = 'private'; }, 'prop': synonym('property'), 'property': function (result, tag) { if (!result.properties) { result.properties = []; } var 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); }, 'protected': function (result) { result.access = 'protected'; }, 'public': function (result) { result.access = 'public'; }, 'readonly': flattenBoolean, 'requires': todo, 'return': synonym('returns'), 'returns': function (result, tag) { if (!result.returns) { result.returns = []; } var returns = { description: parseMarkdown(tag.description) }; if (tag.type) { returns.type = tag.type; } result.returns.push(returns); }, 'see': function (result, tag) { if (!result.sees) { result.sees = []; } result.sees.push(parseMarkdown(tag.description)); }, 'since': flattenDescription, 'static': function (result) { result.scope = 'static'; }, 'summary': flattenMarkdownDescription, 'this': todo, 'throws': function (result, tag) { if (!result.throws) { result.throws = []; } var throws = {}; if (tag.description) { throws.description = parseMarkdown(tag.description); } if (tag.type) { throws.type = tag.type; } result.throws.push(throws); }, 'todo': function (result, tag) { if (!result.todos) { result.todos = []; } result.todos.push(parseMarkdown(tag.description)); }, 'tutorial': todo, 'type': todo, 'typedef': flattenKindShorthand, 'var': synonym('member'), 'variation': function (result, tag) { result.variation = tag.variation; }, 'version': flattenDescription, 'virtual': synonym('abstract') }; function todo() {} function synonym(key) { return function (result, tag) { return flatteners[key](result, tag, key); }; } function flattenBoolean(result, tag, key) { result[key] = true; } function flattenName(result, tag, key) { result[key] = tag.name; } function flattenDescription(result, tag, key) { result[key] = tag.description; } function flattenMarkdownDescription(result, tag, key) { result[key] = parseMarkdown(tag.description); } 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 * @return {Comment} an object conforming to the * [documentation schema](https://github.com/documentationjs/api-json) */ function parseJSDoc(comment, loc, context) { var 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.errors = []; if (result.description) { result.description = parseMarkdown(result.description); } result.tags.forEach(function (tag) { if (tag.errors) { for (var 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 }); } }); return result; } module.exports = parseJSDoc;