Skip to content

feat: opt-in CJK punctuation handling (./cjk + styles/cjk.css) - #90

Merged
jonathanKingston merged 2 commits into
mainfrom
claude/cjk-punctuation
Jul 8, 2026
Merged

feat: opt-in CJK punctuation handling (./cjk + styles/cjk.css)#90
jonathanKingston merged 2 commits into
mainfrom
claude/cjk-punctuation

Conversation

@jonathanKingston

Copy link
Copy Markdown
Collaborator

Closes #85.

Opt-in CJK support, off by default (Latin output and both conformance suites byte-identical), gated like the other optional backends behind @copse/streaming-markdown/cjk (verified absent from the core bundle).

Scoping: what belongs in the renderer vs CSS

Studied @streamdown/cjk and probed this renderer. Two genuine renderer gaps — both CommonMark extensions, not conformance bugs (the reference renderer produces identical output), hence opt-in:

  1. Emphasis around full-width punctuation**「強調」**, **強調。**です stay literal because CommonMark's flanking rule counts 「」。! as blocking punctuation. Most CJK emphasis already works (ideographs are letters); only delimiters whose inner edge is CJK punctuation fail. A post-process pass can't fix this (the ** are already left as text before passes run), so it needs a default-off hook in the flanking classifier that treats full-width punctuation as non-flanking.
  2. Bare autolinks swallowing CJK punctuationhttps://example.com。次 absorbed 。次 into the href; now stops at the first full-width mark when enabled.

Everything else is CSS the host owns — line breaking, Kinsoku, inter-script spacing, full-width kerning. The renderer emits identical structural HTML for every script and never guesses a language, so that ships as an optional styles/cjk.css (word-break / line-break: strict / progressive text-autospace / text-spacing-trim, gated on :lang() or a .sm-cjk hook), not JS.

API

  • setCjkFriendly() (opt-in) wires the two default-off hooks; isCjkPunctuation() exported.
  • Core files inline-emphasis.ts / inline-spans.ts gain default-null injectable hooks only; the behavior lives entirely in the ./cjk entry.

Verification

  • npm run typecheck, npm run build (dist/cjk.* emitted, smoke-tested) — pass
  • npm test — 782 pass in isolation (CJK suite 13/13)
  • npm run check:conformance / npm run check:gfm-conformance — 2/2, baselines byte-identical
  • npm run coverage:ci — gate passes; cjk.ts 100%; baseline unchanged at 99.75

Caveats

  • The core-file changes are additive default-null hooks; justified in-commit because emphasis re-pairing structurally can't be a post-process pass.
  • Autolink boundary splits at CJK punctuation only (matching streamdown's scope), not CJK letters glued to a URL.
  • CSS uses progressive properties (text-autospace, text-spacing-trim) that degrade safely where unsupported.
  • Some test runs flaked on unrelated timing/network tests (Shiki grammar load, a throughput benchmark) under concurrent multi-worktree load — a different one each run, never CJK code; CI runs single-threaded so these shouldn't recur.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ehmok4AzyTQDNkRzW3g8z8


Generated by Claude Code

claude added 2 commits July 7, 2026 08:58
Full-width / ideographic punctuation (「」。!()) is Unicode punctuation, so
CommonMark's emphasis flanking rules never pair a `**` that sits between a CJK
character and one of those marks — `これは**「強調」**です` stays literal, a shape
LLMs emit constantly. This is a documented CommonMark limitation (the reference
renderer produces the same output), not a bug here, so the fix ships as an
opt-in extension (markdown-cjk-friendly), never the default.

A post-process inline pass cannot fix it — the `**` are already left as text by
the time passes run — so the extension is a default-off hook in the flanking
classifier (`setFlankingPunctuationExclusion`) plus a matching bare-autolink
boundary hook (`setBareUrlCjkBoundary`) that stops a run-together
`https://example.com。次` at the first full-width mark. The new
`@copse/streaming-markdown/cjk` entry (`setCjkFriendly`) flips both; its range
table lives behind that entry, like the other optional backends, so it is never
pulled into the default bundle. With it off, Latin output and the CommonMark /
GFM conformance suites are byte-identical.

Line-break and inter-script spacing are the host's CSS to own, not the
renderer's: ship the optional `styles/cjk.css` (word-break / line-break /
text-autospace, gated on `:lang()` or a `.sm-cjk` hook) and document the split
in EXTENDING.md + README. Tests cover the classifier, emphasis/link adjacency,
mixed CJK+Latin, and byte-identical Latin output; teardown resets the registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehmok4AzyTQDNkRzW3g8z8
# Conflicts:
#	docs/EXTENDING.md
#	package.json
@jonathanKingston
jonathanKingston merged commit cf6cdba into main Jul 8, 2026
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.

CJK / ideographic punctuation handling

2 participants