feat: 文章添加toc
This commit is contained in:
@@ -1,115 +1,195 @@
|
||||
import {JSDOM} from 'jsdom'
|
||||
import {createHighlighter} from 'shiki'
|
||||
import { JSDOM } from "jsdom";
|
||||
import { createHighlighter } from "shiki";
|
||||
|
||||
const DEFAULT_THEME = 'github-dark-dimmed'
|
||||
const DEFAULT_THEME = "github-dark-dimmed";
|
||||
|
||||
type SupportedLang = (typeof SUPPORTED_LANGS)[number];
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: [DEFAULT_THEME],
|
||||
langs: []
|
||||
})
|
||||
langs: [],
|
||||
});
|
||||
|
||||
const SUPPORTED_LANGS = [
|
||||
"bash", "json", "yaml", "xml", "html", "css", "scss",
|
||||
"javascript", "typescript", "jsx", "tsx", "vue",
|
||||
"markdown", "sql", "python", "go", "dockerfile", "nginx"
|
||||
] as const
|
||||
"bash",
|
||||
"json",
|
||||
"yaml",
|
||||
"xml",
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"jsx",
|
||||
"tsx",
|
||||
"vue",
|
||||
"markdown",
|
||||
"sql",
|
||||
"python",
|
||||
"go",
|
||||
"dockerfile",
|
||||
"nginx",
|
||||
] as const;
|
||||
|
||||
const loadedLangs = new Set<string>(['text'])
|
||||
const loadedLangs = new Set<string>(["text"]);
|
||||
|
||||
type SupportedLang = typeof SUPPORTED_LANGS[number]
|
||||
|
||||
const supportedSet = new Set<string>(SUPPORTED_LANGS)
|
||||
const supportedSet = new Set<string>(SUPPORTED_LANGS);
|
||||
|
||||
const ensureLangLoaded = async (lang: string): Promise<string> => {
|
||||
if (lang === 'text' || loadedLangs.has(lang)) return lang
|
||||
if (!supportedSet.has(lang)) return 'text'
|
||||
if (lang === "text" || loadedLangs.has(lang)) return lang;
|
||||
if (!supportedSet.has(lang)) return "text";
|
||||
try {
|
||||
await highlighter.loadLanguage(lang as SupportedLang)
|
||||
loadedLangs.add(lang)
|
||||
return lang
|
||||
await highlighter.loadLanguage(lang as SupportedLang);
|
||||
loadedLangs.add(lang);
|
||||
return lang;
|
||||
} catch {
|
||||
return 'text'
|
||||
return "text";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const processHtmlWithJsdom = async (rawHtml: string) => {
|
||||
const dom = new JSDOM(rawHtml)
|
||||
const document = dom.window.document
|
||||
|
||||
const codeBlocks = document.querySelectorAll('pre code')
|
||||
async function processCodeBlocks(document: Document) {
|
||||
const codeBlocks = document.querySelectorAll("pre code");
|
||||
|
||||
for (const codeBlock of codeBlocks) {
|
||||
const preEl = codeBlock.parentElement
|
||||
if (!preEl || preEl.tagName !== 'PRE') continue
|
||||
const preEl = codeBlock.parentElement;
|
||||
if (!preEl || preEl.tagName !== "PRE") continue;
|
||||
|
||||
const codeContent = codeBlock.textContent || ''
|
||||
let lang = 'text'
|
||||
const codeContent = codeBlock.textContent || "";
|
||||
let lang = "text";
|
||||
for (const cls of codeBlock.classList) {
|
||||
if (cls.startsWith('language-')) {
|
||||
lang = cls.slice('language-'.length)
|
||||
break
|
||||
if (cls.startsWith("language-")) {
|
||||
lang = cls.slice("language-".length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lang = await ensureLangLoaded(lang) // ← 唯一改动点:按需加载
|
||||
lang = await ensureLangLoaded(lang);
|
||||
|
||||
const highlighted = highlighter.codeToHtml(codeContent, {
|
||||
lang,
|
||||
theme: DEFAULT_THEME,
|
||||
})
|
||||
});
|
||||
|
||||
// 替换 DOM
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerHTML = highlighted.trim()
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = highlighted.trim();
|
||||
|
||||
const newNode = wrapper.firstElementChild
|
||||
const newNode = wrapper.firstElementChild;
|
||||
if (newNode && preEl.parentNode) {
|
||||
const container = document.createElement('div')
|
||||
container.className = 'code-block'
|
||||
const container = document.createElement("div");
|
||||
container.className = "code-block";
|
||||
|
||||
const header = document.createElement('div')
|
||||
header.className = 'code-header'
|
||||
const header = document.createElement("div");
|
||||
header.className = "code-header";
|
||||
|
||||
const headerLeft = document.createElement('div')
|
||||
headerLeft.className = 'code-header-left'
|
||||
const headerLeft = document.createElement("div");
|
||||
headerLeft.className = "code-header-left";
|
||||
|
||||
const headerRight = document.createElement('div')
|
||||
headerRight.className = 'code-header-right'
|
||||
const headerRight = document.createElement("div");
|
||||
headerRight.className = "code-header-right";
|
||||
|
||||
// 装饰点
|
||||
const actions = document.createElement('div')
|
||||
actions.className = 'code-actions'
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "code-actions";
|
||||
|
||||
const close = document.createElement('span')
|
||||
close.className = 'dot close'
|
||||
const close = document.createElement("span");
|
||||
close.className = "dot close";
|
||||
|
||||
const minimize = document.createElement('span')
|
||||
minimize.className = 'dot minimize'
|
||||
const minimize = document.createElement("span");
|
||||
minimize.className = "dot minimize";
|
||||
|
||||
const maximize = document.createElement('span')
|
||||
maximize.className = 'dot maximize'
|
||||
const maximize = document.createElement("span");
|
||||
maximize.className = "dot maximize";
|
||||
|
||||
actions.append(close, minimize, maximize)
|
||||
actions.append(close, minimize, maximize);
|
||||
|
||||
// 显示语言
|
||||
const langDisplay = document.createElement('div')
|
||||
langDisplay.className = 'code-language'
|
||||
langDisplay.textContent = lang ? lang : 'text'
|
||||
const langDisplay = document.createElement("div");
|
||||
langDisplay.className = "code-language";
|
||||
langDisplay.textContent = lang ? lang : "text";
|
||||
|
||||
const copyBtn = document.createElement('div')
|
||||
copyBtn.className = 'code-copy-btn'
|
||||
copyBtn.textContent = '复制代码'
|
||||
const copyBtn = document.createElement("div");
|
||||
copyBtn.className = "code-copy-btn";
|
||||
copyBtn.textContent = "复制代码";
|
||||
|
||||
headerLeft.append(actions, langDisplay)
|
||||
headerRight.append(copyBtn)
|
||||
headerLeft.append(actions, langDisplay);
|
||||
headerRight.append(copyBtn);
|
||||
|
||||
header.append(headerLeft, headerRight)
|
||||
header.append(headerLeft, headerRight);
|
||||
|
||||
container.append(header, newNode)
|
||||
container.append(header, newNode);
|
||||
|
||||
preEl.parentNode.replaceChild(container, preEl)
|
||||
preEl.parentNode.replaceChild(container, preEl);
|
||||
}
|
||||
}
|
||||
|
||||
return document.body.innerHTML
|
||||
}
|
||||
|
||||
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
export interface TocNode {
|
||||
level: HeadingLevel;
|
||||
id: string;
|
||||
text: string;
|
||||
element: HTMLHeadingElement;
|
||||
}
|
||||
|
||||
function createHeadingId(text: string, doc: Document): string {
|
||||
const base = text.trim() || "heading";
|
||||
|
||||
let id = base;
|
||||
let index = 1;
|
||||
|
||||
while (doc.getElementById(id)) {
|
||||
id = `${base}-${index++}`;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function getToc(document: Document): TocNode[] {
|
||||
const headings = Array.from(
|
||||
document.querySelectorAll<HTMLHeadingElement>("h1, h2, h3, h4, h5, h6"),
|
||||
);
|
||||
|
||||
const result: TocNode[] = [];
|
||||
|
||||
for (const element of headings) {
|
||||
const level = Number(element.tagName.slice(1)) as HeadingLevel;
|
||||
|
||||
const id =
|
||||
element.id || createHeadingId(element.textContent ?? "", document);
|
||||
|
||||
element.id = id;
|
||||
|
||||
const node: TocNode = {
|
||||
level,
|
||||
id,
|
||||
text: element.textContent?.trim() ?? "",
|
||||
element,
|
||||
};
|
||||
result.push(node);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type ParseResult = {
|
||||
html: string;
|
||||
toc: TocNode[];
|
||||
};
|
||||
|
||||
export const processHtmlWithJsdom = async (
|
||||
rawHtml: string,
|
||||
): Promise<ParseResult> => {
|
||||
const dom = new JSDOM(rawHtml);
|
||||
const document = dom.window.document;
|
||||
|
||||
await processCodeBlocks(document);
|
||||
|
||||
const toc = getToc(document)
|
||||
|
||||
return {
|
||||
html: document.body.innerHTML,
|
||||
toc,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user