diff --git a/src/jsdoc/example-tag-parser.js b/src/jsdoc/example-tag-parser.js new file mode 100644 index 000000000000..48fa27cb9d66 --- /dev/null +++ b/src/jsdoc/example-tag-parser.js @@ -0,0 +1,130 @@ +/** @Flow */ + +type Example = { + raw: string, + description?: string, + code?: Array, + returns?: Object +}; + +const token = { + CODE: 'CODE', + DESC_DELIMITER: 'DESC_DELIMITER', + RETURNS_DELIMITER: 'RETURNS_DELIMITER', + COMMENT: 'COMMENT', +}; + +type Token = $Keys; + +const status = { + IN_CODE: 'IN_CODE', + IN_DESCRIPTION: 'IN_DESCRIPTION', + IN_RETURNS: 'IN_RETURNS', + NONE: 'NONE', +}; + +type Status = $Keys; + +function isComment(str: string): Boolean { + return (str.startsWith('//')); +} + +function isCode(str: string): Boolean { + return !isComment(str); +} + +function isDescriptionDelimiter(str: string): Boolean { + return (str === '//-'); +} + +function isReturnsDelimiter(str: string): Boolean { + return (str === '//=>'); +} + +function tokenize(str: string): Token { + if (isCode(str)) return token.CODE; + if (isDescriptionDelimiter(str)) return token.DESC_DELIMITER; + if (isReturnsDelimiter(str)) return token.RETURNS_DELIMITER; + return token.COMMENT; +} + +function stripComment(str: string): string { + return (isComment(str)) ? str.replace('//', '') : str; +} + +function updateExample(line, field, example) { + line = stripComment(line).trim(); + if (example[field]) { + example[field] += `\n${line}`; + } + else { + example[field] = line; + } +} + +function parseToken(currentToken, line, currentStatus, example): Status { + switch (currentToken) { + case token.DESC_DELIMITER: + if (currentStatus === status.NONE) currentStatus = status.IN_DESCRIPTION; + else if (currentStatus === status.IN_DESCRIPTION) currentStatus = status.NONE; // end desc block + else throw new Error(`${currentToken} can't appear after code or returns block`); + break; + case token.RETURNS_DELIMITER: + if (currentStatus === status.NONE || currentStatus === status.IN_CODE) currentStatus = status.IN_RETURNS; + else if (currentStatus === status.IN_RETURNS) currentStatus = status.NONE; // end returns block + else throw new Error(`${currentToken} must appear after code block`); + break; + case token.CODE: + if (currentStatus === status.NONE || currentStatus === status.IN_CODE) updateExample(line, 'code', example); + else throw new Error(`${currentToken} can't appear inside desc or returns block`); + break; + case token.COMMENT: + if (currentStatus === status.IN_DESCRIPTION) updateExample(line, 'description', example); + else if (currentStatus === status.IN_RETURNS) updateExample(line, 'returns', example); + else throw new Error(`${currentToken} must appear inside desc or returns block`); + break; + default: + throw new Error('Unrecognized token'); + } + return currentStatus; +} + +/** + * An example of an "example" tag + * + * * @example + * //- + * // Adds two numbers + * //- + * add(2, 3); + * //=> + * // 5 + * //=> + * + * @returns {Example} Returns the parsed "example" tag + * { + * raw: '//-\n// Adds two numbers\n//-\nadd(2, 3);\n//=>\n// 5\n//=>', + * description: 'Adds two numbers', + * code: 'add(2, 3);', + * returns: '5' + * } + */ +export default function parse(exampleRaw: string): Example { + const example: Example = {}; + let currentStatus: Status = status.NONE; + example.raw = exampleRaw; + try { + const lines = exampleRaw.split('\n'); + for (let line of lines) { + line = line.trim(); + if (!line) continue; + const currentToken = tokenize(line); + currentStatus = parseToken(currentToken, line, currentStatus, example); + } + } + catch (e) { + // That's fine. The example probably doesn't comply with our standard + } + + return example; +} \ No newline at end of file diff --git a/src/jsdoc/parser.js b/src/jsdoc/parser.js index 6f5773092e8f..c151ae5aaadf 100644 --- a/src/jsdoc/parser.js +++ b/src/jsdoc/parser.js @@ -2,12 +2,16 @@ import esprima from 'esprima'; import doctrine from 'doctrine'; import walk from 'esprima-walk'; +import exampleTagParser from './example-tag-parser'; export type ParsedDocs = { - name: string, - description: string, - args?: Array, - returns?: Object, + name: string, + description: string, + args?: Array, + returns?: Object, + access?: string, + examples?: Array, + static?: Boolean }; const parsedData: Array = []; @@ -51,16 +55,34 @@ function handleFunctionType(node: Object) { const args = []; let description = ''; let returns = {}; + let isStatic = false; + let access = 'public'; + let examples = []; if (node.leadingComments && node.leadingComments.length) { const commentsAst = getCommentsAST(node); description = commentsAst.description; for (const tag of commentsAst.tags) { - if (tag.title === 'param') { - args.push(formatTag(tag)); - } - if (tag.title === 'returns') { - returns = formatTag(tag); + switch (tag.title) { + case 'param': + args.push(formatTag(tag)); + break; + case 'returns' : + returns = formatTag(tag); + break; + case 'static': + isStatic = true; + break; + case 'private': + case 'protected': + access = tag.title; + break; + case 'access': + access = tag.access; + break; + case 'example': + examples.push(exampleTagParser(tag.description)); + break; } } } @@ -71,6 +93,9 @@ function handleFunctionType(node: Object) { description, args, returns, + access, + examples, + static: isStatic, }; parsedData.push(item); }