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:
parent
d4de95b465
commit
4038d67ea0
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue