UNPKG

docsify

Version:

A magical documentation generator.

296 lines (253 loc) 8.09 kB
import { marked } from 'marked'; import { isAbsolutePath, getPath, getParentPath } from '../router/util.js'; import { isFn, cached, isPrimitive } from '../util/core.js'; import { tree as treeTpl } from './tpl.js'; import { genTree } from './gen-tree.js'; import { slugify } from './slugify.js'; import { emojify } from './emojify.js'; import { getAndRemoveConfig } from './utils.js'; import { imageCompiler } from './compiler/image.js'; import { headingCompiler } from './compiler/heading.js'; import { highlightCodeCompiler } from './compiler/code.js'; import { paragraphCompiler } from './compiler/paragraph.js'; import { blockquoteCompiler } from './compiler/blockquote.js'; import { taskListCompiler } from './compiler/taskList.js'; import { taskListItemCompiler } from './compiler/taskListItem.js'; import { linkCompiler } from './compiler/link.js'; import { compileMedia } from './compiler/media.js'; import { tableCellCompiler } from './compiler/tableCell.js'; const cachedLinks = {}; export class Compiler { constructor(config, router) { this.config = config; this.router = router; this.cacheTree = {}; this.toc = []; this.blockquoteDepth = 0; this.cacheTOC = {}; this.linkTarget = config.externalLinkTarget || '_blank'; this.linkRel = this.linkTarget === '_blank' ? config.externalLinkRel || 'noopener' : ''; this.contentBase = router.getBasePath(); this.renderer = this._initRenderer(); let compile; const mdConf = config.markdown || {}; if (isFn(mdConf)) { compile = mdConf(marked, this.renderer); } else { marked.setOptions( Object.assign(mdConf, { renderer: Object.assign(this.renderer, mdConf.renderer), }), ); compile = marked; } this._marked = compile; this.compile = text => { let isCached = true; // FIXME: this is not cached. const result = cached(_ => { isCached = false; let html = ''; if (!text) { return text; } if (isPrimitive(text)) { html = compile(text); } else { html = compile.parser(text); } html = config.noEmoji ? html : emojify(html, config.nativeEmoji); slugify.clear(); return html; })(text); const curFileName = this.router.parse().file; if (isCached) { this.toc = this.cacheTOC[curFileName]; } else { this.cacheTOC[curFileName] = [...this.toc]; } return result; }; } /** * Pulls content from file and renders inline on the page as a embedded item. * * This allows you to embed different file types on the returned * page. * The basic format is: * ``` * [filename](_media/example.md ':include') * ``` * * @param {string} href The href to the file to embed in the page. * @param {string} title Title of the link used to make the embed. * * @return {any} Return value description. */ compileEmbed(href, title) { const { str, config } = getAndRemoveConfig(title); let embed; title = str; if (config.include) { if (!isAbsolutePath(href)) { href = getPath( this.contentBase, getParentPath(this.router.getCurrentPath()), href, ); } let media; const mediaType = Array.isArray(config.type) ? config.type[0] : config.type; if (mediaType && (media = compileMedia[mediaType])) { embed = media.call(this, href, title); embed.type = mediaType; } else { let type = 'code'; if (/\.(md|markdown)/.test(href)) { type = 'markdown'; } else if (/\.mmd/.test(href)) { type = 'mermaid'; } else if (/\.html?/.test(href)) { type = 'iframe'; } else if (/\.(mp4|ogg)/.test(href)) { type = 'video'; } else if (/\.mp3/.test(href)) { type = 'audio'; } embed = compileMedia[type](href, title); embed.type = type; } embed.fragment = config.fragment; embed.omitFragmentLine = config.omitFragmentLine; return embed; } } _matchNotCompileLink(link) { const links = this.config.noCompileLinks || []; for (const n of links) { const re = cachedLinks[n] || (cachedLinks[n] = new RegExp(`^${n}$`)); if (re.test(link)) { return link; } } } _initRenderer() { const renderer = new marked.Renderer(); const { linkTarget, linkRel, router, contentBase } = this; // Supports mermaid const origin = {}; // Renderer customizers origin.heading = headingCompiler({ renderer, router, compiler: this, }); origin.blockquoteCompiler = blockquoteCompiler({ renderer, compiler: this, }); origin.code = highlightCodeCompiler({ renderer }); origin.link = linkCompiler({ renderer, router, linkTarget, linkRel, compiler: this, }); origin.paragraph = paragraphCompiler({ renderer }); origin.image = imageCompiler({ renderer, contentBase, router }); origin.list = taskListCompiler({ renderer }); origin.listitem = taskListItemCompiler({ renderer }); origin.tablecell = tableCellCompiler({ renderer }); // @ts-expect-error renderer.origin = origin; return renderer; } /** * Compile sidebar, it uses _sidebar.md (or specific file) or the content's headings toc to render sidebar. * @param {String} text Text content from the sidebar file, maybe empty * @param {Number} level Type of heading (h<level> tag) * @returns {String} Sidebar element */ sidebar(text, level) { const { toc } = this; const currentPath = this.router.getCurrentPath(); let html = ''; // compile sidebar from _sidebar.md if (text) { return this.compile(text); } // compile sidebar from content's headings toc for (let i = 0; i < toc.length; i++) { if (toc[i].ignoreSubHeading) { const deletedHeaderLevel = toc[i].depth; toc.splice(i, 1); // Remove all following headings with greater depth while (i < toc.length && toc[i].depth > deletedHeaderLevel) { toc.splice(i, 1); } i--; } } const tree = this.cacheTree[currentPath] || genTree(toc, level); html = treeTpl(tree); this.cacheTree[currentPath] = tree; return html; } /** * When current content redirect to a new path file, clean pre content headings toc */ resetToc() { this.toc = []; } /** * Compile sub sidebar * @param {Number} level Type of heading (h<level> tag) * @returns {String} Sub-sidebar element */ subSidebar(level) { const currentPath = this.router.getCurrentPath(); const { cacheTree, toc } = this; toc[0] && toc[0].ignoreAllSubs && toc.splice(0); // remove the first heading from the toc if it is a top-level heading toc[0] && toc[0].depth === 1 && toc.shift(); for (let i = 0; i < toc.length; i++) { toc[i].ignoreSubHeading && toc.splice(i, 1) && i--; } const tree = cacheTree[currentPath] || genTree(toc, level); cacheTree[currentPath] = tree; this.toc = []; return treeTpl(tree); } /** * Compile the text to generate HTML heading element based on the level * @param {*} text Text content, for now it is only from the _sidebar.md file * @param {*} level Type of heading (h<level> tag), for now it is always 1 * @returns */ header(text, level) { const tokenHeading = { type: 'heading', raw: text, depth: level, text: text, tokens: [{ type: 'text', raw: text, text: text }], }; // @ts-expect-error return this.renderer.heading(tokenHeading); } /** * Compile cover page * @param {Text} text Text content * @returns {String} Cover page */ cover(text) { const cacheToc = this.toc.slice(); const html = this.compile(text); this.toc = cacheToc.slice(); return html; } }