210 lines
5.4 KiB
TypeScript
210 lines
5.4 KiB
TypeScript
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 = `<span class="codes">${el.innerHTML}</span>`;
|
||
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 `<span>${text}</span>`;
|
||
}
|
||
// 新标签打开
|
||
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 `<a href="${href}" target="${target}" title="${title}">${text}</a>`;
|
||
};
|
||
|
||
/**
|
||
* ### 重点内容扩展
|
||
*
|
||
* ```md
|
||
* 默认 `文本` 表现为红色
|
||
* 使用 `[red bold]文本` 可以自定义类
|
||
* ```
|
||
*/
|
||
this.renderer.codespan = ({ text }) => {
|
||
const clazz = text.match(/\[(.+?)]/);
|
||
if (clazz) {
|
||
return `<span class="${clazz[1]}">${text.substring(text.indexOf("]") + 1)}</span>`;
|
||
} else {
|
||
return `<span class="red">${text}</span>`;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* ### 组件渲染方式(原为图像渲染方式)
|
||
*
|
||
* ```md
|
||
* [] 内文本以 # 开始时,该组件带边框
|
||
* ```
|
||
*
|
||
* 1. 渲染为网页:``
|
||
* 2. 渲染为视频:``
|
||
* 3. 渲染为音频:``
|
||
* 4. 渲染为图片:``
|
||
* 6. 带边框图片:``
|
||
*/
|
||
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 = `<audio class="${clazzStr}" controls><source type="audio/mp3" src="${href}" title="${title}" /></audio>`;
|
||
break;
|
||
case "#":
|
||
elStr = `<video class="${clazzStr}" controls><source type="video/mp4" src="${href}" title="${title}" /></video>`;
|
||
break;
|
||
case "$":
|
||
elStr = `<iframe class="${clazzStr}" src="${href}" allowfullscreen title="${title}"></iframe>`;
|
||
break;
|
||
default:
|
||
elStr = `<img class="${clazzStr}" src="${href}" alt="${title}" />`;
|
||
break;
|
||
}
|
||
return elStr + `<span class="media-tips">${title}</span>`;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* ### 解析 Markdown 文本为 HTML 节点
|
||
*
|
||
* ```js
|
||
* const html = toHTML('# Markdown Content');
|
||
* ```
|
||
*
|
||
* @param mkData Markdown 文本
|
||
* @returns HTML 节点
|
||
*/
|
||
public toHTML(mkData: string | undefined): string | Promise<string> {
|
||
if (mkData) {
|
||
return marked(mkData);
|
||
} else {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
public static getInstance(): Markdown {
|
||
if (!Markdown.instance) {
|
||
Markdown.instance = new Markdown();
|
||
}
|
||
return Markdown.instance;
|
||
}
|
||
}
|