From fac247f60a07d32d2acfdb43391ff6a80d3b4b98 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 17 Jul 2026 10:13:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(mermaid):=20=E6=B5=81=E7=A8=8B=E5=9B=BE?= =?UTF-8?q?=E9=85=8D=E8=89=B2=E5=AF=B9=E9=BD=90=20Catppuccin=20+=20?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E7=BE=8E=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mermaid.initialize 之前用内置 'default'/'dark' 主题,配色与站点 Catppuccin 调色板完全脱节,图是"外来"的样子;且零 mermaid 专用 CSS,SVG 直接套在通用 pre 容器里,宽图横向滚动、无居中无留白。 改动: - mermaid.ts:theme 切到 'base'(base 不硬编码颜色,themeVariables 能完全 控制调色板;'default' 主题硬编码 mainBkg=#ECECFF 会阻断覆盖)。新增 Latte/ Mocha 两套 themeVariables,节点用 surface 色阶、边框/连线用 subtext 色阶、 文字用 primary text,hex 取自 themes/*.tmTheme。flowchart 配 basis 平滑曲线 + diagramPadding 16 + useMaxWidth:true(配合 CSS 消除横向滚动)。 - input.css:新增 .md-content pre[data-mermaid-rendered] 规则,flex 居中 + svg max-width:100% 缩放适配;.mermaid-error 加 accent 色左边条提示。 - mermaid.test.ts:theme 断言同步到 'base'+darkMode;新增 themeVariables 注入测试(钉住 Latte/Mocha 关键 hex)。 范围:纯前端 TS + CSS,零 Rust 改动。 --- input.css | 23 ++++ libs/yggdrasil-core/src/mermaid.test.ts | 42 +++++++- libs/yggdrasil-core/src/mermaid.ts | 133 +++++++++++++++++++++++- 3 files changed, 195 insertions(+), 3 deletions(-) diff --git a/input.css b/input.css index 51865a5..42bcbef 100644 --- a/input.css +++ b/input.css @@ -663,6 +663,29 @@ display: block; } + /* ── mermaid 图表容器 ── + data-mermaid-rendered 由 yggdrasil-core/mermaid.ts 渲染成功后标记在
 上。
+     渲染后 pre 唯一子元素是 (mermaid 块不注入 copy-code 按钮),flex 安全。
+     居中 + 缩放适配宽度,消除常规图的横向滚动;超宽图 overflow-x:auto 兜底。 */
+  .md-content pre[data-mermaid-rendered] {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 24px 16px;
+    overflow-x: auto;
+  }
+
+  .md-content pre[data-mermaid-rendered] > svg {
+    max-width: 100%;
+    height: auto;
+    display: block;
+  }
+
+  /* 渲染失败:保留原始源码,加 accent 色左边条提示(站点 accent 为绿,沿用配色体系) */
+  .md-content pre.mermaid-error {
+    border-left: 3px solid var(--color-paper-accent);
+  }
+
   .anchor {
     display: none;
     text-decoration: none;
diff --git a/libs/yggdrasil-core/src/mermaid.test.ts b/libs/yggdrasil-core/src/mermaid.test.ts
index 69c03ac..e99cb15 100644
--- a/libs/yggdrasil-core/src/mermaid.test.ts
+++ b/libs/yggdrasil-core/src/mermaid.test.ts
@@ -67,10 +67,46 @@ describe('initMermaid', () => {
     window.__initMermaid('.post-content', 'dark');
 
     await vi.waitFor(() => {
-      expect(mockInitialize).toHaveBeenCalledWith(expect.objectContaining({ theme: 'dark' }));
+      // base 主题 + darkMode 标志:让 themeVariables 完全控制 Catppuccin 调色板
+      expect(mockInitialize).toHaveBeenCalledWith(
+        expect.objectContaining({ theme: 'base', darkMode: true }),
+      );
     });
   });
 
+  it('亮/暗主题传入对应的 Catppuccin themeVariables', async () => {
+    // 暗色:Mocha 调色板(背景 #313244、文字 #cdd6f4)
+    const darkRoot = document.createElement('div');
+    darkRoot.className = 'post-content';
+    darkRoot.innerHTML = '
graph TD; A-->B
'; + document.body.appendChild(darkRoot); + window.__initMermaid('.post-content', 'dark'); + await vi.waitFor(() => { + expect(mockInitialize).toHaveBeenCalled(); + }); + const darkCall = mockInitialize.mock.calls.slice(-1)[0]?.[0] as Record; + const darkVars = darkCall.themeVariables as Record; + expect(darkVars.background).toBe('#313244'); + expect(darkVars.primaryTextColor).toBe('#cdd6f4'); + expect(darkVars.lineColor).toBe('#a6adc8'); + + // 亮色:Latte 调色板(背景 #dce0e8、文字 #4c4f69) + document.body.innerHTML = ''; + const lightRoot = document.createElement('div'); + lightRoot.className = 'post-content'; + lightRoot.innerHTML = '
graph TD; A-->B
'; + document.body.appendChild(lightRoot); + window.__initMermaid('.post-content', 'light'); + await vi.waitFor(() => { + expect(mockInitialize).toHaveBeenCalled(); + }); + const lightCall = mockInitialize.mock.calls.slice(-1)[0]?.[0] as Record; + const lightVars = lightCall.themeVariables as Record; + expect(lightVars.background).toBe('#dce0e8'); + expect(lightVars.primaryTextColor).toBe('#4c4f69'); + expect(lightVars.lineColor).toBe('#5c5f77'); + }); + it('幂等:重复调用不重复渲染已处理的块', async () => { const root = document.createElement('div'); root.className = 'post-content'; @@ -137,7 +173,9 @@ describe('initMermaid', () => { await vi.waitFor(() => { expect(mockRender.mock.calls.length).toBeGreaterThan(firstRenderCalls); }); - expect(mockInitialize).toHaveBeenLastCalledWith(expect.objectContaining({ theme: 'dark' })); + expect(mockInitialize).toHaveBeenLastCalledWith( + expect.objectContaining({ theme: 'base', darkMode: true }), + ); expect(root.querySelector('pre')?.dataset.mermaidTheme).toBe('dark'); }); diff --git a/libs/yggdrasil-core/src/mermaid.ts b/libs/yggdrasil-core/src/mermaid.ts index b775e29..97dc59c 100644 --- a/libs/yggdrasil-core/src/mermaid.ts +++ b/libs/yggdrasil-core/src/mermaid.ts @@ -30,6 +30,127 @@ type MermaidApi = { render: (id: string, text: string) => Promise<{ svg: string }>; }; +/** + * Catppuccin themeVariables(与站点 --color-paper-* / .tmTheme 调色板对齐)。 + * + * mermaid 把颜色烤进 SVG 内联 style,无法靠 CSS 原地改主题,故须在 initialize 时注入。 + * 用 `theme: 'base'`(非 'default'/'dark'):base 主题不硬编码颜色,themeVariables 能完全 + * 控制调色板;'default' 主题硬编码 mainBkg=#ECECFF 等会阻断覆盖。 + * + * 设计哲学:极简卡片化——节点用 surface 色阶(卡片感)、边框/连线用 subtext 色阶(克制)、 + * 文字用 primary text。不滥用强调色,绿/紫等 accent 留给作者用 classDef 手动强调。 + * hex 值取自 themes/Catppuccin Latte.tmTheme 与 Catppuccin Mocha.tmTheme。 + * + * 覆盖的字段涵盖 flowchart / sequence / class 三类图(测试文章用到的全部类型)。 + */ + +/** Latte(亮)主题:节点 surface 色阶、文字 #4c4f69、连线 subtext1 #5c5f77。 */ +const LATTE_VARS = { + background: '#dce0e8', // = --color-paper-code-block,图背景与 pre 无缝衔接 + // 节点填充:surface 色阶递进(主/次/三级),卡片质感 + primaryColor: '#e6e9ef', + secondaryColor: '#ccd0da', + tertiaryColor: '#bcc0cc', + mainBkg: '#e6e9ef', + nodeBkg: '#e6e9ef', + secondBkg: '#ccd0da', + // 节点边框:surface1 偏冷灰 + primaryBorderColor: '#bcc0cc', + secondaryBorderColor: '#acb0be', + tertiaryBorderColor: '#9ca0b0', + nodeBorder: '#bcc0cc', + clusterBorder: '#bcc0cc', + labelBoxBorderColor: '#bcc0cc', + // 文字:primary text #4c4f69 + primaryTextColor: '#4c4f69', + secondaryTextColor: '#5c5f77', + tertiaryTextColor: '#6c6f85', + textColor: '#4c4f69', + nodeTextColor: '#4c4f69', + titleColor: '#4c4f69', + classText: '#4c4f69', + labelTextColor: '#4c4f69', + // 连线/箭头:subtext1 #5c5f77 + lineColor: '#5c5f77', + defaultLinkColor: '#5c5f77', + arrowheadColor: '#5c5f77', + // 时序图 + actorBkg: '#e6e9ef', + actorBorder: '#bcc0cc', + actorTextColor: '#4c4f69', + actorLineColor: '#5c5f77', + signalColor: '#5c5f77', + signalTextColor: '#4c4f69', + loopTextColor: '#4c4f69', + sequenceNumberColor: '#eff1f5', + activationBkgColor: '#ccd0da', + activationBorderColor: '#bcc0cc', + // 边标签 / 子图背景 + edgeLabelBackground: '#eff1f5', + labelBoxBkgColor: '#eff1f5', + clusterBkg: 'rgba(239, 241, 245, 0.5)', + // 注释:低饱和黄(Latte yellow #df8e1d) + noteBkgColor: 'rgba(223, 142, 29, 0.15)', + noteBorderColor: '#df8e1d', + noteTextColor: '#4c4f69', + // 字体:与正文 sans 对齐,中文友好 + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif", + fontSize: '16px', +} as const; + +/** Mocha(暗)主题:节点 surface 色阶、文字 #cdd6f4、连线 subtext0 #a6adc8。 */ +const MOCHA_VARS = { + background: '#313244', // = --color-paper-code-block(暗) + primaryColor: '#45475a', + secondaryColor: '#585b70', + tertiaryColor: '#1e1e2e', + mainBkg: '#45475a', + nodeBkg: '#45475a', + secondBkg: '#585b70', + primaryBorderColor: '#585b70', + secondaryBorderColor: '#45475a', + tertiaryBorderColor: '#313244', + nodeBorder: '#585b70', + clusterBorder: '#585b70', + labelBoxBorderColor: '#585b70', + primaryTextColor: '#cdd6f4', + secondaryTextColor: '#bac2de', + tertiaryTextColor: '#a6adc8', + textColor: '#cdd6f4', + nodeTextColor: '#cdd6f4', + titleColor: '#cdd6f4', + classText: '#cdd6f4', + labelTextColor: '#cdd6f4', + lineColor: '#a6adc8', + defaultLinkColor: '#a6adc8', + arrowheadColor: '#a6adc8', + actorBkg: '#45475a', + actorBorder: '#585b70', + actorTextColor: '#cdd6f4', + actorLineColor: '#a6adc8', + signalColor: '#a6adc8', + signalTextColor: '#cdd6f4', + loopTextColor: '#cdd6f4', + sequenceNumberColor: '#1e1e2e', + activationBkgColor: '#585b70', + activationBorderColor: '#6c7086', + edgeLabelBackground: '#1e1e2e', + labelBoxBkgColor: '#1e1e2e', + clusterBkg: 'rgba(30, 30, 46, 0.5)', + noteBkgColor: 'rgba(249, 226, 175, 0.12)', + noteBorderColor: '#f9e2af', + noteTextColor: '#cdd6f4', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif", + fontSize: '16px', +} as const; + +/** 按主题返回对应 Catppuccin themeVariables。 */ +function themeVarsFor(theme: ThemeName): Record { + return theme === 'dark' ? { ...MOCHA_VARS } : { ...LATTE_VARS }; +} + declare global { interface Window { MermaidRenderer?: MermaidApi; @@ -102,8 +223,18 @@ async function renderBlock(pre: HTMLPreElement, source: string, theme: ThemeName const mermaid = await loadMermaid(); mermaid.initialize({ startOnLoad: false, - theme: theme === 'dark' ? 'dark' : 'default', + // base 主题不硬编码颜色,让 themeVariables 完全控制 Catppuccin 调色板。 + theme: 'base', + darkMode: theme === 'dark', securityLevel: 'strict', + // flowchart:平滑曲线 + 边缘留白 + 缩放至容器宽度(配合 CSS 消除横向滚动)。 + flowchart: { + curve: 'basis', + diagramPadding: 16, + useMaxWidth: true, + htmlLabels: true, + }, + themeVariables: themeVarsFor(theme), }); const id = `mermaid-svg-${++renderCounter}`; const { svg } = await mermaid.render(id, source);