Compare commits
2 Commits
771fe6a112
...
f19c7ff77c
| Author | SHA1 | Date |
|---|---|---|
|
|
f19c7ff77c | |
|
|
1a4ae417fe |
|
|
@ -4,7 +4,7 @@ import "./md-table";
|
||||||
import "./md-link";
|
import "./md-link";
|
||||||
import "./md-pins";
|
import "./md-pins";
|
||||||
import "./md-bg";
|
import "./md-bg";
|
||||||
import "./md-emfont";
|
import "./md-font";
|
||||||
import "./md-deck";
|
import "./md-deck";
|
||||||
import "./md-commander/index";
|
import "./md-commander/index";
|
||||||
import "./md-yarn-spinner";
|
import "./md-yarn-spinner";
|
||||||
|
|
@ -22,7 +22,7 @@ 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 { EmfontProps } from "./md-emfont";
|
export type { FontProps } from "./md-font";
|
||||||
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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,39 @@
|
||||||
import { customElement, noShadowDOM } from "solid-element";
|
import { customElement, noShadowDOM } from "solid-element";
|
||||||
import {createEffect, onCleanup} from "solid-js";
|
import { onCleanup } from "solid-js";
|
||||||
import { resolvePath } from "./utils/path";
|
import { resolvePath } from "./utils/path";
|
||||||
|
import { registerStyle } from "./utils/article-style-manager";
|
||||||
|
|
||||||
export interface BgProps {
|
export interface BgProps {
|
||||||
fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
|
fit?: "cover" | "contain" | "fill" | "none" | "scale-down";
|
||||||
}
|
}
|
||||||
|
|
||||||
customElement("md-bg", { fit: "cover" }, (props, { element }) => {
|
customElement("md-bg", { fit: "cover" }, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
// 从 element 的 textContent 获取图片路径
|
const rawSrc = element?.textContent?.trim() || "";
|
||||||
const rawSrc = element?.textContent?.trim() || '';
|
|
||||||
|
|
||||||
// 隐藏原始文本内容
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.textContent = "";
|
element.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
|
const articleEl = element?.closest("article") as HTMLElement;
|
||||||
const articleEl = element?.closest('article') as HTMLElement;
|
const articlePath =
|
||||||
const articlePath = element?.closest('article[data-src]')?.getAttribute('data-src') || '';
|
element?.closest("article[data-src]")?.getAttribute("data-src") || "";
|
||||||
|
|
||||||
// 解析相对路径
|
|
||||||
const resolvedSrc = resolvePath(articlePath, rawSrc);
|
const resolvedSrc = resolvePath(articlePath, rawSrc);
|
||||||
|
|
||||||
createEffect(() => {
|
const handle = registerStyle({
|
||||||
if(!articleEl) return;
|
key: "backgroundImage",
|
||||||
articleEl.dataset.mdbg = resolvedSrc;
|
article: articleEl,
|
||||||
articleEl.style.backgroundImage = `url(${resolvedSrc})`;
|
styles: {
|
||||||
articleEl.style.backgroundSize = props.fit || 'cover';
|
backgroundImage: `url(${resolvedSrc})`,
|
||||||
articleEl.style.backgroundPosition = 'center';
|
backgroundSize: props.fit || "cover",
|
||||||
articleEl.style.backgroundRepeat = 'no-repeat';
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
} as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => handle.dispose());
|
||||||
if(articleEl?.dataset?.mdbg !== resolvedSrc)
|
|
||||||
return;
|
|
||||||
articleEl.style.backgroundImage = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { customElement, noShadowDOM } from "solid-element";
|
|
||||||
import { createEffect, onCleanup } from "solid-js";
|
|
||||||
|
|
||||||
export interface EmfontProps {
|
|
||||||
weight?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
customElement("md-emfont", { weight: "400" }, (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";
|
|
||||||
const linkHref = `https://font.emtech.cc/css/${font}?weight=${weight}`;
|
|
||||||
|
|
||||||
// 避免重复加载:检查是否已有相同字体的 <link>
|
|
||||||
let linkEl = document.head.querySelector(
|
|
||||||
`link[data-md-emfont="${font}"]`,
|
|
||||||
) as HTMLLinkElement | null;
|
|
||||||
|
|
||||||
if (!linkEl) {
|
|
||||||
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) {
|
|
||||||
delete articleEl.dataset.mdEmFont;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { customElement, noShadowDOM } from "solid-element";
|
||||||
|
import { onCleanup } from "solid-js";
|
||||||
|
import { registerStyle } from "./utils/article-style-manager";
|
||||||
|
|
||||||
|
export interface FontProps {
|
||||||
|
/** Font source: "google" (Google Fonts), "emfont" (emtech.cc),
|
||||||
|
* or "local" (already loaded, just set fontFamily) */
|
||||||
|
source?: "google" | "emfont" | "local";
|
||||||
|
/** Font weight (for google and emfont sources) */
|
||||||
|
weight?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLinkHref(
|
||||||
|
font: string,
|
||||||
|
source: FontProps["source"],
|
||||||
|
weight: string,
|
||||||
|
): string | null {
|
||||||
|
if (source === "google") {
|
||||||
|
const family = font.replace(/\s+/g, "+");
|
||||||
|
return `https://fonts.googleapis.com/css2?family=${family}:wght@${weight}&display=swap`;
|
||||||
|
}
|
||||||
|
if (source === "emfont") {
|
||||||
|
return `https://font.emtech.cc/css/${font}?weight=${weight}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
customElement(
|
||||||
|
"md-font",
|
||||||
|
{ source: "local", weight: "400" },
|
||||||
|
(props, { element }) => {
|
||||||
|
noShadowDOM();
|
||||||
|
|
||||||
|
const font = element?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
// Hide original text content
|
||||||
|
if (element) {
|
||||||
|
element.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleEl = element?.closest("article") as HTMLElement;
|
||||||
|
if (!articleEl) return null;
|
||||||
|
|
||||||
|
const source = (props.source as FontProps["source"]) || "google";
|
||||||
|
const weight = props.weight || "400";
|
||||||
|
|
||||||
|
const href = buildLinkHref(font, source, weight);
|
||||||
|
const links = href
|
||||||
|
? [{ href, dataset: { mdFontSource: source, mdFontName: font } }]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handle = registerStyle({
|
||||||
|
key: "fontFamily",
|
||||||
|
article: articleEl,
|
||||||
|
styles: { fontFamily: `"${font}", sans-serif` },
|
||||||
|
links,
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => handle.dispose());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
/**
|
||||||
|
* ArticleStyleManager — centralized lifecycle manager for article-level
|
||||||
|
* style mutations (fontFamily, backgroundImage, etc.) and external
|
||||||
|
* stylesheet <link> injection with reference counting.
|
||||||
|
*
|
||||||
|
* Each md-* component registers a StyleRegistration and gets back a
|
||||||
|
* StyleHandle. On disposal, only that component's contribution is
|
||||||
|
* removed; if a previous registration for the same key is still active,
|
||||||
|
* its styles are restored.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type StyleKey = string;
|
||||||
|
|
||||||
|
interface StyleRegistration {
|
||||||
|
/** Unique namespace key (e.g. "fontFamily", "backgroundImage") */
|
||||||
|
key: StyleKey;
|
||||||
|
/** Target article element */
|
||||||
|
article: HTMLElement;
|
||||||
|
/** CSS properties to apply to the article */
|
||||||
|
styles: Partial<CSSStyleDeclaration>;
|
||||||
|
/**
|
||||||
|
* External <link> stylesheets to inject into <head>.
|
||||||
|
* Reference-counted: removed when no registrations reference it.
|
||||||
|
*/
|
||||||
|
links?: Array<{ href: string; dataset: Record<string, string> }>;
|
||||||
|
/** Called when this registration becomes active */
|
||||||
|
onActivate?: () => void;
|
||||||
|
/** Called when this registration is disposed */
|
||||||
|
onDeactivate?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StyleHandle {
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-key stack of registrations for a single article */
|
||||||
|
interface ArticleRegistry {
|
||||||
|
article: HTMLElement;
|
||||||
|
stacks: Map<StyleKey, StyleRegistration[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleRegistries = new Map<HTMLElement, ArticleRegistry>();
|
||||||
|
/** href -> number of active registrations referencing it */
|
||||||
|
const linkRefCounts = new Map<string, number>();
|
||||||
|
/** href -> <link> element */
|
||||||
|
const linkElements = new Map<string, HTMLLinkElement>();
|
||||||
|
|
||||||
|
function getRegistry(article: HTMLElement): ArticleRegistry {
|
||||||
|
let reg = articleRegistries.get(article);
|
||||||
|
if (!reg) {
|
||||||
|
reg = { article, stacks: new Map() };
|
||||||
|
articleRegistries.set(article, reg);
|
||||||
|
}
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStyles(article: HTMLElement, styles: Partial<CSSStyleDeclaration>) {
|
||||||
|
for (const [prop, value] of Object.entries(styles)) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
(article.style as any)[prop] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStyles(article: HTMLElement, styles: Partial<CSSStyleDeclaration>) {
|
||||||
|
for (const prop of Object.keys(styles)) {
|
||||||
|
(article.style as any)[prop] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementLinkRef(href: string, dataset: Record<string, string>) {
|
||||||
|
const count = linkRefCounts.get(href) || 0;
|
||||||
|
if (count === 0) {
|
||||||
|
const linkEl = document.createElement("link");
|
||||||
|
linkEl.rel = "stylesheet";
|
||||||
|
linkEl.href = href;
|
||||||
|
for (const [key, val] of Object.entries(dataset)) {
|
||||||
|
linkEl.dataset[key] = val;
|
||||||
|
}
|
||||||
|
document.head.appendChild(linkEl);
|
||||||
|
linkElements.set(href, linkEl);
|
||||||
|
}
|
||||||
|
linkRefCounts.set(href, count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementLinkRef(href: string) {
|
||||||
|
const count = linkRefCounts.get(href);
|
||||||
|
if (count === undefined) return;
|
||||||
|
if (count <= 1) {
|
||||||
|
const linkEl = linkElements.get(href);
|
||||||
|
linkEl?.remove();
|
||||||
|
linkRefCounts.delete(href);
|
||||||
|
linkElements.delete(href);
|
||||||
|
} else {
|
||||||
|
linkRefCounts.set(href, count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose debug info on articles
|
||||||
|
function updateDebugData(registry: ArticleRegistry) {
|
||||||
|
const activeKeys: string[] = [];
|
||||||
|
for (const [key, stack] of registry.stacks) {
|
||||||
|
if (stack.length > 0) {
|
||||||
|
activeKeys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activeKeys.length > 0) {
|
||||||
|
registry.article.dataset.mdStyles = activeKeys.join(",");
|
||||||
|
} else {
|
||||||
|
delete registry.article.dataset.mdStyles;
|
||||||
|
if (registry.stacks.size === 0) {
|
||||||
|
articleRegistries.delete(registry.article);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerStyle(reg: StyleRegistration): StyleHandle {
|
||||||
|
const registry = getRegistry(reg.article);
|
||||||
|
let stack = registry.stacks.get(reg.key);
|
||||||
|
if (!stack) {
|
||||||
|
stack = [];
|
||||||
|
registry.stacks.set(reg.key, stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.push(reg);
|
||||||
|
|
||||||
|
// Increment link refcounts
|
||||||
|
for (const link of reg.links || []) {
|
||||||
|
incrementLinkRef(link.href, link.dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the active (top) registration, apply its styles
|
||||||
|
if (stack[stack.length - 1] === reg) {
|
||||||
|
applyStyles(reg.article, reg.styles);
|
||||||
|
reg.onActivate?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDebugData(registry);
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispose() {
|
||||||
|
if (disposed) return;
|
||||||
|
disposed = true;
|
||||||
|
|
||||||
|
// Remove this registration from its stack
|
||||||
|
const currentStack = registry.stacks.get(reg.key);
|
||||||
|
if (!currentStack) return;
|
||||||
|
|
||||||
|
const idx = currentStack.lastIndexOf(reg);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
const wasActive = idx === currentStack.length - 1;
|
||||||
|
currentStack.splice(idx, 1);
|
||||||
|
|
||||||
|
if (currentStack.length === 0) {
|
||||||
|
registry.stacks.delete(reg.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this was the active registration, restore previous or clear
|
||||||
|
if (wasActive) {
|
||||||
|
clearStyles(reg.article, reg.styles);
|
||||||
|
const prev = currentStack.length > 0 ? currentStack[currentStack.length - 1] : null;
|
||||||
|
if (prev) {
|
||||||
|
applyStyles(reg.article, prev.styles);
|
||||||
|
prev.onActivate?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reg.onDeactivate?.();
|
||||||
|
|
||||||
|
// Decrement link refcounts
|
||||||
|
for (const link of reg.links || []) {
|
||||||
|
decrementLinkRef(link.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDebugData(registry);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -10,10 +10,8 @@ import markedCodeBlockYamlTag from "./code-block-yaml-tag";
|
||||||
let globalIconPrefix: string | undefined = undefined;
|
let globalIconPrefix: string | undefined = undefined;
|
||||||
function overrideIconPrefix(path?: string) {
|
function overrideIconPrefix(path?: string) {
|
||||||
globalIconPrefix = path;
|
globalIconPrefix = path;
|
||||||
return {
|
return () => {
|
||||||
[Symbol.dispose]() {
|
globalIconPrefix = undefined;
|
||||||
globalIconPrefix = undefined;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,8 +94,12 @@ const marked = new Marked()
|
||||||
);
|
);
|
||||||
|
|
||||||
export function parseMarkdown(content: string, iconPrefix?: string): string {
|
export function parseMarkdown(content: string, iconPrefix?: string): string {
|
||||||
using prefix = overrideIconPrefix(iconPrefix);
|
const restore = overrideIconPrefix(iconPrefix);
|
||||||
return marked.parse(content.trimStart()) as string;
|
try {
|
||||||
|
return marked.parse(content.trimStart()) as string;
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { marked };
|
export { marked };
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"types": ["node", "jest"]
|
"types": ["node", "jest"],
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue