import { marked } from "marked"; import { gfmHeadingId } from "marked-gfm-heading-id"; import { markedHighlight } from "marked-highlight"; import { mangle } from "marked-mangle"; import Prism from "prismjs"; import "prismjs/themes/prism.css"; import { Toolkit } from "~/index"; export default class Markdown { private static instance: Markdown; renderer = new marked.Renderer(); private constructor() { marked.use(mangle()); marked.use(gfmHeadingId()); marked.use(markedHighlight({ highlight(code: string, lang: string) { if (Prism.languages[lang]) { return Prism.highlight(code, Prism.languages[lang], lang); } else { return code; } } })); // Markdown 解析器配置 marked.setOptions({ renderer: this.renderer, pedantic: false, gfm: true, breaks: true }); Prism.hooks.add("complete", (env: any) => { if (!env.code) return; // 行号渲染调整 const el = env.element; const lineNumber = el.querySelector(".line-numbers-rows") as HTMLDivElement; if (lineNumber) { const clone = lineNumber.cloneNode(true); el.removeChild(lineNumber); // 加容器做滚动 el.innerHTML = `${el.innerHTML}`; el.insertBefore(clone, el.firstChild); if (el.parentNode) { const markdownRoot = el.parentNode.parentNode; if (markdownRoot) { const maxHeight = markdownRoot.dataset.maxHeight; if (maxHeight === "auto") { return; } // 注册双击事件 const lines = lineNumber.children.length; if (lines < 18) { return; } const parent = el.parentNode; parent.addEventListener("dblclick", (): void => { const isExpand = parent.classList.contains("expand"); if (isExpand) { parent.style.maxHeight = maxHeight; parent.classList.remove("expand"); } else { parent.style.maxHeight = lines * 22 + "px"; parent.classList.add("expand"); } const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); } }); } } } }); /** * ### 超链渲染方式 * * 1. 链接前加 ~ 符号会被渲染为新标签打开。例:`[文本](~链接)` * 3. 没有标题的链接将使用文本作为标题 * 4. 没有链接的会被渲染为 span 标签 */ this.renderer.link = function ({ href, title, text }) { title = title ?? text; if (!href) { return `${text}`; } // 新标签打开 let target = "_self"; if (href.startsWith("~")) { target = "_blank"; href = href.substring(1); } // 内部资源链接 if (href.indexOf("@") !== -1) { href = Toolkit.toResURL(href); } { // 处理嵌套 markdown,这可能不是最优解 const tokens = (marked.lexer(text, { inline: true } as any) as any)[0].tokens; text = this.parser.parseInline(tokens); } return `${text}`; }; /** * ### 重点内容扩展 * * ```md * 默认 `文本` 表现为红色 * 使用 `[red bold]文本` 可以自定义类 * ``` */ this.renderer.codespan = ({ text }) => { const clazz = text.match(/\[(.+?)]/); if (clazz) { return `${text.substring(text.indexOf("]") + 1)}`; } else { return `${text}`; } }; /** * ### 组件渲染方式(原为图像渲染方式) * * ```md * [] 内文本以 # 开始时,该组件带边框 * ``` * * 1. 渲染为网页:`![]($/html/index.html)` * 2. 渲染为视频:`![](#/media/video.mp4)` * 3. 渲染为音频:`![](~/media/music.mp3)` * 4. 渲染为图片:`![](/image/photo.png)` * 6. 带边框图片:`![#图片Alt](/image/photo.png)` */ this.renderer.image = ({ href, title, text }) => { const clazz = ["media"]; const hasBorder = text[0] === "#"; if (hasBorder) { clazz.push("border"); } title = title ?? text; const extendTags = ["~", "#", "$"]; let extendTag; if (extendTags.indexOf(href.charAt(0)) !== -1) { extendTag = href.charAt(0); } // 内部资源链接 if (href.indexOf("@") !== -1) { if (extendTag) { href = href.substring(1); } href = Toolkit.toResURL(href); } const clazzStr = clazz.join(" "); let elStr; switch (extendTag) { case "~": elStr = ``; break; case "#": elStr = ``; break; case "$": elStr = ``; break; default: elStr = `${title}`; break; } return elStr + `${title}`; }; } /** * ### 解析 Markdown 文本为 HTML 节点 * * ```js * const html = toHTML('# Markdown Content'); * ``` * * @param mkData Markdown 文本 * @returns HTML 节点 */ public toHTML(mkData: string | undefined): string | Promise { if (mkData) { return marked(mkData); } else { return ""; } } public static getInstance(): Markdown { if (!Markdown.instance) { Markdown.instance = new Markdown(); } return Markdown.instance; } }