feat: 添加 Markdown 图标序列化与渲染支持
This commit is contained in:
1 parent
49f32aaa0a
commit
daf1e5655c
2 files changed
+183
-1
No files matched your search
@@ -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>`;
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
Reference in new issue
Block a user