refactor: 提取 Markdown 渲染逻辑到独立模块并优化组件解耦

This commit is contained in:
seahi committed 2026-09-25 08:54:39 +08:00
1 parent 373b219503
commit 198d4f5075
3 files changed
+352 -359

No files matched your search

+1 -358
View File
@@ -1,365 +1,8 @@
<script lang="ts"> <script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */ let { htmlContent = '' } = $props<{ htmlContent: string }>();
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Marked } from 'marked';
import hljs from 'highlight.js/lib/core';
import 'highlight.js/styles/github.css';
import {
createUniqueHeadingId,
replaceGithubAdmonitions,
replaceHugoAdmonitions,
resolveAdmonitionTitle,
sanitizeMarkdownHtml
} from '$lib/markdown/rendering';
// 按需引入及注册主流核心前端、后端、运维语法包,替代沉重的 190+ 语言全集捆绑
import bash from 'highlight.js/lib/languages/bash';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import json from 'highlight.js/lib/languages/json';
import css from 'highlight.js/lib/languages/css';
import xml from 'highlight.js/lib/languages/xml';
import python from 'highlight.js/lib/languages/python';
import sql from 'highlight.js/lib/languages/sql';
import yaml from 'highlight.js/lib/languages/yaml';
import markdown from 'highlight.js/lib/languages/markdown';
import go from 'highlight.js/lib/languages/go';
import rust from 'highlight.js/lib/languages/rust';
import cpp from 'highlight.js/lib/languages/cpp';
import c from 'highlight.js/lib/languages/c';
import java from 'highlight.js/lib/languages/java';
import php from 'highlight.js/lib/languages/php';
import dockerfile from 'highlight.js/lib/languages/dockerfile';
import nginx from 'highlight.js/lib/languages/nginx';
import ini from 'highlight.js/lib/languages/ini';
import http from 'highlight.js/lib/languages/http';
import diff from 'highlight.js/lib/languages/diff';
hljs.registerLanguage('bash', bash);
hljs.registerLanguage('sh', bash);
hljs.registerLanguage('shell', bash);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('js', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('json', json);
hljs.registerLanguage('css', css);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('html', xml);
hljs.registerLanguage('python', python);
hljs.registerLanguage('py', python);
hljs.registerLanguage('sql', sql);
hljs.registerLanguage('yaml', yaml);
hljs.registerLanguage('yml', yaml);
hljs.registerLanguage('markdown', markdown);
hljs.registerLanguage('md', markdown);
hljs.registerLanguage('go', go);
hljs.registerLanguage('rust', rust);
hljs.registerLanguage('cpp', cpp);
hljs.registerLanguage('c', c);
hljs.registerLanguage('java', java);
hljs.registerLanguage('php', php);
hljs.registerLanguage('dockerfile', dockerfile);
hljs.registerLanguage('nginx', nginx);
hljs.registerLanguage('ini', ini);
hljs.registerLanguage('toml', ini);
hljs.registerLanguage('http', http);
hljs.registerLanguage('diff', diff);
// HTML 特殊字符过滤,防止自定义渲染器中的属性注入 XSS
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// 链接安全性检查,拦截 javascript: 等危险伪协议
function sanitizeUrl(url: string): string {
const clean = url.trim().toLowerCase();
if (
clean.startsWith('javascript:') ||
clean.startsWith('vbscript:') ||
clean.startsWith('data:text/html')
) {
return '#';
}
return url;
}
// 增强 highlight.js 的 bash 语法定义:补足常用运维及工程工具的主命令、子命令以及参数开关
const bashLang = hljs.getLanguage('bash');
if (bashLang && bashLang.keywords && typeof bashLang.keywords === 'object') {
const kw = bashLang.keywords as Record<string, string[] | undefined>;
// 1. 添加主命令(显示为内建或核心命令样式)
if (Array.isArray(kw.built_in)) {
kw.built_in.push(
'docker',
'podman',
'openstack',
'kubectl',
'kubeadm',
'helm',
'systemctl',
'journalctl',
'firewalld',
'iptables',
'crictl',
'nerdctl',
'ansible',
'terraform',
'vagrant',
'wrangler',
'pnpm',
'npm',
'gitea',
'curl',
'wget',
'git',
'cargo',
'bun'
);
}
// 2. 添加操作动作与资源类子命令(显示为字面或特色参数样式)
const customSubcommands = [
'run',
'exec',
'ps',
'build',
'image',
'images',
'container',
'pull',
'push',
'apply',
'get',
'describe',
'delete',
'logs',
'create',
'edit',
'rollout',
'scale',
'port-forward',
'cluster-info',
'server',
'flavor',
'volume',
'network',
'subnet',
'router',
'keypair',
'floating',
'list',
'show',
'start',
'stop',
'restart',
'status',
'enable',
'disable',
'daemon-reload',
'clone',
'commit',
'checkout',
'branch',
'merge',
'rebase',
'fetch',
'install',
'add',
'remove',
'dev',
'deploy',
'attach',
'rmi',
'rm'
];
if (Array.isArray(kw.literal)) {
kw.literal.push(...customSubcommands);
} else {
kw.literal = customSubcommands;
}
// 3. 匹配 -d, --name, -p 等选项开关(限定必须位于空格或行首后面,避免误伤中间带破折号的普通名称)
if (Array.isArray(bashLang.contains)) {
bashLang.contains.unshift({
className: 'attr',
begin: /(?<=\s|^)-+[a-zA-Z0-9_-]+/
});
}
}
let { content = '' } = $props<{ content: string }>();
let container: HTMLElement; let container: HTMLElement;
// 创建独立 Marked 实例
const markedInstance = new Marked();
// This is renderer-local bookkeeping, not UI state.
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const headingIds = new Map<string, number>();
markedInstance.use({
renderer: {
code(token) {
const code = token.text;
let rawLang = (token.lang || '').trim().toLowerCase().split(' ')[0];
// 对常见的习惯表达法强制收向 bash 处理
const bashAliases: Record<string, string> = {
k8s: 'bash',
kubectl: 'bash',
openstack: 'bash',
podman: 'bash',
systemd: 'bash',
systemctl: 'bash',
console: 'bash',
terminal: 'bash',
zsh: 'bash',
sh: 'bash'
};
if (bashAliases[rawLang]) {
rawLang = bashAliases[rawLang];
}
let highlighted: string;
let language = 'plaintext';
if (rawLang && hljs.getLanguage(rawLang)) {
language = rawLang;
highlighted = hljs.highlight(code, { language }).value;
} else {
try {
const auto = hljs.highlightAuto(code);
highlighted = auto.value || escapeHtml(code);
if (auto.language) language = auto.language;
} catch (err: any) {
highlighted = escapeHtml(code);
}
}
const title = escapeHtml(rawLang || language || 'text');
const encodedCode = encodeURIComponent(code);
const lines = highlighted.split('\n');
const openSpans: string[] = [];
const wrappedLines = lines
.map((line) => {
const prefix = openSpans.join('');
const matches = line.matchAll(/<span [^>]+>|<\/span>/g);
for (const m of matches) {
if (m[0] === '</span>') {
openSpans.pop();
} else {
openSpans.push(m[0]);
}
}
const suffix = '</span>'.repeat(openSpans.length);
return `<span class="code-line">${prefix}${line}${suffix}</span>`;
})
.join('\n');
const chevronRight =
'<svg class="chevron-icon" viewBox="0 0 320 512" fill="currentColor" width="0.625em" height="1em"><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"></path></svg>';
const listOlIcon =
'<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z"></path></svg>';
const barsIcon =
'<svg viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path></svg>';
const copyIcon =
'<svg viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"><path d="M433.941 65.941l-51.882-51.882A48 48 0 00348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 00-14.059-33.941zM266 464H54a6 6 0 01-6-6V150a6 6 0 016-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 01-6 6zm128-96H182a6 6 0 01-6-6V54a6 6 0 016-6h106v88c0 13.255 10.745 24 24 24h88v200a6 6 0 01-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 011.757 4.243V112z"></path></svg>';
const checkIcon =
'<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>';
const ellipsisIcon =
'<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z"></path></svg>';
return `<div class="code-block highlight is-open show-line-numbers">
<div class="code-block-header">
<button class="code-block-title-btn" aria-hidden="true">
${chevronRight}
<span class="code-title">${title}</span>
</button>
<div class="code-header-actions">
<button class="action-icon line-number-button is-active" title="Toggle line numbers">${listOlIcon}</button>
<button class="action-icon wrap-code-button" title="Toggle code wrap">${barsIcon}</button>
<button class="action-icon copy-code-button" title="Copy code" data-code="${encodedCode}">
<span class="copy-icon">${copyIcon}</span>
<span class="check-icon">${checkIcon}</span>
</button>
<span class="ellipsis-icon" aria-hidden="true">${ellipsisIcon}</span>
</div>
</div>
<pre class="code-pre"><code class="hljs language-${language}">${wrappedLines}</code></pre>
</div>`;
},
image(token) {
const src = sanitizeUrl(token.href || '');
const alt = escapeHtml(token.text || '');
const title = escapeHtml(token.title || alt);
let figcaption = '';
if (title) {
figcaption = `<figcaption>${title}</figcaption>`;
}
return `<figure><a data-fancybox="gallery" data-caption="${title}" href="${escapeHtml(src)}"><img src="${escapeHtml(src)}" alt="${alt}" title="${title}"></a>${figcaption}</figure>`;
},
heading(token) {
const text = token.text;
const level = token.depth;
const id = createUniqueHeadingId(text, headingIds);
return `<h${level} id="${id}">${text}</h${level}>`;
}
}
});
function parseAdmonitions(markdown: string) {
const icons = {
note: '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"></path></svg>',
abstract:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M48 48a48 48 0 1 0 48 48 48.14 48.14 0 0 0-48-48zm0 160a48 48 0 1 0 48 48 48.14 48.14 0 0 0-48-48zm0 160a48 48 0 1 0 48 48 48.14 48.14 0 0 0-48-48zm448 16H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"></path></svg>',
info: '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"></path></svg>',
tip: '<svg viewBox="0 0 352 512" fill="currentColor" height="1em" width="1em"><path d="M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.64 18.99 42.74 58.8 52.42 92.16h160.03c9.68-33.35 35.78-73.16 52.42-92.16C335.55 260.85 352 220.37 352 176 352 78.8 273.2 0 176 0S0 78.8 0 176z"></path></svg>',
success:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg>',
question:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zM262.655 90c-54.497 0-89.255 22.957-116.549 63.758-3.536 5.286-2.353 12.415 2.715 16.258l34.699 26.31c5.205 3.947 12.621 3.008 16.665-2.122 17.864-22.658 30.113-35.797 57.303-35.797 20.429 0 45.698 13.148 45.698 32.958 0 14.976-12.363 22.667-32.534 33.976C247.128 238.528 216 254.941 216 296v4c0 6.627 5.373 12 12 12h56c6.627 0 12-5.373 12-12v-1.333c0-28.462 83.186-29.647 83.186-106.667 0-58.002-60.165-102-116.531-102zM256 338c-25.365 0-46 20.635-46 46 0 25.364 20.635 46 46 46s46-20.636 46-46c0-25.365-20.635-46-46-46z"></path></svg>',
warning:
'<svg viewBox="0 0 576 512" fill="currentColor" height="1em" width="1em"><path d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.466-32.004 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"></path></svg>',
failure:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"></path></svg>',
danger:
'<svg viewBox="0 0 448 512" fill="currentColor" height="1em" width="1em"><path d="M439.15 453.06L297.17 384l141.99-69.06c7.9-3.95 11.11-13.56 7.15-21.46L432 264.85c-3.95-7.9-13.56-11.11-21.47-7.16L224 348.41 37.47 257.69c-7.9-3.95-17.51-.75-21.47 7.16L1.69 293.48c-3.96 7.9-.76 17.51 7.15 21.46L150.83 384 8.85 453.06c-7.9 3.95-11.11 13.56-7.15 21.47l14.31 28.63c3.95 7.9 13.56 11.11 21.47 7.15L224 419.59l186.53 90.72c7.9 3.95 17.51.75 21.47-7.15l14.31-28.63c3.95-7.91.75-17.52-7.16-21.47zM150 237.28l-5.48 25.87c-2.67 12.62 5.42 24.85 16.45 24.85h126.08c11.03 0 19.12-12.23 16.45-24.85l-5.5-25.87c41.78-22.41 70-62.8 70-109.28C368 61.85 303.73 0 224 0S80 61.85 80 128c0 46.48 28.22 86.87 70 109.28zM280 112c17.65 0 32 14.35 32 32s-14.35 32-32 32-32-14.35-32-32 14.35-32 32-32zm-112 0c17.65 0 32 14.35 32 32s-14.35 32-32 32-32-14.35-32-32 14.35-32 32-32z"></path></svg>',
bug: '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M511.98 256.03c0-34.92-27.46-64-62.24-64h-38.38c-8.91-44.57-30.82-84.34-60.77-116.14l27.13-27.13c12.5-12.5 12.5-32.76 0-45.25l-22.63-22.63c-12.5-12.5-32.76-12.5-45.25 0l-43.14 43.14A220.617 220.617 0 0 0 256 16.03c-3.69 0-7.38.15-11.05.35l-43.15-43.15c-12.5-12.5-32.76-12.5-45.25 0l-22.63 22.63c-12.5 12.5-12.5 32.76 0 45.25l27.13 27.13c-29.95 31.8-51.86 71.57-60.77 116.14H62.26c-34.78 0-62.24 29.08-62.24 64 0 34.39 26.89 62.48 61 63.92V336.1c0 51.52 19.34 98.4 51.05 133.72l-25.56 25.56c-12.5 12.5-12.5 32.76 0 45.25l22.63 22.63c12.5 12.5 32.76 12.5 45.25 0l42.66-42.66c30.2 16.92 64.71 27.42 101 27.42s70.8-10.5 101-27.42l42.66 42.66c12.5 12.5 32.76 12.5 45.25 0l22.63-22.63c12.5-12.5 12.5-32.76 0-45.25l-25.56-25.56C430.68 434.5 450 387.62 450 336.1v-23.79c34.11-1.44 61.98-29.53 61.98-63.92v-8.36zM152 280.03c0-13.25 10.75-24 24-24s24 10.75 24 24v56c0 13.25-10.75 24-24 24s-24-10.75-24-24v-56zm160 0c0-13.25 10.75-24 24-24s24 10.75 24 24v56c0 13.25-10.75 24-24 24s-24-10.75-24-24v-56z"></path></svg>',
example:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h34.71L15.32 424.64A29.85 29.85 0 0 0 8 444v16c0 26.51 21.49 48 48 48h24a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H56c-8.84 0-16-7.16-16-16v-4.52a6.01 6.01 0 0 1 1.76-4.24l19.24-19.24a48.28 48.28 0 0 0 7.58 3.06L64 448c0 35.35 28.65 64 64 64s64-28.65 64-64-28.65-64-64-64c-9.67 0-18.77 2.21-27.05 6.04L75.64 361.7A29.8 29.8 0 0 0 56 352h-7.66c10.37-4.14 17.66-14.24 17.66-26.34v-3.31c0-15.46-12.54-28-28-28H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22c2.21 0 4 1.79 4 4v3.31c0 2.21-1.79 4-4 4H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h15.4c17.21 0 33.15 6.94 44.97 19.46l5.4-6.23a43.83 43.83 0 0 1 7.23-7.19h-15zM480 352H208a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h272a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM208 112h272a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16H208a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zM480 208H208a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h272a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM88 32H16C7.16 32 0 39.16 0 48v20c0 8.84 7.16 16 16 16h16v64c0 8.84 7.16 16 16 16h16c8.84 0 16-7.16 16-16V48c0-8.84-7.16-16-16-16h-48c0-8.84 7.16-16 16-16zm64 160c0-35.35-28.65-64-64-64s-64 28.65-64 64 28.65 64 64 64 64-28.65 64-64zm-32 0c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32z"></path></svg>',
quote:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M464 256h-80v-64c0-35.3 28.7-64 64-64h8c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24h-8c-88.4 0-160 71.6-160 160v240c0 26.5 21.5 48 48 48h128c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48zm-288 0H96v-64c0-35.3 28.7-64 64-64h8c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24h-8C71.6 32 0 103.6 0 192v240c0 26.5 21.5 48 48 48h128c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48z"></path></svg>',
important:
'<svg viewBox="0 0 192 512" fill="currentColor" height="1em" width="1em"><path d="M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.33l7.12 256.33c.35 12.63 10.71 22.34 23.36 22.34h80.51c12.65 0 23.01-9.71 23.36-22.34l7.12-256.33c.37-13.37-10.36-24.33-23.75-24.33H49.02c-13.4 0-24.12 10.96-23.76 24.33z"></path></svg>',
caution:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"></path></svg>'
};
function renderAdmonition(type: string, rawTitle: string, content: string) {
const typeLower = type.toLowerCase();
const displayTitle = escapeHtml(resolveAdmonitionTitle(type, rawTitle));
const icon = icons[typeLower as keyof typeof icons] || icons.note;
return `\n<div class="details admonition ${typeLower} open">\n<div class="details-summary admonition-title"><span class="icon">${icon}</span><span>${displayTitle}</span></div>\n<div class="details-content">\n<div class="admonition-content">\n\n${content}\n\n</div>\n</div>\n</div>\n`;
}
let html = replaceGithubAdmonitions(markdown, renderAdmonition);
html = replaceHugoAdmonitions(html, renderAdmonition);
return html;
}
function renderMarkdown(raw: string) {
headingIds.clear();
const preprocessedContent = parseAdmonitions(raw);
return sanitizeMarkdownHtml(markedInstance.parse(preprocessedContent) as string);
}
let htmlContent = $derived(renderMarkdown(content));
async function bindEvents() { async function bindEvents() {
if (typeof window !== 'undefined' && document.querySelector('[data-fancybox]')) { if (typeof window !== 'undefined' && document.querySelector('[data-fancybox]')) {
const { Fancybox } = await import('@fancyapps/ui/dist/index.js'); const { Fancybox } = await import('@fancyapps/ui/dist/index.js');
@@ -2,6 +2,7 @@
import { tick, untrack } from 'svelte'; import { tick, untrack } from 'svelte';
import MarkdownEditor from './MarkdownEditor.svelte'; import MarkdownEditor from './MarkdownEditor.svelte';
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte'; import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
import { renderMarkdown } from '$lib/markdown/renderer';
import OutlinePopover from './OutlinePopover.svelte'; import OutlinePopover from './OutlinePopover.svelte';
import { activeHeadingAt, extractOutline, type OutlineHeading } from '$lib/editor/outline'; import { activeHeadingAt, extractOutline, type OutlineHeading } from '$lib/editor/outline';
import type { EditorOutline } from '$lib/editor/outline.svelte'; import type { EditorOutline } from '$lib/editor/outline.svelte';
@@ -21,6 +22,9 @@
let preview: HTMLDivElement | undefined = $state(); let preview: HTMLDivElement | undefined = $state();
let previewHeadings: HTMLElement[] = []; let previewHeadings: HTMLElement[] = [];
let navigatingPreview = false; let navigatingPreview = false;
let previewHtml = $derived(mode === 'preview' ? renderMarkdown(value || '') : '');
$effect(() => { $effect(() => {
const resume = () => { const resume = () => {
navigatingPreview = false; navigatingPreview = false;
@@ -126,7 +130,7 @@
navigatingPreview = false; navigatingPreview = false;
}} }}
> >
<MarkdownRenderer content={value} /> <MarkdownRenderer htmlContent={previewHtml} />
</div> </div>
{/if} {/if}
+346
View File
@@ -0,0 +1,346 @@
import { Marked } from 'marked';
import hljs from 'highlight.js/lib/core';
import {
createUniqueHeadingId,
replaceGithubAdmonitions,
replaceHugoAdmonitions,
resolveAdmonitionTitle,
sanitizeMarkdownHtml
} from '$lib/markdown/rendering';
// 按需引入及注册主流核心前端、后端、运维语法包
import bash from 'highlight.js/lib/languages/bash';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import json from 'highlight.js/lib/languages/json';
import css from 'highlight.js/lib/languages/css';
import xml from 'highlight.js/lib/languages/xml';
import python from 'highlight.js/lib/languages/python';
import sql from 'highlight.js/lib/languages/sql';
import yaml from 'highlight.js/lib/languages/yaml';
import markdown from 'highlight.js/lib/languages/markdown';
import go from 'highlight.js/lib/languages/go';
import rust from 'highlight.js/lib/languages/rust';
import cpp from 'highlight.js/lib/languages/cpp';
import c from 'highlight.js/lib/languages/c';
import java from 'highlight.js/lib/languages/java';
import php from 'highlight.js/lib/languages/php';
import dockerfile from 'highlight.js/lib/languages/dockerfile';
import nginx from 'highlight.js/lib/languages/nginx';
import ini from 'highlight.js/lib/languages/ini';
import http from 'highlight.js/lib/languages/http';
import diff from 'highlight.js/lib/languages/diff';
hljs.registerLanguage('bash', bash);
hljs.registerLanguage('sh', bash);
hljs.registerLanguage('shell', bash);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('js', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('json', json);
hljs.registerLanguage('css', css);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('html', xml);
hljs.registerLanguage('python', python);
hljs.registerLanguage('py', python);
hljs.registerLanguage('sql', sql);
hljs.registerLanguage('yaml', yaml);
hljs.registerLanguage('yml', yaml);
hljs.registerLanguage('markdown', markdown);
hljs.registerLanguage('md', markdown);
hljs.registerLanguage('go', go);
hljs.registerLanguage('rust', rust);
hljs.registerLanguage('cpp', cpp);
hljs.registerLanguage('c', c);
hljs.registerLanguage('java', java);
hljs.registerLanguage('php', php);
hljs.registerLanguage('dockerfile', dockerfile);
hljs.registerLanguage('nginx', nginx);
hljs.registerLanguage('ini', ini);
hljs.registerLanguage('toml', ini);
hljs.registerLanguage('http', http);
hljs.registerLanguage('diff', diff);
// HTML 特殊字符过滤,防止自定义渲染器中的属性注入 XSS
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// 链接安全性检查,拦截 javascript: 等危险伪协议
function sanitizeUrl(url: string): string {
const clean = url.trim().toLowerCase();
if (
clean.startsWith('javascript:') ||
clean.startsWith('vbscript:') ||
clean.startsWith('data:text/html')
) {
return '#';
}
return url;
}
// 增强 highlight.js 的 bash 语法定义:补足常用运维及工程工具的主命令、子命令以及参数开关
const bashLang = hljs.getLanguage('bash');
if (bashLang && bashLang.keywords && typeof bashLang.keywords === 'object') {
const kw = bashLang.keywords as Record<string, string[] | undefined>;
// 1. 添加主命令
if (Array.isArray(kw.built_in)) {
kw.built_in.push(
'docker',
'podman',
'openstack',
'kubectl',
'kubeadm',
'helm',
'systemctl',
'journalctl',
'firewalld',
'iptables',
'crictl',
'nerdctl',
'ansible',
'terraform',
'vagrant',
'wrangler',
'pnpm',
'npm',
'gitea',
'curl',
'wget',
'git',
'cargo',
'bun'
);
}
// 2. 添加操作动作与资源类子命令
const customSubcommands = [
'run',
'exec',
'ps',
'build',
'image',
'images',
'container',
'pull',
'push',
'apply',
'get',
'describe',
'delete',
'logs',
'create',
'edit',
'rollout',
'scale',
'port-forward',
'cluster-info',
'server',
'flavor',
'volume',
'network',
'subnet',
'router',
'keypair',
'floating',
'list',
'show',
'start',
'stop',
'restart',
'status',
'enable',
'disable',
'daemon-reload',
'clone',
'commit',
'checkout',
'branch',
'merge',
'rebase',
'fetch',
'install',
'add',
'remove',
'dev',
'deploy',
'attach',
'rmi',
'rm'
];
if (Array.isArray(kw.literal)) {
kw.literal.push(...customSubcommands);
} else {
kw.literal = customSubcommands;
}
// 3. 匹配选项开关
if (Array.isArray(bashLang.contains)) {
bashLang.contains.unshift({
className: 'attr',
begin: /(?<=\s|^)-+[a-zA-Z0-9_-]+/
});
}
}
// 创建独立 Marked 实例
const markedInstance = new Marked();
const headingIds = new Map<string, number>();
markedInstance.use({
renderer: {
code(token) {
const code = token.text;
let rawLang = (token.lang || '').trim().toLowerCase().split(' ')[0];
const bashAliases: Record<string, string> = {
k8s: 'bash',
kubectl: 'bash',
openstack: 'bash',
podman: 'bash',
systemd: 'bash',
systemctl: 'bash',
console: 'bash',
terminal: 'bash',
zsh: 'bash',
sh: 'bash'
};
if (bashAliases[rawLang]) {
rawLang = bashAliases[rawLang];
}
let highlighted: string;
let language = 'plaintext';
if (rawLang && hljs.getLanguage(rawLang)) {
language = rawLang;
highlighted = hljs.highlight(code, { language }).value;
} else {
try {
const auto = hljs.highlightAuto(code);
highlighted = auto.value || escapeHtml(code);
if (auto.language) language = auto.language;
} catch {
highlighted = escapeHtml(code);
}
}
const title = escapeHtml(rawLang || language || 'text');
const encodedCode = encodeURIComponent(code);
const lines = highlighted.split('\n');
const openSpans: string[] = [];
const wrappedLines = lines
.map((line) => {
const prefix = openSpans.join('');
const matches = line.matchAll(/<span [^>]+>|<\/span>/g);
for (const m of matches) {
if (m[0] === '</span>') {
openSpans.pop();
} else {
openSpans.push(m[0]);
}
}
const suffix = '</span>'.repeat(openSpans.length);
return `<span class="code-line">${prefix}${line}${suffix}</span>`;
})
.join('\n');
const chevronRight =
'<svg class="chevron-icon" viewBox="0 0 320 512" fill="currentColor" width="0.625em" height="1em"><path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"></path></svg>';
const listOlIcon =
'<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22.83a157.41 157.41 0 0 0-11 12.31l-5.61 7c-4 5.07-5.25 10.13-2.8 14.88l1.05 1.93c3 5.76 6.29 7.88 12.25 7.88h4.73c10.33 0 15.94 2.44 15.94 9.09 0 4.72-4.2 8.22-14.36 8.22a41.54 41.54 0 0 1-15.47-3.12c-6.49-3.88-11.74-3.5-15.6 3.12l-5.59 9.31c-3.72 6.13-3.19 11.72 2.63 15.94 7.71 4.69 20.38 9.44 37 9.44 34.16 0 48.5-22.75 48.5-44.12-.03-14.38-9.12-29.76-28.73-34.88zM496 224H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM16 160h64a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H64V40a8 8 0 0 0-8-8H32a8 8 0 0 0-7.14 4.42l-8 16A8 8 0 0 0 24 64h8v64H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8zm-3.91 160H80a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H41.32c3.29-10.29 48.34-18.68 48.34-56.44 0-29.06-25-39.56-44.47-39.56-21.36 0-33.8 10-40.46 18.75-4.37 5.59-3 10.84 2.8 15.37l8.58 6.88c5.61 4.56 11 2.47 16.12-2.44a13.44 13.44 0 0 1 9.46-3.84c3.33 0 9.28 1.56 9.28 8.75C51 248.19 0 257.31 0 304.59v4C0 316 5.08 320 12.09 320z"></path></svg>';
const barsIcon =
'<svg viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"><path d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path></svg>';
const copyIcon =
'<svg viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"><path d="M433.941 65.941l-51.882-51.882A48 48 0 00348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 00-14.059-33.941zM266 464H54a6 6 0 01-6-6V150a6 6 0 016-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 01-6 6zm128-96H182a6 6 0 01-6-6V54a6 6 0 016-6h106v88c0 13.255 10.745 24 24 24h88v200a6 6 0 01-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 011.757 4.243V112z"></path></svg>';
const checkIcon =
'<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>';
const ellipsisIcon =
'<svg viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"><path d="M328 256c0 39.8-32.2 72-72 72s-72-32.2-72-72 32.2-72 72-72 72 32.2 72 72zm104-72c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72zm-352 0c-39.8 0-72 32.2-72 72s32.2 72 72 72 72-32.2 72-72-32.2-72-72-72z"></path></svg>';
return `<div class="code-block highlight is-open show-line-numbers">
<div class="code-block-header">
<button class="code-block-title-btn" aria-hidden="true">
${chevronRight}
<span class="code-title">${title}</span>
</button>
<div class="code-header-actions">
<button class="action-icon line-number-button is-active" title="Toggle line numbers">${listOlIcon}</button>
<button class="action-icon wrap-code-button" title="Toggle code wrap">${barsIcon}</button>
<button class="action-icon copy-code-button" title="Copy code" data-code="${encodedCode}">
<span class="copy-icon">${copyIcon}</span>
<span class="check-icon">${checkIcon}</span>
</button>
<span class="ellipsis-icon" aria-hidden="true">${ellipsisIcon}</span>
</div>
</div>
<pre class="code-pre"><code class="hljs language-${language}">${wrappedLines}</code></pre>
</div>`;
},
image(token) {
const src = sanitizeUrl(token.href || '');
const alt = escapeHtml(token.text || '');
const title = escapeHtml(token.title || alt);
let figcaption = '';
if (title) {
figcaption = `<figcaption>${title}</figcaption>`;
}
return `<figure><a data-fancybox="gallery" data-caption="${title}" href="${escapeHtml(src)}"><img src="${escapeHtml(src)}" alt="${alt}" title="${title}"></a>${figcaption}</figure>`;
},
heading(token) {
const text = token.text;
const level = token.depth;
const id = createUniqueHeadingId(text, headingIds);
return `<h${level} id="${id}">${text}</h${level}>`;
}
}
});
function parseAdmonitions(markdown: string) {
const icons = {
note: '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"></path></svg>',
abstract:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M48 48a48 48 0 1 0 48 48 48.14 48.14 0 0 0-48-48zm0 160a48 48 0 1 0 48 48 48.14 48.14 0 0 0-48-48zm0 160a48 48 0 1 0 48 48 48.14 48.14 0 0 0-48-48zm448 16H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"></path></svg>',
info: '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"></path></svg>',
tip: '<svg viewBox="0 0 352 512" fill="currentColor" height="1em" width="1em"><path d="M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.64 18.99 42.74 58.8 52.42 92.16h160.03c9.68-33.35 35.78-73.16 52.42-92.16C335.55 260.85 352 220.37 352 176 352 78.8 273.2 0 176 0S0 78.8 0 176z"></path></svg>',
success:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg>',
question:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zM262.655 90c-54.497 0-89.255 22.957-116.549 63.758-3.536 5.286-2.353 12.415 2.715 16.258l34.699 26.31c5.205 3.947 12.621 3.008 16.665-2.122 17.864-22.658 30.113-35.797 57.303-35.797 20.429 0 45.698 13.148 45.698 32.958 0 14.976-12.363 22.667-32.534 33.976C247.128 238.528 216 254.941 216 296v4c0 6.627 5.373 12 12 12h56c6.627 0 12-5.373 12-12v-1.333c0-28.462 83.186-29.647 83.186-106.667 0-58.002-60.165-102-116.531-102zM256 338c-25.365 0-46 20.635-46 46 0 25.364 20.635 46 46 46s46-20.636 46-46c0-25.365-20.635-46-46-46z"></path></svg>',
warning:
'<svg viewBox="0 0 576 512" fill="currentColor" height="1em" width="1em"><path d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.466-32.004 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"></path></svg>',
failure:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"></path></svg>',
danger:
'<svg viewBox="0 0 448 512" fill="currentColor" height="1em" width="1em"><path d="M439.15 453.06L297.17 384l141.99-69.06c7.9-3.95 11.11-13.56 7.15-21.46L432 264.85c-3.95-7.9-13.56-11.11-21.47-7.16L224 348.41 37.47 257.69c-7.9-3.95-17.51-.75-21.47 7.16L1.69 293.48c-3.96 7.9-.76 17.51 7.15 21.46L150.83 384 8.85 453.06c-7.9 3.95-11.11 13.56-7.15 21.47l14.31 28.63c3.95 7.9 13.56 11.11 21.47 7.15L224 419.59l186.53 90.72c7.9 3.95 17.51.75 21.47-7.15l14.31-28.63c3.95-7.91.75-17.52-7.16-21.47zM150 237.28l-5.48 25.87c-2.67 12.62 5.42 24.85 16.45 24.85h126.08c11.03 0 19.12-12.23 16.45-24.85l-5.5-25.87c41.78-22.41 70-62.8 70-109.28C368 61.85 303.73 0 224 0S80 61.85 80 128c0 46.48 28.22 86.87 70 109.28zM280 112c17.65 0 32 14.35 32 32s-14.35 32-32 32-32-14.35-32-32 14.35-32 32-32zm-112 0c17.65 0 32 14.35 32 32s-14.35 32-32 32-32-14.35-32-32 14.35-32 32-32z"></path></svg>',
bug: '<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M511.98 256.03c0-34.92-27.46-64-62.24-64h-38.38c-8.91-44.57-30.82-84.34-60.77-116.14l27.13-27.13c12.5-12.5 12.5-32.76 0-45.25l-22.63-22.63c-12.5-12.5-32.76-12.5-45.25 0l-43.14 43.14A220.617 220.617 0 0 0 256 16.03c-3.69 0-7.38.15-11.05.35l-43.15-43.15c-12.5-12.5-32.76-12.5-45.25 0l-22.63 22.63c-12.5 12.5-12.5 32.76 0 45.25l27.13 27.13c-29.95 31.8-51.86 71.57-60.77 116.14H62.26c-34.78 0-62.24 29.08-62.24 64 0 34.39 26.89 62.48 61 63.92V336.1c0 51.52 19.34 98.4 51.05 133.72l-25.56 25.56c-12.5 12.5-12.5 32.76 0 45.25l22.63 22.63c12.5 12.5 32.76 12.5 45.25 0l42.66-42.66c30.2 16.92 64.71 27.42 101 27.42s70.8-10.5 101-27.42l42.66 42.66c12.5 12.5 32.76 12.5 45.25 0l22.63-22.63c12.5-12.5 12.5-32.76 0-45.25l-25.56-25.56C430.68 434.5 450 387.62 450 336.1v-23.79c34.11-1.44 61.98-29.53 61.98-63.92v-8.36zM152 280.03c0-13.25 10.75-24 24-24s24 10.75 24 24v56c0 13.25-10.75 24-24 24s-24-10.75-24-24v-56zm160 0c0-13.25 10.75-24 24-24s24 10.75 24 24v56c0 13.25-10.75 24-24 24s-24-10.75-24-24v-56z"></path></svg>',
example:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M61.77 401l17.5-20.15a19.92 19.92 0 0 0 5.07-14.19v-3.31C84.34 356 80.5 352 73 352H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h34.71L15.32 424.64A29.85 29.85 0 0 0 8 444v16c0 26.51 21.49 48 48 48h24a8 8 0 0 0 8-8v-16a8 8 0 0 0-8-8H56c-8.84 0-16-7.16-16-16v-4.52a6.01 6.01 0 0 1 1.76-4.24l19.24-19.24a48.28 48.28 0 0 0 7.58 3.06L64 448c0 35.35 28.65 64 64 64s64-28.65 64-64-28.65-64-64-64c-9.67 0-18.77 2.21-27.05 6.04L75.64 361.7A29.8 29.8 0 0 0 56 352h-7.66c10.37-4.14 17.66-14.24 17.66-26.34v-3.31c0-15.46-12.54-28-28-28H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h22c2.21 0 4 1.79 4 4v3.31c0 2.21-1.79 4-4 4H16a8 8 0 0 0-8 8v16a8 8 0 0 0 8 8h15.4c17.21 0 33.15 6.94 44.97 19.46l5.4-6.23a43.83 43.83 0 0 1 7.23-7.19h-15zM480 352H208a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h272a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM208 112h272a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16H208a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zM480 208H208a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h272a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM88 32H16C7.16 32 0 39.16 0 48v20c0 8.84 7.16 16 16 16h16v64c0 8.84 7.16 16 16 16h16c8.84 0 16-7.16 16-16V48c0-8.84-7.16-16-16-16h-48c0-8.84 7.16-16 16-16zm64 160c0-35.35-28.65-64-64-64s-64 28.65-64 64 28.65 64 64 64 64-28.65 64-64zm-32 0c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32zm0-24c0 17.67-14.33 32-32 32s-32-14.33-32-32 14.33-32 32-32 32 14.33 32 32z"></path></svg>',
quote:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M464 256h-80v-64c0-35.3 28.7-64 64-64h8c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24h-8c-88.4 0-160 71.6-160 160v240c0 26.5 21.5 48 48 48h128c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48zm-288 0H96v-64c0-35.3 28.7-64 64-64h8c13.3 0 24-10.7 24-24V56c0-13.3-10.7-24-24-24h-8C71.6 32 0 103.6 0 192v240c0 26.5 21.5 48 48 48h128c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48z"></path></svg>',
important:
'<svg viewBox="0 0 192 512" fill="currentColor" height="1em" width="1em"><path d="M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.33l7.12 256.33c.35 12.63 10.71 22.34 23.36 22.34h80.51c12.65 0 23.01-9.71 23.36-22.34l7.12-256.33c.37-13.37-10.36-24.33-23.75-24.33H49.02c-13.4 0-24.12 10.96-23.76 24.33z"></path></svg>',
caution:
'<svg viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"><path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"></path></svg>'
};
function renderAdmonition(type: string, rawTitle: string, content: string) {
const typeLower = type.toLowerCase();
const displayTitle = escapeHtml(resolveAdmonitionTitle(type, rawTitle));
const icon = icons[typeLower as keyof typeof icons] || icons.note;
return `\n<div class="details admonition ${typeLower} open">\n<div class="details-summary admonition-title"><span class="icon">${icon}</span><span>${displayTitle}</span></div>\n<div class="details-content">\n<div class="admonition-content">\n\n${content}\n\n</div>\n</div>\n</div>\n`;
}
let html = replaceGithubAdmonitions(markdown, renderAdmonition);
html = replaceHugoAdmonitions(html, renderAdmonition);
return html;
}
export function renderMarkdown(raw: string) {
headingIds.clear();
const preprocessedContent = parseAdmonitions(raw);
return sanitizeMarkdownHtml(markedInstance.parse(preprocessedContent) as string);
}