import marked from 'marked';
import { isAbsolutePath, getPath, getParentPath } from '../router/util';
import { isFn, merge, cached, isPrimitive } from '../util/core';
import { tree as treeTpl } from './tpl';
import { genTree } from './gen-tree';
import { slugify } from './slugify';
import { emojify } from './emojify';
import { getAndRemoveConfig, removeAtag } from './utils';
import { imageCompiler } from './compiler/image';
import { highlightCodeCompiler } from './compiler/code';
import { paragraphCompiler } from './compiler/paragraph';
import { taskListCompiler } from './compiler/taskList';
import { taskListItemCompiler } from './compiler/taskListItem';
import { linkCompiler } from './compiler/link';
const cachedLinks = {};
const compileMedia = {
markdown(url) {
return {
url,
};
},
mermaid(url) {
return {
url,
};
},
iframe(url, title) {
return {
html: ``,
};
},
video(url, title) {
return {
html: ``,
};
},
audio(url, title) {
return {
html: ``,
};
},
code(url, title) {
let lang = url.match(/\.(\w+)$/);
lang = title || (lang && lang[1]);
if (lang === 'md') {
lang = 'markdown';
}
return {
url,
lang,
};
},
};
export class Compiler {
constructor(config, router) {
this.config = config;
this.router = router;
this.cacheTree = {};
this.toc = [];
this.cacheTOC = {};
this.linkTarget = config.externalLinkTarget || '_blank';
this.linkRel =
this.linkTarget === '_blank' ? config.externalLinkRel || 'noopener' : '';
this.contentBase = router.getBasePath();
const renderer = this._initRenderer();
this.heading = renderer.heading;
let compile;
const mdConf = config.markdown || {};
if (isFn(mdConf)) {
compile = mdConf(marked, renderer);
} else {
marked.setOptions(
merge(mdConf, {
renderer: merge(renderer, mdConf.renderer),
})
);
compile = marked;
}
this._marked = compile;
this.compile = text => {
let isCached = true;
// eslint-disable-next-line no-unused-vars
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);
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 {type} Return value description.
*/
compileEmbed(href, title) {
const { str, config } = getAndRemoveConfig(title);
let embed;
title = str;
if (config.include) {
if (!isAbsolutePath(href)) {
href = getPath(
process.env.SSR ? '' : this.contentBase,
getParentPath(this.router.getCurrentPath()),
href
);
}
let media;
if (config.type && (media = compileMedia[config.type])) {
embed = media.call(this, href, title);
embed.type = config.type;
} 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].call(this, href, title);
embed.type = type;
}
embed.fragment = config.fragment;
return embed;
}
}
_matchNotCompileLink(link) {
const links = this.config.noCompileLinks || [];
for (let i = 0; i < links.length; i++) {
const n = links[i];
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;
const _self = this;
const origin = {};
/**
* Render anchor tag
* @link https://github.com/markedjs/marked#overriding-renderer-methods
* @param {String} text Text content
* @param {Number} level Type of heading (h tag)
* @returns {String} Heading element
*/
origin.heading = renderer.heading = function(text, level) {
let { str, config } = getAndRemoveConfig(text);
const nextToc = { level, title: removeAtag(str) };
if (//g.test(str)) {
str = str.replace('', '');
nextToc.title = removeAtag(str);
nextToc.ignoreSubHeading = true;
}
if (/{docsify-ignore}/g.test(str)) {
str = str.replace('{docsify-ignore}', '');
nextToc.title = removeAtag(str);
nextToc.ignoreSubHeading = true;
}
if (//g.test(str)) {
str = str.replace('', '');
nextToc.title = removeAtag(str);
nextToc.ignoreAllSubs = true;
}
if (/{docsify-ignore-all}/g.test(str)) {
str = str.replace('{docsify-ignore-all}', '');
nextToc.title = removeAtag(str);
nextToc.ignoreAllSubs = true;
}
const slug = slugify(config.id || str);
const url = router.toURL(router.getCurrentPath(), { id: slug });
nextToc.slug = url;
_self.toc.push(nextToc);
return `${str}`;
};
origin.code = highlightCodeCompiler({ renderer });
origin.link = linkCompiler({
renderer,
router,
linkTarget,
linkRel,
compilerClass: _self,
});
origin.paragraph = paragraphCompiler({ renderer });
origin.image = imageCompiler({ renderer, contentBase, router });
origin.list = taskListCompiler({ renderer });
origin.listitem = taskListItemCompiler({ renderer });
renderer.origin = origin;
return renderer;
}
/**
* Compile sidebar
* @param {String} text Text content
* @param {Number} level Type of heading (h tag)
* @returns {String} Sidebar element
*/
sidebar(text, level) {
const { toc } = this;
const currentPath = this.router.getCurrentPath();
let html = '';
if (text) {
html = this.compile(text);
} else {
for (let i = 0; i < toc.length; i++) {
if (toc[i].ignoreSubHeading) {
const deletedHeaderLevel = toc[i].level;
toc.splice(i, 1);
// Remove headers who are under current header
for (
let j = i;
j < toc.length && deletedHeaderLevel < toc[j].level;
j++
) {
toc.splice(j, 1) && j-- && i++;
}
i--;
}
}
const tree = this.cacheTree[currentPath] || genTree(toc, level);
html = treeTpl(tree, '');
this.cacheTree[currentPath] = tree;
}
return html;
}
/**
* Compile sub sidebar
* @param {Number} level Type of heading (h tag)
* @returns {String} Sub-sidebar element
*/
subSidebar(level) {
if (!level) {
this.toc = [];
return;
}
const currentPath = this.router.getCurrentPath();
const { cacheTree, toc } = this;
toc[0] && toc[0].ignoreAllSubs && toc.splice(0);
toc[0] && toc[0].level === 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);
}
header(text, level) {
return this.heading(text, level);
}
article(text) {
return this.compile(text);
}
/**
* 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;
}
}