feat: implement icon directive with relative path fallback

Refactor icon rendering to use a custom `marked-directive` instead of
a global icon prefix. This allows icons to use a CSS-based fallback
chain (searching up to 10 directory levels for an `assets/` folder),
making icon paths more robust and removing the need to pass path
prefixes through the component tree.
This commit is contained in:
2026-07-08 16:24:09 +08:00
parent 749b1a6a4b
commit 2dba19e1f8
5 changed files with 69 additions and 93 deletions
+1 -10
View File
@@ -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<ArticleProps & ParentProps> = (props) => {
);
const [contentDom, setContentDom] = createSignal<HTMLDivElement>();
// 解析 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<ArticleProps & ParentProps> = (props) => {
<div
class="relative"
ref={setContentDom}
innerHTML={parseMarkdown(content()!, iconPrefix())}
innerHTML={parseMarkdown(content()!)}
/>
{props.children}
</ArticleDomCtx.Provider>
-6
View File
@@ -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;
}
+2 -8
View File
@@ -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 }) => {
</Show>
<Show when={!content.loading && !content.error && content()}>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: markdown render */}
<div
class="prose"
innerHTML={parseMarkdown(content()!, iconPrefix())}
/>
<div class="prose" innerHTML={parseMarkdown(content()!)} />
</Show>
</div>
);