feat: add Vue 3 adapter (@coseeing/see-mark/vue)#41
Open
jamliaoo wants to merge 19 commits into
Open
Conversation
Review fix: the default-slot closure now rebuilds children per invocation instead of returning one cached VNode array — a component may call slots.default() twice in one render (or re-render later), and reusing mounted VNodes violates Vue's uniqueness contract.
…SeeMark mathjax-full's AsciiMath is built on a MathJax-v2 legacy shim that calls arguments.callee during bootstrap — illegal under strict mode. Vite and esbuild convert CJS to (always-strict) ESM, so merely importing any @coseeing/see-mark entry crashed at startup; webpack consumers were unaffected because their CJS wrapper stays sloppy-mode. The AsciiMath runtime now initializes on the first conversion instead of at module scope (load-asciimath.cjs, kept as literal call-time requires via the commonjs plugin's ignore option). Importing SeeMark and rendering LaTeX/Nemeth now works under Vite; converting AsciiMath there still hits the upstream shim limitation and is documented in the README.
Browser verification caught custom `components` overrides silently not
reacting under Vite: the CJS-only build makes bundlers resolve
require('vue') to vue.cjs.js while the app imports the esm-bundler build,
so two Vue reactivity instances coexist and props tracked by one never
invalidate computeds created by the other.
The vue entry now also builds to lib/see-mark-vue.mjs with `import 'vue'`
kept intact; exports route bundlers to it (import) while Node — CJS and
ESM alike — stays on the CJS build (node condition), where AsciiMath keeps
working. AsciiMath init failures now rethrow one actionable message
covering both the ESM require gap and the strict-mode shim crash.
mathjax-full import specifiers gain explicit .js extensions so the ESM
output resolves under Node's stricter ESM rules too.
The symlinked SeeMark repo carries its own node_modules/vue (a devDependency), so esbuild inlined a second full Vue runtime into the prebundled chunk — two reactivity instances, and the components-prop toggle silently stopped reacting. resolve.dedupe forces every vue import to this app's copy (registry consumers are unaffected: npm never installs a dependency's devDependencies). Also define the __VUE_* feature flags normally injected by @vitejs/plugin-vue, silencing the esm-bundler warning.
…rift Deep review (multi-agent, adversarially verified) found three ways the Vue adapter diverged from its 'no more dangerous than React' posture, all reproduced empirically: - raw <script> in markdown EXECUTED on mount: a VNode-built script is not parser-inserted, unlike React's inert script elements and the HTML adapter's innerHTML-inert string. Now dropped from raw passthrough (<style> stays — it behaves identically in both adapters). - Vue-reserved props ref/key/ref_for/ref_key passed through from raw HTML, letting untrusted markup hijack the host component's $refs and corrupt keyed diffing. Now stripped. - htmlparser2 lowercases camelCase SVG attributes (viewBox -> viewbox), which Vue's case-sensitive SVG setAttribute renders inert — a Vue-only drift from React/HTML for raw inline SVG. Now restored via a lookup map. Also fixes the full-syntax SSR test to assert server-node identity (h1, which carries no MJX-* id) instead of mere existence, and corrects the README security section (on* was NOT the only exception).
The ESM build was added on a wrong diagnosis of the dual-Vue-runtime bug.
An isolated experiment (clean registry-style layout + real Vite build)
shows a registry consumer works fine with the CJS-only build: Vite's
optimizeDeps unifies require('vue') and the app's import 'vue' into a
single Vue instance (the bundle's reactive core appears exactly once).
The real dual-Vue cause is the playground's file:/symlink layout, where
the SeeMark repo's own vue devDependency becomes a second physical copy —
fixed by resolve.dedupe, independent of build format. Registry installs
never install a dependency's devDependencies, so they never hit it.
So /vue reverts to CommonJS-only, matching the React and HTML entries.
Lazy AsciiMath loading stays (bundlers convert even CJS to strict ESM, so
it's needed regardless of build format). The playground keeps dedupe and
regains optimizeDeps.include (Vite skips CJS pre-bundling for linked
packages). Browser-verified: custom components toggle correctly (no
dual-Vue) and the script/ref/svg security fixes remain intact.
- remove examples/vue-playground: only the Vue adapter had an example, so keep this branch focused on the library and defer the playground to a separate PR - drop the leftover .js import extensions in tex-to-mml.js (dead remnant of the reverted ESM build; the CJS bundle resolves extension-less imports) - restore .gitignore to main so staged-delivery evidence stays untracked rather than gitignored - README: remove the dangling examples/vue-playground reference, keeping the global->globalThis note inline
Trim the comments added with the Vue adapter so each states why, not how: drop Vue-internal function references, the strict-mode/arguments.callee mechanics, cloneVNode internals, and step-by-step restatements of the code. The security rationale (why on*/script/ref are dropped) is kept — it is the part that stops the guard being refactored away.
The Vue adapter targets Vite/esbuild apps, where MathJax's AsciiMath legacy shim crashes (arguments.callee under strict mode). With AsciiMath's default backtick delimiter, ordinary inline code routed to MathJax and threw at render time under Vite's dep pre-bundling — a crash on the default happy path, found by adversarial review (our playground had masked it with enableAsciimath: false). The Vue parser factory now defaults enableAsciimath to false (React/HTML keep true); backticks render as code spans out of the box, and math users opt in explicitly where it works. cross-adapter parity now sets enableAsciimath: true so it still compares one shared configuration across adapters.
jamliaoo
force-pushed
the
feat/vue-adapter
branch
from
July 14, 2026 07:22
5ac7d8f to
b5723ac
Compare
3 tasks
jamliaoo
marked this pull request as ready for review
July 17, 2026 07:41
wendyyuchensun
approved these changes
Jul 20, 2026
Collaborator
There was a problem hiding this comment.
Vue adapter 的部分 LGTM。
比較麻煩的是「MathJax AsciiMath 需要 lazy load」這個 workaround。但覺得可以後續再追加處理。
查了一輪,目前沒有其他單一 library 能像 mathjax 一樣,同時涵蓋 LaTeX → MathML、AsciiMath → MathML、MathML → SVG 三種轉換,所以整套換掉(如 KaTeX/Temml)改動會不小。
不考慮 hack 的話,感覺改動最小做法是只替換 AsciiMath → MathML 這一段,改用 mathup,其餘維持 MathJax。實測過可行:mathup 是原生 ESM、無 legacy 語法,import 階段不會踩到 Vite/esbuild 的 strict mode 問題,產出的 MathML 也能直接餵給 MathJax 的 mml2svg。
兩個後續問題事項:
- mathup 是 AsciiMath 的「方言」,不是完全相容——常用語法(分數、根號、上下標、矩陣、極限)都沒問題,但
forall、color()、stackrel()、cancel()、frac{}{}等指令不支援,會退化成逐字母輸出,對既有內容算 breaking change。 - mathup 是 ESM-only,被 rollup 打進 CJS lib 後會遇到 default export interop 問題(
require(esm)拿到的是 namespace),需要像 React converter 處理 html-react-parser 那樣加一層typeof mathup === 'function' ? mathup : mathup.default的 guard。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景描述 (Why)
SeeMark 已有 React(
@coseeing/see-mark)與 HTML(/html)兩個 adapter,共用Stage 1 markup pipeline。本 PR 新增第三個對外 adapter
@coseeing/see-mark/vue,讓社群/外部 Vue 3 專案能用同一套 a11y markdown pipeline,並維持與 React/HTML
相同的語意輸出。
挑戰:coseeing 內部沒有 Vue consumer,無法靠真實場景驗證 —— 因此以「與 React
adapter 語意等價」作為正確性主保證(三方 parity test)。
實作方法 (How)
v-html包 HTML adapter):converter 走 htmlparser2walk →
h(),自訂 component 是真正的 Vue 3 組件(functional /defineComponent/ SFC),children 走 default slot。
createMarkdownToVueParser(對稱createMarkdownToReactParser)<SeeMark>便利組件(只快取 parser、不快取 VNode —— VNode 每次 render 必須唯一)。cross-adapter-parity.test.js擴充成 React/HTML/Vue 三方比對,同一 markdown 必須產生相同語意 DOM,15 種 component types 缺一個 coverage 就 fail CI。
hydration 後互動與 reactivity 正常)。
字串
on*、drop<script>(VNode script 掛載即執行)、剝除ref/key/ref_for/ref_key、還原 SVG camelCase 屬性;
javascript:URL 與<style>仍原樣通過(明文信任邊界:adapter 不是 sanitizer)。
import 崩潰;Vue entry 預設
enableAsciimath: false,避免 Vite 下反引號行內程式碼觸發 MathJax legacy shim 的 strict-mode 崩潰(要用可明確 opt-in)。
實際變更
src/entries/vue.js、src/parsers/create-markdown-to-vue-parser.js、src/markup-converters/vue/(converter +<SeeMark>+ 15 個 default components)cross-adapter-parity.test.js擴充三方比對;新增 converter / parser / seemark /SSR-hydration 測試
rollup.config.mjs加第三 entry;package.json加./vueexport、optional peervue ^3.2.0、vue 相關 devDepsload-asciimath.cjs)/vue章節(用法、自訂組件、SSR、bundler 相容、trust model)測試驗證
lib/see-mark-vue.cjsgitignored 目錄
補充說明
v4 未修);真正的 strict-safe AsciiMath 需要另開 PR 處理。