Initial project
This commit is contained in:
209
src/utils/Markdown.ts
Normal file
209
src/utils/Markdown.ts
Normal file
@ -0,0 +1,209 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user