feat(viewer): Add md-embed component

This commit is contained in:
hyper 2026-06-25 20:43:40 +08:00
parent f19c7ff77c
commit f734da6acd
4 changed files with 71 additions and 0 deletions

View File

@ -53,3 +53,5 @@ bler
> [!CAUTION] > [!CAUTION]
> Negative potential consequences of an action. > Negative potential consequences of an action.
:md-embed[./style-test.md]

1
content/style-test.md Normal file
View File

@ -0,0 +1 @@
:md-font[Pacifico]{source=google} hello?

View File

@ -5,6 +5,7 @@ import "./md-link";
import "./md-pins"; import "./md-pins";
import "./md-bg"; import "./md-bg";
import "./md-font"; import "./md-font";
import "./md-embed";
import "./md-deck"; import "./md-deck";
import "./md-commander/index"; import "./md-commander/index";
import "./md-yarn-spinner"; import "./md-yarn-spinner";
@ -23,6 +24,7 @@ export type { DiceProps } from "./md-dice";
export type { TableProps } from "./md-table"; export type { TableProps } from "./md-table";
export type { BgProps } from "./md-bg"; export type { BgProps } from "./md-bg";
export type { FontProps } from "./md-font"; export type { FontProps } from "./md-font";
export type { EmbedProps } from "./md-embed";
export type { YarnSpinnerProps } from "./md-yarn-spinner"; export type { YarnSpinnerProps } from "./md-yarn-spinner";
export type { TokenProps } from "./md-token"; export type { TokenProps } from "./md-token";

View File

@ -0,0 +1,66 @@
import { customElement, noShadowDOM } from "solid-element";
import { createResource, Show, createMemo, createEffect } from "solid-js";
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);
// 计算嵌入文件的 iconPrefix用 createMemo 包装避免每个 embed 都重新计算
const iconPrefix = createMemo(() => resolvePath(resolvedPath, "./assets"));
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()!, iconPrefix())}
/>
</Show>
</div>
);
});