5b9c6b5fa1
下游回饋之字型樹需求。決策:字型屬消費端領域,聚珍不應內建字型堆疊(原 _lang.css
之 Noto 後備係越權,且曾蓋掉消費端字型釀 bug)。採「完全移除、交消費端 CSS」(A 案),
否決「以參數管字型」(B 案——JS 管字型須注入 CSS、違反 JS 注入結構/CSS 給視覺之分工;
而缺 halt 字型須逐字型微調 margin 之指引,由既有 jiya.halfWidth/jiya.bias +文檔承擔)。
移除:
- 刪 src/css/_lang.css,juzhen.css 移除其 @import。dist/juzhen.css 現零 font-family
規則(3.1→2.5kb)。
聚珍保留之三項非管理性本分(確保不礙消費端字型與 locl):
- _normalize.css `jz-* { font: inherit }`:原子繼承消費端字型、不強加。
- _jiya.css 半形 font-feature-settings 顯式改 "locl" 1, "halt" 1(預設+halt-island 兩
處):確保泛 CJK 字型(思源/Noto CJK)之地域標點字形不因設 halt 而失。
- render 只讀不改 lang。body 文本之 locl 全由消費端字型+正確 lang 決定。
驗證(真實 Chromium):
- halt 模式:消費端逐 lang 字型生效並繼承至 jz-inner,無強制 Noto。
- margin 後備:半形盒 0.5em、字身依墨色偏側位移正確、jz-inner 繼承消費端字型、
feature normal(locl 既定開),皆不受字型移除影響。
- demo/index.html、resize-check.html 改示範「消費端逐 lang 設 font-family」之推薦範式;
fallback.html 更新註解。
- README 新增「字型與 locl」節+改寫 CSS 基礎設定字型項;ARCHITECTURE §6.9 改寫
(A 案決策、否決 B 案、三項本分)+檔案樹。
- test:新增 CSS 回歸(不內建 font-family、半形保留 locl)。57 測試通過。
- dist/ 重建(僅 juzhen.css 變動,JS 不變)。
511 lines
26 KiB
JavaScript
511 lines
26 KiB
JavaScript
// 聚珍切片 1 jsdom 測試(架構 §12):驗證 DOM 變換正確性——包裝結構、
|
||
// bd-* class、邊界、冪等、revert 還原、copy-clean、lang 政策分支、作用域。
|
||
|
||
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
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:全角省略號 …… 後接 CJK 不插中西間隙(問題 B:原 DOTS_CJK `|…` 誤匹配)", () =>
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hant">他竟然走了……留下一地</p>');
|
||
createJuzhen({ lang: { style: "quanjiao" } }).render(document.body);
|
||
const p = document.querySelector("p");
|
||
// …… 屬 CJK 標點(已 charify 為 bd-liga),後接 CJK 不應有西文間隙。
|
||
assert.equal(countTag(p, "jz-hws"), 0, "…… 與後接 CJK 之間不應有 jz-hws");
|
||
});
|
||
|
||
test("spacing:貨幣符號 € 視為西文——與 CJK 邊界補隙、手工空格正規化(問題 D)", () =>
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hant">售價 €1099 都行</p>');
|
||
createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body);
|
||
const p = document.querySelector("p");
|
||
assert.equal(/售價\s€/.test(p.textContent), false, "售價↔€ 之手工空格應正規化剝除");
|
||
assert.ok(countTag(p, "jz-hws") >= 2, "價↔€ 與 9↔都 兩處中西邊界各補 jz-hws");
|
||
});
|
||
|
||
test("spacing:擴充西文集——拼音聲調 ā/希臘 α/西里爾 Я 皆視為西文補隙(問題 D 完整化)", () =>
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hant">中ā文α字Я尾</p>');
|
||
createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body);
|
||
const p = document.querySelector("p");
|
||
// ā(U+0101 拉丁擴充)、α(U+03B1 希臘,驗證希臘集補全)、Я(U+042F 西里爾) 皆為西文。
|
||
assert.ok(countTag(p, "jz-hws") >= 5, "ā/α/Я 與相鄰 CJK 各成邊界、補 jz-hws");
|
||
});
|
||
|
||
test("spacing:CJK 標點介於 CJK 間一律不補中西隙(標點稽核:。!?「」()《》……—— 等)", () =>
|
||
{
|
||
const samples = [
|
||
"前。後", "前!後", "前?後", "前,後", "前、後", "前;後", "前:後",
|
||
"前「後", "前」後", "前(後", "前)後", "前《後", "前》後", "前【後", "前】後",
|
||
"前〔後", "前〕後", "前……後", "前——後", "前·後", "前~後", "前“後", "前”後",
|
||
];
|
||
for (const s of samples)
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hant">' + s + "</p>");
|
||
createJuzhen({ lang: { style: "quanjiao" } }).render(document.body);
|
||
assert.equal(countTag(document.querySelector("p"), "jz-hws"), 0, s + " 不應有 jz-hws");
|
||
}
|
||
});
|
||
|
||
test("spacing:收尾彎引號 ” 後接拉丁/數字不補隙(標點稽核移除 QUOTE_AN ” 偏例,與 」)》 一致)", () =>
|
||
{
|
||
// 原 pangu QUOTE_AN 令 ”+字母數字補隙,使 ” 成唯一會補隙之收尾標點(其他收尾標點皆不)。
|
||
const { document } = setupDom('<p lang="zh-Hans">“中文”ABC 與 ”123</p>');
|
||
createJuzhen({ jiya: false, longWord: false, jinze: false }).render(document.body);
|
||
const p = document.querySelector("p");
|
||
// “”ABC、”123 中之彎引號鄰拉丁/數字皆不補隙;惟「與」兩側作者空格為 CJK↔Latin
|
||
// 邊界(與|ABC 之 ABC 結尾、123|與 之類)仍會處理,故只斷言不含「彎引號旁之隙」——
|
||
// 以 textContent 不變 + 彎引號未被 jz-hws 包裹驗證。
|
||
const hwsTexts = Array.from(p.querySelectorAll("jz-hws")).map((h) => h.textContent);
|
||
assert.equal(hwsTexts.includes("”"), false, "收尾彎引號 ” 不應被包入 jz-hws(不補隙)");
|
||
});
|
||
|
||
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"), "繁體內建全角");
|
||
});
|
||
|
||
// ----- 字形偏側(jz-ink-*)+ margin 後備模式(.feedback)-----
|
||
|
||
test("偏側:jz-ink-* 恆於 charify 標記(halt 預設模式亦然,root 不設 margin 屬性)", () =>
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hans">好,「走」。</p>');
|
||
createJuzhen(JIYA_ONLY).render(document.body);
|
||
// 簡體點號偏左;角括號「」墨色強偏一側 → 開 right、閉 left(圓方括號才 center)。
|
||
assert.ok(document.querySelector("jz-char.bd-pause").classList.contains("jz-ink-left"), "簡體 ,偏左");
|
||
assert.ok(document.querySelector("jz-char.bd-open").classList.contains("jz-ink-right"), "角括號「墨偏右 → right");
|
||
assert.ok(document.querySelector("jz-char.bd-close").classList.contains("jz-ink-left"), "角括號」墨偏左 → left");
|
||
assert.equal(document.body.getAttribute("data-jz-halfwidth"), null, "halt 預設模式不設 root 屬性");
|
||
});
|
||
|
||
test("偏側:繁體點號居中(jz-ink-center),!?恆居中", () =>
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hant">走,真?</p>');
|
||
createJuzhen(JIYA_ONLY).render(document.body);
|
||
assert.ok(document.querySelector("jz-char.bd-pause").classList.contains("jz-ink-center"), "繁體 ,居中");
|
||
assert.ok(document.querySelector("jz-char.bd-stop").classList.contains("jz-ink-center"), "?居中");
|
||
});
|
||
|
||
test("margin 模式:render 於 root 設 data-jz-halfwidth=margin,revert 清除", () =>
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hans">好,走。</p>');
|
||
const jz = createJuzhen({ ...JIYA_ONLY, jiya: { halfWidth: "margin" } });
|
||
jz.render(document.body);
|
||
assert.equal(document.body.getAttribute("data-jz-halfwidth"), "margin", "margin 模式設 root 屬性");
|
||
// 偏側 class 與 jz-half 並存,CSS 始能定向收縮。
|
||
const comma = document.querySelector("jz-char.bd-pause");
|
||
assert.ok(comma.classList.contains("jz-half") && comma.classList.contains("jz-ink-left"), ",半形+偏左");
|
||
jz.revert(document.body);
|
||
assert.equal(document.body.getAttribute("data-jz-halfwidth"), null, "revert 清除 root 屬性");
|
||
});
|
||
|
||
test("margin 模式:消費端 bias 覆寫參考表(如把簡體 ,改判居中)", () =>
|
||
{
|
||
const { document } = setupDom('<p lang="zh-Hans">好,走</p>');
|
||
createJuzhen({
|
||
...JIYA_ONLY,
|
||
jiya: { halfWidth: "margin", bias: { "zh-Hans": { ",": "center" } } },
|
||
}).render(document.body);
|
||
const comma = document.querySelector("jz-char.bd-pause");
|
||
assert.ok(comma.classList.contains("jz-ink-center"), "覆寫後 ,判居中");
|
||
assert.ok(!comma.classList.contains("jz-ink-left"), "不再用參考預設之偏左");
|
||
});
|
||
|
||
// ----- 長英文詞(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 被黑名單停用");
|
||
});
|
||
|
||
// ----- 下游回饋修復(問題一 1a/1b、問題二)-----
|
||
|
||
test("1a:pill 後接 CJK 標點時,pill 左緣仍補 jz-hws(穿越 jz-jinze)", () =>
|
||
{
|
||
const { document } = setupDom("<p>代码<code>build</code>,后</p>");
|
||
createJuzhen({ pillSelector: "code, kbd", lang: { default: "zh-Hans", style: "quanjiao" } })
|
||
.render(document.body);
|
||
// jinze 已把 code+,納入 jz-jinze;spacing 仍須能找到 code 左鄰字「码」並補間隙。
|
||
const hws = document.querySelector("jz-hws");
|
||
assert.ok(hws && hws.textContent === "码", "pill 左側「码」包入 jz-hws(不因右側標點/jz-jinze 而遺失)");
|
||
});
|
||
|
||
test("1a:pill 後接 CJK 標點且有作者空格 → 空格被剝除、左緣補間隙", () =>
|
||
{
|
||
const { document } = setupDom("<p>代码 <code>build</code>,后</p>");
|
||
createJuzhen({ pillSelector: "code, kbd", lang: { default: "zh-Hans", style: "quanjiao" } })
|
||
.render(document.body);
|
||
assert.ok(document.querySelector("jz-hws"), "補了 jz-hws");
|
||
assert.equal(document.querySelector("p").textContent, "代码build,后", "作者空格被剝除(與無空格輸入一致)");
|
||
});
|
||
|
||
test("1b:CJK↔CJK 贅餘作者空格被剝除(含跨樣式行內標籤)", () =>
|
||
{
|
||
const { document } = setupDom('<p><span class="b">事件</span> 元件</p>');
|
||
createJuzhen({ lang: { default: "zh-Hans" } }).render(document.body);
|
||
assert.equal(document.querySelector("p").textContent, "事件元件", "跨 <span> 之 CJK↔CJK 空格剝除");
|
||
});
|
||
|
||
test("1b:CJK↔CJK 不插 jz-hws(僅剝空格,無留隙)", () =>
|
||
{
|
||
const { document } = setupDom("<p>事件 元件</p>");
|
||
createJuzhen({ spacing: true, jiya: false, jinze: false, longWord: false }).render(document.body);
|
||
assert.equal(countTag(document.body, "jz-hws"), 0, "CJK↔CJK 不需間隙,僅刪空格");
|
||
assert.equal(document.querySelector("p").textContent, "事件元件");
|
||
});
|
||
|
||
test("問題二:justifyAtoms:false → root 設 data-jz-atoms=inline,revert 清除", () =>
|
||
{
|
||
const { document } = setupDom("<p lang='zh-Hans'>测试,(甲)。</p>");
|
||
const jz = createJuzhen({ lang: { default: "zh-Hans" }, justifyAtoms: false });
|
||
jz.render(document.body);
|
||
assert.equal(document.body.getAttribute("data-jz-atoms"), "inline", "設 inline 原子屬性");
|
||
jz.revert(document.body);
|
||
assert.equal(document.body.getAttribute("data-jz-atoms"), null, "revert 清除");
|
||
});
|
||
|
||
test("問題二:justifyAtoms 預設 true → 不設屬性(維持 inline-block 原子)", () =>
|
||
{
|
||
const { document } = setupDom("<p lang='zh-Hans'>测试,</p>");
|
||
createJuzhen({ lang: { default: "zh-Hans" } }).render(document.body);
|
||
assert.equal(document.body.getAttribute("data-jz-atoms"), null, "預設不設屬性");
|
||
});
|
||
|
||
// ----- 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 不退化:仍擠句內");
|
||
});
|
||
|
||
// ----- text-indent 漏溢防護(下游回報)-----
|
||
|
||
test("CSS:jz-* 原子歸零「區塊容器首/末行級」繼承屬性,免 inline-block 重套(透明原則)", () =>
|
||
{
|
||
// jsdom 無版面,無法量寬;改驗構建 CSS 確含原子歸零規則。
|
||
// 機制見 src/css/_normalize.css:此類繼承屬性會被 inline-block 原子(jz-char/
|
||
// jz-jinze)重新套用於其內部首/末行,故須於原子上歸零(段落本身不受影響)。
|
||
// 整類僅兩項(headless Chrome 逐屬性實測:letter-spacing/word-spacing/text-align
|
||
// 系皆不洩漏)。
|
||
const cssPath = fileURLToPath(new URL("../dist/juzhen.css", import.meta.url));
|
||
const css = readFileSync(cssPath, "utf8");
|
||
assert.match(css, /text-indent\s*:\s*0/, "構建 CSS 應含 text-indent:0 重設");
|
||
assert.match(css, /hanging-punctuation\s*:\s*none/, "構建 CSS 應含 hanging-punctuation:none 重設");
|
||
});
|
||
|
||
test("CSS:jz-* 原子 text-decoration:inherit,補裝飾盒跳過原子致下劃線斷缺(透明原則·欠套用面)", () =>
|
||
{
|
||
// 另一機制:text-decoration 非繼承、由段落裝飾盒繪製並跳過原子級 inline-block,
|
||
// 致下劃線於每個標點原子下斷缺。令原子 inherit 自繪、接續(headless Chrome 像素掃描
|
||
// 實測 halt/margin coverage 皆回 1.0)。
|
||
const cssPath = fileURLToPath(new URL("../dist/juzhen.css", import.meta.url));
|
||
const css = readFileSync(cssPath, "utf8");
|
||
assert.match(css, /text-decoration\s*:\s*inherit/, "構建 CSS 應含 text-decoration:inherit");
|
||
});
|
||
|
||
test("CSS:聚珍不內建 font-family(字型交消費端),半形顯式保留 locl(下游回饋)", () =>
|
||
{
|
||
// 字型屬消費端領域,聚珍不內建任何字型堆疊(移除 _lang.css);僅以 _normalize 之
|
||
// `font: inherit` 使原子繼承消費端字型、不強加,並於半形 feature 顯式併入 locl,
|
||
// 確保泛 CJK 字型之地域標點字形不因設 halt 而失。body locl 由消費端字型+lang 決定。
|
||
const cssPath = fileURLToPath(new URL("../dist/juzhen.css", import.meta.url));
|
||
const css = readFileSync(cssPath, "utf8");
|
||
assert.equal(/font-family/.test(css), false, "聚珍 CSS 不應內建任何 font-family");
|
||
assert.match(css, /font-feature-settings:\s*"locl"\s+1,\s*"halt"\s+1/, "半形 feature 應顯式保留 locl");
|
||
});
|