fix(mermaid): 改用 script 标签加载 IIFE bundle,修正全局变量取值
根因:mermaid bundle 是 IIFE 格式(与项目其他前端库一致),挂全局变量 而非 ES module export。但 mermaid.ts 错用了 import() 取 .default—— IIFE 无 export,mod.default 是 undefined,导致 mermaid.initialize 调用 TypeError,流程图永远不渲染(数学公式不受影响,因为是服务端渲染)。 修复: - mermaid.ts: loadMermaidBundle 改用动态注入 <script src=/mermaid/mermaid.js> 标签,加载后从 window.MermaidRenderer 取(与 codemirror/lightbox 的 Reflect::get(window,...) 模式一致) - mermaid-renderer/index.ts: 显式 globalThis.MermaidRenderer = mermaid, 避免 vite IIFE 把 export default 编译成 .default 多一层 - markdown.rs: mermaid 代码块跳过 syntect 高亮(无语法定义且会包 <span> 污染 textContent),直接输出转义纯源码 前端 28 测试 + Rust 557 测试 + 双 target 编译通过。
This commit is contained in:
parent
ac04b23b59
commit
445b92f407
@ -1,5 +1,12 @@
|
||||
// 把 mermaid 默认导出包成 IIFE 全局 bundle。
|
||||
// yggdrasil-core 的 mermaid.ts 通过动态 import('/mermaid/mermaid.js') 拿到 .default。
|
||||
// 把 mermaid 包成 IIFE 全局 bundle。
|
||||
//
|
||||
// 关键:vite IIFE + `export default X` 会产出 `window.MermaidRenderer.default = X`
|
||||
// (多一层 .default)。为避免 mermaid.ts 取值时纠结这一层,这里把 mermaid 对象
|
||||
// 直接赋给 window.MermaidRenderer 作为构建时副作用,使 window.MermaidRenderer
|
||||
// 本身即 mermaid API(initialize/render)。
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
// 构建时副作用:把 mermaid 挂到全局。ES module 的顶层赋值会被 vite 保留。
|
||||
;(globalThis as unknown as { MermaidRenderer: typeof mermaid }).MermaidRenderer = mermaid;
|
||||
|
||||
export default mermaid;
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
/**
|
||||
* Mermaid 流程图懒加载渲染。
|
||||
*
|
||||
* 扫描 `pre > code.language-mermaid` 代码块,在进入视口时动态 import 独立 bundle
|
||||
* 扫描 `pre > code.language-mermaid` 代码块,在进入视口时动态加载独立 IIFE bundle
|
||||
* `public/mermaid/mermaid.js`(~1MB,只在有图且可见时加载,不影响无图文章首屏),
|
||||
* 把 mermaid 源码渲染成 SVG 注入到父 <pre>。
|
||||
*
|
||||
* bundle 是 IIFE 格式(与项目其他前端库一致),挂全局变量 `window.MermaidRenderer`。
|
||||
* 故不能用 ES module 的 `import().default`(IIFE 无 export)——改用动态注入
|
||||
* `<script src="/mermaid/mermaid.js">` 标签,加载后从 window 取 MermaidRenderer。
|
||||
*
|
||||
* 范式照搬 post-content.ts:querySelectorAll + 幂等守卫 + 注入 DOM。
|
||||
* mermaid 无官方 SSR 支持,纯客户端渲染;服务端 markdown.rs 只产出带
|
||||
* `language-mermaid` class 的普通代码块,不挂 data 属性。
|
||||
@ -17,21 +21,40 @@ type MermaidApi = {
|
||||
render: (id: string, text: string) => Promise<{ svg: string }>;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
MermaidRenderer?: MermaidApi;
|
||||
}
|
||||
}
|
||||
|
||||
let mermaidPromise: Promise<MermaidApi> | null = null;
|
||||
|
||||
/**
|
||||
* 动态加载 mermaid 独立 bundle 的底层函数(可注入以便测试)。
|
||||
* 动态加载 mermaid 独立 IIFE bundle 的底层函数(可注入以便测试)。
|
||||
*
|
||||
* 用绝对路径 '/mermaid/mermaid.js' 动态 import:Vite 无法静态分析此字面量,
|
||||
* 故加 @vite-ignore 避免构建时报错,且该路径无类型声明需用函数间接构造 import
|
||||
* 字面量以绕过 tsc 的模块解析(TS2307)。bundle 加载后 default export 即 mermaid API。
|
||||
* 测试时可通过重赋值 `loadMermaidBundle` 替换为 mock。
|
||||
* IIFE bundle 挂在 `window.MermaidRenderer`(非 ES module export),故用动态注入
|
||||
* `<script>` 标签的方式加载,标签 onload 后从 window 取。测试时可通过
|
||||
* [`_resetMermaidLoader`] 注入 mock。
|
||||
*/
|
||||
export let loadMermaidBundle: () => Promise<MermaidApi> = async () => {
|
||||
const url = '/mermaid/mermaid.js';
|
||||
const mod = (await import(/* @vite-ignore */ url)) as { default: MermaidApi };
|
||||
return mod.default;
|
||||
};
|
||||
export let loadMermaidBundle: () => Promise<MermaidApi> = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
// 已加载(同页多次调用 / SPA 导航)直接复用。
|
||||
if (window.MermaidRenderer) {
|
||||
resolve(window.MermaidRenderer);
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.src = '/mermaid/mermaid.js';
|
||||
script.onload = () => {
|
||||
if (window.MermaidRenderer) {
|
||||
resolve(window.MermaidRenderer);
|
||||
} else {
|
||||
reject(new Error('mermaid bundle loaded but window.MermaidRenderer undefined'));
|
||||
}
|
||||
};
|
||||
script.onerror = () => reject(new Error('failed to load /mermaid/mermaid.js'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
/** 重置加载函数(测试用,重新注入 mock 后必须重置 mermaidPromise 缓存)。 */
|
||||
export function _resetMermaidLoader(loader?: () => Promise<MermaidApi>): void {
|
||||
|
||||
@ -183,6 +183,21 @@ pub fn render_markdown_enhanced(md: &str) -> RenderedContent {
|
||||
code_buffer.push_str(&text);
|
||||
}
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
// mermaid 代码块:前端 yggdrasil-core 扫描 language-mermaid 渲染成 SVG,
|
||||
// 源码不应被 syntect 高亮(无语法定义,且会包 <span> 污染 textContent 提取)。
|
||||
// 直接输出转义后的纯源码,前端 textContent 无损拿到原始 mermaid 文本。
|
||||
let is_mermaid = code_lang
|
||||
.as_deref()
|
||||
.map(|l| l.split_whitespace().next() == Some("mermaid"))
|
||||
.unwrap_or(false);
|
||||
if is_mermaid {
|
||||
let escaped = crate::utils::html::escape_html(&code_buffer);
|
||||
html.push_str(r#"<pre><code class="language-mermaid">"#);
|
||||
html.push_str(&escaped);
|
||||
html.push_str("</code></pre>");
|
||||
in_codeblock = false;
|
||||
continue;
|
||||
}
|
||||
// 使用 syntect 对代码块进行服务端语法高亮。
|
||||
let highlighted =
|
||||
crate::highlight::server::highlight_code(&code_buffer, code_lang.as_deref());
|
||||
@ -907,4 +922,24 @@ console.log(1)
|
||||
result.html
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_markdown_mermaid_block_not_highlighted() {
|
||||
// mermaid 块跳过 syntect 高亮:源码应是转义纯文本,不被 <span> 包裹。
|
||||
// 前端 mermaid.ts 用 textContent 无损提取渲染成 SVG。
|
||||
let result = render_markdown_enhanced("```mermaid\ngraph LR\n A --> B\n```");
|
||||
assert!(
|
||||
result.html.contains(r#"class="language-mermaid""#),
|
||||
"应保留 language-mermaid class, got: {}",
|
||||
result.html
|
||||
);
|
||||
// 不应被 syntect 高亮(无 text plain span)。
|
||||
assert!(
|
||||
!result.html.contains("text plain"),
|
||||
"mermaid 源码不应被 syntect 高亮, got: {}",
|
||||
result.html
|
||||
);
|
||||
// 源码内容保留(HTML 转义后)。
|
||||
assert!(result.html.contains("graph LR"));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user