// 聚珍切片 1 jsdom 測試(架構 §12):驗證 DOM 變換正確性——包裝結構、 // bd-* class、邊界、冪等、revert 還原、copy-clean、lang 政策分支、作用域。 import assert from "node:assert/strict"; import { test } from "node:test"; import { setupDom, countTag } from "./helpers.mjs"; import { createJuzhen, createCjkAutospace } from "../dist/juzhen.mjs"; // ----- 中西間隙(spacing)----- test("spacing:CJK↔Latin 邊界以 jz-hws 包裹左側字(margin 留隙,不注入空白)", () => { const { document } = setupDom("

我用Neovim寫程式

"); createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body); const p = document.querySelector("p"); assert.equal(countTag(p, "jz-hws"), 2, "用|Neovim 與 Neovim|寫 兩處邊界,各包左側字"); assert.equal(p.textContent, "我用Neovim寫程式", "textContent 完全不變(無注入空白,純 copy-clean)"); assert.equal(p.querySelector("jz-hws").textContent.length, 1, "jz-hws 內為單一邊界字"); }); test("spacing:textContent 完全不變(margin 留隙,不注入也不刪語義字元)", () => { const { document } = setupDom("

用Neovim,很爽。

"); createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body); assert.equal(document.querySelector("p").textContent, "用Neovim,很爽。"); }); test("spacing:冪等——重跑不重複插入 jz-hws", () => { const { document } = setupDom("

我用Neovim寫程式

"); const jz = createJuzhen({ jiya: false, longWord: false, jinze: false }); jz.render(document.body); const first = countTag(document.body, "jz-hws"); jz.render(document.body); assert.equal(countTag(document.body, "jz-hws"), first, "第二次 render 不應增加"); }); test("spacing:revert 還原至原文(無 jz-hws,textContent 回到原樣)", () => { const { document } = setupDom("

我用Neovim寫程式

"); const jz = createJuzhen({ jiya: false, longWord: false, jinze: false }); jz.render(document.body); jz.revert(document.body); assert.equal(countTag(document.body, "jz-hws"), 0); assert.equal(document.querySelector("p").textContent, "我用Neovim寫程式"); }); test("spacing:跨樣式行內邊界(Neovim)亦識別(I7)", () => { const { document } = setupDom("

Neovim

"); createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body); // 是|Neovim 邊界(跨 )應插入 jz-hws。 assert.equal(countTag(document.querySelector("p"), "jz-hws"), 1); }); test("spacing:avoid 子樹(code/pre)不處理", () => { const { document } = setupDom("

foo是bar

"); createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body); // code 內 foo是bar 不應被 charify/spacing;但 pill 邊界(看|code、code|這)會加間隙。 assert.equal(document.querySelector("code").textContent, "foo是bar"); }); // ----- pill 邊界(spacing Pass B)----- test("spacing:pill(code)兩側與 CJK 相鄰插入 jz-hws", () => { const { document } = setupDom("

vim編輯

"); createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body); assert.equal(countTag(document.querySelector("p"), "jz-hws"), 2); }); // ----- 標點擠壓(jiya)----- test("jiya:標點 charify 為 jz-char>jz-inner,標 bd-* class,textContent 不變(copy-clean)", () => { const { document } = setupDom('

他說:「好,走。」

'); createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body); const p = document.querySelector("p"); assert.equal(p.textContent, "他說:「好,走。」", "標點被包裝但文字不變"); assert.ok(countTag(p, "jz-char") >= 5); assert.ok(countTag(p, "jz-inner") >= 5); const pause = p.querySelector("jz-char.bd-pause"); assert.ok(pause, ",/:應標 bd-pause"); const stop = p.querySelector("jz-char.bd-stop"); assert.ok(stop, "。應標 bd-stop"); }); test("jiya:開明式(zh-Hans)句內點號半形(jz-half),句末點號不收(機制 C)", () => { const { document } = setupDom('

好,走。

'); createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body); const p = document.querySelector("p"); const comma = p.querySelector("jz-char.bd-pause"); // , const period = p.querySelector("jz-char.bd-stop"); // 。 assert.ok(comma.classList.contains("jz-half"), ",開明式半形(halt)"); assert.ok(!period.classList.contains("jz-half"), "。句末保持全形"); }); test("jiya:開明式括號引號半形(開類/閉類皆 jz-half)", () => { const { document } = setupDom('

說「好」嗎

'); createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body); const p = document.querySelector("p"); assert.ok(p.querySelector("jz-char.bd-open").classList.contains("jz-half"), "「半形"); assert.ok(p.querySelector("jz-char.bd-close").classList.contains("jz-half"), "」半形"); }); test("jiya:全角式(zh-Hant)baseline 一律不收(孤立標點無 jz-half)", () => { const { document } = setupDom('

好,走。

'); createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body); assert.equal(countTag(document.querySelector("p"), "jz-char.jz-half"), 0, "全角式不擠(無相鄰標點)"); }); test("jiya 機制 A:句末標點後接閉引號,前者半形(。」→ 。jz-half,全角亦然)", () => { const { document } = setupDom('

好。」嗎

'); createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body); const period = document.querySelector("jz-char.bd-stop"); // 。 assert.ok(period.classList.contains("jz-half"), "。鄰接閉引號 → 半形(行內連續擠壓)"); }); test("jiya 機制 A:?!→ 僅 ?半形,!保持全形 1em(修正居中字形單側收縮缺陷)", () => { const { document } = setupDom('

怎么办?!真

'); createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body); const stops = document.querySelectorAll("jz-char.bd-stop"); // ? ! assert.equal(stops.length, 2); assert.ok(stops[0].classList.contains("jz-half"), "?鄰接 ! → 半形"); assert.ok(!stops[1].classList.contains("jz-half"), "!保持全形(1em,不被單側收縮拖拽重疊)"); }); test("jiya 機制 A:連續開類,後者半形(「「→ 第二個 jz-half)", () => { const { document } = setupDom('

「「好

'); createJuzhen({ spacing: false, longWord: false, jinze: false }).render(document.body); const opens = document.querySelectorAll("jz-char.bd-open"); assert.equal(opens.length, 2); assert.ok(!opens[0].classList.contains("jz-half"), "首個開類左側對漢字/行首,不由鄰接收"); assert.ok(opens[1].classList.contains("jz-half"), "第二個開類內側 → 半形"); }); test("jiya:revert 還原標點(無 jz-char,textContent 不變)", () => { const { document } = setupDom('

好,走。

'); const jz = createJuzhen({ spacing: false, longWord: false, jinze: false }); jz.render(document.body); jz.revert(document.body); assert.equal(countTag(document.body, "jz-char"), 0); assert.equal(countTag(document.body, "jz-inner"), 0); assert.equal(document.querySelector("p").textContent, "好,走。"); }); // ----- 全域擠壓政策(lang.style)+逐 lang 覆寫(lang.policy)----- const JIYA_ONLY = { spacing: false, longWord: false, jinze: false }; test("政策:全域 style=banjiao → 各 lang 句末點號亦半形(。得 jz-half)", () => { const { document } = setupDom('

好。

'); createJuzhen({ ...JIYA_ONLY, lang: { style: "banjiao" } }).render(document.body); const period = document.querySelector("jz-char.bd-stop"); assert.ok(period.classList.contains("jz-half"), "半角式:句末亦半形"); }); test("政策:全域 style=kaiming 套用於繁體(覆寫內建全角預設,句內 ,半形)", () => { const { document } = setupDom('

好,走

'); createJuzhen({ ...JIYA_ONLY, lang: { style: "kaiming" } }).render(document.body); assert.ok(document.querySelector("jz-char.bd-pause").classList.contains("jz-half"), "全域開明 → 繁體句內亦擠"); }); test("政策:全域 style=quanjiao,但 policy 逐 lang 覆寫 zh-Hans→kaiming(最高優先)", () => { const { document } = setupDom( '

简,

繁,

', ); createJuzhen({ ...JIYA_ONLY, lang: { style: "quanjiao", policy: { "zh-Hans": "kaiming" } }, }).render(document.body); const ps = document.querySelectorAll("p"); assert.ok(ps[0].querySelector("jz-char.bd-pause").classList.contains("jz-half"), "簡體 policy 開明 → 擠"); assert.ok(!ps[1].querySelector("jz-char.bd-pause").classList.contains("jz-half"), "繁體用全域全角 → 不擠"); }); test("政策:未設 style/policy 時回退內建預設(繁全角不擠、簡開明擠)", () => { const { document } = setupDom( '

简,

繁,

', ); createJuzhen(JIYA_ONLY).render(document.body); const ps = document.querySelectorAll("p"); assert.ok(ps[0].querySelector("jz-char.bd-pause").classList.contains("jz-half"), "簡體內建開明"); assert.ok(!ps[1].querySelector("jz-char.bd-pause").classList.contains("jz-half"), "繁體內建全角"); }); // ----- 長英文詞(longWord)----- test("longWord:[A-Za-z]{6,} 包進 span[lang],textContent 不變", () => { const { document } = setupDom("

用 TypeScript 寫

"); createJuzhen({ spacing: false, jiya: false, jinze: false }).render(document.body); const p = document.querySelector("p"); const span = p.querySelector('span[lang="en"]'); assert.ok(span, "TypeScript 應被包裝"); assert.equal(span.textContent, "TypeScript"); assert.equal(p.textContent, "用 TypeScript 寫"); }); test("longWord:短詞(<6)不包裝", () => { const { document } = setupDom("

vim 好

"); createJuzhen({ spacing: false, jiya: false, jinze: false }).render(document.body); assert.equal(countTag(document.body, 'span[lang="en"]'), 0); }); // ----- 禁則(jinze):行首行末避頭尾 ----- test("jinze 避頭:閉類/點號綁到前一字(。」不置行首),中段文字仍可斷", () => { const { document } = setupDom('

他說好。」嗎

'); createJuzhen({ spacing: false, longWord: false }).render(document.body); const p = document.querySelector("p"); const group = p.querySelector("jz-jinze"); assert.ok(group, "應有綁定群組"); assert.equal(group.textContent, "好。」", "前一字 好 + 。」 綁為 nowrap 單元"); assert.equal(countTag(group, "jz-char"), 2, "群組含 。與 」"); assert.equal(p.textContent, "他說好。」嗎", "文字不變(他說 在外、嗎 在外)"); }); test("jinze 避尾:開類綁到後一字(「不置行末),長引文中段可斷", () => { const { document } = setupDom('

說「好」嗎

'); createJuzhen({ spacing: false, longWord: false }).render(document.body); const group = document.querySelector("jz-jinze"); assert.ok(group, "應有綁定群組"); assert.equal(group.textContent, "「好」", "「綁後字、」綁前字 → 「好」一體"); }); test("jinze 避頭:孤立逗號亦綁前字(逗號不可置行首)", () => { const { document } = setupDom('

中文,好

'); createJuzhen({ spacing: false, longWord: false }).render(document.body); const group = document.querySelector("jz-jinze"); assert.equal(group.textContent, "文,", "前字 文 + , 綁定(中 在外可斷)"); }); test("jinze:revert 還原(無 jz-jinze/jz-char,文字回原樣)", () => { const { document } = setupDom('

他說好。」嗎

'); const jz = createJuzhen({ spacing: false, longWord: false }); jz.render(document.body); jz.revert(document.body); assert.equal(countTag(document.body, "jz-jinze"), 0); assert.equal(countTag(document.body, "jz-char"), 0); assert.equal(document.querySelector("p").textContent, "他說好。」嗎"); }); // ----- 作用域與分塊(§3.6)----- test("scope.include:未命中子樹不處理", () => { const { document } = setupDom('

我用Vim

他用Emacs寫

'); createJuzhen({ jiya: false, jinze: false, scope: { include: ".jz" }, }).render(document.body); const ps = document.querySelectorAll("p"); assert.equal(countTag(ps[0], "jz-hws"), 0, "include 外不處理"); assert.ok(countTag(ps[1], "jz-hws") >= 1, "include 內處理"); }); test("分塊:data-juzhen 白名單只啟用列出功能", () => { const { document } = setupDom('

用Vim,好

'); createJuzhen({}).render(document.body); const p = document.querySelector("p"); assert.ok(countTag(p, "jz-hws") >= 1, "spacing 啟用"); assert.equal(countTag(p, "jz-char"), 0, "jiya 未在白名單,停用"); }); test("分塊:data-juzhen-off 黑名單停用列出功能,其餘繼承", () => { const { document } = setupDom('

用Vim,好

'); createJuzhen({}).render(document.body); const p = document.querySelector("p"); assert.ok(countTag(p, "jz-hws") >= 1, "spacing 繼承啟用"); assert.equal(countTag(p, "jz-char"), 0, "jiya 被黑名單停用"); }); // ----- v1 相容層(§3.5)----- test("compat:createCjkAutospace().apply() 可跑,產生中西間隙(margin 模型,copy-clean)", () => { const { document } = setupDom("

我用Neovim

"); createCjkAutospace().apply(document.body); const p = document.querySelector("p"); assert.ok(p.querySelector("jz-hws"), "間隙仍產生(jz-hws 包左側字 + margin)"); assert.equal(p.textContent, "我用Neovim", "textContent 不變(無注入空白)"); }); test("compat:punctuationSqueeze:false 關閉 jiya", () => { const { document } = setupDom('

好,走

'); createCjkAutospace({ punctuationSqueeze: false }).apply(document.body); assert.equal(countTag(document.body, "jz-char"), 0); }); test("compat:預設強制 kaiming,句內點號半形(jz-half)(不因預設 zh-Hant 而退化)", () => { const { document } = setupDom('

好,走

'); createCjkAutospace().apply(document.body); const comma = document.querySelector("jz-char.bd-pause"); assert.ok(comma && comma.classList.contains("jz-half"), "compat 不退化:仍擠句內"); });