Repository navigation
fix(muya): treat CJK as punctuation for strong/em flanking (#4307) - #4401
Conversation
Strong/em delimited with `**` directly against a CJK character whose inner content is punctuation-bounded did not bold in muya, e.g. `例子例子**"加粗"**例子例子`, `日本語**(強調)**日本語`, `한국어**[강조]**한국어`, and the non-BMP `𠀀𠀁**"加粗"**𠀀𠀁`. The legacy muyajs engine bolded these. CommonMark §6.2 classifies CJK ideographs / Hangul / Kana as "other" (Lo), neither whitespace nor punctuation, so a `**` run wrapped by CJK with punctuation-bounded content is not left/right-flanking and stays literal. muya has TWO inline-tokenization paths and both carry the same CJK-as- punctuation widening the legacy engine shipped: - Static / export path (marked@16): a `cjkEmStrong` tokenizer override that rebuilds marked's emStrong flanking regexes with CJK folded into the punctuation class and removed from the alphanumeric class, registered in getHighlightHtml and getClipboardHtml. Faithful copy of marked's emStrong body; rules are swapped in/out per-call so the shared tokenizer rules are never left mutated. - Live editor path (inlineRenderer): CJK widening added to canOpen/canCloseEmphasis in inlineRenderer/utils.ts, plus full code-point reading so the non-BMP CJK Ext-B surrogate-pair branch is live. CJK ranges (matching legacy CJK_REG): Hiragana+Katakana U+3040–U+30FF, CJK Ext-A U+3400–U+4DBF, CJK Unified U+4E00–U+9FFF, CJK Compatibility U+F900–U+FAFF, Hangul Syllables U+AC00–U+D7AF, Halfwidth Katakana U+FF66–U+FF9D, and CJK Ext-B U+20000–U+2A6DF (non-BMP). The widening is additive — it never bolds anything CommonMark accepts as non-emphasis — so the CommonMark 0.31 + GFM conformance suites are unchanged (1347/1347, no unexpected passes). Promotes the #4399 `it.fails` placeholders to passing `it` cases and adds live-editor-path coverage plus negative cases. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR fixes a MarkText-specific regression in @muyajs/core where strong/emphasis delimiters (** / *) directly adjacent to CJK characters fail to open/close when the inner content is punctuation-bounded (e.g. 中文**"加粗"**中文). It restores the legacy muyajs behavior by widening the emphasis “flanking” boundary rules so CJK characters are treated as punctuation boundaries, consistently across both the static/export marked pipeline and the live editor inline lexer.
Changes:
- Add a marked tokenizer extension that overrides
emStrongwith CJK-widened flanking rules for static/export and clipboard HTML rendering. - Update the live editor inline flanking checks to treat CJK as punctuation, with correct non-BMP (surrogate-pair) handling.
- Promote and expand regression tests to cover both render paths, including negative cases.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/muya/src/utils/marked/getHighlightHtml.ts | Registers the new CJK-aware marked emStrong extension for static/export HTML. |
| packages/muya/src/utils/marked/getClipboardHtml.ts | Registers the same CJK-aware extension to keep clipboard HTML consistent with export rendering. |
| packages/muya/src/utils/marked/extensions/cjkEmStrong.ts | Implements a marked Tokenizer.emStrong override that treats CJK as punctuation for flanking. |
| packages/muya/src/state/tests/strongCjkFlanking.spec.ts | Promotes prior expected-fail tests and adds coverage for both static and live-editor tokenization paths. |
| packages/muya/src/inlineRenderer/utils.ts | Widens canOpenEmphasis/canCloseEmphasis to accept CJK as punctuation boundaries and correctly reads full code points. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ]; | ||
| for (const src of NEGATIVE_CASES) { | ||
| it(`does not bold: ${src}`, () => { | ||
| expect(rendersStrong(src)).toBe(false); |
There was a problem hiding this comment.
Good catch — done in 7b0b312. Added a rendersEm helper mirroring rendersStrong and strengthened the static/export-path negative cases to assert NEITHER <strong> NOR <em> is rendered, so a flanking regression that surfaces as unexpected italics now fails the test. (Verified all three negative cases render as plain literal text — no <strong>/<em>.)
The live editor (inlineRenderer) negative cases already cover both: tokenizesEmphasis returns true for either a strong or em token, so its expect(...).toBe(false) already rejects both tags — only the static path had the <strong>-only gap.
Green: lint (0 errors), lint:types, check-circular, test (511 passed), test:spec (1347 passed).
…r em The static/export-path negative cases in strongCjkFlanking.spec.ts only checked for <strong> via rendersStrong. Since #4307 widens the emphasis/strong flanking logic, a regression could surface as unexpected <em> output while still passing a <strong>-only assertion. Add a rendersEm helper mirroring rendersStrong and assert NEITHER tag is produced for the negative cases. The live editor path already covers this: tokenizesEmphasis returns true for either a strong or em token, so its negative .toBe(false) already rejects both. Only the static path needed strengthening. Addresses Copilot review on #4401. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…4422) These 8 specs import the legacy `muya/lib` (muyajs) alias and exercise engine-level markdown / slug / word-extraction behavior that @muyajs/core now owns and covers with its own suites: - markdown-basic / list-indentation / nested-mixed-lists / export-markdown -> muya commonmark.spec + gfm.spec + roundTrip.spec conformance - markdown-footnotes -> muya footnote.spec / footnoteHtml.spec - markdown-strong-cjk -> muya strongCjkFlanking.spec (#4401) - slugger -> muya generateGithubSlug (getTOC.spec) - extract-word -> muya replaceCurrentWord.spec + current-word logic They test code the app no longer uses (the muyajs engine) and would break when packages/muyajs is removed in Phase H. Desktop-specific unit specs (i18n, match-electron-accelerator, native-theme) are kept. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
The gap (#4307)
Strong/em delimited with
**directly against a CJK character whose innercontent is punctuation-bounded did not bold in muya (
@muyajs/core),even though the legacy
packages/muyajsengine bolded it. Examples thatshould bold but didn't:
例子例子**"加粗"**例子例子日本語**(強調)**日本語한국어**[강조]**한국어𠀀𠀁**"加粗"**𠀀𠀁(non-BMP, CJK Ext-B)Root cause. CommonMark §6.2 classifies CJK ideographs / Hangul / Kana as
"other" (Lo, Letter) — neither whitespace nor punctuation. So a
**runwrapped by CJK with punctuation-bounded content (a quote/paren/bracket on the
inner side) is neither left- nor right-flanking, and the delimiters stay
literal. This is spec-conformant (GitHub behaves the same), but CJK scripts
don't use inter-word spaces, so the rule denies emphasis to virtually any CJK
paragraph that wraps
**with punctuation. Typora, VSCode markdownlint,Joplin — and the legacy muyajs engine MarkText shipped — all widen the
flanking check so CJK counts as a boundary.
The two paths fixed
muya has two inline-tokenization paths, and both now carry the same
additive CJK-as-punctuation widening:
Static / export path (marked@16) —
state/renderToStaticHTML→utils/marked/getHighlightHtml, plusgetClipboardHtml, runmarked.parse, whose built-inemStrongtokenizer applies CommonMark flanking literally. New module
utils/marked/extensions/cjkEmStrong.tsregisters atokenizer.emStrongoverride: a faithful copy of [email protected]'s
emStrongbody that swaps inCJK-widened versions of marked's own flanking regexes
(
emStrongLDelim,emStrongRDelimAst,emStrongRDelimUnd,punctuation, and theunicodeAlphaNumeric"other" rule) for theduration of the call, then restores them in a
finallyso the sharedtokenizer rules are never left mutated. CJK is folded into the punctuation
class and removed from the alphanumeric class. No fork of marked.
Live editor path (inlineRenderer) — muya's own inline lexer
(
inlineRenderer/lexer.ts+utils.ts). The CJK widening is added tocanOpenEmphasis/canCloseEmphasisininlineRenderer/utils.ts,matching the legacy
packages/muyajs/lib/parser/utils.jsexactly,including full code-point reading (
lastCodePointChar/codePointCharAt) so the non-BMP CJK Ext-B surrogate-pair branch is live.Both paths use the same ranges and agree on every test case.
CJK ranges (matching legacy
CJK_REG)Conformance — no regression
The widening is additive: it only ever lets emphasis open/close where
CommonMark refused (around CJK), never the reverse. The CommonMark 0.31 + GFM
conformance suites are unchanged:
test:spec: 1347 → 1347 passed,expected-failures.jsonunchanged,zero unexpected passes (the spec corpus has no CJK at emphasis
boundaries, so nothing flips).
Tests
This branch promotes the
it.failsplaceholders from #4399(
state/__tests__/strongCjkFlanking.spec.ts) to passingitcases, now thatboth paths bold them, and adds:
tokenizer) for the same four[Bug] 似乎不能正常处理**加粗语法的配对 #4307 examples + sanity cases.
a * foo bar*,a_foo bar_,*(*foo)) on both pathsto guard against over-widening.
Verify (all green)
pnpm -C packages/muya lint(0 errors) ·lint:types(clean) ·check-circular(clean) ·test(511 passed) ·test:spec(1347 passed).🤖 Generated with Claude Code