Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .agents/codemap/modules/avalonia-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
module: avalonia-extensions
sources: ["src/StreamMark.Highlighting.TextMate/**", "src/StreamMark.RaTeX/**", "src/StreamMark.Mermaid/**"]
commit: e01b99a
updated: 2026-10-16
updated: 2026-10-18
---
# avalonia-extensions(三个渲染扩展包:TextMate 代码高亮 / RaTeX 公式 / Mermaid 图表)

Expand All @@ -18,7 +18,8 @@ updated: 2026-10-16
| `TextMatePlugin` | `src/StreamMark.Highlighting.TextMate/TextMatePlugin.cs:6` | `Name => "textmate"`;`UseHighlighter(Highlighter)`(第 12 行);构造参数 `prewarm` 会同步调 `Highlighter.Prewarm`(第 14-20 行) |
| `TextMateSyntaxHighlighter` | `src/StreamMark.Highlighting.TextMate/TextMateSyntaxHighlighter.cs:17` | `ISyntaxHighlighter` 实现;`SupportsLanguage` / `CreateSession` / `GetThemeColors` / `Prewarm` / `PrewarmAsync` / `ResolveScope` |
| `MermaidPlugin` | `src/StreamMark.Mermaid/MermaidPlugin.cs:6` | `Name => "mermaid"`;`builder.AddDiagramRenderer(renderer ?? new MermaidDiagramRenderer())`(第 10 行,**每次插件实例新建渲染器**,没有 `Shared`) |
| `MermaidDiagramRenderer` | `src/StreamMark.Mermaid/MermaidDiagramRenderer.cs:15` | `IDiagramRenderer` 实现;`CanRender` 只认 `mermaid`(第 27 行);`Render`(第 44 行)/ `RenderAsync`(第 31 行)/ 静态 `ResolveSvg`(第 126 行)/ `CreateOptions`(第 129 行) |
| `MermaidDiagramRenderer` | `src/StreamMark.Mermaid/MermaidDiagramRenderer.cs` | `IDiagramRenderer` 实现;`CanRender` 只认 `mermaid`;`Theme`(`MermaidTheme`,默认 `MermaidJs`);`Render` / `RenderAsync` / 内部 `BuildSvg`(Mermaider → 按配色方案重涂,测试直接看最终 SVG)/ 静态 `ResolveSvg` / `CreateOptions(context)`(Native 映射)与 `CreateOptions(context, theme)` |
| `MermaidTheme` / `MermaidPalette` / `MermaidThemer` | `src/StreamMark.Mermaid/MermaidTheme.cs`、`MermaidPalette.cs`、`MermaidThemer.cs` | 配色方案:公开枚举 `MermaidJs`(暗色 dark / 亮色 default)、`MermaidJsNeutral`(亮色 neutral)、`Native`;内部 `MermaidPalette` 存 mermaid 12.2 三个主题在流程图上的实际颜色;`MermaidThemer` 负责 `Detect`(按首条语句判断图表类别)、`Apply`(用 `Surface`/`Border`/`Line`/`Default` 钉住节点连线,`Gradient=false`、`Elevation=0`)、`PaintSvg`(用 `XDocument` 按语义类重涂子图与连线标签) |
| `SvgImageRasterizer` | `src/StreamMark.Mermaid/SvgImageRasterizer.cs:13` | `ISvgRasterizer` 实现(给 `ImagePipelineOptions.SvgRasterizer` 用),与 `IDiagramRenderer` 无关 |

## Key types and functions
Expand Down Expand Up @@ -53,7 +54,7 @@ updated: 2026-10-16
- **RaTeX 排版缓存(连错误一起缓存)**:`RaTeXMathRenderer._map` + `_lru`(`RaTeXMathRenderer.cs:15-16`,键 `(latex, display)`,条目里同时存 `Layout` 与 `Error`,第 88 行);命中失败结果同样更新 LRU(第 43-49 行);排版在锁外执行,偶发重复排版被判为无害(第 52 行注释);`Clear`(第 79 行)与 `CacheCount`(第 26 行)。
- **RaTeX 的字体缓存**:KaTeX TTF 按 fontId 缓存 `GlyphTypeface`(`KaTeXFonts.Typefaces`,第 22 行),并通过私有的 `KaTeXFontCollection`(第 113 行,不注册到 `FontManager`)持有内存流;系统回退字形按**码点**缓存(`SystemFallback`,第 23、69 行)。字体对象因此比单个公式活得久。
- **RaTeX 每个 `RaTeXMathLayout` 最多缓存 4 个字号的可绘制图元**:`_plans`(`MaxCachedSizes = 4`,`RaTeXMathLayout.cs:18-20`,超出时丢最早的一个,第 93-96 行)。`GlyphRun` 在首次按某字号绘制时构建 —— 即首次绘制发生在界面线程。
- **Mermaid 没有渲染器内部缓存**:`MermaidDiagramRenderer` 无状态(只保留 `ConfigureOptions` / `ErrorReported` 两个回调,第 21、24 行);缓存由视图侧的静态 `DiagramBlockView.DiagramCache` 持有(键 `语言\n明暗\n代码`,容量 32,`Rendering/DiagramBlockView.cs:72,250-281`),RaTeX 的缓存也在渲染器里而不是视图里 —— 两者位置不一致。
- **Mermaid 没有渲染器内部缓存**:`MermaidDiagramRenderer` 无状态(只保留 `ConfigureOptions` / `ErrorReported` 两个回调,第 21、24 行);缓存由视图侧的静态 `DiagramBlockView.DiagramCache` 持有(键 = 渲染器身份 + 语言 + 明暗 + 实际传给渲染器的前景 / 底色 / 强调色 + 字体 + 字号 + 代码,容量 32,`Rendering/DiagramBlockView.cs` 的 `RenderKey`),RaTeX 的缓存也在渲染器里而不是视图里 —— 两者位置不一致。
- **进程级共享的默认渲染器**:只有 RaTeX 有 `RaTeXMathRenderer.Shared`(`RaTeXMathRenderer.cs:12`,`RaTeXPlugin` 默认用它);`MermaidPlugin` 每次 `new MermaidDiagramRenderer()`(`MermaidPlugin.cs:10`),`TextMatePlugin` 每次 `new TextMateSyntaxHighlighter()`(`TextMatePlugin.cs:10`)。

## Contracts it exposes
Expand All @@ -69,7 +70,7 @@ updated: 2026-10-16
- To add 一种代码语言 / 别名:先在 `LanguageAliases.Aliases` 里加"围栏名 → TextMateSharp language id"(`LanguageAliases.cs:11`,表里没有的名字会被当成 language id 或扩展名再去查一次,`TextMateSyntaxHighlighter.cs:140`)→ 再确认 `TextMateSharp.Grammars` 2.0.4 真的有这门语法(`Directory.Packages.props:18-19`);能力边界写在 `docs/decisions.md:32`:**Kotlin 在 Grammars 包里不存在**,TOML 被有意映射到 INI(`LanguageAliases.cs:33`)→ 最后在宿主注册时预热,`new TextMatePlugin(prewarm: ["csharp", "rust"])`(`TextMatePlugin.cs:6,14`)或 `highlighter.PrewarmAsync(theme, …)`(`TextMateSyntaxHighlighter.cs:76`)。漏掉预热的症状是首个代码块首次 `Update` 卡数百毫秒(注释 `TextMateSyntaxHighlighter.cs:14-15`);语言不存在时的症状是 `SupportsLanguage == false`,代码块按纯文本显示且完全不加载语法(`TextMateSyntaxHighlighter.cs:46-56`,测试 `TextMateHighlighterTests.cs:225` 用 `kotlin` 断言 false)。
- To add 一种图表语言:这一步**不需要改视图**——`BlockViewFactory` 只要 `FindDiagramRenderer(language)` 返回非空就把闭合的代码块换成 `DiagramBlockView`(`BlockViewFactory.cs:95`)。所以只要写一个 `IDiagramRenderer`(`CanRender` 认自己的语言名、`RenderAsync` 在后台跑、失败返回 `null`)→ `builder.AddDiagramRenderer(...)`(`Plugins/IMarkdownPlugin.cs:56`)→ 在宿主 `MarkdownServices { Plugins = [...] }` 注册(`README.md:33`)。漏掉注册的症状是代码块保持普通代码显示,不会有任何图表(`BlockViewFactory.cs:96`);把 `CanRender` 写成大小写敏感会比较少见的围栏写法漏掉(Mermaid 自己做了 `Trim()` + `OrdinalIgnoreCase`,`MermaidDiagramRenderer.cs:27`)。若要改 Mermaid 的配色 / 字体,优先用 `MermaidDiagramRenderer.ConfigureOptions`(`MermaidDiagramRenderer.cs:21`)而不是改 `CreateOptions`(第 129 行)。
- To add 一种公式语法 / 命令(例如新的 `\xxx`、新的字体):C# 侧**不需要改**——能力全在原生引擎里,`RaTeXEngine.Layout` 只是通道(`RaTeXEngine.cs:22-52`)。只有当原生返回的显示列表出现**新的条目类型或字段**时才需要在 C# 侧配合:在 `DisplayList` / `DisplayItem` 加字段(`DisplayList.cs:18`)并在 `RaTeXMathLayout.BuildPlan` 的 `switch (item.Type)` 加分支(`RaTeXMathLayout.cs:102`)。漏掉分支的症状是静默不画——`BuildPlan` 没有 `default` 分支,未知 `Type` 被直接忽略(协议本身就允许忽略未来类型,`DisplayList.cs:20`)。若要新增宿主主题化的默认颜色语义,走哨兵色(`RaTeXMathLayout.cs:16`)而不是给每条图元染色。
- To add 主题 / 配色:Mermaid 走 `CreateOptions`(`MermaidDiagramRenderer.cs:129`,`Fg`/`Bg`/`Accent`/`FontSize`/`Font`/`Tint`);TextMate 走构造函数的两上主题名(`TextMateSyntaxHighlighter.cs:29`)。两条路径都不会让已经缓存的结果失效(见下方陷阱)。
- To add 主题 / 配色:Mermaid 先选 `MermaidDiagramRenderer.Theme`(`MermaidJs` 默认 / `MermaidJsNeutral` / `Native`);要新增一套固定配色就在 `MermaidPalette.For` 加一个方案(颜色取自 mermaid.js 对应主题的实际样式表),宿主侧细调用 `ConfigureOptions`;TextMate 走构造函数的两个主题名(`TextMateSyntaxHighlighter.cs:29`)。图表缓存键包含颜色,换色会重画;渲染器自身的配置(`Theme`、`ConfigureOptions`)变化靠键里的渲染器身份区分,所以换配置要换渲染器实例。

## Invariants and traps
- **TextMateSharp 2.0.4 不是线程安全的**:`Registry` 用普通 `Dictionary` 存语法、`Grammar` 分词时惰性编译规则并写内部表、主题匹配缓存也会在分词中被写入 —— 因此对 Registry / Grammar / Theme 的**每一次**访问都必须持有 `ThemeContext.Gate`(注释 `ThemeContext.cs:13-18`),落地在第 44 行的 `Gate`、`TextMateSyntaxHighlighter.cs:91-95` 的预热锁与 `TextMateHighlightSession.cs:97` 的会话锁。**每个主题各一把锁**(明暗互不阻塞),共享锁或去掉锁的表现是随机的颜色错乱 / 崩溃而不是异常。
Expand All @@ -81,7 +82,9 @@ updated: 2026-10-16
- **CJK / emoji 不是 KaTeX 字体能的**:字体名以 `CJK-` / `Emoji-` 开头时直接跳过 KaTeX 字体走系统回退(`KaTeXFonts.cs:49-50`);系统回退先试 `FontFamily.Default` 再 `FontManager.TryMatchCharacter`(第 71-82 行),结果(含 `null`)按码点缓存,字形索引 0 视为缺字(第 52-61 行),缺字就直接跳过不画以免出豆腐块(第 25 行)。所有这些都要求 Avalonia 平台已初始化 —— 而图元只在 `Draw` 里构建(`RaTeXMathLayout.cs:56,81`),也就是界面线程首次绘制时,所以**不能**在后台线程构造 `GlyphRun`。
- **Mermaid 的 SVG 必须先展开 CSS 函数再交给 Svg.Skia**:Mermaider 0.15.1 的输出确实依赖 `color-mix(`(测试里对此有断言,`MermaidRendererTests.cs:114`),而 Svg.Skia 不支持 `color-mix()`,会画成黑色填充并丢连线(`CssColorResolver.cs:8-10`)——所以 `Render` 的顺序是 `MermaidRenderer.RenderSvg` → `ResolveSvg` → `SKSvg.FromSvg`(`MermaidDiagramRenderer.cs:67,83,89`),并且 `ResolveSvg` 之后整份文档里不应再出现 `var(` / `color-mix(`(测试 `MermaidRendererTests.cs:111`、`CssColorResolverTests.cs:36`)。混合规则:百分比归一化、预乘 alpha 在 sRGB 中插值、两者之和 <100% 时按比例降 alpha、非 sRGB 空间按 sRGB 近似(`CssColorResolver.cs:261-264,306-319`);循环变量靠 `MaxDepth = 32` 终止(第 18、77、217 行)。变量来源只解析根 `<svg style="…">` 与 `<style>` 里的 `svg { … }` 规则(第 40-52 行),内联声明优先(第 68-70 行)。
- **Svg.Skia 版本被钉死在 5.1.1,托管依赖却被抬到 5.2.3,`Svg.Animation` 例外**:`Directory.Packages.props:21` 的集中版本写的是 `Svg.Skia 5.2.3`,`StreamMark.Mermaid.csproj:14-21` 用 `VersionOverride` 把它压回 `5.1.1`,并把 `Svg.Model` / `Svg.Custom` / `ShimSkiaSharp` / `Svg.SceneGraph` 覆盖到 `5.2.3`,`Svg.Animation` 保持 `5.1.1`。原因写在 csproj 注释与 `docs/decisions.md:30`:5.2.x 的 Svg.Skia 依赖 SkiaSharp 4 / HarfBuzzSharp 14.2,与 Avalonia 12.1.3(SkiaSharp 3.119.4 / HarfBuzzSharp 8.3.1.3)不兼容;而 5.1.1 的 `Svg.Model` 里 `TypeDescriptor` 用法会产生 IL2026,5.2.3 已修。Svg.Animation 5.2.x 依赖 SkiaSharp 4,所以不能跟着升。**把 `VersionOverride` 删掉就会拿 5.2.3 的 Svg.Skia,表现是原生 Skia 库冲突 / 崩溃,而不是编译错误。**
- **图表缓存键不含前景色 / 强调色 / 字号**:`_renderKey = $"{code.Language}\n{style.IsDark}\n{code.Code}"`(`Rendering/DiagramBlockView.cs:72`),但渲染上下文还带 `Foreground` / `Accent` / `FontSize` / `FontFamily`(第 103-104 行)。宿主若在同一个 `IsDark` 下换强调色或字号,已渲染的图表(含静态 `DiagramCache` 里的)不会重画 —— 这是本模块与视图层交界处最容易踩的坑。
- **宿主主题色常带透明度,绝不能直接丢掉 alpha**:AtomUI 暗色的 `ColorText` 是 85% 的白、`ColorFillQuaternary`(代码块底)是 4% 的白,以前 `Opaque()` 只取 RGB,得到 `Bg = Fg = #F0F0F0`,Mermaider 把节点填充画成「前景 10% 叠在背景上」→ 白节点白字(亮色下是黑节点黑字)。现在 `DiagramBlockView.ResolveEnvironment` 把颜色叠到块背后真实的底色上(沿可视树向上叠加祖先的纯色背景,找不到时用 #1E1E1E / 白)再传给渲染器,`MermaidDiagramRenderer.CreateOptions` 对仍带透明度的输入也用 `DiagramRenderContext.Flatten` 兜底;全屏查看器的内容底色同样取这个不透明底色。回归测试:`MermaidThemeTests.NodeText_IsReadableOnTheNodeFill`、`DiagramBlockViewTests.SemiTransparentThemeColors_*`。
- **图表只在挂上可视树之后才渲染**:底色取决于祖先背景,没挂上时算不准(还会白渲染一次),`DiagramBlockView.Refresh` 在未挂载时只更新可见性,`OnAttachedToVisualTree` 再补上;缓存键里有渲染器身份(`RuntimeHelpers.GetHashCode`),同一进程里两个配置不同的渲染器(比如不同配色方案)不会互相命中缓存。
- **`MermaidJs` 配色靠两种手段,别只改其中一种**:节点 / 连线 / 文字用 Mermaider 的 `Surface` / `Border` / `Line` / `Fg` / `Muted` / `Default` 钉住(`Accent` 取节点描边色,决策节点才不会偏色);子图(面板色 + 描边,去掉标题条)和连线标签(矩形、无描边、不透明)是 Mermaider 自己的涂法,选项改不了,所以 `MermaidThemer.PaintSvg` 用 `XDocument` 按 `subgraph` / `edge-label` 语义类重涂(Mermaider 固定为 0.15.1,输出结构变了要回来看这里)。`DataPalette` 只对结构类图表(流程图 / 状态图 / 类图 / 时序图 / block)换成同一个中性灰:它们的色板只给容器自动着色,不换的话子图标题会各有各的色相;饼图、甘特图、思维导图等的色板是分类色,换了就全变灰。**色板里放蓝色(例如 #123456)会被当作 `Info` 角色色相排除、静默回退到默认的 slate**,探测色板用途时要用灰色。
- **静态图表缓存的容量淘汰不释放资源**:`DiagramCache.Add` 超容量时只 `Items.Remove` + `Order.RemoveFirst`(`Rendering/DiagramBlockView.cs:275-279`),没有调用 `DiagramResult.Dispose`,而 `SkiaPictureDiagram` 的 `SKPicture` / `SKSvg` 只由 `PictureHandle.Release` 释放(`SkiaPictureDiagram.cs:74-87`,没有终结器)—— 淘汰后的原生图元要等所有绘制操作也释放(`PictureDrawOperation.Dispose`,第 142 行)才可能归零,静态缓存本身不再持有引用。**这一条我只从代码读出,没有实测泄漏**:(unverified)。
- **`Draw` 可能晚于 `Dispose`**:自定义绘制操作在渲染线程执行,`PictureHandle` 因此做引用计数,`DiagramResult` 或场景里的绘制操作谁是最后一个持有者谁负责释放(`SkiaPictureDiagram.cs:15-18,45,50`);`Dispose` 幂等(`Dispose` 里 `Interlocked.Exchange` 再 `Release`,测试 `MermaidRendererTests.cs:208`)。
- **三个插件的实例语义不一致**:`RaTeXPlugin` 默认共享 `RaTeXMathRenderer.Shared`(`RaTeXPlugin.cs:10`,因此缓存与 `Clear()` 是进程级的,测试 `PluginSystemTests.cs:166` 就依赖这一点),`TextMatePlugin` 与 `MermaidPlugin` 每次新建(`TextMatePlugin.cs:10`、`MermaidPlugin.cs:10`)—— 后者意味着多份 `MarkdownServices` 会各自加载一遍语法、各自渲染同一张图表(只有视图侧静态缓存能省掉一部分)。
Expand Down
Loading
Loading