feat: 添加 Markdown 图标序列化与渲染支持

This commit is contained in:
seahi committed 2026-09-16 11:53:13 +08:00
1 parent 49f32aaa0a
commit daf1e5655c
2 files changed
+183 -1

No files matched your search

+164
View File
@@ -0,0 +1,164 @@
/**
* Markdown 正文里的图标需要以 HTML 字符串输出(通过 {@html} 渲染),
* 无法直接挂载 lucide-svelte 的 Svelte 组件,因此这里把所需的 lucide 图标
* 序列化成等价的 SVG 标记,保证全站图标风格统一。
*
* 图标数据取自 lucide(https://lucide.dev),仅保留用到的子集。
*/
type IconNode = [tag: string, attrs: Record<string, string>][];
const ICON_NODES = {
pencil: [
[
'path',
{
d: 'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z'
}
],
['path', { d: 'm15 5 4 4' }]
],
'text-align-start': [
['path', { d: 'M21 5H3' }],
['path', { d: 'M15 12H3' }],
['path', { d: 'M17 19H3' }]
],
info: [
['circle', { cx: '12', cy: '12', r: '10' }],
['path', { d: 'M12 16v-4' }],
['path', { d: 'M12 8h.01' }]
],
lightbulb: [
[
'path',
{
d: 'M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5'
}
],
['path', { d: 'M9 18h6' }],
['path', { d: 'M10 22h4' }]
],
'circle-check': [
['circle', { cx: '12', cy: '12', r: '10' }],
['path', { d: 'm9 12 2 2 4-4' }]
],
'circle-question-mark': [
['circle', { cx: '12', cy: '12', r: '10' }],
['path', { d: 'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3' }],
['path', { d: 'M12 17h.01' }]
],
'triangle-alert': [
['path', { d: 'm21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3' }],
['path', { d: 'M12 9v4' }],
['path', { d: 'M12 17h.01' }]
],
'circle-x': [
['circle', { cx: '12', cy: '12', r: '10' }],
['path', { d: 'm15 9-6 6' }],
['path', { d: 'm9 9 6 6' }]
],
zap: [
[
'path',
{
d: 'M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z'
}
]
],
bug: [
['path', { d: 'M12 20v-9' }],
['path', { d: 'M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z' }],
['path', { d: 'M14.12 3.88 16 2' }],
['path', { d: 'M21 21a4 4 0 0 0-3.81-4' }],
['path', { d: 'M21 5a4 4 0 0 1-3.55 3.97' }],
['path', { d: 'M22 13h-4' }],
['path', { d: 'M3 21a4 4 0 0 1 3.81-4' }],
['path', { d: 'M3 5a4 4 0 0 0 3.55 3.97' }],
['path', { d: 'M6 13H2' }],
['path', { d: 'm8 2 1.88 1.88' }],
['path', { d: 'M9 7.13V6a3 3 0 1 1 6 0v1.13' }]
],
'flask-conical': [
[
'path',
{
d: 'M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2'
}
],
['path', { d: 'M6.453 15h11.094' }],
['path', { d: 'M8.5 2h7' }]
],
quote: [
[
'path',
{
d: 'M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z'
}
],
[
'path',
{
d: 'M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z'
}
]
],
'circle-alert': [
['circle', { cx: '12', cy: '12', r: '10' }],
['line', { x1: '12', x2: '12', y1: '8', y2: '12' }],
['line', { x1: '12', x2: '12.01', y1: '16', y2: '16' }]
],
'octagon-alert': [
['path', { d: 'M12 16h.01' }],
['path', { d: 'M12 8v4' }],
[
'path',
{
d: 'M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z'
}
]
],
'chevron-right': [['path', { d: 'm9 18 6-6-6-6' }]],
'list-ordered': [
['path', { d: 'M11 5h10' }],
['path', { d: 'M11 12h10' }],
['path', { d: 'M11 19h10' }],
['path', { d: 'M4 4h1v5' }],
['path', { d: 'M4 9h2' }],
['path', { d: 'M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02' }]
],
'text-wrap': [
['path', { d: 'm16 16-3 3 3 3' }],
['path', { d: 'M3 12h14.5a1 1 0 0 1 0 7H13' }],
['path', { d: 'M3 19h6' }],
['path', { d: 'M3 5h18' }]
],
copy: [
['rect', { width: '14', height: '14', x: '8', y: '8', rx: '2', ry: '2' }],
['path', { d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' }]
],
check: [['path', { d: 'M20 6 9 17l-5-5' }]],
ellipsis: [
['circle', { cx: '12', cy: '12', r: '1' }],
['circle', { cx: '19', cy: '12', r: '1' }],
['circle', { cx: '5', cy: '12', r: '1' }]
]
} satisfies Record<string, IconNode>;
export type MarkdownIconName = keyof typeof ICON_NODES;
const SVG_ATTRIBUTES =
'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
'stroke-linecap="round" stroke-linejoin="round" width="1em" height="1em"';
function renderNode([tag, attrs]: IconNode[number]): string {
const attributes = Object.entries(attrs)
.map(([key, value]) => ` ${key}="${value}"`)
.join('');
return `<${tag}${attributes}></${tag}>`;
}
/** 渲染一个 lucide 图标的 SVG 字符串,className 用于挂载主题样式。 */
export function markdownIcon(name: MarkdownIconName, className = ''): string {
const children = ICON_NODES[name].map(renderNode).join('');
const classAttribute = className ? ` class="${className}"` : '';
return `<svg${classAttribute} ${SVG_ATTRIBUTES}>${children}</svg>`;
}
+19 -1
View File
@@ -64,6 +64,9 @@ export function sanitizeMarkdownHtml(html: string): string {
'time', 'time',
'svg', 'svg',
'path', 'path',
'circle',
'line',
'rect',
'button', 'button',
'span', 'span',
'pre', 'pre',
@@ -75,8 +78,23 @@ export function sanitizeMarkdownHtml(html: string): string {
a: ['href', 'target', 'rel', 'data-fancybox', 'data-caption'], a: ['href', 'target', 'rel', 'data-fancybox', 'data-caption'],
img: ['src', 'alt', 'title', 'loading', 'width', 'height'], img: ['src', 'alt', 'title', 'loading', 'width', 'height'],
button: ['type', 'title', 'data-code', 'aria-hidden'], button: ['type', 'title', 'data-code', 'aria-hidden'],
svg: ['viewBox', 'viewbox', 'fill', 'width', 'height', 'xmlns', 'focusable'], svg: [
'viewBox',
'viewbox',
'fill',
'width',
'height',
'xmlns',
'focusable',
'stroke',
'stroke-width',
'stroke-linecap',
'stroke-linejoin'
],
path: ['d', 'fill'], path: ['d', 'fill'],
circle: ['cx', 'cy', 'r'],
line: ['x1', 'y1', 'x2', 'y2'],
rect: ['x', 'y', 'width', 'height', 'rx', 'ry'],
time: ['datetime'] time: ['datetime']
}, },
allowedSchemes: ['http', 'https', 'mailto', 'tel'], allowedSchemes: ['http', 'https', 'mailto', 'tel'],