mirror of
https://github.com/docsifyjs/docsify.git
synced 2025-12-08 19:55:52 +00:00
* Render native emoji with image fallback Fix #779 * Deprecate emoji plugin * Add emoji tests * Remove console.log statement * Fix emoji image alt attribute * Set nativeEmoji to false by default (non-breaking) * Fix parsing emoji in HTML comments and script tags * Add nativeEmoji and update noEmoji details * Add Emoji plugin deprecation notice * Fix ESLint issues * Create build:emoji task - Auto-generate emoji data from GitHub API - Auto-generate emoji markdown for website - Add emoji page to navigation * Fix rendering of GitHub emoji without unicode * Adjust and match size of native and image emoji * Update emoji test snapshot * Update docs test snapshot * Fix ci/codesandbox error * Update native emoji font-stack * Fix rendering of native multi-character emoji * Kick GitHub Workflow * Replace rollup’s uglify plugin with terser * Switch “npm ci” instead of “npm i” for stability * Change emoji data from default to named export * Revert "Replace rollup’s uglify plugin with terser" This reverts commit 7ba85136361c72839516900d91cca806fac94fee. * Revert "Switch “npm ci” instead of “npm i” for stability" This reverts commit d52b476a387250740d934e8fd7df7ba274dd17a0. * Revert "Change emoji data from default to named export" This reverts commit 3f2dd467cf9c7a74d8c53c2ee52cc63837b00a3c. * Specify codesandbox template and node version * Update codesandbox config * Revert "Revert "Replace rollup’s uglify plugin with terser"" This reverts commit e06fed49f0383c485e01f1758228849ad0085bc8. * Revert "Revert "Revert "Replace rollup’s uglify plugin with terser""" This reverts commit 27d49521f61976dedcbbf210e1811839853e0e47. * Update codesandbox config * Revert "Update codesandbox config" This reverts commit 5120dd23d45fbd4b2c893db33acbf7014e57c023. * Fix codesandbox uglify error * Emoji docs tweaks * Restore and update emoji plugin code * Restore and update emoji plugin docs * Prettier updates * Match lowercase shortcodes only Co-authored-by: Koy Zhuang <369491420@qq.com>
351 lines
8.9 KiB
JavaScript
351 lines
8.9 KiB
JavaScript
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: `<iframe src="${url}" ${
|
|
title || 'width=100% height=400'
|
|
}></iframe>`,
|
|
};
|
|
},
|
|
video(url, title) {
|
|
return {
|
|
html: `<video src="${url}" ${title || 'controls'}>Not Support</video>`,
|
|
};
|
|
},
|
|
audio(url, title) {
|
|
return {
|
|
html: `<audio src="${url}" ${title || 'controls'}>Not Support</audio>`,
|
|
};
|
|
},
|
|
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, 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 {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<level> tag)
|
|
* @returns {String} Heading element
|
|
*/
|
|
origin.heading = renderer.heading = function (text, level) {
|
|
let { str, config } = getAndRemoveConfig(text);
|
|
const nextToc = { level, title: removeAtag(str) };
|
|
|
|
if (/<!-- {docsify-ignore} -->/g.test(str)) {
|
|
str = str.replace('<!-- {docsify-ignore} -->', '');
|
|
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 (/<!-- {docsify-ignore-all} -->/g.test(str)) {
|
|
str = str.replace('<!-- {docsify-ignore-all} -->', '');
|
|
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 `<h${level} id="${slug}"><a href="${url}" data-id="${slug}" class="anchor"><span>${str}</span></a></h${level}>`;
|
|
};
|
|
|
|
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<level> 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, '<ul>{inner}</ul>');
|
|
this.cacheTree[currentPath] = tree;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
/**
|
|
* Compile sub sidebar
|
|
* @param {Number} level Type of heading (h<level> 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;
|
|
}
|
|
}
|