diff --git a/src/lib/markdown/icons.ts b/src/lib/markdown/icons.ts new file mode 100644 index 0000000..481c3e6 --- /dev/null +++ b/src/lib/markdown/icons.ts @@ -0,0 +1,164 @@ +/** + * Markdown 正文里的图标需要以 HTML 字符串输出(通过 {@html} 渲染), + * 无法直接挂载 lucide-svelte 的 Svelte 组件,因此这里把所需的 lucide 图标 + * 序列化成等价的 SVG 标记,保证全站图标风格统一。 + * + * 图标数据取自 lucide(https://lucide.dev),仅保留用到的子集。 + */ +type IconNode = [tag: string, attrs: Record][]; + +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; + +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}>`; +} + +/** 渲染一个 lucide 图标的 SVG 字符串,className 用于挂载主题样式。 */ +export function markdownIcon(name: MarkdownIconName, className = ''): string { + const children = ICON_NODES[name].map(renderNode).join(''); + const classAttribute = className ? ` class="${className}"` : ''; + return `${children}`; +} diff --git a/src/lib/markdown/rendering.ts b/src/lib/markdown/rendering.ts index d9f086d..647ac60 100644 --- a/src/lib/markdown/rendering.ts +++ b/src/lib/markdown/rendering.ts @@ -64,6 +64,9 @@ export function sanitizeMarkdownHtml(html: string): string { 'time', 'svg', 'path', + 'circle', + 'line', + 'rect', 'button', 'span', 'pre', @@ -75,8 +78,23 @@ export function sanitizeMarkdownHtml(html: string): string { a: ['href', 'target', 'rel', 'data-fancybox', 'data-caption'], img: ['src', 'alt', 'title', 'loading', 'width', 'height'], 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'], + circle: ['cx', 'cy', 'r'], + line: ['x1', 'y1', 'x2', 'y2'], + rect: ['x', 'y', 'width', 'height', 'rx', 'ry'], time: ['datetime'] }, allowedSchemes: ['http', 'https', 'mailto', 'tel'],