feat: add md-emfont component and format code

- Add `md-emfont` custom element for dynamic font loading
- Register `md-emfont` in component index
- Reformat `Article.tsx` and `index.ts` using double quotes and
  consistent spacing
- Fix indentation in `index.html`
This commit is contained in:
hypercross 2026-05-11 11:25:57 +08:00
parent d4de95b465
commit 4038d67ea0
4 changed files with 133 additions and 56 deletions

View File

@ -1,22 +1,32 @@
import { Component, createEffect, onCleanup, Show, createResource, createMemo } from 'solid-js'; import {
import { parseMarkdown } from '../markdown'; Component,
import { extractSection } from '../data-loader'; createEffect,
import mermaid from 'mermaid'; onCleanup,
import {getIndexedData} from "../data-loader/file-index"; Show,
import { resolvePath } from './utils/path'; createResource,
import { useLocation } from '@solidjs/router'; createMemo,
} 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 { useLocation } from "@solidjs/router";
export interface ArticleProps { export interface ArticleProps {
src: string; src: string;
section?: string; // 指定要显示的标题(不含 # section?: string; // 指定要显示的标题(不含 #
iconPath?: string; // 图标路径前缀,默认为 "./assets",空字符串表示禁用 iconPath?: string; // 图标路径前缀,默认为 "./assets",空字符串表示禁用
onLoaded?: () => void; onLoaded?: () => void;
onError?: (error: Error) => void; onError?: (error: Error) => void;
class?: string; // 额外的 class 用于样式控制 class?: string; // 额外的 class 用于样式控制
scrollToHash?: boolean; // 是否自动滚动到 hash scrollToHash?: boolean; // 是否自动滚动到 hash
} }
async function fetchArticleContent(params: { src: string; section?: string }): Promise<string> { async function fetchArticleContent(params: {
src: string;
section?: string;
}): Promise<string> {
const text = await getIndexedData(params.src); const text = await getIndexedData(params.src);
// 如果指定了 section提取对应内容 // 如果指定了 section提取对应内容
return params.section ? extractSection(text, params.section) : text; return params.section ? extractSection(text, params.section) : text;
@ -28,17 +38,17 @@ async function fetchArticleContent(params: { src: string; section?: string }): P
function scrollToHash(hash: string) { function scrollToHash(hash: string) {
if (!hash) return; if (!hash) return;
// 移除 # 前缀 // 移除 # 前缀
const id = hash.startsWith('#') ? hash.slice(1) : hash; const id = hash.startsWith("#") ? hash.slice(1) : hash;
if (!id) return; if (!id) return;
// 使用 decodeURIComponent 解码 ID处理中文等特殊字符 // 使用 decodeURIComponent 解码 ID处理中文等特殊字符
const decodedId = decodeURIComponent(id); const decodedId = decodeURIComponent(id);
// 尝试查找元素 // 尝试查找元素
const element = document.getElementById(decodedId); const element = document.getElementById(decodedId);
if (element) { if (element) {
// 使用 scrollIntoView 滚动到元素 // 使用 scrollIntoView 滚动到元素
element.scrollIntoView({ behavior: 'instant', block: 'start' }); element.scrollIntoView({ behavior: "instant", block: "start" });
return true; return true;
} }
return false; return false;
@ -52,12 +62,12 @@ export const Article: Component<ArticleProps> = (props) => {
const location = useLocation(); const location = useLocation();
const [content, { refetch }] = createResource( const [content, { refetch }] = createResource(
() => ({ src: props.src, section: props.section }), () => ({ src: props.src, section: props.section }),
fetchArticleContent fetchArticleContent,
); );
// 解析 iconPath默认为 "./assets",空字符串表示禁用 // 解析 iconPath默认为 "./assets",空字符串表示禁用
const iconPrefix = createMemo(() => { const iconPrefix = createMemo(() => {
if (props.iconPath === '') return undefined; // 空字符串禁用图标前缀 if (props.iconPath === "") return undefined; // 空字符串禁用图标前缀
return resolvePath(props.src, props.iconPath ?? "./assets"); return resolvePath(props.src, props.iconPath ?? "./assets");
}); });
@ -67,7 +77,7 @@ export const Article: Component<ArticleProps> = (props) => {
props.onLoaded?.(); props.onLoaded?.();
// 内容加载完成后,渲染 mermaid 图表 // 内容加载完成后,渲染 mermaid 图表
void mermaid.run(); void mermaid.run();
// 内容渲染后检查 hash 并滚动 // 内容渲染后检查 hash 并滚动
scrollToHash(location.hash); scrollToHash(location.hash);
} }
@ -78,7 +88,7 @@ export const Article: Component<ArticleProps> = (props) => {
}); });
return ( return (
<article class={`prose ${props.class || ''}`} data-src={props.src}> <article class={`prose ${props.class || ""}`} data-src={props.src}>
<Show when={content.loading}> <Show when={content.loading}>
<div class="text-gray-500">...</div> <div class="text-gray-500">...</div>
</Show> </Show>
@ -86,7 +96,10 @@ export const Article: Component<ArticleProps> = (props) => {
<div class="text-red-500">{content.error?.message}</div> <div class="text-red-500">{content.error?.message}</div>
</Show> </Show>
<Show when={!content.loading && !content.error && content()}> <Show when={!content.loading && !content.error && content()}>
<div class="relative" innerHTML={parseMarkdown(content()!, iconPrefix())} /> <div
class="relative"
innerHTML={parseMarkdown(content()!, iconPrefix())}
/>
</Show> </Show>
</article> </article>
); );

View File

@ -1,28 +1,30 @@
// 导入自定义元素以注册 // 导入自定义元素以注册
import './md-dice'; import "./md-dice";
import './md-table'; import "./md-table";
import './md-link'; import "./md-link";
import './md-pins'; import "./md-pins";
import './md-bg'; import "./md-bg";
import './md-deck'; import "./md-emfont";
import './md-commander/index'; import "./md-deck";
import './md-yarn-spinner'; import "./md-commander/index";
import './md-token'; import "./md-yarn-spinner";
import './md-token-viewer'; import "./md-token";
import "./md-token-viewer";
// 导出组件 // 导出组件
export { Article } from './Article'; export { Article } from "./Article";
export type { ArticleProps } from './Article'; export type { ArticleProps } from "./Article";
export { MobileSidebar, DesktopSidebar } from './Sidebar'; export { MobileSidebar, DesktopSidebar } from "./Sidebar";
export type { SidebarProps } from './Sidebar'; export type { SidebarProps } from "./Sidebar";
export { FileTreeNode, HeadingNode } from './FileTree'; export { FileTreeNode, HeadingNode } from "./FileTree";
// 导出数据类型 // 导出数据类型
export type { DiceProps } from './md-dice'; 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 { YarnSpinnerProps } from './md-yarn-spinner'; export type { EmfontProps } from "./md-emfont";
export type { TokenProps } from './md-token'; export type { YarnSpinnerProps } from "./md-yarn-spinner";
export type { TokenProps } from "./md-token";
// 导出 md-commander 相关 // 导出 md-commander 相关
export type { export type {
@ -39,10 +41,13 @@ export type {
TrackerAttributeType, TrackerAttributeType,
TrackerCommand, TrackerCommand,
TrackerViewMode, TrackerViewMode,
} from './md-commander/types'; } from "./md-commander/types";
export { TabBar } from './md-commander/TabBar'; export { TabBar } from "./md-commander/TabBar";
export { TrackerView } from './md-commander/TrackerView'; export { TrackerView } from "./md-commander/TrackerView";
export { CommanderEntries } from './md-commander/CommanderEntries'; export { CommanderEntries } from "./md-commander/CommanderEntries";
export { CommanderInput } from './md-commander/CommanderInput'; export { CommanderInput } from "./md-commander/CommanderInput";
export { useCommander } from './md-commander/hooks'; export { useCommander } from "./md-commander/hooks";
export { initializeCommands, getCommands } from './md-commander/stores/commandsStore'; export {
initializeCommands,
getCommands,
} from "./md-commander/stores/commandsStore";

View File

@ -0,0 +1,59 @@
import { customElement, noShadowDOM } from "solid-element";
import { createEffect, onCleanup } from "solid-js";
export interface EmfontProps {
weight?: string;
words?: string;
}
customElement(
"md-emfont",
{ weight: "400", words: "" },
(props, { element }) => {
noShadowDOM();
// 从 element 的 textContent 获取字体名称
const font = element?.textContent?.trim() || "";
// 隐藏原始文本内容
if (element) {
element.textContent = "";
}
// 从父节点 article 获取当前 article 元素
const articleEl = element?.closest("article") as HTMLElement;
// 构建 emfont CSS URL
const weight = props.weight || "400";
// 从 article 文本内容中提取唯一字符作为 words用于字体子集化
const words =
props.words || [...new Set(articleEl?.textContent || "")].join("");
const linkHref = `https://font.emtech.cc/css/${font}?weight=${weight}&words=${encodeURIComponent(words)}`;
// 创建 <link> 元素并注入 <head>
const linkEl = document.createElement("link");
linkEl.rel = "stylesheet";
linkEl.href = linkHref;
linkEl.dataset.mdEmfont = font;
document.head.appendChild(linkEl);
createEffect(() => {
if (!articleEl) return;
articleEl.dataset.mdEmfont = font;
articleEl.style.fontFamily = font;
});
onCleanup(() => {
// 清理 article 上的样式
if (articleEl?.dataset?.mdEmfont === font) {
articleEl.style.fontFamily = "";
}
// 移除注入的 <link>
if (linkEl.parentNode) {
linkEl.parentNode.removeChild(linkEl);
}
});
return null;
},
);

View File

@ -1,11 +1,11 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TTRPG Tools</title> <title>TTRPG Tools</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>