remons-render-markdown 2.0.1-beta.4 → 2.0.1-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -1,581 +1,581 @@
1
- # render-markdown
2
-
3
- 一个基于 React 的 Markdown 渲染组件,支持 GFM(GitHub Flavored Markdown)Tabs 标签、代码高亮、Mermaid 图表等功能。
4
-
5
- 渲染效果
6
- ![整体渲染效果](https://remons.cn:3008/upload/md_assets/%E6%95%B4%E4%BD%93%E6%B8%B2%E6%9F%93%E6%95%88%E6%9E%9C.png)
7
-
8
- ## 技术方案
9
-
10
- ### 技术栈
11
-
12
- | 类别 | 技术选型 | 说明 |
13
- |------|---------|------|
14
- | 框架 | React 18 | 组件库基础框架(peer 依赖) |
15
- | 构建工具 | Vite(库模式,ESM 单产物) | `vite.lib.config.ts` 构建库,`vite.config.ts` 跑示例页 |
16
- | 语言 | TypeScript | 提供类型定义(`dist/index.d.ts`) |
17
- | 样式方案 | Less + CSS Modules | 统一提取为 `dist/index.css`,需手动引入 |
18
- | 图片预览 | PhotoSwipe v5(`photoswipe`,零依赖 ESM) | Pointer Events 手势:以指针/手指为锚点连续缩放、拖拽带惯性、双指外划与竖向拖出关闭 |
19
- | 代码规范 | ESLint + Prettier | 统一代码风格 |
20
- | 发布工具 | changesets | 语义化版本管理与 changelog 生成 |
21
-
22
- ### 核心功能
23
-
24
- - **GFM 支持**: 完整的 GitHub Flavored Markdown 渲染
25
- - **增量渲染**: 基于 morphdom 的 DOM 差异化更新,适用于流式输出场景
26
- - **代码高亮**: 基于 highlight.js 的代码语法高亮
27
- - **Mermaid 图表**: 支持 Mermaid 语法渲染流程图、时序图等
28
- - **Tab 标签页**: 支持 Tab 标签页语法
29
- - **Alert 提示框**: 支持警告、提示等信息框
30
- - **目录锚点**: 从标题 token 流自动采集,无需在正文写 `[[TOC]]` 指令
31
- - **代码复制**: 一键复制代码块
32
- - **代码折叠**: 支持代码块折叠展开
33
- - **深色模式**: 自动适配深色/浅色主题
34
- - **多实例安全**: 工具栏 / 图片预览 / 图表均在组件容器作用域内工作,互不干扰
35
-
36
- ### 本地开发与示例页
37
-
38
- ```bash
39
- npm install
40
- npm run dev # 打开 http://localhost:5173,示例页覆盖全部能力与修复点
41
- npm run typecheck # tsc --noEmit
42
- npm run build # 产出 dist/index.mjs + dist/index.css + *.d.ts
43
- ```
44
-
45
- ## API
46
-
47
- ### 安装
48
-
49
- ```bash
50
- npm install remons-render-markdown
51
- ```
52
-
53
- 产物为 **ESM only**(`"type": "module"`,无 `require` 导出),请配合打包器使用。
54
- `highlight.js`、`react`、`react-dom`、`@ant-design/icons` 为 **peer 依赖**,需由使用方自行安装。
55
- 其余运行时依赖(`markdown-it`、`morphdom`、`photoswipe`、`@panzoom/panzoom` 等)在 `dependencies` 中声明,构建时 external,由包管理器解析。
56
-
57
- ### 导出内容
58
-
59
- ```typescript
60
- // 默认导出 + 命名导出
61
- import RenderMarkdown, {
62
- markdownFormat,
63
- languagesCommon,
64
- initHighlighter,
65
- useIncrementalRender,
66
- MD_PERSIST_ATTR,
67
- MermaidChart,
68
- renderMermaid,
69
- ThemeProvider,
70
- useTheme,
71
- initTheme,
72
- addExcludedSelector,
73
- removeExcludedSelector,
74
- defaultValidateLink,
75
- registerCdn,
76
- } from 'remons-render-markdown';
77
-
78
- // 类型
79
- import type { RenderMarkdownProps, MarkdownPlugin, AnchorItem, ThemeMode } from 'remons-render-markdown';
80
-
81
- // 样式(构建产物为 dist/index.css)
82
- import 'remons-render-markdown/style.css';
83
- ```
84
-
85
- #### 默认导出
86
-
87
- - **RenderMarkdown**: React 组件,用于渲染 Markdown 内容
88
-
89
- #### 命名导出
90
-
91
- - **markdownFormat**: Markdown 解析函数(**异步**),返回 `{ anchor: AnchorItem[], info: string }`
92
- - **languagesCommon**: 默认支持的语言包配置(javascript, typescript, css, json, bash, xml, plaintext)
93
- - **initHighlighter**: 注册高亮语言包的函数(必须调用,否则代码块无高亮)
94
- - **useIncrementalRender**: 增量渲染 hook,使用 morphdom 进行 DOM 差异化更新,适用于流式输出场景
95
- - **MD_PERSIST_ATTR**: `'data-md-persist'`,标记需跨增量渲染保留的 JS 动态节点
96
- - **MermaidChart**: 独立图表组件(**推荐**)
97
- - **renderMermaid**: 旧函数式用法,已 `@deprecated`,保留以兼容
98
- - **ThemeProvider** / **useTheme** / **initTheme**: 主题相关;`initTheme()` 需在入口显式调用(库不再在 import 时产生全局副作用)
99
- - **addExcludedSelector** / **removeExcludedSelector**: 图片预览的全局排除名单增删(默认名单已含 `pre` 与 `.mermaid-react-root`)
100
- - **defaultValidateLink**: 本库默认链接校验(markdown-it 原生名单 + 放行无脚本特征的内联 svg),自定义 `validateLink` 时可拿它做基线复用
101
- - **registerCdn**: 注册懒加载资源(如 mermaid)的默认 CDN 地址
102
-
103
- ### RenderMarkdown 组件 Props
104
-
105
- > **初始化语义**:除 `content` 外的配置项仅在组件挂载时读取一次,渲染途中修改不会生效(需变更 `key` 重新挂载)。`backTopTarget` 同样只在首次渲染时读取。
106
-
107
- | 参数 | 类型 | 必填 | 默认值 | 说明 |
108
- |------|------|------|--------|------|
109
- | content | string | ✅ | - | Markdown 内容 |
110
- | enableMermaid | boolean | ❌ | `true` | 是否把 mermaid 代码块渲染为图表 |
111
- | showCollapseButton | boolean | ❌ | `true` | 是否显示代码块折叠按钮 |
112
- | codeType | string | ❌ | - | 按纯代码渲染(如 `'go'`),不传则按 Markdown 渲染 |
113
- | html | boolean | ❌ | `true` | 是否渲染源码中的原生 HTML;不可信内容请设为 `false`(增量/非增量均生效,初始化语义) |
114
- | validateLink | (url: string) => boolean | ❌ | `defaultValidateLink` | 覆盖链接校验(初始化语义);不传则用本库默认(markdown-it 名单 + 放行内联 svg),详见《注意事项》第 12 条 |
115
- | incremental | boolean | ❌ | `false` | 是否启用 morphdom 增量渲染 |
116
- | incrementalThrottleMs | number | ❌ | `16` | 增量渲染节流窗口(ms) |
117
- | toolbarDebounceMs | number | ❌ | `100` | content 变化后工具栏/图片/图表扫描的防抖延迟(ms) |
118
- | showToc | boolean | ❌ | `false` | 是否显示目录(TOC)按钮和侧边栏 |
119
- | showBackTop | boolean | ❌ | `false` | 是否显示返回顶部按钮 |
120
- | backTopTarget | HTMLElement \| null | ❌ | `document.body` | 返回顶部监听的滚动容器(仅首次渲染读取) |
121
- | footer | React.ReactNode | ❌ | - | 内容区下方的自定义底部 |
122
- | customRenderers | MarkdownPlugin[] | ❌ | - | 自定义 markdown-it 插件,在内置插件之后按序执行 |
123
- | transformSource | (text: string) => string | ❌ | - | 渲染前改写 mermaid 源码 |
124
- | defaultCollapsed | boolean | ❌ | `true` | 代码块 / 图表默认折叠状态 |
125
- | showDriverGuide | boolean | ❌ | `false` | 是否显示图表新手引导 |
126
- | isPrintPreview | boolean | ❌ | `false` | 打印模式(跳过交互式工具栏) |
127
- | excludedSelectors | string[] | ❌ | - | 本实例的图片预览排除容器 |
128
- | cdn | Record<string, string> | ❌ | - | 懒加载资源地址,如 `{ mermaid: 'https://...' }` |
129
-
130
- **已重命名的 prop(旧名仍可用,标记为 deprecated):**
131
-
132
- | 旧名 | 新名 |
133
- |------|------|
134
- | `isSlotMermaid` | `enableMermaid` |
135
- | `isShowCollapsed` | `showCollapseButton` |
136
- | `mermaidDebounce` | `toolbarDebounceMs` |
137
- | `useIncremental` | `incremental` |
138
- | `chartConfig` | `transformSource` |
139
-
140
- ### markdownFormat 函数
141
-
142
- 解析 Markdown 内容并返回结构化数据。**异步函数**(内部等待 KaTeX CDN 就绪)。
143
-
144
- **参数:**
145
-
146
- - `content: string` - Markdown 文本内容
147
- - `customPlugins?: MarkdownPlugin[]` - 自定义 markdown-it 插件
148
- - `options?: { html?: boolean; validateLink?: (url: string) => boolean }` - `html` 默认 `true`,渲染不可信内容时设为 `false`;`validateLink` 不传则用本库默认 `defaultValidateLink`(放行无脚本特征的 `data:image/svg+xml`),传入则整体接管
149
-
150
- **返回值(Promise):**
151
-
152
- ```typescript
153
- {
154
- anchor: Array<{
155
- title: string; // 标题文本(纯文本)
156
- href: string; // 锚点链接(与标题 DOM 上的 id 一致)
157
- nodeName: string; // 节点名称,如 "H1", "H2"
158
- nodeTitle: string; // 完整节点 HTML(已转义,勿直接 innerHTML 展示)
159
- children: Array<...>; // 子标题
160
- }>;
161
- info: string; // 渲染后的 HTML 字符串
162
- }
163
- ```
164
-
165
- **降级行为:** KaTeX 加载失败时不抛错,公式降级为原文本并 `console.warn`;未注册高亮语言时代码块仅不染色,不影响渲染。
166
-
167
- ### initHighlighter 函数
168
-
169
- 注册自定义的代码高亮语言。`highlight.js` 为 peer + external 依赖,库不会自带语言包,**必须调用一次**才会高亮。
170
-
171
- **参数:**
172
-
173
- - `languages: Record<string, any>` - 语言包对象,格式为 `{ languageName: languageModule }`
174
-
175
- **示例:**
176
-
177
- ```typescript
178
- import javascript from 'highlight.js/lib/languages/javascript';
179
- import python from 'highlight.js/lib/languages/python';
180
-
181
- initHighlighter({
182
- javascript,
183
- python,
184
- });
185
- ```
186
-
187
- ## 使用方式
188
-
189
- ### 基础用法
190
-
191
- ```tsx
192
- import RenderMarkdown, { initHighlighter, languagesCommon, ThemeProvider } from 'remons-render-markdown';
193
- import 'remons-render-markdown/style.css';
194
-
195
- // 入口注册一次即可
196
- initHighlighter(languagesCommon);
197
-
198
- function App() {
199
- const markdownContent = `
200
- # Hello World
201
-
202
- This is a **Markdown** renderer.
203
-
204
- \`\`\`javascript
205
- console.log('Hello');
206
- \`\`\`
207
- `;
208
-
209
- return <RenderMarkdown content={markdownContent} />;
210
- }
211
-
212
- export default () => (
213
- <ThemeProvider>
214
- <App />
215
- </ThemeProvider>
216
- );
217
- ```
218
-
219
- ### 完整示例
220
-
221
- ```tsx
222
- import RenderMarkdown from 'remons-render-markdown';
223
- import 'remons-render-markdown/style.css';
224
-
225
- function ArticlePage() {
226
- const content = `
227
- # 文章标题
228
-
229
- 这是一篇示例文章。
230
-
231
- ## 代码示例
232
-
233
- \`\`\`javascript
234
- const greeting = 'Hello World';
235
- console.log(greeting);
236
- \`\`\`
237
-
238
- ## Mermaid 图表
239
-
240
- \`\`\`mermaid
241
- graph TD
242
- A[开始] --> B[处理]
243
- B --> C[结束]
244
- \`\`\`
245
- `;
246
-
247
- return (
248
- <RenderMarkdown
249
- content={content}
250
- enableMermaid
251
- showCollapseButton
252
- showToc
253
- showBackTop
254
- footer={<span>—— 完 ——</span>}
255
- html={false}
256
- />
257
- );
258
- }
259
- ```
260
-
261
- ### 使用 markdownFormat 获取目录结构
262
-
263
- ```tsx
264
- import { useEffect, useState } from 'react';
265
- import { markdownFormat, type AnchorItem } from 'remons-render-markdown';
266
-
267
- function TableOfContents({ content }: { content: string }) {
268
- const [anchor, setAnchor] = useState<AnchorItem[]>([]);
269
-
270
- useEffect(() => {
271
- let cancelled = false;
272
- markdownFormat(content).then(({ anchor }) => {
273
- if (!cancelled) setAnchor(anchor);
274
- });
275
- return () => { cancelled = true; };
276
- }, [content]);
277
-
278
- // anchor 为树形结构,无需在正文写 [[TOC]] 指令
279
- return (
280
- <ul>
281
- {anchor.map((item) => (
282
- <li key={item.href}>
283
- <a href={`#${item.href}`}>{item.title}</a>
284
- </li>
285
- ))}
286
- </ul>
287
- );
288
- }
289
- ```
290
-
291
- ### 自定义代码高亮语言
292
-
293
- ```tsx
294
- import RenderMarkdown, { initHighlighter, languagesCommon } from 'remons-render-markdown';
295
- import 'remons-render-markdown/style.css';
296
- import python from 'highlight.js/lib/languages/python';
297
- import go from 'highlight.js/lib/languages/go';
298
-
299
- // 注册额外的语言
300
- initHighlighter({
301
- ...languagesCommon, // 包含默认语言
302
- python,
303
- go,
304
- });
305
-
306
- function App() {
307
- return (
308
- <RenderMarkdown
309
- content={`
310
- \`\`\`python
311
- def hello():
312
- print('Hello')
313
- \`\`\`
314
-
315
- \`\`\`go
316
- package main
317
- import "fmt"
318
- func main() {
319
- fmt.Println("Hello")
320
- }
321
- \`\`\`
322
- `}
323
- />
324
- );
325
- }
326
- ```
327
-
328
- ### 仅渲染特定代码类型
329
-
330
- ```tsx
331
- import RenderMarkdown from 'remons-render-markdown';
332
- import 'remons-render-markdown/style.css';
333
-
334
- function CodeViewer() {
335
- const code = `
336
- const x = 1;
337
- const y = 2;
338
- console.log(x + y);
339
- `;
340
-
341
- return (
342
- <RenderMarkdown
343
- content={code}
344
- codeType="javascript"
345
- enableMermaid={false}
346
- />
347
- );
348
- }
349
- ```
350
-
351
- ### 使用 useIncrementalRender 实现增量渲染
352
-
353
- `useIncrementalRender` 适用于流式输出场景(如 AI 对话),通过 morphdom 实现高效的 DOM 差异化更新。
354
- 一般场景直接给 `<RenderMarkdown incremental />` 即可,需自行控制容器时才直接使用该 hook。
355
-
356
- ```tsx
357
- import { useIncrementalRender } from 'remons-render-markdown';
358
-
359
- function StreamingContent({ content }) {
360
- const { anchors, hasContent, setInnerRef } = useIncrementalRender({
361
- content,
362
- // 可选:节流时间,默认 16ms
363
- throttleMs: 16,
364
- });
365
-
366
- return (
367
- <div>
368
- {hasContent && <div ref={setInnerRef} />}
369
- {anchors.length > 0 && (
370
- <ul>
371
- {anchors.map((item) => (
372
- <li key={item.href}>
373
- <a href={`#${item.href}`}>{item.title}</a>
374
- </li>
375
- ))}
376
- </ul>
377
- )}
378
- </div>
379
- );
380
- }
381
- ```
382
-
383
- **参数:**
384
-
385
- | 参数 | 类型 | 必填 | 默认值 | 说明 |
386
- |------|------|------|--------|------|
387
- | content | string | ✅ | - | Markdown 内容(**唯一响应式参数**) |
388
- | codeType | string | ❌ | - | 代码类型,不传则按 Markdown 渲染(初始化语义) |
389
- | customRenderers | MarkdownPlugin[] | ❌ | - | 自定义 markdown-it 插件(初始化语义) |
390
- | throttleMs | number | ❌ | `16` | 节流窗口(ms)(初始化语义) |
391
- | onNodeDiscarded | (el: HTMLElement) => void | ❌ | - | 节点被 diff 移除后触发(含被移除子树的子节点),用于清理 React Root 等挂载状态 |
392
- | onPatched | () => void | ❌ | - | 每次 patch 完成后、浏览器绘制前同步触发,用于恢复运行时标记 |
393
-
394
- **初始化语义(重要):** 除 `content` 外的所有选项仅在挂载时读取一次,渲染途中传入新值**不感知**(不会重新初始化,也不会触发重渲染)。需要切换行为时,请变更外层组件的 `key` 使其重新挂载。
395
-
396
- **节流语义:** 真节流(leading + trailing)——首次变化立即渲染,期间最多每 `throttleMs` 渲染一次,窗口末尾的残余变化会补一次收尾渲染(而非 debounce 式的“等静默后才渲染”)。
397
-
398
- **返回值:**
399
-
400
- | 属性 | 类型 | 说明 |
401
- |------|------|------|
402
- | anchors | AnchorItem[] | 文档锚点列表 |
403
- | hasContent | boolean | 是否有内容 |
404
- | setInnerRef | (node: HTMLDivElement) => void | 内容容器的 ref 回调 |
405
-
406
- **增量渲染与自定义插件(customRenderers)的配合约定:**
407
-
408
- 1. 插件产出的容器节点(如 `:::badge` / `:::linkCard` / `<plugin-container>`)按"标签 + className"语义匹配,内容位移时不同类型的块不会被错配到旧节点上。
409
- 2. 元素上 JS 后加的 `data-*` 属性(如初始化守卫位 `data-xxx-inited`)在 diff 后会被保留,"只初始化一次"的守卫逻辑不会在增量渲染中被重复触发。
410
- 3. JS 动态插入的额外子节点(如 React 挂载点、收起按钮)默认会被 diff 移除;若需跨增量渲染保留,请给节点设置 `data-md-persist` 属性(见导出常量 `MD_PERSIST_ATTR`)。
411
- 4. 被 diff 移除的节点会触发 `onNodeDiscarded`,请在此卸载挂载在其上的 React Root,避免内存泄漏。
412
-
413
- ### 关于语法
414
-
415
- #### mermaid 图表
416
-
417
- ```code
418
- ---
419
- title: 图表标题
420
- ---
421
- ......... 图表内容
422
- ```
423
-
424
- - 折叠图
425
- ![mermaid 渲染图](https://remons.cn:3008/upload/md_assets/mermaid%20%E7%BC%A9%E7%95%A5%E5%9B%BE.png)
426
-
427
- - 缩略图
428
- ![mermaid 渲染图](https://remons.cn:3008/upload/md_assets/mermaid%20%E5%B1%95%E5%BC%80%E5%9B%BE.png)
429
- #### Tabs 标签页
430
-
431
- 依赖于 `@mdit/plugin-tab` 插件,参考 [mdit-plugin-tab](https://mdit-plugins.github.io/zh/tab.html) 的文档。
432
- *暂不支持 tabs 嵌套*
433
-
434
- 语法示例:
435
-
436
- ```markdown
437
- :::markdown-tabs
438
-
439
- @tab:active tab1
440
- ```javascript
441
- console.log('Hello');
442
- ```
443
-
444
- @tab tab2
445
- ```typescript
446
- console.log('World');
447
- ```
448
- :::
449
- ```
450
- ![tabs 渲染图](https://remons.cn:3008/upload/md_assets/tabs.png)
451
-
452
- #### Alert 提示框
453
-
454
- 语法示例:
455
-
456
- ```markdown
457
- > [!warning]
458
- > 我是一个警告信息
459
- ```
460
-
461
- ![alert 渲染图](https://remons.cn:3008/upload/md_assets/alert.png)
462
-
463
- #### MermaidChart 组件(推荐)/ renderMermaid 方法(已废弃)
464
-
465
- 独立渲染 Mermaid 图表,支持缩放、全屏、下载、源码查看等功能。
466
-
467
- ```tsx
468
- import { MermaidChart } from 'remons-render-markdown';
469
- import 'remons-render-markdown/style.css';
470
-
471
- function MermaidComponent() {
472
- const mermaidCode = `
473
- ---
474
- title: 流程图示例
475
- ---
476
- graph TD
477
- A[开始] --> B[处理]
478
- B --> C[结束]
479
- `;
480
-
481
- // 组件形式:元素类型稳定,父组件重渲染不会卸载重建子树
482
- return <MermaidChart source={mermaidCode} showSourceView showCollapse />;
483
- }
484
- ```
485
-
486
- 旧写法 `renderMermaid({ source })` 仍可用,但已标 `@deprecated`:它每次调用都返回新的元素类型,导致父组件重渲染时子树完整卸载重建(缩放/折叠/全屏状态丢失)。
487
-
488
- | 参数 | 类型 | 必填 | 默认值 | 说明 |
489
- |------|------|------|--------|------|
490
- | source | string | ✅ | - | Mermaid 代码字符串,支持设置标题 |
491
- | debounceMs | number | ❌ | `300` | 图表渲染的 debounce 时间(毫秒) |
492
- | enablePanzoom | boolean | ❌ | `true` | 是否启用缩放和平移 |
493
- | showDownload | boolean | ❌ | `true` | 是否显示下载按钮(SVG/PNG) |
494
- | showSourceView | boolean | ❌ | `false` | 是否显示源码查看按钮和工具栏 |
495
- | showCollapse | boolean | ❌ | `false` | 是否显示折叠/展开按钮 |
496
- | defaultCollapsed | boolean | ❌ | `true` | 默认折叠状态 |
497
- | className | string | ❌ | `""` | 自定义样式类名 |
498
- | minHeight | number | ❌ | `200` | 最小高度(像素) |
499
- | securityLevel | `'strict' \| 'loose'` | ❌ | `'strict'` | mermaid 安全级别;`strict` 禁用图内点击事件与原始 HTML,仅完全信任图源时才用 `loose` |
500
- | transformSource | (text: string) => string | ❌ | - | 渲染前改写源码(初始化语义);旧名 `chartConfig` 仍可用 |
501
- | cdn | Record<string, string> | ❌ | - | 自定义 mermaid 库地址(mermaid 本体仍走 CDN 懒加载,全局仅加载一次) |
502
- | showDriverGuide | boolean | ❌ | `false` | 是否显示新手引导 |
503
- | driverGuideOnce | boolean | ❌ | `true` | 引导是否每个元素只弹一次(设为 `false` 在流式重渲染下会反复弹) |
504
- | isPrintPreview | boolean | ❌ | `false` | 打印模式 |
505
-
506
- **功能特性:**
507
- - 缩放:支持放大、缩小、重置视图
508
- - 全屏:支持浏览器原生全屏模式
509
- - 下载:支持下载 SVG 和 PNG 格式
510
- - 源码查看:点击按钮可查看 Mermaid 源码
511
- - 折叠/展开:支持图表的折叠和展开
512
- - 缩略图模式:最小化时显示缩略图
513
- - 深色模式:自动适配主题
514
-
515
- ### 主题(ThemeProvider / useTheme / initTheme)
516
-
517
- 库不再在 `import` 时修改全局 `data-theme`(避免引入隐式副作用)。需要跟随外部主题切换时:
518
-
519
- ```tsx
520
- import { ThemeProvider, useTheme, initTheme, type ThemeMode } from 'remons-render-markdown';
521
-
522
- // 1)入口(main.tsx)显式同步一次已存储的主题
523
- initTheme();
524
-
525
- // 2)应用外层包 Provider
526
- <ThemeProvider><App /></ThemeProvider>;
527
-
528
- // 3)任意子组件内读写模式:light | dark | system
529
- const { mode, theme, isDark, setMode } = useTheme();
530
- setMode('dark');
531
- ```
532
-
533
- 防 FOUC:可在 `index.html` 内联脚本里提前给 `<html>` 写 `data-theme`(读 `localStorage['app-theme-mode']`),示例页即采用此方式。
534
-
535
- ### 图片预览排除名单
536
-
537
- ```tsx
538
- import { addExcludedSelector, removeExcludedSelector } from 'remons-render-markdown';
539
-
540
- // 默认全局名单:`pre`(代码块里当源码展示的图)与 `.mermaid-react-root`(图表挂载点里的装饰图,
541
- // 如标题行的 mermaid 图标)—— 它们是 UI 控件而不是文档插图,不该弹出全屏预览
542
-
543
- // 全局:处于这些容器内的图片不进入预览画廊
544
- addExcludedSelector('.copy-password-container');
545
- removeExcludedSelector('.copy-password-container'); // 可撤销,避免只增不减的隐式泄漏
546
-
547
- // 实例级:仅影响当前组件(该 prop 每次渲染实时生效,中途变更无需重挂载)
548
- <RenderMarkdown content={md} excludedSelectors={['.avatar-group']} />;
549
- ```
550
-
551
- 预览基于 PhotoSwipe:桌面滚轮即以指针位置为锚点缩放,双击放大到原始像素(可继续放大到 2 倍),未放大时拖拽平移带惯性;
552
- 触屏单指拖拽有惯性、双指外划或竖向拖出关闭,单击图片切回/放大、点背景关闭;Esc 关闭、←/→ 切换多图。
553
- 遮罩默认是半透明 + 毛玻璃(不是 PhotoSwipe 自带的纯黑),并随 `data-theme` 切换。
554
- 样式(`.pswp` 及其 CSS 变量 `--pswp-bg`、`--pswp-icon-color` 等)已打包进 `style.css`,根节点带 `md-image-preview` class,直接覆盖 `--pswp-bg` 即可改遮罩颜色。
555
-
556
- ## 注意事项
557
-
558
- 1. **样式引入**:务必引入 `remons-render-markdown/style.css`(实际路径 `dist/index.css`)以确保正确的样式渲染
559
- 2. **ESM only**:产物无 CommonJS 入口;在 Next.js 等 SSR 项目中需作为客户端组件使用(`'use client'`)
560
- 3. **代码高亮**:`highlight.js` 为 peer 依赖,必须调用 `initHighlighter(...)` 注册语言(库不自带语言包)
561
- 4. **Mermaid 支持**:`enableMermaid`(旧名 `isSlotMermaid`)为 `true` 时自动将 mermaid 代码块渲染为图表;mermaid 库走 CDN 懒加载,可用 `cdn={{ mermaid: '...' }}` 或 `registerCdn` 指定地址,**未配置且加载失败时会报错而非静默**。图表插在代码块 `<pre>` 的**后面**(作为兄弟节点),`<pre>` 本身保留在 DOM 里、仅由 `pre:has(code.mermaid-render-noCode){display:none}` 收起——它是增量 diff 认领该块的锚点,自定义样式时不要把它从 DOM 里移除
562
- 5. **安全默认值**:mermaid `securityLevel` 默认 `strict`;外部链接自动 `target="_blank"` + `rel="noopener"`;渲染不可信内容(AI 输出/用户上传)时请设 `html={false}` 关闭原生 HTML(该开关在增量与非增量两种模式下都会透传给 markdown-it)
563
- 6. **初始化语义**:除 `content` 外的 props 均仅在挂载时读取一次,途中变更需改 `key` 重挂载
564
- 7. **多实例**:工具栏/图片预览/图表均限定在本实例容器作用域,卸载时会回收 React Root 与打开中的预览遮罩;`fullscreenchange` 为页面级事件,图表只响应自身的全屏进出,退出全屏时还原进入前的展开状态(不影响其它图表)
565
- 8. **性能**:Mermaid 渲染延迟执行(`toolbarDebounceMs`,默认 100ms)以优化首屏
566
- 9. **视图位姿**:图表重渲染(流式输出、主题切换)与折叠/展开**不再重置**用户的缩放/平移(`noBind` + `bind/destroy` 只切换事件绑定);进入/退出全屏会重建视图,属预期
567
- 10. **全屏背景可定制**:由 CSS 变量 `--remons-markdown-mermaid-fullscreen-bg` 控制(浅色为渐变,深色已适配为 `#1a1d2e → #232840`),使用方可覆盖该变量
568
- 11. **非增量模式的代码块复用**:内容变化时会尽量复用旧 `<pre>`(先按「语言 + 原文」精确匹配,再按同语言的文档顺序兼容内容变更)以避开工具栏重建闪烁。顺序复用是启发式,因此**同一语言的代码块数量发生变化(中间插块/删块)时会全量放弃该级复用**,该语言的变动块会被重建(短暂闪烁但不会把 A 块的折叠态/滚动位带到 B 块);需要严格不闪烁请使用 `incremental`
569
- 12. **链接校验默认为 `defaultValidateLink`(markdown-it 名单 + 放行内联 svg)**:markdown-it 原生只放行 `data:image/(gif|png|jpeg|webp);`,`data:image/svg+xml` 会被判为不安全链接丢弃,表现为正文里原样显示 `![...](data:...)` 源码而不是图片(图标、图表、AI 产出的内联 svg 全中招)。本库不会再出现这种退化:**不传 `validateLink` 也会接管**,在原生名单之上只额外放行内联 svg。为何还要多一层:`validateLink` 同时管图片语法和链接语法,`<img src="data:image/svg+xml,...">` 本身是惰性的(SVG 作为图片不执行脚本、不加载外部资源),但 `<a href="data:image/svg+xml,...">` 被导航时 SVG 会作为**文档**渲染、脚本可执行;所以默认校验会解码 data URI(base64 形态也先 `atob`)并拒掉带 `<script>` / `on*=` 事件属性 / `<!ENTITY>` / `foreignObject` 特征的 svg,解不出来的也当不可信处理。**这是纵深防御的启发式过滤,不是沙箱**:渲染真正不可信的内容仍应靠 `html={false}`(第 13 条)。传入 `validateLink` 则**整体接管**,要保留库默认可写 `validateLink={(url) => myRule(url) || defaultValidateLink(url)}`;不要把“方便”写成 `() => true`,那会连带放行 `javascript:`/`vbscript:`/`file:`
570
- 13. **`validateLink` 与 `html` 的分工**:`validateLink` 只作用于 Markdown 的链接/图片语法;`html=true` 放行的原生 HTML 块是**原样透传**的,不经过 `validateLink`(所以 `<img src="data:image/svg+xml,...">` 本来就能显示,但反过来 `<a href="javascript:...">` 也不受它拦截)。渲染不可信内容时真正的防护是 `html={false}`,此时原生标签会被转义成文本展示(示例页第 7 区);也别把超长 base64 塞进这类会被转义展示的 HTML 里,正文会被刷屏
571
- 14. **流式输出中的半截围栏不会去渲染图表**:markdown-it 对「有开头没结尾」的 ``` 围栏会吞到输入末尾并照常输出一个完整 code token(`haveEndMarker` 不记入 token),因此残缺的 Mermaid 源码会被当成写完的图表送 `mermaid.render`——实测一份文档的 180 个前缀里有 69 个会报错。现在渲染阶段会给未闭合的 `<code>` 追加 `md-fence-open` 标记:带此标记的块**本轮不挂图表**,以普通代码块形式展示逐步输入的内容,围栏一旦闭合(下一帧)自动切换为图表。直接用 `markdownFormat` 拿 HTML 的使用方也会看到该标记 class,可自行判断“这段还没写完”
572
- 15. **图表挂载点跨渲染复用**:扫描(`renderMermaidWithControls`)以「代码块紧邻 + 源码指纹」认领挂载点(`data-md-mermaid-src`,FNV-1a 指纹):源码没变就**完全不碰**那个节点,源码变了只往同一个 React Root 推新 props(不卸载重建),块被删除或围栏重新打开时挂载点才被卸载移除。因此图表闭合后即使后面正文继续增长,也不会重挂、不闪白、不丢缩放/全屏/折叠状态;pre 被插块挤开位置时同指纹图表会被搬回原位;非增量模式整块重建时 `<pre>` 与其图表会被一起快照再搬回。实测一份文档从 300 推到 540 字符期间:`mermaid.render` 调用 1 次、挂载点元素身份变化 0 次、SVG 消失 0 次(修复前分别为 4 次 / 8 次 / 反复交替)
573
- 16. **图片预览已换为 PhotoSwipe v5**(旧实现为 viewerjs):预览实例按点击创建、关闭动画结束后自行销毁,所以不存在“实例内部图片列表过期”问题,增量渲染重建 `<img>` 也无需通知谁;对外 API(`addExcludedSelector`/`removeExcludedSelector`/`excludedSelectors`/`isPrintPreview`)不变。能力差异:没有旋转、没有底部缩略图导航条、没有缩放百分比浮层(多图切换交给箭头/滑动/键盘);图片尚无尺寸时不传 `width/height`(由 PhotoSwipe 加载后自行测量,传 0 会把 fit/fill 缩放级别算歪),`srcset` 只采纳带 `w` 描述符的候选项(`1x, 2x` 会被误判)
574
- 17. **预览的缩放级与遮罩颜色已偏离 PhotoSwipe 默认值**:`secondaryZoomLevel`/`maxZoomLevel` 官方默认都是 `'fill'`(短边铺满),于是双击放大一到顶、滚轮再滚也不动,小图还会因 fit/fill 被 100% 封顶而直接被判为“不可缩放”(放大按钮不出现);本库改为 `fit` → `1`(原始像素)→ `2`(2 倍原始像素)。另:官方无 `minZoomLevel` 选项,下限是 `min(fit, initial, secondary)` 而 `fit` 被封顶为 1,所以图片不比视口大时 100% 就是最小、根本缩不下去(不比视口小时最小也只到“长边贴屏边”);本库在 `slide.zoomLevels.min`(公开字段)上把下限放到 `fit * 0.5`,要恢复官方行为把 `MIN_ZOOM_FACTOR_OF_FIT` 改成 1。遮罩则由官方纯黑 `#000` 改为半透明 + `backdrop-filter` 毛玻璃并跟随 `data-theme`,使用方可覆盖 `.pswp.md-image-preview` 上的 `--pswp-bg`(注意别只写 `.pswp`,特异度会输给这条);**不要覆盖 `--pswp-icon-color-secondary`**(默认中灰 #4f4f4f),放大镜/关闭/箭头的实心部分靠它与白色描边拉开反差,改成白色系会让图标糊成一块实心
575
- 18. **预览的默认排除名单已含 `pre` 与 `.mermaid-react-root`**:前者是代码块里当源码展示的图(不是插图),后者是图表挂载点内的装饰图(如标题行的 mermaid 图标)—— 它们是 UI 控件而不是文档插图,误弹全屏预览会把图表自身的交互盖掉。名单只影响「是否进预览」:命中排除的图片仍会正常派发点击事件(以前在捕获阶段 `stopImmediatePropagation` 的写法会连带屏蔽 React 绑在图表内部的事件委托,现已去掉),要用 `removeExcludedSelector('.mermaid-react-root')` 可撤回某条默认项
576
- 19. **新增两个可覆盖变量**:`--remons-markdown-mermaid-surface`(图表容器背景,浅色 `transparent` / 深色 `#2e3350`,旧实现是在全局样式里用 `!important` 盖组件的 `var`,现已收敛到变量)与 `--remons-markdown-tabs-mask`(tabs 头部横向溢出时左右的渐隐遮罩色,旧实现写死白色,深色下会变成一层白纱)
577
- 20. **圆角跟随 token(轻微视觉变更)**:`.markdown img` / `iframe` 旧代码读的是 `--remons-markdown-border-radius` —— 这个变量从未在任何地方定义,因此一直吃 `6px` 回退;现在改为跟随实际存在的 `--remons-markdown-radius-base`(默认 `8px`)。同理:图表折叠时的底部渐隐旧实现是 `transparent` 渐变到 `var(--remons-markdown-color-bg)`(浅色下也是 `transparent`)—— 遮罩从来没出现过,现在渐变到 `--remons-markdown-color-bg-white`,两套主题都可见
578
- 21. **样式不再拉远程字体**:旧版 `assets/css/index.global.less` 里有一条 Google Fonts 的 `@import`,但它不是文件首条声明,会被构建里的 CSS minifier 直接丢弃(dev 生效、prod 不生效),已删除;字体栈统一由 `--remons-markdown-font-family-base` 控制,需要自定义字体时由使用方自己 `@font-face`。alert 图标仍依赖 `iconfont` 的远程样式表(见该文件内的说明)
579
- 22. **`!important` 已收敛(覆盖变得更简单)**:`.markdown` 的标题 margin、`.remons-markdown-circle` 的圆角/阴影、tabs 按钮的边框与阴影、TOC 的 `backdrop-filter` 均不再带 `!important`,使用方可直接用同特异度规则覆盖。附带修正一个旧 bug:标题的 `margin: 6px 0 !important` 会把自己下面的 `h*:first-child { margin-top: 0 }` 压死,所以首屏第一个标题上方一直多一段 6px,现在归零真正生效
580
-
581
-
1
+ # render-markdown
2
+
3
+ 一个基于 React 的 Markdown 渲染组件,支持 GFM(GitHub Flavored Markdown)Tabs 标签、代码高亮、Mermaid 图表等功能。
4
+
5
+ 渲染效果
6
+ ![整体渲染效果](https://remons.cn:3008/upload/md_assets/%E6%95%B4%E4%BD%93%E6%B8%B2%E6%9F%93%E6%95%88%E6%9E%9C.png)
7
+
8
+ ## 技术方案
9
+
10
+ ### 技术栈
11
+
12
+ | 类别 | 技术选型 | 说明 |
13
+ |------|---------|------|
14
+ | 框架 | React 18 | 组件库基础框架(peer 依赖) |
15
+ | 构建工具 | Vite(库模式,ESM 单产物) | `vite.lib.config.ts` 构建库,`vite.config.ts` 跑示例页 |
16
+ | 语言 | TypeScript | 提供类型定义(`dist/index.d.ts`) |
17
+ | 样式方案 | Less + CSS Modules | 统一提取为 `dist/index.css`,需手动引入 |
18
+ | 图片预览 | PhotoSwipe v5(`photoswipe`,零依赖 ESM) | Pointer Events 手势:以指针/手指为锚点连续缩放、拖拽带惯性、双指外划与竖向拖出关闭 |
19
+ | 代码规范 | ESLint + Prettier | 统一代码风格 |
20
+ | 发布工具 | changesets | 语义化版本管理与 changelog 生成 |
21
+
22
+ ### 核心功能
23
+
24
+ - **GFM 支持**: 完整的 GitHub Flavored Markdown 渲染
25
+ - **增量渲染**: 基于 morphdom 的 DOM 差异化更新,适用于流式输出场景
26
+ - **代码高亮**: 基于 highlight.js 的代码语法高亮
27
+ - **Mermaid 图表**: 支持 Mermaid 语法渲染流程图、时序图等
28
+ - **Tab 标签页**: 支持 Tab 标签页语法
29
+ - **Alert 提示框**: 支持警告、提示等信息框
30
+ - **目录锚点**: 从标题 token 流自动采集,无需在正文写 `[[TOC]]` 指令
31
+ - **代码复制**: 一键复制代码块
32
+ - **代码折叠**: 支持代码块折叠展开
33
+ - **深色模式**: 自动适配深色/浅色主题
34
+ - **多实例安全**: 工具栏 / 图片预览 / 图表均在组件容器作用域内工作,互不干扰
35
+
36
+ ### 本地开发与示例页
37
+
38
+ ```bash
39
+ npm install
40
+ npm run dev # 打开 http://localhost:5173,示例页覆盖全部能力与修复点
41
+ npm run typecheck # tsc --noEmit
42
+ npm run build # 产出 dist/index.mjs + dist/index.css + *.d.ts
43
+ ```
44
+
45
+ ## API
46
+
47
+ ### 安装
48
+
49
+ ```bash
50
+ npm install remons-render-markdown
51
+ ```
52
+
53
+ 产物为 **ESM only**(`"type": "module"`,无 `require` 导出),请配合打包器使用。
54
+ `highlight.js`、`react`、`react-dom`、`@ant-design/icons` 为 **peer 依赖**,需由使用方自行安装。
55
+ 其余运行时依赖(`markdown-it`、`morphdom`、`photoswipe`、`@panzoom/panzoom` 等)在 `dependencies` 中声明,构建时 external,由包管理器解析。
56
+
57
+ ### 导出内容
58
+
59
+ ```typescript
60
+ // 默认导出 + 命名导出
61
+ import RenderMarkdown, {
62
+ markdownFormat,
63
+ languagesCommon,
64
+ initHighlighter,
65
+ useIncrementalRender,
66
+ MD_PERSIST_ATTR,
67
+ MermaidChart,
68
+ renderMermaid,
69
+ ThemeProvider,
70
+ useTheme,
71
+ initTheme,
72
+ addExcludedSelector,
73
+ removeExcludedSelector,
74
+ defaultValidateLink,
75
+ registerCdn,
76
+ } from 'remons-render-markdown';
77
+
78
+ // 类型
79
+ import type { RenderMarkdownProps, MarkdownPlugin, AnchorItem, ThemeMode } from 'remons-render-markdown';
80
+
81
+ // 样式(构建产物为 dist/index.css)
82
+ import 'remons-render-markdown/style.css';
83
+ ```
84
+
85
+ #### 默认导出
86
+
87
+ - **RenderMarkdown**: React 组件,用于渲染 Markdown 内容
88
+
89
+ #### 命名导出
90
+
91
+ - **markdownFormat**: Markdown 解析函数(**异步**),返回 `{ anchor: AnchorItem[], info: string }`
92
+ - **languagesCommon**: 默认支持的语言包配置(javascript, typescript, css, json, bash, xml, plaintext)
93
+ - **initHighlighter**: 注册高亮语言包的函数(必须调用,否则代码块无高亮)
94
+ - **useIncrementalRender**: 增量渲染 hook,使用 morphdom 进行 DOM 差异化更新,适用于流式输出场景
95
+ - **MD_PERSIST_ATTR**: `'data-md-persist'`,标记需跨增量渲染保留的 JS 动态节点
96
+ - **MermaidChart**: 独立图表组件(**推荐**)
97
+ - **renderMermaid**: 旧函数式用法,已 `@deprecated`,保留以兼容
98
+ - **ThemeProvider** / **useTheme** / **initTheme**: 主题相关;`initTheme()` 需在入口显式调用(库不再在 import 时产生全局副作用)
99
+ - **addExcludedSelector** / **removeExcludedSelector**: 图片预览的全局排除名单增删(默认名单已含 `pre` 与 `.mermaid-react-root`)
100
+ - **defaultValidateLink**: 本库默认链接校验(markdown-it 原生名单 + 放行无脚本特征的内联 svg),自定义 `validateLink` 时可拿它做基线复用
101
+ - **registerCdn**: 注册懒加载资源(如 mermaid)的默认 CDN 地址
102
+
103
+ ### RenderMarkdown 组件 Props
104
+
105
+ > **初始化语义**:除 `content` 外的配置项仅在组件挂载时读取一次,渲染途中修改不会生效(需变更 `key` 重新挂载)。`backTopTarget` 同样只在首次渲染时读取。
106
+
107
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
108
+ |------|------|------|--------|------|
109
+ | content | string | ✅ | - | Markdown 内容 |
110
+ | enableMermaid | boolean | ❌ | `true` | 是否把 mermaid 代码块渲染为图表 |
111
+ | showCollapseButton | boolean | ❌ | `true` | 是否显示代码块折叠按钮 |
112
+ | codeType | string | ❌ | - | 按纯代码渲染(如 `'go'`),不传则按 Markdown 渲染 |
113
+ | html | boolean | ❌ | `true` | 是否渲染源码中的原生 HTML;不可信内容请设为 `false`(增量/非增量均生效,初始化语义) |
114
+ | validateLink | (url: string) => boolean | ❌ | `defaultValidateLink` | 覆盖链接校验(初始化语义);不传则用本库默认(markdown-it 名单 + 放行内联 svg),详见《注意事项》第 12 条 |
115
+ | incremental | boolean | ❌ | `false` | 是否启用 morphdom 增量渲染 |
116
+ | incrementalThrottleMs | number | ❌ | `16` | 增量渲染节流窗口(ms) |
117
+ | toolbarDebounceMs | number | ❌ | `100` | content 变化后工具栏/图片/图表扫描的防抖延迟(ms) |
118
+ | showToc | boolean | ❌ | `false` | 是否显示目录(TOC)按钮和侧边栏 |
119
+ | showBackTop | boolean | ❌ | `false` | 是否显示返回顶部按钮 |
120
+ | backTopTarget | HTMLElement \| null | ❌ | `document.body` | 返回顶部监听的滚动容器(仅首次渲染读取) |
121
+ | footer | React.ReactNode | ❌ | - | 内容区下方的自定义底部 |
122
+ | customRenderers | MarkdownPlugin[] | ❌ | - | 自定义 markdown-it 插件,在内置插件之后按序执行 |
123
+ | transformSource | (text: string) => string | ❌ | - | 渲染前改写 mermaid 源码 |
124
+ | defaultCollapsed | boolean | ❌ | `true` | 代码块 / 图表默认折叠状态 |
125
+ | showDriverGuide | boolean | ❌ | `false` | 是否显示图表新手引导 |
126
+ | isPrintPreview | boolean | ❌ | `false` | 打印模式(跳过交互式工具栏) |
127
+ | excludedSelectors | string[] | ❌ | - | 本实例的图片预览排除容器 |
128
+ | cdn | Record<string, string> | ❌ | - | 懒加载资源地址,如 `{ mermaid: 'https://...' }` |
129
+
130
+ **已重命名的 prop(旧名仍可用,标记为 deprecated):**
131
+
132
+ | 旧名 | 新名 |
133
+ |------|------|
134
+ | `isSlotMermaid` | `enableMermaid` |
135
+ | `isShowCollapsed` | `showCollapseButton` |
136
+ | `mermaidDebounce` | `toolbarDebounceMs` |
137
+ | `useIncremental` | `incremental` |
138
+ | `chartConfig` | `transformSource` |
139
+
140
+ ### markdownFormat 函数
141
+
142
+ 解析 Markdown 内容并返回结构化数据。**异步函数**(内部等待 KaTeX CDN 就绪)。
143
+
144
+ **参数:**
145
+
146
+ - `content: string` - Markdown 文本内容
147
+ - `customPlugins?: MarkdownPlugin[]` - 自定义 markdown-it 插件
148
+ - `options?: { html?: boolean; validateLink?: (url: string) => boolean }` - `html` 默认 `true`,渲染不可信内容时设为 `false`;`validateLink` 不传则用本库默认 `defaultValidateLink`(放行无脚本特征的 `data:image/svg+xml`),传入则整体接管
149
+
150
+ **返回值(Promise):**
151
+
152
+ ```typescript
153
+ {
154
+ anchor: Array<{
155
+ title: string; // 标题文本(纯文本)
156
+ href: string; // 锚点链接(与标题 DOM 上的 id 一致)
157
+ nodeName: string; // 节点名称,如 "H1", "H2"
158
+ nodeTitle: string; // 完整节点 HTML(已转义,勿直接 innerHTML 展示)
159
+ children: Array<...>; // 子标题
160
+ }>;
161
+ info: string; // 渲染后的 HTML 字符串
162
+ }
163
+ ```
164
+
165
+ **降级行为:** KaTeX 加载失败时不抛错,公式降级为原文本并 `console.warn`;未注册高亮语言时代码块仅不染色,不影响渲染。
166
+
167
+ ### initHighlighter 函数
168
+
169
+ 注册自定义的代码高亮语言。`highlight.js` 为 peer + external 依赖,库不会自带语言包,**必须调用一次**才会高亮。
170
+
171
+ **参数:**
172
+
173
+ - `languages: Record<string, any>` - 语言包对象,格式为 `{ languageName: languageModule }`
174
+
175
+ **示例:**
176
+
177
+ ```typescript
178
+ import javascript from 'highlight.js/lib/languages/javascript';
179
+ import python from 'highlight.js/lib/languages/python';
180
+
181
+ initHighlighter({
182
+ javascript,
183
+ python,
184
+ });
185
+ ```
186
+
187
+ ## 使用方式
188
+
189
+ ### 基础用法
190
+
191
+ ```tsx
192
+ import RenderMarkdown, { initHighlighter, languagesCommon, ThemeProvider } from 'remons-render-markdown';
193
+ import 'remons-render-markdown/style.css';
194
+
195
+ // 入口注册一次即可
196
+ initHighlighter(languagesCommon);
197
+
198
+ function App() {
199
+ const markdownContent = `
200
+ # Hello World
201
+
202
+ This is a **Markdown** renderer.
203
+
204
+ \`\`\`javascript
205
+ console.log('Hello');
206
+ \`\`\`
207
+ `;
208
+
209
+ return <RenderMarkdown content={markdownContent} />;
210
+ }
211
+
212
+ export default () => (
213
+ <ThemeProvider>
214
+ <App />
215
+ </ThemeProvider>
216
+ );
217
+ ```
218
+
219
+ ### 完整示例
220
+
221
+ ```tsx
222
+ import RenderMarkdown from 'remons-render-markdown';
223
+ import 'remons-render-markdown/style.css';
224
+
225
+ function ArticlePage() {
226
+ const content = `
227
+ # 文章标题
228
+
229
+ 这是一篇示例文章。
230
+
231
+ ## 代码示例
232
+
233
+ \`\`\`javascript
234
+ const greeting = 'Hello World';
235
+ console.log(greeting);
236
+ \`\`\`
237
+
238
+ ## Mermaid 图表
239
+
240
+ \`\`\`mermaid
241
+ graph TD
242
+ A[开始] --> B[处理]
243
+ B --> C[结束]
244
+ \`\`\`
245
+ `;
246
+
247
+ return (
248
+ <RenderMarkdown
249
+ content={content}
250
+ enableMermaid
251
+ showCollapseButton
252
+ showToc
253
+ showBackTop
254
+ footer={<span>—— 完 ——</span>}
255
+ html={false}
256
+ />
257
+ );
258
+ }
259
+ ```
260
+
261
+ ### 使用 markdownFormat 获取目录结构
262
+
263
+ ```tsx
264
+ import { useEffect, useState } from 'react';
265
+ import { markdownFormat, type AnchorItem } from 'remons-render-markdown';
266
+
267
+ function TableOfContents({ content }: { content: string }) {
268
+ const [anchor, setAnchor] = useState<AnchorItem[]>([]);
269
+
270
+ useEffect(() => {
271
+ let cancelled = false;
272
+ markdownFormat(content).then(({ anchor }) => {
273
+ if (!cancelled) setAnchor(anchor);
274
+ });
275
+ return () => { cancelled = true; };
276
+ }, [content]);
277
+
278
+ // anchor 为树形结构,无需在正文写 [[TOC]] 指令
279
+ return (
280
+ <ul>
281
+ {anchor.map((item) => (
282
+ <li key={item.href}>
283
+ <a href={`#${item.href}`}>{item.title}</a>
284
+ </li>
285
+ ))}
286
+ </ul>
287
+ );
288
+ }
289
+ ```
290
+
291
+ ### 自定义代码高亮语言
292
+
293
+ ```tsx
294
+ import RenderMarkdown, { initHighlighter, languagesCommon } from 'remons-render-markdown';
295
+ import 'remons-render-markdown/style.css';
296
+ import python from 'highlight.js/lib/languages/python';
297
+ import go from 'highlight.js/lib/languages/go';
298
+
299
+ // 注册额外的语言
300
+ initHighlighter({
301
+ ...languagesCommon, // 包含默认语言
302
+ python,
303
+ go,
304
+ });
305
+
306
+ function App() {
307
+ return (
308
+ <RenderMarkdown
309
+ content={`
310
+ \`\`\`python
311
+ def hello():
312
+ print('Hello')
313
+ \`\`\`
314
+
315
+ \`\`\`go
316
+ package main
317
+ import "fmt"
318
+ func main() {
319
+ fmt.Println("Hello")
320
+ }
321
+ \`\`\`
322
+ `}
323
+ />
324
+ );
325
+ }
326
+ ```
327
+
328
+ ### 仅渲染特定代码类型
329
+
330
+ ```tsx
331
+ import RenderMarkdown from 'remons-render-markdown';
332
+ import 'remons-render-markdown/style.css';
333
+
334
+ function CodeViewer() {
335
+ const code = `
336
+ const x = 1;
337
+ const y = 2;
338
+ console.log(x + y);
339
+ `;
340
+
341
+ return (
342
+ <RenderMarkdown
343
+ content={code}
344
+ codeType="javascript"
345
+ enableMermaid={false}
346
+ />
347
+ );
348
+ }
349
+ ```
350
+
351
+ ### 使用 useIncrementalRender 实现增量渲染
352
+
353
+ `useIncrementalRender` 适用于流式输出场景(如 AI 对话),通过 morphdom 实现高效的 DOM 差异化更新。
354
+ 一般场景直接给 `<RenderMarkdown incremental />` 即可,需自行控制容器时才直接使用该 hook。
355
+
356
+ ```tsx
357
+ import { useIncrementalRender } from 'remons-render-markdown';
358
+
359
+ function StreamingContent({ content }) {
360
+ const { anchors, hasContent, setInnerRef } = useIncrementalRender({
361
+ content,
362
+ // 可选:节流时间,默认 16ms
363
+ throttleMs: 16,
364
+ });
365
+
366
+ return (
367
+ <div>
368
+ {hasContent && <div ref={setInnerRef} />}
369
+ {anchors.length > 0 && (
370
+ <ul>
371
+ {anchors.map((item) => (
372
+ <li key={item.href}>
373
+ <a href={`#${item.href}`}>{item.title}</a>
374
+ </li>
375
+ ))}
376
+ </ul>
377
+ )}
378
+ </div>
379
+ );
380
+ }
381
+ ```
382
+
383
+ **参数:**
384
+
385
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
386
+ |------|------|------|--------|------|
387
+ | content | string | ✅ | - | Markdown 内容(**唯一响应式参数**) |
388
+ | codeType | string | ❌ | - | 代码类型,不传则按 Markdown 渲染(初始化语义) |
389
+ | customRenderers | MarkdownPlugin[] | ❌ | - | 自定义 markdown-it 插件(初始化语义) |
390
+ | throttleMs | number | ❌ | `16` | 节流窗口(ms)(初始化语义) |
391
+ | onNodeDiscarded | (el: HTMLElement) => void | ❌ | - | 节点被 diff 移除后触发(含被移除子树的子节点),用于清理 React Root 等挂载状态 |
392
+ | onPatched | () => void | ❌ | - | 每次 patch 完成后、浏览器绘制前同步触发,用于恢复运行时标记 |
393
+
394
+ **初始化语义(重要):** 除 `content` 外的所有选项仅在挂载时读取一次,渲染途中传入新值**不感知**(不会重新初始化,也不会触发重渲染)。需要切换行为时,请变更外层组件的 `key` 使其重新挂载。
395
+
396
+ **节流语义:** 真节流(leading + trailing)——首次变化立即渲染,期间最多每 `throttleMs` 渲染一次,窗口末尾的残余变化会补一次收尾渲染(而非 debounce 式的“等静默后才渲染”)。
397
+
398
+ **返回值:**
399
+
400
+ | 属性 | 类型 | 说明 |
401
+ |------|------|------|
402
+ | anchors | AnchorItem[] | 文档锚点列表 |
403
+ | hasContent | boolean | 是否有内容 |
404
+ | setInnerRef | (node: HTMLDivElement) => void | 内容容器的 ref 回调 |
405
+
406
+ **增量渲染与自定义插件(customRenderers)的配合约定:**
407
+
408
+ 1. 插件产出的容器节点(如 `:::badge` / `:::linkCard` / `<plugin-container>`)按"标签 + className"语义匹配,内容位移时不同类型的块不会被错配到旧节点上。
409
+ 2. 元素上 JS 后加的 `data-*` 属性(如初始化守卫位 `data-xxx-inited`)在 diff 后会被保留,"只初始化一次"的守卫逻辑不会在增量渲染中被重复触发。
410
+ 3. JS 动态插入的额外子节点(如 React 挂载点、收起按钮)默认会被 diff 移除;若需跨增量渲染保留,请给节点设置 `data-md-persist` 属性(见导出常量 `MD_PERSIST_ATTR`)。
411
+ 4. 被 diff 移除的节点会触发 `onNodeDiscarded`,请在此卸载挂载在其上的 React Root,避免内存泄漏。
412
+
413
+ ### 关于语法
414
+
415
+ #### mermaid 图表
416
+
417
+ ```code
418
+ ---
419
+ title: 图表标题
420
+ ---
421
+ ......... 图表内容
422
+ ```
423
+
424
+ - 折叠图
425
+ ![mermaid 渲染图](https://remons.cn:3008/upload/md_assets/mermaid%20%E7%BC%A9%E7%95%A5%E5%9B%BE.png)
426
+
427
+ - 缩略图
428
+ ![mermaid 渲染图](https://remons.cn:3008/upload/md_assets/mermaid%20%E5%B1%95%E5%BC%80%E5%9B%BE.png)
429
+ #### Tabs 标签页
430
+
431
+ 依赖于 `@mdit/plugin-tab` 插件,参考 [mdit-plugin-tab](https://mdit-plugins.github.io/zh/tab.html) 的文档。
432
+ *暂不支持 tabs 嵌套*
433
+
434
+ 语法示例:
435
+
436
+ ```markdown
437
+ :::markdown-tabs
438
+
439
+ @tab:active tab1
440
+ ```javascript
441
+ console.log('Hello');
442
+ ```
443
+
444
+ @tab tab2
445
+ ```typescript
446
+ console.log('World');
447
+ ```
448
+ :::
449
+ ```
450
+ ![tabs 渲染图](https://remons.cn:3008/upload/md_assets/tabs.png)
451
+
452
+ #### Alert 提示框
453
+
454
+ 语法示例:
455
+
456
+ ```markdown
457
+ > [!warning]
458
+ > 我是一个警告信息
459
+ ```
460
+
461
+ ![alert 渲染图](https://remons.cn:3008/upload/md_assets/alert.png)
462
+
463
+ #### MermaidChart 组件(推荐)/ renderMermaid 方法(已废弃)
464
+
465
+ 独立渲染 Mermaid 图表,支持缩放、全屏、下载、源码查看等功能。
466
+
467
+ ```tsx
468
+ import { MermaidChart } from 'remons-render-markdown';
469
+ import 'remons-render-markdown/style.css';
470
+
471
+ function MermaidComponent() {
472
+ const mermaidCode = `
473
+ ---
474
+ title: 流程图示例
475
+ ---
476
+ graph TD
477
+ A[开始] --> B[处理]
478
+ B --> C[结束]
479
+ `;
480
+
481
+ // 组件形式:元素类型稳定,父组件重渲染不会卸载重建子树
482
+ return <MermaidChart source={mermaidCode} showSourceView showCollapse />;
483
+ }
484
+ ```
485
+
486
+ 旧写法 `renderMermaid({ source })` 仍可用,但已标 `@deprecated`:它每次调用都返回新的元素类型,导致父组件重渲染时子树完整卸载重建(缩放/折叠/全屏状态丢失)。
487
+
488
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
489
+ |------|------|------|--------|------|
490
+ | source | string | ✅ | - | Mermaid 代码字符串,支持设置标题 |
491
+ | debounceMs | number | ❌ | `300` | 图表渲染的 debounce 时间(毫秒) |
492
+ | enablePanzoom | boolean | ❌ | `true` | 是否启用缩放和平移 |
493
+ | showDownload | boolean | ❌ | `true` | 是否显示下载按钮(SVG/PNG) |
494
+ | showSourceView | boolean | ❌ | `false` | 是否显示源码查看按钮和工具栏 |
495
+ | showCollapse | boolean | ❌ | `false` | 是否显示折叠/展开按钮 |
496
+ | defaultCollapsed | boolean | ❌ | `true` | 默认折叠状态 |
497
+ | className | string | ❌ | `""` | 自定义样式类名 |
498
+ | minHeight | number | ❌ | `200` | 最小高度(像素) |
499
+ | securityLevel | `'strict' \| 'loose'` | ❌ | `'strict'` | mermaid 安全级别;`strict` 禁用图内点击事件与原始 HTML,仅完全信任图源时才用 `loose` |
500
+ | transformSource | (text: string) => string | ❌ | - | 渲染前改写源码(初始化语义);旧名 `chartConfig` 仍可用 |
501
+ | cdn | Record<string, string> | ❌ | - | 自定义 mermaid 库地址(mermaid 本体仍走 CDN 懒加载,全局仅加载一次) |
502
+ | showDriverGuide | boolean | ❌ | `false` | 是否显示新手引导 |
503
+ | driverGuideOnce | boolean | ❌ | `true` | 引导是否每个元素只弹一次(设为 `false` 在流式重渲染下会反复弹) |
504
+ | isPrintPreview | boolean | ❌ | `false` | 打印模式 |
505
+
506
+ **功能特性:**
507
+ - 缩放:支持放大、缩小、重置视图
508
+ - 全屏:支持浏览器原生全屏模式
509
+ - 下载:支持下载 SVG 和 PNG 格式
510
+ - 源码查看:点击按钮可查看 Mermaid 源码
511
+ - 折叠/展开:支持图表的折叠和展开
512
+ - 缩略图模式:最小化时显示缩略图
513
+ - 深色模式:自动适配主题
514
+
515
+ ### 主题(ThemeProvider / useTheme / initTheme)
516
+
517
+ 库不再在 `import` 时修改全局 `data-theme`(避免引入隐式副作用)。需要跟随外部主题切换时:
518
+
519
+ ```tsx
520
+ import { ThemeProvider, useTheme, initTheme, type ThemeMode } from 'remons-render-markdown';
521
+
522
+ // 1)入口(main.tsx)显式同步一次已存储的主题
523
+ initTheme();
524
+
525
+ // 2)应用外层包 Provider
526
+ <ThemeProvider><App /></ThemeProvider>;
527
+
528
+ // 3)任意子组件内读写模式:light | dark | system
529
+ const { mode, theme, isDark, setMode } = useTheme();
530
+ setMode('dark');
531
+ ```
532
+
533
+ 防 FOUC:可在 `index.html` 内联脚本里提前给 `<html>` 写 `data-theme`(读 `localStorage['app-theme-mode']`),示例页即采用此方式。
534
+
535
+ ### 图片预览排除名单
536
+
537
+ ```tsx
538
+ import { addExcludedSelector, removeExcludedSelector } from 'remons-render-markdown';
539
+
540
+ // 默认全局名单:`pre`(代码块里当源码展示的图)与 `.mermaid-react-root`(图表挂载点里的装饰图,
541
+ // 如标题行的 mermaid 图标)—— 它们是 UI 控件而不是文档插图,不该弹出全屏预览
542
+
543
+ // 全局:处于这些容器内的图片不进入预览画廊
544
+ addExcludedSelector('.copy-password-container');
545
+ removeExcludedSelector('.copy-password-container'); // 可撤销,避免只增不减的隐式泄漏
546
+
547
+ // 实例级:仅影响当前组件(该 prop 每次渲染实时生效,中途变更无需重挂载)
548
+ <RenderMarkdown content={md} excludedSelectors={['.avatar-group']} />;
549
+ ```
550
+
551
+ 预览基于 PhotoSwipe:桌面滚轮即以指针位置为锚点缩放,双击放大到原始像素(可继续放大到 2 倍),未放大时拖拽平移带惯性;
552
+ 触屏单指拖拽有惯性、双指外划或竖向拖出关闭,单击图片切回/放大、点背景关闭;Esc 关闭、←/→ 切换多图。
553
+ 遮罩默认是半透明 + 毛玻璃(不是 PhotoSwipe 自带的纯黑),并随 `data-theme` 切换。
554
+ 样式(`.pswp` 及其 CSS 变量 `--pswp-bg`、`--pswp-icon-color` 等)已打包进 `style.css`,根节点带 `md-image-preview` class,直接覆盖 `--pswp-bg` 即可改遮罩颜色。
555
+
556
+ ## 注意事项
557
+
558
+ 1. **样式引入**:务必引入 `remons-render-markdown/style.css`(实际路径 `dist/index.css`)以确保正确的样式渲染
559
+ 2. **ESM only**:产物无 CommonJS 入口;在 Next.js 等 SSR 项目中需作为客户端组件使用(`'use client'`)
560
+ 3. **代码高亮**:`highlight.js` 为 peer 依赖,必须调用 `initHighlighter(...)` 注册语言(库不自带语言包)
561
+ 4. **Mermaid 支持**:`enableMermaid`(旧名 `isSlotMermaid`)为 `true` 时自动将 mermaid 代码块渲染为图表;mermaid 库走 CDN 懒加载,可用 `cdn={{ mermaid: '...' }}` 或 `registerCdn` 指定地址,**未配置且加载失败时会报错而非静默**。图表插在代码块 `<pre>` 的**后面**(作为兄弟节点),`<pre>` 本身保留在 DOM 里、仅由 `pre:has(code.mermaid-render-noCode){display:none}` 收起——它是增量 diff 认领该块的锚点,自定义样式时不要把它从 DOM 里移除
562
+ 5. **安全默认值**:mermaid `securityLevel` 默认 `strict`;外部链接自动 `target="_blank"` + `rel="noopener"`;渲染不可信内容(AI 输出/用户上传)时请设 `html={false}` 关闭原生 HTML(该开关在增量与非增量两种模式下都会透传给 markdown-it)
563
+ 6. **初始化语义**:除 `content` 外的 props 均仅在挂载时读取一次,途中变更需改 `key` 重挂载
564
+ 7. **多实例**:工具栏/图片预览/图表均限定在本实例容器作用域,卸载时会回收 React Root 与打开中的预览遮罩;`fullscreenchange` 为页面级事件,图表只响应自身的全屏进出,退出全屏时还原进入前的展开状态(不影响其它图表)
565
+ 8. **性能**:Mermaid 渲染延迟执行(`toolbarDebounceMs`,默认 100ms)以优化首屏
566
+ 9. **视图位姿**:图表重渲染(流式输出、主题切换)与折叠/展开**不再重置**用户的缩放/平移(`noBind` + `bind/destroy` 只切换事件绑定);进入/退出全屏会重建视图,属预期
567
+ 10. **全屏背景可定制**:由 CSS 变量 `--remons-markdown-mermaid-fullscreen-bg` 控制(浅色为渐变,深色已适配为 `#1a1d2e → #232840`),使用方可覆盖该变量
568
+ 11. **非增量模式的代码块复用**:内容变化时会尽量复用旧 `<pre>`(先按「语言 + 原文」精确匹配,再按同语言的文档顺序兼容内容变更)以避开工具栏重建闪烁。顺序复用是启发式,因此**同一语言的代码块数量发生变化(中间插块/删块)时会全量放弃该级复用**,该语言的变动块会被重建(短暂闪烁但不会把 A 块的折叠态/滚动位带到 B 块);需要严格不闪烁请使用 `incremental`
569
+ 12. **链接校验默认为 `defaultValidateLink`(markdown-it 名单 + 放行内联 svg)**:markdown-it 原生只放行 `data:image/(gif|png|jpeg|webp);`,`data:image/svg+xml` 会被判为不安全链接丢弃,表现为正文里原样显示 `![...](data:...)` 源码而不是图片(图标、图表、AI 产出的内联 svg 全中招)。本库不会再出现这种退化:**不传 `validateLink` 也会接管**,在原生名单之上只额外放行内联 svg。为何还要多一层:`validateLink` 同时管图片语法和链接语法,`<img src="data:image/svg+xml,...">` 本身是惰性的(SVG 作为图片不执行脚本、不加载外部资源),但 `<a href="data:image/svg+xml,...">` 被导航时 SVG 会作为**文档**渲染、脚本可执行;所以默认校验会解码 data URI(base64 形态也先 `atob`)并拒掉带 `<script>` / `on*=` 事件属性 / `<!ENTITY>` / `foreignObject` 特征的 svg,解不出来的也当不可信处理。**这是纵深防御的启发式过滤,不是沙箱**:渲染真正不可信的内容仍应靠 `html={false}`(第 13 条)。传入 `validateLink` 则**整体接管**,要保留库默认可写 `validateLink={(url) => myRule(url) || defaultValidateLink(url)}`;不要把“方便”写成 `() => true`,那会连带放行 `javascript:`/`vbscript:`/`file:`
570
+ 13. **`validateLink` 与 `html` 的分工**:`validateLink` 只作用于 Markdown 的链接/图片语法;`html=true` 放行的原生 HTML 块是**原样透传**的,不经过 `validateLink`(所以 `<img src="data:image/svg+xml,...">` 本来就能显示,但反过来 `<a href="javascript:...">` 也不受它拦截)。渲染不可信内容时真正的防护是 `html={false}`,此时原生标签会被转义成文本展示(示例页第 7 区);也别把超长 base64 塞进这类会被转义展示的 HTML 里,正文会被刷屏
571
+ 14. **流式输出中的半截围栏不会去渲染图表**:markdown-it 对「有开头没结尾」的 ``` 围栏会吞到输入末尾并照常输出一个完整 code token(`haveEndMarker` 不记入 token),因此残缺的 Mermaid 源码会被当成写完的图表送 `mermaid.render`——实测一份文档的 180 个前缀里有 69 个会报错。现在渲染阶段会给未闭合的 `<code>` 追加 `md-fence-open` 标记:带此标记的块**本轮不挂图表**,以普通代码块形式展示逐步输入的内容,围栏一旦闭合(下一帧)自动切换为图表。直接用 `markdownFormat` 拿 HTML 的使用方也会看到该标记 class,可自行判断“这段还没写完”
572
+ 15. **图表挂载点跨渲染复用**:扫描(`renderMermaidWithControls`)以「代码块紧邻 + 源码指纹」认领挂载点(`data-md-mermaid-src`,FNV-1a 指纹):源码没变就**完全不碰**那个节点,源码变了只往同一个 React Root 推新 props(不卸载重建),块被删除或围栏重新打开时挂载点才被卸载移除。因此图表闭合后即使后面正文继续增长,也不会重挂、不闪白、不丢缩放/全屏/折叠状态;pre 被插块挤开位置时同指纹图表会被搬回原位;非增量模式整块重建时 `<pre>` 与其图表会被一起快照再搬回。实测一份文档从 300 推到 540 字符期间:`mermaid.render` 调用 1 次、挂载点元素身份变化 0 次、SVG 消失 0 次(修复前分别为 4 次 / 8 次 / 反复交替)
573
+ 16. **图片预览已换为 PhotoSwipe v5**(旧实现为 viewerjs):预览实例按点击创建、关闭动画结束后自行销毁,所以不存在“实例内部图片列表过期”问题,增量渲染重建 `<img>` 也无需通知谁;对外 API(`addExcludedSelector`/`removeExcludedSelector`/`excludedSelectors`/`isPrintPreview`)不变。能力差异:没有旋转、没有底部缩略图导航条、没有缩放百分比浮层(多图切换交给箭头/滑动/键盘);图片尚无尺寸时不传 `width/height`(由 PhotoSwipe 加载后自行测量,传 0 会把 fit/fill 缩放级别算歪),`srcset` 只采纳带 `w` 描述符的候选项(`1x, 2x` 会被误判)
574
+ 17. **预览的缩放级与遮罩颜色已偏离 PhotoSwipe 默认值**:`secondaryZoomLevel`/`maxZoomLevel` 官方默认都是 `'fill'`(短边铺满),于是双击放大一到顶、滚轮再滚也不动,小图还会因 fit/fill 被 100% 封顶而直接被判为“不可缩放”(放大按钮不出现);本库改为 `fit` → `1`(原始像素)→ `2`(2 倍原始像素)。另:官方无 `minZoomLevel` 选项,下限是 `min(fit, initial, secondary)` 而 `fit` 被封顶为 1,所以图片不比视口大时 100% 就是最小、根本缩不下去(不比视口小时最小也只到“长边贴屏边”);本库在 `slide.zoomLevels.min`(公开字段)上把下限放到 `fit * 0.5`,要恢复官方行为把 `MIN_ZOOM_FACTOR_OF_FIT` 改成 1。遮罩则由官方纯黑 `#000` 改为半透明 + `backdrop-filter` 毛玻璃并跟随 `data-theme`,使用方可覆盖 `.pswp.md-image-preview` 上的 `--pswp-bg`(注意别只写 `.pswp`,特异度会输给这条);**不要覆盖 `--pswp-icon-color-secondary`**(默认中灰 #4f4f4f),放大镜/关闭/箭头的实心部分靠它与白色描边拉开反差,改成白色系会让图标糊成一块实心
575
+ 18. **预览的默认排除名单已含 `pre` 与 `.mermaid-react-root`**:前者是代码块里当源码展示的图(不是插图),后者是图表挂载点内的装饰图(如标题行的 mermaid 图标)—— 它们是 UI 控件而不是文档插图,误弹全屏预览会把图表自身的交互盖掉。名单只影响「是否进预览」:命中排除的图片仍会正常派发点击事件(以前在捕获阶段 `stopImmediatePropagation` 的写法会连带屏蔽 React 绑在图表内部的事件委托,现已去掉),要用 `removeExcludedSelector('.mermaid-react-root')` 可撤回某条默认项
576
+ 19. **新增两个可覆盖变量**:`--remons-markdown-mermaid-surface`(图表容器背景,浅色 `transparent` / 深色 `#2e3350`,旧实现是在全局样式里用 `!important` 盖组件的 `var`,现已收敛到变量)与 `--remons-markdown-tabs-mask`(tabs 头部横向溢出时左右的渐隐遮罩色,旧实现写死白色,深色下会变成一层白纱)
577
+ 20. **圆角跟随 token(轻微视觉变更)**:`.markdown img` / `iframe` 旧代码读的是 `--remons-markdown-border-radius` —— 这个变量从未在任何地方定义,因此一直吃 `6px` 回退;现在改为跟随实际存在的 `--remons-markdown-radius-base`(默认 `8px`)。同理:图表折叠时的底部渐隐旧实现是 `transparent` 渐变到 `var(--remons-markdown-color-bg)`(浅色下也是 `transparent`)—— 遮罩从来没出现过,现在渐变到 `--remons-markdown-color-bg-white`,两套主题都可见
578
+ 21. **样式不再拉远程字体**:旧版 `assets/css/index.global.less` 里有一条 Google Fonts 的 `@import`,但它不是文件首条声明,会被构建里的 CSS minifier 直接丢弃(dev 生效、prod 不生效),已删除;字体栈统一由 `--remons-markdown-font-family-base` 控制,需要自定义字体时由使用方自己 `@font-face`。alert 图标仍依赖 `iconfont` 的远程样式表(见该文件内的说明)
579
+ 22. **`!important` 已收敛(覆盖变得更简单)**:`.markdown` 的标题 margin、`.remons-markdown-circle` 的圆角/阴影、tabs 按钮的边框与阴影、TOC 的 `backdrop-filter` 均不再带 `!important`,使用方可直接用同特异度规则覆盖。附带修正一个旧 bug:标题的 `margin: 6px 0 !important` 会把自己下面的 `h*:first-child { margin-top: 0 }` 压死,所以首屏第一个标题上方一直多一段 6px,现在归零真正生效
580
+
581
+