diff --git a/src/components/Article.tsx b/src/components/Article.tsx index 10fd247..cb5ef1c 100644 --- a/src/components/Article.tsx +++ b/src/components/Article.tsx @@ -7,20 +7,17 @@ import { onCleanup, Show, createResource, - createMemo, createSignal, } from "solid-js"; import { parseMarkdown } from "../markdown"; import { extractSection } from "../data-loader"; import mermaid from "mermaid"; import { getIndexedData } from "../data-loader/file-index"; -import { resolvePath } from "./utils/path"; import { useNavigateWithParams } from "./useNavigateWithParams"; export interface ArticleProps { src: string; section?: string; // 指定要显示的标题(不含 #) - iconPath?: string; // 图标路径前缀,默认为 "./assets",空字符串表示禁用 onLoaded?: () => void; onError?: (error: Error) => void; class?: string; // 额外的 class 用于样式控制 @@ -83,12 +80,6 @@ export const Article: Component = (props) => { ); const [contentDom, setContentDom] = createSignal(); - // 解析 iconPath,默认为 "./assets",空字符串表示禁用 - const iconPrefix = createMemo(() => { - if (props.iconPath === "") return undefined; // 空字符串禁用图标前缀 - return resolvePath(props.src, props.iconPath ?? "./assets"); - }); - createEffect(() => { const data = content(); if (data) { @@ -144,7 +135,7 @@ export const Article: Component = (props) => {
{props.children} diff --git a/src/components/md-deck/CardLayer.tsx b/src/components/md-deck/CardLayer.tsx index 6c2893b..8730fbc 100644 --- a/src/components/md-deck/CardLayer.tsx +++ b/src/components/md-deck/CardLayer.tsx @@ -4,7 +4,6 @@ import { getLayerStyle } from "./hooks/dimensions"; import type { CardData, CardSide, LayerConfig } from "./types"; import { DeckStore } from "./hooks/deckStore"; import { processVariables } from "../utils/csv-loader"; -import { resolvePath } from "../utils/path"; import type { LayerInteractionHandlers } from "./hooks/useLayerInteraction"; export interface CardLayerProps { @@ -26,13 +25,8 @@ export function CardLayer(props: CardLayerProps) { const draggingState = () => props.store.state.draggingState; function renderLayerContent(content: string) { - const iconPath = resolvePath( - props.store.state.cards.sourcePath, - props.cardData.iconPath ?? "./assets", - ); return parseMarkdown( processVariables(content, props.cardData, props.store.state.cards), - iconPath, ) as string; } diff --git a/src/components/md-embed.tsx b/src/components/md-embed.tsx index b5fdba4..1b6db7c 100644 --- a/src/components/md-embed.tsx +++ b/src/components/md-embed.tsx @@ -1,5 +1,5 @@ import { customElement, noShadowDOM } from "solid-element"; -import { createResource, Show, createMemo, createEffect } from "solid-js"; +import { createResource, Show, createEffect } from "solid-js"; import { getIndexedData } from "../data-loader/file-index"; import { extractSection } from "../data-loader"; import { parseMarkdown } from "../markdown"; @@ -27,9 +27,6 @@ customElement("md-embed", { headingBase: 0 }, (props, { element }) => { const articlePath = articleEl?.getAttribute("data-src") || ""; const resolvedPath = resolvePath(articlePath, filePath); - // 计算嵌入文件的 iconPrefix,用 createMemo 包装避免每个 embed 都重新计算 - const iconPrefix = createMemo(() => resolvePath(resolvedPath, "./assets")); - const [content] = createResource( () => ({ path: resolvedPath, section }), async ({ path, section }) => { @@ -56,10 +53,7 @@ customElement("md-embed", { headingBase: 0 }, (props, { element }) => { {/* biome-ignore lint/security/noDangerouslySetInnerHtml: markdown render */} -
+
); diff --git a/src/markdown/icon.ts b/src/markdown/icon.ts new file mode 100644 index 0000000..37b1cb0 --- /dev/null +++ b/src/markdown/icon.ts @@ -0,0 +1,62 @@ +/** + * marked-directive 扩展:支持 :[icon] 和 :[icon.ext] 行内图标语法 + * + * 生成的 CSS 包含从当前目录向上查找的 fallback 链: + * url('./assets/icon.png'), + * url('../assets/icon.png'), + * url('../../assets/icon.png'), + * ... + * + * 支持的扩展名:.svg, .png, .gif, .jpg, .jpeg, .webp(默认 .png) + */ + +const SUPPORTED_EXTENSIONS = ["svg", "png", "gif", "jpg", "jpeg", "webp"]; + +/** 生成多层 fallback 路径,向上查找最多 10 级 */ +function buildIconSrc(iconName: string, extension: string): string { + const levels = 10; + const urls: string[] = []; + for (let i = 0; i < levels; i++) { + const prefix = i === 0 ? "./" : "../".repeat(i); + urls.push(`url('${prefix}assets/${iconName}.${extension}')`); + } + return urls.join(", "); +} + +export const iconDirective = { + level: "inline" as const, + marker: ":", + renderer(token: any) { + // meta.name 非空表示这是一个命名指令(如 :div),不是图标 + if (token.meta.name) return false; + + const iconText = token.text || ""; + + let iconName = iconText; + let extension = "png"; + + const lastDotIndex = iconName.lastIndexOf("."); + if (lastDotIndex > 0) { + const potentialExt = iconName.slice(lastDotIndex + 1).toLowerCase(); + if (SUPPORTED_EXTENSIONS.includes(potentialExt)) { + extension = potentialExt; + iconName = iconName.slice(0, lastDotIndex); + } + } + + const label = token.attrs?.label as string | undefined; + const inner = label + ? `${label}${label}` + : ""; + + const style = `style="--icon-src: ${buildIconSrc(iconName, extension)}"`; + const iconHtml = `${inner}`; + + const repeat = parseInt(`${token.attrs?.repeat || ""}`); + const join = token.attrs?.join || ""; + const separator = join ? `<${join}>` : ""; + if (isNaN(repeat) || repeat < 1) return iconHtml; + + return Array(repeat).fill(iconHtml).join(separator); + }, +}; diff --git a/src/markdown/index.ts b/src/markdown/index.ts index 01dc613..22a968c 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -6,14 +6,7 @@ import markedTable from "./table"; import { gfmHeadingId } from "marked-gfm-heading-id"; import markedColumns from "./columns"; import markedCodeBlockYamlTag from "./code-block-yaml-tag"; - -let globalIconPrefix: string | undefined = undefined; -function overrideIconPrefix(path?: string) { - globalIconPrefix = path; - return () => { - globalIconPrefix = undefined; - }; -} +import { iconDirective } from "./icon"; // 使用 marked-directive 来支持指令语法 const marked = new Marked() @@ -33,73 +26,15 @@ const marked = new Marked() marker: ":::::", level: "container", }, - { - level: "inline", - marker: ":", - // :[icon] 或 :[icon.ext] 语法 - // 支持的扩展名: .svg, .png, .gif, .jpg, .jpeg, .webp - // 如果不指定扩展名,默认为 .png - renderer(token) { - if (!token.meta.name) { - const iconText = token.text || ""; - - // 已知支持的图片扩展名 - const supportedExtensions = [ - "svg", - "png", - "gif", - "jpg", - "jpeg", - "webp", - ]; - - // 检查是否包含扩展名(查找最后一个点) - let iconName = iconText; - let extension = "png"; // 默认扩展名 - - const lastDotIndex = iconName.lastIndexOf("."); - if (lastDotIndex > 0) { - const potentialExt = iconName - .slice(lastDotIndex + 1) - .toLowerCase(); - if (supportedExtensions.includes(potentialExt)) { - extension = potentialExt; - iconName = iconName.slice(0, lastDotIndex); - } - } - - const label = token.attrs?.label as string | undefined; - const inner = label - ? `${label}${label}` - : ""; - const style = globalIconPrefix - ? `style="--icon-src: url('${globalIconPrefix}/${iconName}.${extension}')"` - : ""; - const iconHtml = `${inner}`; - - const repeat = parseInt(`${token.attrs?.repeat || ""}`); - const join = token.attrs?.join || ""; - const separator = join ? `<${join}>` : ""; - if (isNaN(repeat) || repeat < 1) return iconHtml; - - return Array(repeat).fill(iconHtml).join(separator); - } - return false; - }, - }, + iconDirective, ]), { extensions: [...markedColumns()], }, ); -export function parseMarkdown(content: string, iconPrefix?: string): string { - const restore = overrideIconPrefix(iconPrefix); - try { - return marked.parse(content.trimStart()) as string; - } finally { - restore(); - } +export function parseMarkdown(content: string): string { + return marked.parse(content.trimStart()) as string; } export { marked };