UNPKG

esxdoc

Version:

Good Documentation Generator For JavaScript

1,124 lines (979 loc) 35.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs); var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _escapeHtml = require('escape-html'); var _escapeHtml2 = _interopRequireDefault(_escapeHtml); var _iceCap = require('ice-cap'); var _iceCap2 = _interopRequireDefault(_iceCap); var _util = require('./util.js'); var _DocResolver = require('./DocResolver.js'); var _DocResolver2 = _interopRequireDefault(_DocResolver); var _NPMUtil = require('../../Util/NPMUtil.js'); var _NPMUtil2 = _interopRequireDefault(_NPMUtil); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * Builder base class. */ class DocBuilder { /** * create instance. * @param {Taffy} data - doc object database. * @param {ESDocConfig} config - esdoc config is used build output. */ constructor(data, config) { this._data = data; this._config = config; new _DocResolver2.default(this).resolve(); } /* eslint-disable no-unused-vars */ /** * execute building output. * @abstract * @param {function} callback - is called with some data. */ exec(callback) {} /** * find doc object. * @param {...Object} cond - find condition. * @returns {DocObject[]} found doc objects. * @private */ _find(...cond) { return this._orderedFind(null, ...cond); } /** * find all identifiers with kind grouping. * @returns {{class: DocObject[], interface: DocObject[], function: DocObject[], variable: DocObject[], typedef: DocObject[], external: DocObject[]}} found doc objects. * @private */ _findAllIdentifiersKindGrouping() { const result = { class: this._find([{ kind: 'class', interface: false }]), interface: this._find([{ kind: 'class', interface: true }]), function: this._find([{ kind: 'function' }]), variable: this._find([{ kind: 'variable' }]), typedef: this._find([{ kind: 'typedef' }]), external: this._find([{ kind: 'external' }]).filter(v => !v.builtinExternal) }; return result; } /** * fuzzy find doc object by name. * - equal with longname * - equal with name * - include in longname * - include in ancestor * * @param {string} name - target identifier name. * @param {string} [kind] - target kind. * @returns {DocObject[]} found doc objects. * @private */ _findByName(name, kind = null) { let docs; if (kind) { docs = this._orderedFind(null, { longname: name, kind: kind }); } else { docs = this._orderedFind(null, { longname: name }); } if (docs.length) return docs; if (kind) { docs = this._orderedFind(null, { name: name, kind: kind }); } else { docs = this._orderedFind(null, { name: name }); } if (docs.length) return docs; const regexp = new RegExp(`[~]${ name.replace('*', '\\*') }$`); // if name is `*`, need to escape. if (kind) { docs = this._orderedFind(null, { longname: { regex: regexp }, kind: kind }); } else { docs = this._orderedFind(null, { longname: { regex: regexp } }); } if (docs.length) return docs; // inherited method? const matched = name.match(/(.*)[.#](.*)$/); // instance method(Foo#bar) or static method(Foo.baz) if (matched) { const parent = matched[1]; const childName = matched[2]; const parentDoc = this._findByName(parent, 'class')[0]; if (parentDoc && parentDoc._custom_extends_chains) { for (const superLongname of parentDoc._custom_extends_chains) { const docs = this._find({ memberof: superLongname, name: childName }); if (docs.length) return docs; } } } return []; } /** * find doc objects that is ordered. * @param {string} order - doc objects order(``column asec`` or ``column desc``). * @param {...Object} cond - condition objects * @returns {DocObject[]} found doc objects. * @private */ _orderedFind(order, ...cond) { const data = this._data(...cond); if (order) { return data.order(`${ order }, name asec`).map(v => v); } else { return data.order('name asec').map(v => v); } } /** * read html template. * @param {string} fileName - template file name. * @return {string} html of template. * @protected */ _readTemplate(fileName) { const filePath = _path2.default.resolve(__dirname, `./template/${ fileName }`); return _fs2.default.readFileSync(filePath, { encoding: 'utf-8' }); } /** * get target's essential info. * @returns {{title: string, version: string, url: string}} * @private */ _getInfo() { const config = this._config; let packageObj = {}; if (config.package) { const packagePath = config.package; try { const json = _fs2.default.readFileSync(packagePath, { encoding: 'utf-8' }); packageObj = JSON.parse(json); } catch (e) { // ignore } } // repository url let url = null; if (packageObj.repository) { if (packageObj.repository.url) { url = packageObj.repository.url; } else { url = packageObj.repository; } if (typeof url === 'string') { if (url.indexOf('git@github.com:') === 0) { // url: git@github.com:foo/bar.git const matched = url.match(/^git@github\.com:(.*)\.git$/); if (matched && matched[1]) { url = `https://github.com/${ matched[1] }`; } } else if (url.match(/^[\w\d\-_]+\/[\w\d\-_]+$/)) { // url: foo/bar url = `https://github.com/${ url }`; } else if (url.match(/^git\+https:\/\/github.com\/.*\.git$/)) { // git+https://github.com/foo/bar.git const matched = url.match(/^git\+(https:\/\/github.com\/.*)\.git$/); url = matched[1]; } else if (url.match(/(https?:\/\/.*$)/)) { // other url const matched = url.match(/(https?:\/\/.*$)/); url = matched[1]; } else { url = ''; } } else { url = null; } } const indexInfo = { title: config.title || packageObj.name, version: config.version || packageObj.version, url: url }; return indexInfo; } /** * build common layout output. * @return {IceCap} layout output. * @private */ _buildLayoutDoc() { const info = this._getInfo(); const ice = new _iceCap2.default(this._readTemplate('layout.html'), { autoClose: false }); const packageObj = _NPMUtil2.default.findPackage(); if (packageObj) { ice.text('esdocVersion', `(${ packageObj.version })`); } else { ice.drop('esdocVersion'); } if (info.url) { ice.attr('repoURL', 'href', info.url); if (info.url.match(new RegExp('^https?://github.com/'))) { ice.attr('repoURL', 'class', 'repo-url-github'); } } else { ice.drop('repoURL'); } ice.drop('testLink', !this._config.test); // see StaticFileBuilder#exec ice.loop('userScript', this._config.scripts || [], (i, userScript, ice) => { const name = `user/script/${ i }-${ _path2.default.basename(userScript) }`; ice.attr('userScript', 'src', name); }); ice.loop('userStyle', this._config.styles || [], (i, userStyle, ice) => { const name = `user/css/${ i }-${ _path2.default.basename(userStyle) }`; ice.attr('userStyle', 'href', name); }); ice.drop('manualHeaderLink', !this._config.manual); if (this._config.manual && this._config.manual.globalIndex) { ice.drop('manualHeaderLink'); } ice.load('nav', this._buildNavDoc()); return ice; } /** * build common navigation output. * @return {IceCap} navigation output. * @private */ _buildNavDoc() { const html = this._readTemplate('nav.html'); const ice = new _iceCap2.default(html); const kinds = ['class', 'function', 'variable', 'typedef', 'external']; const allDocs = this._find({ kind: kinds }).filter(v => !v.builtinExternal); const kindOrder = { class: 0, interface: 1, function: 2, variable: 3, typedef: 4, external: 5 }; allDocs.sort((a, b) => { const filePathA = a.longname.split('~')[0]; const filePathB = b.longname.split('~')[0]; const dirPathA = _path2.default.dirname(filePathA); const dirPathB = _path2.default.dirname(filePathB); const kindA = a.interface ? 'interface' : a.kind; const kindB = b.interface ? 'interface' : b.kind; if (dirPathA === dirPathB) { if (kindA === kindB) { return a.longname > b.longname ? 1 : -1; } else { return kindOrder[kindA] > kindOrder[kindB] ? 1 : -1; } } else { return dirPathA > dirPathB ? 1 : -1; } }); let lastDirPath = '.'; ice.loop('doc', allDocs, (i, doc, ice) => { const filePath = doc.longname.split('~')[0].replace(/^.*?[/]/, ''); const dirPath = _path2.default.dirname(filePath); const kind = doc.interface ? 'interface' : doc.kind; const kindText = kind.charAt(0).toUpperCase(); const kindClass = `kind-${ kind }`; ice.load('name', this._buildDocLinkHTML(doc.longname)); ice.load('kind', kindText); ice.attr('kind', 'class', kindClass); ice.text('dirPath', dirPath); ice.drop('dirPath', lastDirPath === dirPath); lastDirPath = dirPath; }); return ice; } /** * find doc object for each access. * @param {DocObject} doc - parent doc object. * @param {string} kind - kind property condition. * @param {boolean} isStatic - static property condition * @returns {Array[]} found doc objects. * @property {Array[]} 0 - ['Public', DocObject[]] * @property {Array[]} 1 - ['Protected', DocObject[]] * @property {Array[]} 2 - ['Private', DocObject[]] * @private */ _findAccessDocs(doc, kind, isStatic = true) { const cond = { kind: kind, static: isStatic }; if (doc) cond.memberof = doc.longname; /* eslint-disable default-case */ switch (kind) { case 'class': cond.interface = false; break; case 'interface': cond.kind = 'class'; cond.interface = true; break; case 'member': cond.kind = ['member', 'get', 'set']; break; } const publicDocs = this._find(cond, { access: 'public' }).filter(v => !v.builtinExternal); const protectedDocs = this._find(cond, { access: 'protected' }).filter(v => !v.builtinExternal); const privateDocs = this._find(cond, { access: 'private' }).filter(v => !v.builtinExternal); const accessDocs = [['Public', publicDocs], ['Protected', protectedDocs], ['Private', privateDocs]]; return accessDocs; } /** * build summary output html by parent doc. * @param {DocObject} doc - parent doc object. * @param {string} kind - target kind property. * @param {string} title - summary title. * @param {boolean} [isStatic=true] - target static property. * @returns {string} html of summary. * @private */ _buildSummaryHTML(doc, kind, title, isStatic = true) { const accessDocs = this._findAccessDocs(doc, kind, isStatic); let html = ''; for (const accessDoc of accessDocs) { const docs = accessDoc[1]; if (!docs.length) continue; let prefix = ''; if (docs[0].static) prefix = 'Static '; const _title = `${ prefix }${ accessDoc[0] } ${ title }`; const result = this._buildSummaryDoc(docs, _title); if (result) { html += result.html; } } return html; } /** * build summary output html by docs. * @param {DocObject[]} docs - target docs. * @param {string} title - summary title. * @param {boolean} innerLink - if true, link in summary is inner link. * @return {IceCap} summary output. * @private */ _buildSummaryDoc(docs, title, innerLink) { if (docs.length === 0) return null; const ice = new _iceCap2.default(this._readTemplate('summary.html')); ice.text('title', title); ice.loop('target', docs, (i, doc, ice) => { ice.text('generator', doc.generator ? '*' : ''); ice.text('async', doc.async ? 'async' : ''); ice.load('name', this._buildDocLinkHTML(doc.longname, null, innerLink, doc.kind)); ice.load('signature', this._buildSignatureHTML(doc)); ice.load('description', (0, _util.shorten)(doc, true)); ice.text('abstract', doc.abstract ? 'abstract' : ''); ice.text('access', doc.access); if (['get', 'set'].includes(doc.kind)) { ice.text('kind', doc.kind); } else { ice.drop('kind'); } if (['member', 'method', 'get', 'set'].includes(doc.kind)) { ice.text('static', doc.static ? 'static' : ''); } else { ice.drop('static'); } ice.text('since', doc.since); ice.load('deprecated', this._buildDeprecatedHTML(doc)); ice.load('experimental', this._buildExperimentalHTML(doc)); ice.text('version', doc.version); }); return ice; } /** * build detail output html by parent doc. * @param {DocObject} doc - parent doc object. * @param {string} kind - target kind property. * @param {string} title - detail title. * @param {boolean} [isStatic=true] - target static property. * @returns {string} html of detail. * @private */ _buildDetailHTML(doc, kind, title, isStatic = true) { const accessDocs = this._findAccessDocs(doc, kind, isStatic); let html = ''; for (const accessDoc of accessDocs) { const docs = accessDoc[1]; if (!docs.length) continue; let prefix = ''; if (docs[0].static) prefix = 'Static '; const _title = `${ prefix }${ accessDoc[0] } ${ title }`; const result = this._buildDetailDocs(docs, _title); if (result) html += result.html; } return html; } /* eslint-disable max-statements */ /** * build detail output html by docs. * @param {DocObject[]} docs - target docs. * @param {string} title - detail title. * @return {IceCap} detail output. * @private */ _buildDetailDocs(docs, title) { const ice = new _iceCap2.default(this._readTemplate('details.html')); ice.text('title', title); ice.drop('title', !docs.length); ice.loop('detail', docs, (i, doc, ice) => { const scope = doc.static ? 'static' : 'instance'; ice.attr('anchor', 'id', `${ scope }-${ doc.kind }-${ doc.name }`); ice.text('generator', doc.generator ? '*' : ''); ice.text('async', doc.async ? 'async' : ''); ice.text('name', doc.name); ice.load('signature', this._buildSignatureHTML(doc)); ice.load('description', doc.description || this._buildOverrideMethodDescription(doc)); ice.text('abstract', doc.abstract ? 'abstract' : ''); ice.text('access', doc.access); if (['get', 'set'].includes(doc.kind)) { ice.text('kind', doc.kind); } else { ice.drop('kind'); } if (doc.export && doc.importPath && doc.importStyle) { const link = this._buildFileDocLinkHTML(doc, doc.importPath); ice.into('importPath', `import ${ doc.importStyle } from '${ link }'`, (code, ice) => { ice.load('importPathCode', code); }); } else { ice.drop('importPath'); } if (['member', 'method', 'get', 'set'].includes(doc.kind)) { ice.text('static', doc.static ? 'static' : ''); } else { ice.drop('static'); } ice.load('source', this._buildFileDocLinkHTML(doc, 'source')); ice.text('since', doc.since, 'append'); ice.load('deprecated', this._buildDeprecatedHTML(doc)); ice.load('experimental', this._buildExperimentalHTML(doc)); ice.text('version', doc.version, 'append'); ice.load('see', this._buildDocsLinkHTML(doc.see), 'append'); ice.load('todo', this._buildDocsLinkHTML(doc.todo), 'append'); ice.load('override', this._buildOverrideMethod(doc)); ice.load('decorator', this._buildDecoratorHTML(doc), 'append'); let isFunction = false; if (['method', 'constructor', 'function'].indexOf(doc.kind) !== -1) isFunction = true; if (doc.kind === 'typedef' && doc.params && doc.type.types[0] === 'function') isFunction = true; if (isFunction) { ice.load('properties', this._buildProperties(doc.params, 'Params:')); } else { ice.load('properties', this._buildProperties(doc.properties, 'Properties:')); } // return if (doc.return) { ice.load('returnDescription', doc.return.description); const typeNames = []; for (const typeName of doc.return.types) { typeNames.push(this._buildTypeDocLinkHTML(typeName)); } if (typeof doc.return.nullable === 'boolean') { const nullable = doc.return.nullable; ice.load('returnType', `${ typeNames.join(' | ') } (nullable: ${ nullable })`); } else { ice.load('returnType', typeNames.join(' | ')); } ice.load('returnProperties', this._buildProperties(doc.properties, 'Return Properties:')); } else { ice.drop('returnParams'); } // throws if (doc.throws) { ice.loop('throw', doc.throws, (i, exceptionDoc, ice) => { ice.load('throwName', this._buildDocLinkHTML(exceptionDoc.types[0])); ice.load('throwDesc', exceptionDoc.description); }); } else { ice.drop('throwWrap'); } // fires if (doc.emits) { ice.loop('emit', doc.emits, (i, emitDoc, ice) => { ice.load('emitName', this._buildDocLinkHTML(emitDoc.types[0])); ice.load('emitDesc', emitDoc.description); }); } else { ice.drop('emitWrap'); } // listens if (doc.listens) { ice.loop('listen', doc.listens, (i, listenDoc, ice) => { ice.load('listenName', this._buildDocLinkHTML(listenDoc.types[0])); ice.load('listenDesc', listenDoc.description); }); } else { ice.drop('listenWrap'); } // example ice.into('example', doc.examples, (examples, ice) => { ice.loop('exampleDoc', examples, (i, exampleDoc, ice) => { const parsed = (0, _util.parseExample)(exampleDoc); ice.text('exampleCode', parsed.body); ice.text('exampleCaption', parsed.caption); }); }); // tests ice.into('tests', doc._custom_tests, (tests, ice) => { ice.loop('test', tests, (i, test, ice) => { const testDoc = this._find({ longname: test })[0]; ice.load('test', this._buildFileDocLinkHTML(testDoc, testDoc.testFullDescription)); }); }); }); return ice; } /** * get output html page title. use ``title`` in {@link ESDocConfig}. * @param {DocObject} doc - target doc object. * @returns {string} page title. * @private */ _getTitle(doc = '') { const name = doc.name || doc.toString(); if (!name) { if (this._config.title) { return `${ this._config.title } API Document`; } else { return 'API Document'; } } if (this._config.title) { return `${ name } | ${ this._config.title } API Document`; } else { return `${ name } | API Document`; } } /** * get base url html page. it is used html base tag. * @param {string} fileName - output file path. * @returns {string} base url. * @protected */ _getBaseUrl(fileName) { const baseUrl = '../'.repeat(fileName.split('/').length - 1); return baseUrl; } /** * gat url of output html page. * @param {DocObject} doc - target doc object. * @returns {string} url of output html. it is relative path from output root dir. * @private */ _getURL(doc) { let inner = false; if (['variable', 'function', 'member', 'typedef', 'method', 'constructor', 'get', 'set'].includes(doc.kind)) { inner = true; } if (inner) { const scope = doc.static ? 'static' : 'instance'; const fileName = this._getOutputFileName(doc); return `${ fileName }#${ scope }-${ doc.kind }-${ doc.name }`; } else { const fileName = this._getOutputFileName(doc); return fileName; } } /** * get file name of output html page. * @param {DocObject} doc - target doc object. * @returns {string} file name. * @private */ _getOutputFileName(doc) { switch (doc.kind) { case 'variable': return 'variable/index.html'; case 'function': return 'function/index.html'; case 'member': // fall case 'method': // fall case 'constructor': // fall case 'set': // fall case 'get': { // fal const parentDoc = this._find({ longname: doc.memberof })[0]; return this._getOutputFileName(parentDoc); } case 'external': return 'external/index.html'; case 'typedef': return 'typedef/index.html'; case 'class': return `class/${ doc.longname }.html`; case 'file': return `file/${ doc.longname }.html`; case 'testFile': return `test-file/${ doc.longname }.html`; case 'testDescribe': return 'test.html'; case 'testIt': return 'test.html'; default: throw new Error('DocBuilder: can not resolve file name.'); } } /** * build html link to file page. * @param {DocObject} doc - target doc object. * @param {string} text - link text. * @returns {string} html of link. * @private */ _buildFileDocLinkHTML(doc, text = null) { if (!doc) return ''; let fileDoc; if (doc.kind === 'file' || doc.kind === 'testFile') { fileDoc = doc; } else { const filePath = doc.longname.split('~')[0]; fileDoc = this._find({ kind: ['file', 'testFile'], longname: filePath })[0]; } if (!fileDoc) return ''; if (!text) text = fileDoc.name; if (doc.kind === 'file' || doc.kind === 'testFile') { return `<span><a href="${ this._getURL(fileDoc) }">${ text }</a></span>`; } else { return `<span><a href="${ this._getURL(fileDoc) }#lineNumber${ doc.lineNumber }">${ text }</a></span>`; } } /** * build html link of type. * @param {string} typeName - type name(e.g. ``number[]``, ``Map<number, string>``) * @returns {string} html of link. * @private * @todo re-implement with parser combinator. */ _buildTypeDocLinkHTML(typeName) { // e.g. number[] let matched = typeName.match(/^(.*?)\[\]$/); if (matched) { typeName = matched[1]; return `<span>${ this._buildDocLinkHTML(typeName, typeName) }<span>[]</span></span>`; } // e.g. function(a: number, b: string): boolean matched = typeName.match(/function *\((.*?)\)(.*)/); if (matched) { const functionLink = this._buildDocLinkHTML('function'); if (!matched[1] && !matched[2]) return `<span>${ functionLink }<span>()</span></span>`; let innerTypes = []; if (matched[1]) { // bad hack: Map.<string, boolean> => Map.<string\Z boolean> // bad hack: {a: string, b: boolean} => {a\Y string\Z b\Y boolean} const inner = matched[1].replace(/<.*?>/g, a => a.replace(/,/g, '\\Z')).replace(/{.*?}/g, a => a.replace(/,/g, '\\Z').replace(/:/g, '\\Y')); innerTypes = inner.split(',').map(v => { const tmp = v.split(':').map(v => v.trim()); if (tmp.length !== 2) throw new SyntaxError(`Invalid function type annotation: \`${ matched[0] }\``); const paramName = tmp[0]; const typeName = tmp[1].replace(/\\Z/g, ',').replace(/\\Y/g, ':'); return `${ paramName }: ${ this._buildTypeDocLinkHTML(typeName) }`; }); } let returnType = ''; if (matched[2]) { const type = matched[2].split(':')[1]; if (type) returnType = `: ${ this._buildTypeDocLinkHTML(type.trim()) }`; } return `<span>${ functionLink }<span>(${ innerTypes.join(', ') })</span>${ returnType }</span>`; } // e.g. {a: number, b: string} matched = typeName.match(/^\{(.*?)\}$/); if (matched) { if (!matched[1]) return '{}'; // bad hack: Map.<string, boolean> => Map.<string\Z boolean> // bad hack: {a: string, b: boolean} => {a\Y string\Z b\Y boolean} const inner = matched[1].replace(/<.*?>/g, a => a.replace(/,/g, '\\Z')).replace(/{.*?}/g, a => a.replace(/,/g, '\\Z').replace(/:/g, '\\Y')); const innerTypes = inner.split(',').map(v => { const tmp = v.split(':').map(v => v.trim()); const paramName = tmp[0]; let typeName = tmp[1].replace(/\\Z/g, ',').replace(/\\Y/g, ':'); if (typeName.includes('|')) { typeName = typeName.replace(/^\(/, '').replace(/\)$/, ''); const typeNames = typeName.split('|').map(v => v.trim()); const html = []; for (const unionType of typeNames) { html.push(this._buildTypeDocLinkHTML(unionType)); } return `${ paramName }: ${ html.join('|') }`; } else { return `${ paramName }: ${ this._buildTypeDocLinkHTML(typeName) }`; } }); return `{${ innerTypes.join(', ') }}`; } // e.g. Map<number, string> matched = typeName.match(/^(.*?)\.?<(.*?)>$/); if (matched) { const mainType = matched[1]; // bad hack: Map.<string, boolean> => Map.<string\Z boolean> // bad hack: {a: string, b: boolean} => {a\Y string\Z b\Y boolean} const inner = matched[2].replace(/<.*?>/g, a => a.replace(/,/g, '\\Z')).replace(/{.*?}/g, a => a.replace(/,/g, '\\Z').replace(/:/g, '\\Y')); const innerTypes = inner.split(',').map(v => { return v.split('|').map(vv => { vv = vv.trim().replace(/\\Z/g, ',').replace(/\\Y/g, ':'); return this._buildTypeDocLinkHTML(vv); }).join('|'); }); const html = `${ this._buildDocLinkHTML(mainType, mainType) }<${ innerTypes.join(', ') }>`; return html; } if (typeName.indexOf('...') === 0) { typeName = typeName.replace('...', ''); if (typeName.includes('|')) { const typeNames = typeName.replace('(', '').replace(')', '').split('|'); const typeLinks = typeNames.map(v => this._buildDocLinkHTML(v)); return `...(${ typeLinks.join('|') })`; } else { return `...${ this._buildDocLinkHTML(typeName) }`; } } else if (typeName.indexOf('?') === 0) { typeName = typeName.replace('?', ''); return `?${ this._buildDocLinkHTML(typeName) }`; } else { return this._buildDocLinkHTML(typeName); } } /** * build html link to identifier. * @param {string} longname - link to this. * @param {string} [text] - link text. default is name property of doc object. * @param {boolean} [inner=false] - if true, use inner link. * @param {string} [kind] - specify target kind property. * @returns {string} html of link. * @private */ _buildDocLinkHTML(longname, text = null, inner = false, kind = null) { if (!longname) return ''; if (typeof longname !== 'string') throw new Error(JSON.stringify(longname)); const doc = this._findByName(longname, kind)[0]; if (!doc) { // if longname is HTML tag, not escape. if (longname.indexOf('<') === 0) { return `<span>${ longname }</span>`; } else { return `<span>${ (0, _escapeHtml2.default)(text || longname) }</span>`; } } if (doc.kind === 'external') { text = doc.name; return `<span><a href="${ doc.externalLink }">${ text }</a></span>`; } else { text = (0, _escapeHtml2.default)(text || doc.name); const url = this._getURL(doc, inner); if (url) { return `<span><a href="${ url }">${ text }</a></span>`; } else { return `<span>${ text }</span>`; } } } /** * build html links to identifiers * @param {string[]} longnames - link to these. * @param {string} [text] - link text. default is name property of doc object. * @param {boolean} [inner=false] - if true, use inner link. * @param {string} [separator='\n'] - used link separator. * @returns {string} html links. * @private */ _buildDocsLinkHTML(longnames, text = null, inner = false, separator = '\n') { if (!longnames) return ''; if (!longnames.length) return ''; const links = []; for (const longname of longnames) { if (!longname) continue; const link = this._buildDocLinkHTML(longname, text, inner); links.push(`<li>${ link }</li>`); } if (!links.length) return ''; return `<ul>${ links.join(separator) }</ul>`; } /** * build identifier signature html. * @param {DocObject} doc - target doc object. * @returns {string} signature html. * @private */ _buildSignatureHTML(doc) { // call signature const callSignatures = []; if (doc.params) { for (const param of doc.params) { const paramName = param.name; if (paramName.indexOf('.') !== -1) continue; // for object property if (paramName.indexOf('[') !== -1) continue; // for array property const types = []; for (const typeName of param.types) { types.push(this._buildTypeDocLinkHTML(typeName)); } callSignatures.push(`${ paramName }: ${ types.join(' | ') }`); } } // return signature const returnSignatures = []; if (doc.return) { for (const typeName of doc.return.types) { returnSignatures.push(this._buildTypeDocLinkHTML(typeName)); } } // type signature let typeSignatures = []; if (doc.type) { for (const typeName of doc.type.types) { typeSignatures.push(this._buildTypeDocLinkHTML(typeName)); } } // callback is not need type. because type is always function. if (doc.kind === 'function') { typeSignatures = []; } let html = ''; if (callSignatures.length) { html = `(${ callSignatures.join(', ') })`; } else if (['function', 'method', 'constructor'].includes(doc.kind)) { html = '()'; } if (returnSignatures.length) html = `${ html }: ${ returnSignatures.join(' | ') }`; if (typeSignatures.length) html = `${ html }: ${ typeSignatures.join(' | ') }`; return html; } /** * build properties output. * @param {ParsedParam[]} [properties=[]] - properties in doc object. * @param {string} title - output title. * @return {IceCap} built properties output. * @private */ _buildProperties(properties = [], title = 'Properties:') { const ice = new _iceCap2.default(this._readTemplate('properties.html')); ice.text('title', title); ice.loop('property', properties, (i, prop, ice) => { ice.autoDrop = false; ice.attr('property', 'data-depth', prop.name.split('.').length - 1); ice.text('name', prop.name); ice.attr('name', 'data-depth', prop.name.split('.').length - 1); ice.load('description', prop.description); const typeNames = []; for (const typeName of prop.types) { typeNames.push(this._buildTypeDocLinkHTML(typeName)); } ice.load('type', typeNames.join(' | ')); // appendix const appendix = []; if (prop.optional) { appendix.push('<li>optional</li>'); } if ('defaultValue' in prop) { appendix.push(`<li>default: ${ prop.defaultValue }</li>`); } if (typeof prop.nullable === 'boolean') { appendix.push(`<li>nullable: ${ prop.nullable }</li>`); } if (appendix.length) { ice.load('appendix', `<ul>${ appendix.join('\n') }</ul>`); } else { ice.text('appendix', ''); } }); if (!properties || properties.length === 0) { ice.drop('properties'); } return ice; } /** * build deprecated html. * @param {DocObject} doc - target doc object. * @returns {string} if doc is not deprecated, returns empty. * @private */ _buildDeprecatedHTML(doc) { if (doc.deprecated) { const deprecated = [`this ${ doc.kind } was deprecated.`]; if (typeof doc.deprecated === 'string') deprecated.push(doc.deprecated); return deprecated.join(' '); } else { return ''; } } /** * build experimental html. * @param {DocObject} doc - target doc object. * @returns {string} if doc is not experimental, returns empty. * @private */ _buildExperimentalHTML(doc) { if (doc.experimental) { const experimental = [`this ${ doc.kind } is experimental.`]; if (typeof doc.experimental === 'string') experimental.push(doc.experimental); return experimental.join(' '); } else { return ''; } } /** * build method of ancestor class link html. * @param {DocObject} doc - target doc object. * @returns {string} html link. if doc does not override ancestor method, returns empty. * @private */ _buildOverrideMethod(doc) { const parentDoc = this._findByName(doc.memberof)[0]; if (!parentDoc) return ''; if (!parentDoc._custom_extends_chains) return ''; const chains = [...parentDoc._custom_extends_chains].reverse(); for (const longname of chains) { const superClassDoc = this._findByName(longname)[0]; if (!superClassDoc) continue; const superMethodDoc = this._find({ name: doc.name, memberof: superClassDoc.longname })[0]; if (!superMethodDoc) continue; return this._buildDocLinkHTML(superMethodDoc.longname, `${ superClassDoc.name }#${ superMethodDoc.name }`, true); } return ''; } /** * build method of ancestor class description. * @param {DocObject} doc - target doc object. * @returns {string} description. if doc does not override ancestor method, returns empty. * @private */ _buildOverrideMethodDescription(doc) { const parentDoc = this._findByName(doc.memberof)[0]; if (!parentDoc) return ''; if (!parentDoc._custom_extends_chains) return ''; const chains = [...parentDoc._custom_extends_chains].reverse(); for (const longname of chains) { const superClassDoc = this._findByName(longname)[0]; if (!superClassDoc) continue; const superMethodDoc = this._find({ name: doc.name, memberof: superClassDoc.longname })[0]; if (!superMethodDoc) continue; if (superMethodDoc.description) return superMethodDoc.description; } return ''; } _buildDecoratorHTML(doc) { if (!doc.decorators) return ''; const links = []; for (const decorator of doc.decorators) { const link = this._buildDocLinkHTML(decorator.name, decorator.name, false, 'function'); if (decorator.arguments) { links.push(`<li>${ link }${ decorator.arguments }</li>`); } else { links.push(`<li>${ link }</li>`); } } if (!links.length) return ''; return `<ul>${ links.join('\n') }</ul>`; } // _buildAuthorHTML(doc, separator = '\n') { // if (!doc.author) return ''; // // var html = []; // for (var author of doc.author) { // var matched = author.match(/(.*?) *<(.*?)>/); // if (matched) { // var name = matched[1]; // var link = matched[2]; // if (link.indexOf('http') === 0) { // html.push(`<li><a href="${link}">${name}</a></li>`) // } else { // html.push(`<li><a href="mailto:${link}">${name}</a></li>`) // } // } else { // html.push(`<li>${author}</li>`) // } // } // // return `<ul>${html.join(separator)}</ul>`; // } } exports.default = DocBuilder; /* eslint-disable max-lines */