ttrpg-tools/src/components/md-embed.tsx

64 lines
2.1 KiB
TypeScript
Raw Normal View History

2026-06-25 20:43:40 +08:00
import { customElement, noShadowDOM } from "solid-element";
import { createResource, Show, createEffect } from "solid-js";
2026-06-25 20:43:40 +08:00
import { getIndexedData } from "../data-loader/file-index";
import { extractSection } from "../data-loader";
import { parseMarkdown } from "../markdown";
import { resolvePath } from "./utils/path";
import mermaid from "mermaid";
export interface EmbedProps {
/** 是否在嵌入内容的每一级标题前提升 heading 层级 */
headingBase?: number;
}
customElement("md-embed", { headingBase: 0 }, (props, { element }) => {
noShadowDOM();
const rawPath = element?.textContent?.trim() || "";
if (element) element.textContent = "";
// 解析 section支持 path#section 语法)
const hashIndex = rawPath.indexOf("#");
const filePath = hashIndex >= 0 ? rawPath.slice(0, hashIndex) : rawPath;
const section = hashIndex >= 0 ? rawPath.slice(hashIndex + 1) : undefined;
// 从父节点 article 获取路径上下文
const articleEl = element?.closest("article[data-src]") as HTMLElement;
const articlePath = articleEl?.getAttribute("data-src") || "";
const resolvedPath = resolvePath(articlePath, filePath);
const [content] = createResource(
() => ({ path: resolvedPath, section }),
async ({ path, section }) => {
const text = await getIndexedData(path);
return section ? extractSection(text, section) : text;
},
);
createEffect(() => {
if (content() && !content.loading) {
void mermaid.run();
}
});
return (
<div class="md-embed not-prose">
<Show when={content.loading}>
<div class="text-gray-400 italic">...</div>
</Show>
<Show when={content.error}>
<div class="text-red-500">
{content.error?.message || "未知错误"}
</div>
</Show>
<Show when={!content.loading && !content.error && content()}>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: markdown render */}
<div
class="prose"
innerHTML={parseMarkdown(content()!, resolvedPath)}
/>
2026-06-25 20:43:40 +08:00
</Show>
</div>
);
});