Skip to content

fix: Alipay real-device KaTeX fonts via CDN + streaming flicker/marker, Minifish exports, faster demo streaming - #12

Merged
Div627 merged 3 commits into
mainfrom
fix/latex
Jul 7, 2026
Merged

fix: Alipay real-device KaTeX fonts via CDN + streaming flicker/marker, Minifish exports, faster demo streaming#12
Div627 merged 3 commits into
mainfrom
fix/latex

Conversation

@Div627

@Div627 Div627 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

🤔 This is a ...

  • 🆕 New feature
  • 🐞 Bug fix
  • 📝 Site / documentation improvement
  • 📽️ Demo improvement
  • 💄 Component style improvement
  • 🤖 TypeScript definition improvement
  • 📦 Bundle size optimization
  • ⚡️ Performance optimization
  • ⭐️ Feature enhancement
  • 🌐 Internationalization
  • 🛠 Refactoring
  • 🎨 Code style optimization
  • ✅ Test Case
  • 🔀 Branch merge
  • ⏩ Workflow
  • ⌨️ Accessibility improvement
  • ❓ Other (about what?)

🔗 Related Issues

Surfaced while integrating @ant-design/x-markdown-mini@1.0.1 into a real Minifish 2.0 consumer (appId 2021006167675338) and testing KaTeX on the Alipay real device.

💡 Background and Solution

Four issues, all exposed only on real devices / real consumers (invisible in the simulator or in this repo's own examples):

  1. Alipay real-device KaTeX rendered in the system font (PingFang SC), not KaTeX. The loader tried package-local /katex-fonts/*.ttf paths and base64 data: URIs first — but on a real device my.loadFontFace only accepts a whitelisted https network font, and local/base64 paths don't resolve in a real consumer bundle (they only worked because this repo's own examples/ sync katex-fonts to the project root). Fix: ship a shared KATEX_FONT_CDN map (20 mdn.alipayobjects.com ttf URLs, CORS *), and make the Alipay branch load CDN ttf only. WeChat keeps its local-first (/miniprogram_npm/.../katex-fonts is a real 构建npm path) + CDN fallback. Consumers must add the font CDN domain to their mini-program 下载合法域名.

  2. Whole-page flicker while streaming formulas. MiniNodeRenderer.didUpdate re-registered a font-ready reflow on every streamed chunk, and the ready callback fired synchronously once fonts were cached — so the root renderer unmounted/remounted the entire tree every chunk. Fix: ensureKatexFonts() / onKatexFontsReady() (fires at most once, only when not-yet-ready) / areKatexFontsReady(); components reflow at most once, idle-gated (skip when no formula / fonts ready / mid-stream).

  3. List markers split across lines while streaming. Per-char streaming animation broke 1. into two <text> boxes; inside the 28rpx-wide .md-list-marker flex item they wrapped. Fix: exclude markers from the per-char animation branch (render atomically, matching WeChat).

  4. <markdown> failed to load under Minifish. Minifish honors package.json#exports, and Node's * subpath glob doesn't auto-append extensions, so .../es/Markdown/index (no extension, as usingComponents requires) didn't resolve. Fix: add explicit ./es/Markdown/index + ./es/MiniNodeRenderer/index.js maps.

Also: the loader is now externalized to dist/shared/loadKatexFonts.js (shipped once per package root, like flattenInline.js) instead of inlined into every component wrapper; the base64 katex-font-data.js generator and the Alipay-root ttf copy are removed (smaller package). Demo streaming pace was sped up to a realistic LLM cadence (default chunkSize 1→4, interval 120→30ms).

Version bump + changelog + republishing the WeChat example fixture are intentionally left to the separate release step.

📝 Change Log

Language Changelog
🇺🇸 English Alipay real-device KaTeX now loads fonts from a whitelisted CDN — add the font CDN domain (mdn.alipayobjects.com) to your mini-program download allowlist. Fixed whole-page flicker and split list markers while streaming formulas. Added ./es/Markdown/index and ./es/MiniNodeRenderer/index export subpaths so bundlers that honor package.json#exports (e.g. Minifish) can resolve the components. Smaller package (removed inlined base64 fonts; loader shipped once per root).
🇨🇳 Chinese 支付宝真机 KaTeX 改为从白名单 CDN 加载字体——需在小程序「下载合法域名」中加入字体 CDN 域名(mdn.alipayobjects.com。修复流式渲染公式时整页闪动、以及列表序号(如 1.)换行的问题。新增 ./es/Markdown/index./es/MiniNodeRenderer/index 导出子路径,使尊重 package.json#exports 的构建工具(如 Minifish)能正确解析组件。包体减小(移除内联 base64 字体;loader 每个包根仅发一份)。

Summary by CodeRabbit

  • 新功能

    • 优化了流式内容的展示节奏,默认会更快地分批呈现文本。
    • 增加了对更多模块入口的支持,便于按需引入组件。
  • Bug 修复

    • 改进了公式字体加载与渲染时机,减少闪烁和重复重排。
    • 修复了列表标记在逐字动画下可能被拆分换行的问题。
    • 调整了字体相关资源的发布方式,提升不同平台的兼容性。

Div627 and others added 3 commits July 7, 2026 15:17
Speed up the simulated SSE stream player used by both alipay/wechat
component + JS demo pages: default chunkSize 1→4, interval 120→30ms, and
newline/punctuation pauses +180/+100 → +40/+20. The demo now streams at a
realistic LLM cadence instead of a slow per-character crawl. Both
integration pages inherit it (neither passes overrides).

Update the two characterization tests that locked the old
"one char / 120ms" contract to assert the new brisk defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…flicker

Real-device (Minifish 2.0 consumer) findings: the package's local
/katex-fonts/*.ttf paths and base64 data URIs do NOT resolve on Alipay真机
in a real consumer app (they only worked in this repo's own examples, which
sync katex-fonts to the project root). loadFontFace on device only accepts a
whitelisted https network font.

loadKatexFonts.ts:
- Add shared KATEX_FONT_CDN map (20 mdn.alipayobjects.com ttf URLs).
- Alipay branch = CDN ttf only; drop ALIPAY_LOCAL_BASES, loadInlineFontData
  and the base64 katex-font-data.js generator (smaller package).
- WeChat keeps local-first (its /miniprogram_npm/.../katex-fonts is a real
  resolvable 构建npm path) + shared CDN fallback.
- Replace loadKatexFonts(onReady) with ensureKatexFonts() / onKatexFontsReady()
  (fires at most once, only when not-yet-ready) / areKatexFontsReady().

Flicker: Alipay Markdown/MiniNodeRenderer now reflow at most once, gated on
idle (skip when no formula / fonts already ready / mid-stream), instead of
re-registering a reflow on every streamed chunk — kills the whole-page flash
during streaming. Add ensureKatexFonts() to WeChat MiniNodeRenderer for
JS-接入 parity (rich-text resolves KaTeX fonts via loadFontFace).

Build: externalize loadKatexFonts.js as dist/shared/loadKatexFonts.js (like
flattenInline.js) so the 11KB loader is shipped once per root, not inlined
into every component wrapper. Ship ttf to miniprogram_dist only.

exports: add no-extension subpath maps ./es/Markdown/index and
./es/MiniNodeRenderer/index → .js so Minifish (which honors package.json
exports; Node's * glob won't auto-append extensions) can load <markdown>.

Consumers must whitelist the font CDN domain in 小程序下载合法域名.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The streaming text animation splits a text leaf into per-character <text>
boxes. For a list marker ("1." / "•") this created a line-break opportunity
between "1" and ".", and since .md-list-marker is a 28rpx-wide flex item,
the two char boxes overflowed and wrapped to separate lines.

Markers are structural atomic text, not typed content, so exclude them from
the per-char animation branch (gate on !u.isMarker(node)) — they now render
as a single <text>, matching WeChat's atomic isListItem marker rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 239d8fbd-134f-4a61-94aa-864ae8a6d3ef

📥 Commits

Reviewing files that changed from the base of the PR and between 154f80a and 0e0424d.

📒 Files selected for processing (18)
  • examples/alipay/pages/streaming.js
  • examples/wechat/pages/streaming.js
  • package.json
  • scripts/copy-component-assets.mjs
  • scripts/copy-miniprogram-dist.mjs
  • scripts/prepare-publish.mjs
  • src/__tests__/alipayInlineLayout.test.ts
  • src/__tests__/examplesStreaming.test.ts
  • src/__tests__/platformStyles.test.ts
  • src/__tests__/plugins.test.ts
  • src/components/alipay/Markdown/index.ts
  • src/components/alipay/MiniNodeRenderer/index.axml
  • src/components/alipay/MiniNodeRenderer/index.sjs
  • src/components/alipay/MiniNodeRenderer/index.ts
  • src/components/shared/loadKatexFonts.ts
  • src/components/wechat/Markdown/index.ts
  • src/components/wechat/MiniNodeRenderer/index.ts
  • tsup.config.ts

📝 Walkthrough

Walkthrough

本次变更重构了 KaTeX 字体加载机制,从内联 base64/本地字体方案改为基于 CDN 映射与就绪回调(ensureKatexFonts/areKatexFontsReady/onKatexFontsReady)的方案,并为支付宝/微信 Markdown 与 MiniNodeRenderer 组件引入重排门控逻辑;同时新增列表 marker 动画例外、调整流式播放默认节奏、更新构建脚本与 exports 映射。

Changes

KaTeX 字体加载重构

Layer / File(s) Summary
共享字体加载器重构
src/components/shared/loadKatexFonts.ts
移除内联 base64/本地字体逻辑,改用 KATEX_FONT_CDN 映射,拆分 resolveApi/startLoading/flushReadyCallbacks,新增 ensureKatexFonts、areKatexFontsReady、onKatexFontsReady 导出。
支付宝 Markdown 重排门控
src/components/alipay/Markdown/index.ts
新增 hasKatexNodes 检测与实例级重排状态(_katexReflowed/_katexReflowArmed/_isStreaming),通过 _maybeKatexReflow 统一控制字体就绪后重排,仅一次且流式期间跳过。
支付宝 MiniNodeRenderer 重排门控
src/components/alipay/MiniNodeRenderer/index.ts
改用共享字体就绪 API,新增 _maybeKatexReflow 方法与生命周期集成,新增 didUnmount 清理定时器。
微信端字体注册接入
src/components/wechat/Markdown/index.ts, src/components/wechat/MiniNodeRenderer/index.ts
改用 ensureKatexFonts 替代 loadKatexFonts,新增 hasKatexNodes 检测并在生命周期中触发注册。
测试与构建脚本更新
src/__tests__/platformStyles.test.ts, src/__tests__/plugins.test.ts, scripts/copy-component-assets.mjs, scripts/copy-miniprogram-dist.mjs, tsup.config.ts
测试断言更新为验证新字体加载/重排逻辑;构建脚本移除 katex-font-data.js 生成,仅微信端复制 ttf 字体并复制 loadKatexFonts.js;tsup 将该模块外部化并加入 shared helpers 构建。

列表 Marker 动画例外处理

Layer / File(s) Summary
Marker 判定与动画排除
src/components/alipay/MiniNodeRenderer/index.sjs, index.axml, src/__tests__/alipayInlineLayout.test.ts
新增 isMarker 判定函数并导出,AXML 逐字动画条件排除 marker 节点,新增测试用例验证。

流式播放默认节奏调整

Layer / File(s) Summary
播放器默认参数与测试
examples/alipay/pages/streaming.js, examples/wechat/pages/streaming.js, src/__tests__/examplesStreaming.test.ts
默认 chunkSize 增大、interval 缩短,降低换行/标点结尾延迟补偿,测试用例与期望帧序列同步更新。

Exports 映射补充

Layer / File(s) Summary
子路径导出映射
package.json, scripts/prepare-publish.mjs
新增 Markdown/MiniNodeRenderer 无扩展名子路径到 .js 文件的显式映射,优先于通配规则。

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MarkdownComponent as Markdown 组件
  participant loadKatexFonts as loadKatexFonts.ts
  participant Platform as my/wx loadFontFace

  MarkdownComponent->>loadKatexFonts: ensureKatexFonts()
  loadKatexFonts->>Platform: resolveApi() + startLoading()
  Platform-->>loadKatexFonts: 所有字体加载完成
  loadKatexFonts->>loadKatexFonts: flushReadyCallbacks()
  MarkdownComponent->>MarkdownComponent: onPatch -> _maybeKatexReflow()
  alt 字体已就绪且非流式
    MarkdownComponent->>MarkdownComponent: _katexFontReflow() 立即重排
  else 未就绪
    MarkdownComponent->>loadKatexFonts: onKatexFontsReady(cb)
    loadKatexFonts-->>MarkdownComponent: 就绪后回调触发重排(仅一次)
  end
Loading

Poem

兔子敲键盘,字体飞上云端,
CDN 里蹦出 ttf 的光,
marker 不再逐字拆散,
流式的字符跑得更欢畅,
ensureKatexFonts 一声令下,
重排只做一次,恰到好处地安放 🐇✨


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes KaTeX font loading and streaming animations for Alipay and WeChat mini-programs. Key changes include switching Alipay to a whitelist of HTTPS CDN TTF fonts to resolve real-device rendering issues, removing base64 font data modules to reduce package size, and implementing a once-only, idle-gated reflow mechanism to eliminate page flickering during streaming. Additionally, it introduces explicit subpath mappings in package.json for better module resolution and prevents list markers from being split across lines during animations. The review feedback recommends executing the callback immediately in onKatexFontsReady if fonts are already loaded to avoid silent failures, and adding Array.isArray checks within the hasKatexNodes helper functions to prevent potential runtime errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +244 to +248
export function onKatexFontsReady(cb: () => void): void {
ensureKatexFonts();
if (ready) return;
readyCallbacks.push(cb);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If onKatexFontsReady is called when ready is already true, the callback is currently ignored and never executed. This violates the standard contract of 'onReady' style APIs, where listeners registered after the resource is ready should be executed immediately.

While the current components check areKatexFontsReady() before calling this, any future callers or third-party integrations that do not perform this check will experience silent failures where their callbacks are never executed.

Additionally, the flicker prevention is already fully handled by the guards inside _maybeKatexReflow (checking this._katexReflowed and areKatexFontsReady()), so making onKatexFontsReady return early without calling the callback is redundant for flicker prevention and introduces a major API footgun.

Executing the callback immediately when already ready makes the API robust and standard.

Suggested change
export function onKatexFontsReady(cb: () => void): void {
ensureKatexFonts();
if (ready) return;
readyCallbacks.push(cb);
}
export function onKatexFontsReady(cb: () => void): void {
ensureKatexFonts();
if (ready) {
callSafe(cb);
return;
}
readyCallbacks.push(cb);
}

Comment on lines +78 to +82
function hasKatexNodes(nodes: any[] | undefined): boolean {
return !!nodes && nodes.some((node: any) =>
!!node && (String((node.attrs || {}).class || '').indexOf('katex') > -1 ||
hasKatexNodes(node.children)));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime errors, it is safer to verify that nodes is actually an array using Array.isArray(nodes) before calling .some(). If node.children is ever parsed as a non-array object or other unexpected type, calling .some() directly on it would throw a TypeError.

Suggested change
function hasKatexNodes(nodes: any[] | undefined): boolean {
return !!nodes && nodes.some((node: any) =>
!!node && (String((node.attrs || {}).class || '').indexOf('katex') > -1 ||
hasKatexNodes(node.children)));
}
function hasKatexNodes(nodes: any[] | undefined): boolean {
return !!nodes && Array.isArray(nodes) && nodes.some((node: any) =>
!!node && (String((node.attrs || {}).class || '').indexOf('katex') > -1 ||
hasKatexNodes(node.children)));
}

Comment on lines +12 to +16
function hasKatexNodes(nodes: any[] | undefined): boolean {
return !!nodes && nodes.some((node: any) =>
!!node && (String((node.attrs || {}).class || '').indexOf('katex') > -1 ||
hasKatexNodes(node.children)));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime errors, it is safer to verify that nodes is actually an array using Array.isArray(nodes) before calling .some(). If node.children is ever parsed as a non-array object or other unexpected type, calling .some() directly on it would throw a TypeError.

Suggested change
function hasKatexNodes(nodes: any[] | undefined): boolean {
return !!nodes && nodes.some((node: any) =>
!!node && (String((node.attrs || {}).class || '').indexOf('katex') > -1 ||
hasKatexNodes(node.children)));
}
function hasKatexNodes(nodes: any[] | undefined): boolean {
return !!nodes && Array.isArray(nodes) && nodes.some((node: any) =>
!!node && (String((node.attrs || {}).class || '').indexOf('katex') > -1 ||
hasKatexNodes(node.children)));
}

@Div627
Div627 merged commit ddcc316 into main Jul 7, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant