UNPKG

@compodoc/compodoc

Version:

The missing documentation tool for your Angular application

413 lines (370 loc) 14.5 kB
import * as _ from './collection.util'; import { ts, SyntaxKind } from "ts-morph"; import * as _ts from "./ts-internal"; /** * Standard JSDoc tags whose inline text should NOT be folded into the description. * For any other (custom) tag, text on the same line as the tag is included. */ const SKIP_TAG_INLINE_CONTENT = new Set([ "param", "arg", "argument", "parameter", "returns", "return", "throws", "exception", "throw", "type", "typedef", "template", "deprecated", "example", "see", "ignore", "internal", ]); export class JsdocParserUtil { public isVariableLike(node: ts.Node): node is ts.VariableLikeDeclaration { if (node) { switch (node.kind) { case SyntaxKind.BindingElement: case SyntaxKind.EnumMember: case SyntaxKind.Parameter: case SyntaxKind.PropertyAssignment: case SyntaxKind.PropertyDeclaration: case SyntaxKind.PropertySignature: case SyntaxKind.ShorthandPropertyAssignment: case SyntaxKind.VariableDeclaration: return true; } } return false; } isTopmostModuleDeclaration(node: ts.ModuleDeclaration): boolean { if ( (node as any).nextContainer && (node as any).nextContainer.kind === ts.SyntaxKind.ModuleDeclaration ) { const next = <ts.ModuleDeclaration>(node as any).nextContainer; if (node.name.end + 1 === next.name.pos) { return false; } } return true; } getRootModuleDeclaration(node: ts.ModuleDeclaration): ts.Node { while ( node.parent && node.parent.kind === ts.SyntaxKind.ModuleDeclaration ) { let parent = <ts.ModuleDeclaration>node.parent; if (node.name.pos === parent.name.end + 1) { node = parent; } else { break; } } return node; } public getMainCommentOfNode( node: ts.Node, sourceFile?: ts.SourceFile, ): string { let description: string = ""; if ( node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclarationList ) { node = node.parent.parent; } else if (node.kind === ts.SyntaxKind.ModuleDeclaration) { if (!this.isTopmostModuleDeclaration(<ts.ModuleDeclaration>node)) { return null; } else { node = this.getRootModuleDeclaration( <ts.ModuleDeclaration>node, ); } } const comments = _ts.getJSDocCommentRanges( node as any, sourceFile.text, ); if (comments && comments.length) { let comment: ts.CommentRange; if (node.kind === ts.SyntaxKind.SourceFile) { if (comments.length === 1) { return null; } comment = comments[0]; } else { comment = comments[comments.length - 1]; } description = sourceFile.text.substring(comment.pos, comment.end); } return description; } public parseComment(text: string): string { let comment = ""; let shortText = 0; function readBareLine(line: string) { comment += "\n" + line; if (line === "" && shortText === 0) { // Ignore } else if (line === "" && shortText === 1) { shortText = 2; } else { if (shortText === 2) { comment += (comment === "" ? "" : "\n") + line; } } } const CODE_FENCE = /^\s*```(?!.*```)/; let inCode = false; let inExample = false; // first line with @example, end line with empty string or string or */ let exampleHasCodeFence = false; // track if the example already has code fences function readLine(line: string, index: number) { const originalLine = line; line = line.replace(/^\s*\*? ?/, ""); line = line.replace(/\s*$/, ""); if (CODE_FENCE.test(line)) { inCode = !inCode; if (inExample) { exampleHasCodeFence = true; } } if (line.indexOf("@example") !== -1) { inExample = true; exampleHasCodeFence = false; // Check if the next non-empty line has a code fence const lines = text.split(/\r\n?|\n/); for (let i = index + 1; i < lines.length; i++) { const nextLine = lines[i] .replace(/^\s*\*? ?/, "") .replace(/\s*$/, ""); if (nextLine === "") continue; // Skip empty lines if (CODE_FENCE.test(nextLine)) { exampleHasCodeFence = true; } break; // Only check the first non-empty line } if (!exampleHasCodeFence) { line = "```html"; } else { // Skip the @example line if it already has code fences return; } } // Preserve empty lines within code blocks by using a placeholder if (inCode && inExample && exampleHasCodeFence && line === "") { line = "___COMPODOC_EMPTY_LINE___"; } if (inExample && line === "") { inExample = false; if (!exampleHasCodeFence) { line = "```"; } else { // Don't add closing fence if example has its own return; } } if (!inCode) { const tag = /^@(\S+)/.exec(line); const SeeTag = /^@see/.exec(line); if (SeeTag) { line = line.replace(/^@see/, "See"); } if (tag && !SeeTag) { const tagName = tag[1].toLowerCase(); if (!SKIP_TAG_INLINE_CONTENT.has(tagName)) { const textAfterTag = line.slice(tag[0].length).trim(); if (textAfterTag) { readBareLine(textAfterTag); } } return; } } readBareLine(line); } text = text.replace(/^\s*\/\*+/, ""); text = text.replace(/\*+\/\s*$/, ""); text.split(/\r\n?|\n/).forEach((line, index) => readLine(line, index)); return comment; } private getJSDocTags(node: ts.Node, kind: SyntaxKind): ts.JSDocTag[] { const docs = this.getJSDocs(node); if (docs) { const result: ts.JSDocTag[] = []; for (const doc of docs) { if (ts.isJSDocParameterTag(doc)) { if (doc.kind === kind) { result.push(doc); } } else if (ts.isJSDoc(doc)) { result.push( ..._.filter(doc.tags, (tag) => tag.kind === kind), ); } else if ((doc as any) && (doc as any).kind === kind) { result.push(doc as ts.JSDocTag); } else if ( (doc as any) && typeof (doc as any).kind === "number" ) { // Ignore other JSDoc node kinds to avoid hard crashes on TS AST variations. continue; } else { throw new Error("Unexpected type"); } } return result; } } public getJSDocs(node: ts.Node): ReadonlyArray<ts.JSDoc | ts.JSDocTag> { // TODO: jsDocCache is internal, see if there's a way around it let cache: ReadonlyArray<ts.JSDoc | ts.JSDocTag> = (node as any) .jsDocCache; if (!cache) { cache = this.getJSDocsWorker(node, []).filter((x) => x); (node as any).jsDocCache = cache; } return cache; } // Try to recognize this pattern when node is initializer // of variable declaration and JSDoc comments are on containing variable statement. // /** // * @param {number} name // * @returns {number} // */ // var x = function(name) { return name.length; } private getJSDocsWorker(node: ts.Node, cache): ReadonlyArray<any> { const parent = node.parent; const isInitializerOfVariableDeclarationInStatement = this.isVariableLike(parent) && (parent as any).initializer === node && ts.isVariableStatement(parent.parent.parent); const isVariableOfVariableDeclarationStatement = this.isVariableLike(node) && ts.isVariableStatement(parent.parent); const variableStatementNode = isInitializerOfVariableDeclarationInStatement ? parent.parent.parent : isVariableOfVariableDeclarationStatement ? parent.parent : undefined; if (variableStatementNode) { cache = this.getJSDocsWorker(variableStatementNode, cache); } // Also recognize when the node is the RHS of an assignment expression const isSourceOfAssignmentExpressionStatement = parent && parent.parent && ts.isBinaryExpression(parent) && parent.operatorToken.kind === SyntaxKind.EqualsToken && ts.isExpressionStatement(parent.parent); if (isSourceOfAssignmentExpressionStatement) { cache = this.getJSDocsWorker(parent.parent, cache); } const isModuleDeclaration = ts.isModuleDeclaration(node) && parent && ts.isModuleDeclaration(parent); const isPropertyAssignmentExpression = parent && ts.isPropertyAssignment(parent); if (isModuleDeclaration || isPropertyAssignmentExpression) { cache = this.getJSDocsWorker(parent, cache); } // Pull parameter comments from declaring function as well if (ts.isParameter(node)) { cache = _.concat(cache, this.getJSDocParameterTags(node)); } if (this.isVariableLike(node) && (node as any).initializer) { const initializerJsDoc = ((node as any).initializer as any).jsDoc; if (initializerJsDoc) { cache = _.concat(cache, initializerJsDoc); } } const nodeJsDoc = (node as any).jsDoc; if (nodeJsDoc) { cache = _.concat(cache, nodeJsDoc); } return cache; } private getJSDocParameterTags( param: ts.ParameterDeclaration, ): ReadonlyArray<ts.JSDocParameterTag> { const func = param.parent as ts.FunctionLikeDeclaration; const tags = this.getJSDocTags( func, SyntaxKind.JSDocParameterTag, ) as ts.JSDocParameterTag[]; if (!param.name) { // this is an anonymous jsdoc param from a `function(type1, type2): type3` specification const i = func.parameters.indexOf(param); const paramTags = _.filter(tags, (tag) => ts.isJSDocParameterTag(tag), ); if (paramTags && 0 <= i && i < paramTags.length) { return [paramTags[i]]; } } else if (ts.isIdentifier(param.name)) { const name = param.name.text; return _.filter(tags, (tag) => { if (ts && ts.isJSDocParameterTag(tag)) { const t = tag as any; if (typeof t.parameterName !== "undefined") { return t.parameterName.text === name; } else if (typeof t.name !== "undefined") { if (typeof t.name.escapedText !== "undefined") { return t.name.escapedText === name; } } } }); } else { // TODO: it's a destructured parameter, so it should look up an "object type" series of multiple lines // But multi-line object types aren't supported yet either return undefined; } } public parseJSDocNode(node): string { let rawDescription = ""; if (typeof node.comment === "string") { rawDescription += node.comment; } else { if (node.comment) { const len = node.comment.length; for (let i = 0; i < len; i++) { const JSDocNode = node.comment[i]; switch (JSDocNode.kind) { case SyntaxKind.JSDocComment: rawDescription += JSDocNode.comment; break; case SyntaxKind.JSDocText: rawDescription += JSDocNode.text; break; case SyntaxKind.JSDocLink: if (JSDocNode.name) { let text = JSDocNode.name.escapedText; if ( text === undefined && JSDocNode.name.left && JSDocNode.name.right ) { text = JSDocNode.name.left.escapedText + "." + JSDocNode.name.right.escapedText; } rawDescription += JSDocNode.text + "{@link " + text + "}"; } break; default: break; } } } } return rawDescription; } }