@@ -0,0 +1,318 @@
// 聚珍切片 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 ( "<p>我用Neovim寫程式</p>" ) ;
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 ( "<p>用Neovim,很爽。</p>" ) ;
createJuzhen ( { jiya : false , longWord : false , jinze : false } ) . render ( document . body ) ;
assert . equal ( document . querySelector ( "p" ) . textContent , "用Neovim,很爽。" ) ;
} ) ;
test ( "spacing:冪等——重跑不重複插入 jz-hws" , ( ) =>
{
const { document } = setupDom ( "<p>我用Neovim寫程式</p>" ) ;
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 ( "<p>我用Neovim寫程式</p>" ) ;
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:跨樣式行內邊界(<strong>是</strong>Neovim)亦識別(I7) " , ( ) =>
{
const { document } = setupDom ( "<p>這<strong>是</strong>Neovim</p>" ) ;
createJuzhen ( { jiya : false , longWord : false , jinze : false } ) . render ( document . body ) ;
// 是|Neovim 邊界(跨 </strong>)應插入 jz-hws。
assert . equal ( countTag ( document . querySelector ( "p" ) , "jz-hws" ) , 1 ) ;
} ) ;
test ( "spacing: avoid 子樹(code/pre)不處理" , ( ) =>
{
const { document } = setupDom ( "<p>看<code>foo是bar</code>這</p>" ) ;
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 ( "<p>用<code>vim</code>編輯</p>" ) ;
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 ( '<p lang="zh-Hans">他說:「好,走。」</p>' ) ;
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 ( '<p lang="zh-Hans">好,走。</p>' ) ;
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 ( '<p lang="zh-Hans">說「好」嗎</p>' ) ;
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 ( '<p lang="zh-Hant">好,走。</p>' ) ;
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 ( '<p lang="zh-Hant">好。」嗎</p>' ) ;
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 ( '<p lang="zh-Hans">怎么办?!真</p>' ) ;
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 ( '<p lang="zh-Hant">「「好</p>' ) ;
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 ( '<p lang="zh-Hans">好,走。</p>' ) ;
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 ( '<p lang="zh-Hant">好。</p>' ) ;
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 ( '<p lang="zh-Hant">好,走</p>' ) ;
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 (
'<p lang="zh-Hans">简,</p><p lang="zh-Hant">繁,</p>' ,
) ;
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 (
'<p lang="zh-Hans">简,</p><p lang="zh-Hant">繁,</p>' ,
) ;
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 ( "<p>用 TypeScript 寫</p>" ) ;
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 ( "<p>vim 好</p>" ) ;
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 ( '<p lang="zh-Hant">他說好。」嗎</p>' ) ;
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 ( '<p lang="zh-Hant">說「好」嗎</p>' ) ;
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 ( '<p lang="zh-Hans">中文,好</p>' ) ;
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 ( '<p lang="zh-Hant">他說好。」嗎</p>' ) ;
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 ( '<p>我用Vim</p><div class="jz"><p>他用Emacs寫</p></div>' ) ;
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 ( '<p data-juzhen="spacing" lang="zh-Hans">用Vim,好</p>' ) ;
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 ( '<p data-juzhen-off="jiya" lang="zh-Hans">用Vim,好</p>' ) ;
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 ( "<p>我用Neovim</p>" ) ;
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 ( '<p lang="zh-Hant">好,走</p>' ) ;
createCjkAutospace ( { punctuationSqueeze : false } ) . apply ( document . body ) ;
assert . equal ( countTag ( document . body , "jz-char" ) , 0 ) ;
} ) ;
test ( "compat:預設強制 kaiming,句內點號半形(jz-half)(不因預設 zh-Hant 而退化)" , ( ) =>
{
const { document } = setupDom ( '<p lang="zh-Hant">好,走</p>' ) ;
createCjkAutospace ( ) . apply ( document . body ) ;
const comma = document . querySelector ( "jz-char.bd-pause" ) ;
assert . ok ( comma && comma . classList . contains ( "jz-half" ) , "compat 不退化:仍擠句內" ) ;
} ) ;