feat: 聚珍切片 1 — 中西間隙/標點擠壓/禁則 + v1 相容層
從零重寫之第一垂直切片(依 ARCHITECTURE.md 契約),涵蓋並超越 v1 全部功能。 核心基座(src/core):unicode 字元表+標點子類+字形側位;locale 語言與 全角/開明政策(全域 lang.style +逐 lang policy);dom(jz-* 工廠+可還原 標記);finder 遍歷引擎(avoid/scope.include/邏輯 run/pill 鄰字/分塊閘); pass 聚合管線。 功能 pass(src/typeset): - spacing:中西間隙採 margin 模型——包裹邊界左側字、以 margin-right 留隙, 不注入空白字元(textContent 完全不變、複製緊湊、justify 不暴增、不撐框、 行首乾淨)。含跨樣式邊界、pill、原檔空格移除。 - gapTrim(lineedge):layout pass,行末去隙,修正 justify 右緣內縮。 - longword:長英文詞 <span lang> + hyphens。 - jiya:標點 charify + 全角/開明/半角(全域 + 逐 lang);半形以字型 OpenType halt 渲染(按字形正確定位、不重疊,居中字形 !? 與繁體 locl 句逗點號皆正確); 行內連續擠壓。 - jinze:行首行末避頭尾(閉類/點號綁前字、開類綁後字,以詞元為邊界、長引文 中段可斷);亦為 justify 綁定載體。 - lineedge:行端擠壓 layout pass(括號引號;ResizeObserver 重算,瀏覽器專用)。 §8 justify:jz-char inline-block 原子,句末標點單份間距。 v1 相容層(compat/v1):createCjkAutospace().apply 映射,IIFE 全域別名。 構建:esbuild → ESM/IIFE/CSS;tsc → d.ts。測試:39 個 jsdom 案例。 demo:簡繁並列+justify+強制繁體開明+標點寬度量測,載 Noto webfont 作參考 (擠壓依賴字型 halt)。間隔號 · 不觸發中西間隙;200% 等詞元不被禁則切斷。
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// 標點擠壓(架構 §6.3)。完整 clreq 擠壓之「行內」部分——三條規則並集,
|
||||
// 以方向性 jz-cl(收左半形空白)/jz-cr(收右)落地,CSS 以負 margin 渲染,
|
||||
// 跨瀏覽器一致(I4),不依賴 halt/text-spacing-trim。
|
||||
//
|
||||
// 機制 C baseline 寬度政策(依 lang,I3):
|
||||
// 全角式 quanjiao — 一律不收;
|
||||
// 開明式 kaiming — 句內點號(,、;:)+ 開類/閉類括號引號半形,
|
||||
// 句末點號(。!?)保持全形;
|
||||
// 半角式 banjiao — 全部標點半形。
|
||||
// 機制 A 行內連續擠壓(jiyaAdjacencyPass,lang 無關):相鄰標點之對向空白
|
||||
// 對消一次(每相鄰對省半形),如 。」→ 。收右 → 1.5em。
|
||||
// 機制 B 行端擠壓 → typeset/lineedge.ts(layout pass,瀏覽器專用)。
|
||||
//
|
||||
// 同時為 §8:所有 jz-char 經 CSS 設 inline-block,成 justify 單一原子項(I5)。
|
||||
|
||||
import { bdClassOf, createJz, revertPass } from "../core/dom.js";
|
||||
import { resolveStyle } from "../core/locale.js";
|
||||
import type { PunctStyle } from "../core/locale.js";
|
||||
import { blankSides, classifyBiaodian } from "../core/unicode.js";
|
||||
import type { CharifyPass, RenderContext, StandalonePass, WrapRequest } from "../types.js";
|
||||
|
||||
const PASS = "jiya";
|
||||
|
||||
/** baseline 政策下,該標點是否擠成半形(機制 C)。半形以字型 halt 渲染,
|
||||
* 方向由字型決定,故此處只需「半形與否」之布林。 */
|
||||
function baselineHalf(style: PunctStyle, bdClass: string): boolean
|
||||
{
|
||||
if (style === "quanjiao") { return false; }
|
||||
if (style === "banjiao")
|
||||
{
|
||||
// 半角式:所有可壓縮標點半形(句末亦半)。
|
||||
return bdClass === "bd-pause" || bdClass === "bd-stop"
|
||||
|| bdClass === "bd-open" || bdClass === "bd-close";
|
||||
}
|
||||
// kaiming:句內點號+括號引號半形,句末點號保持全形。
|
||||
return bdClass === "bd-pause" || bdClass === "bd-open" || bdClass === "bd-close";
|
||||
}
|
||||
|
||||
/** parent 已是 jz-char(本 pass 產物)→ 冪等跳過。 */
|
||||
function inJzChar(node: Node): boolean
|
||||
{
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1)
|
||||
{
|
||||
const name = (p as Element).nodeName;
|
||||
if (name === "JZ-CHAR" || name === "JZ-INNER") { return true; }
|
||||
p = p.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----- charify + baseline(機制 C)-----
|
||||
|
||||
export const jiyaPass: CharifyPass = {
|
||||
name: PASS,
|
||||
kind: "charify",
|
||||
order: 20,
|
||||
enabled: (o) => o.features.jiya,
|
||||
collect(node, ctx): WrapRequest[]
|
||||
{
|
||||
if (inJzChar(node)) { return []; }
|
||||
if (!ctx.finder.featureEnabledFor(node, PASS)) { return []; }
|
||||
|
||||
const data = node.data;
|
||||
const out: WrapRequest[] = [];
|
||||
let style: PunctStyle | null = null;
|
||||
|
||||
for (let i = 0; i < data.length; i += 1)
|
||||
{
|
||||
const bdClass = classifyBiaodian(data.charAt(i));
|
||||
if (!bdClass) { continue; }
|
||||
if (style === null) { style = resolveStyle(node, ctx.options.locale); }
|
||||
const classes = baselineHalf(style, bdClass) ? [ bdClass, "jz-half" ] : [ bdClass ];
|
||||
out.push({
|
||||
start: i,
|
||||
end: i + 1,
|
||||
pass: PASS,
|
||||
build: (text) =>
|
||||
{
|
||||
const outer = createJz("jz-char", PASS, "wrap", { classes });
|
||||
const inner = createJz("jz-inner", PASS, "wrap", { text });
|
||||
outer.appendChild(inner);
|
||||
return outer;
|
||||
},
|
||||
});
|
||||
}
|
||||
return out;
|
||||
},
|
||||
revert(ctx: RenderContext): void
|
||||
{
|
||||
revertPass(PASS, ctx.root);
|
||||
},
|
||||
};
|
||||
|
||||
// ----- 行內連續擠壓(機制 A)-----
|
||||
//
|
||||
// 對 DOM 直接相鄰(nextSibling 即下一 jz-char、其間無文本)之標點對 (L, R):
|
||||
// 收掉「指向彼此」之那一側——L 若有右空白則收 L(半形),否則 R 若有左空白
|
||||
// 則收 R。每相鄰對只收一字 → 該對約 1.5em。鏈狀連續標點逐對處理。加 jz-half
|
||||
// 冪等(並集),與 baseline 疊加(已半形則 no-op)。class 掛於 charify 之
|
||||
// jz-char,隨其解包清除,本 pass 無獨立產物。
|
||||
//
|
||||
// 半形以字型 halt 渲染(居中字形如 ?!亦正確、不重疊);例:?!→ 只收 ?,
|
||||
// !保持全形 1em。
|
||||
|
||||
export const jiyaAdjacencyPass: StandalonePass = {
|
||||
name: "jiyaAdjacency",
|
||||
kind: "standalone",
|
||||
order: 25,
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx: RenderContext): void
|
||||
{
|
||||
const { finder } = ctx;
|
||||
const parents = new Set<Node>();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) =>
|
||||
{
|
||||
if (c.parentNode) { parents.add(c.parentNode); }
|
||||
});
|
||||
|
||||
for (const parent of parents)
|
||||
{
|
||||
let child: Node | null = parent.firstChild;
|
||||
while (child)
|
||||
{
|
||||
const next = child.nextSibling;
|
||||
if (
|
||||
child.nodeType === 1 && (child as Element).nodeName === "JZ-CHAR"
|
||||
&& next && next.nodeType === 1 && (next as Element).nodeName === "JZ-CHAR"
|
||||
)
|
||||
{
|
||||
const L = child as Element;
|
||||
const R = next as Element;
|
||||
if (finder.featureEnabledFor(L, PASS))
|
||||
{
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) { L.classList.add("jz-half"); }
|
||||
else if (rSides.left) { R.classList.add("jz-half"); }
|
||||
}
|
||||
}
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
},
|
||||
revert(): void
|
||||
{
|
||||
// 無獨立產物:class 掛於 jiya charify 之 jz-char,隨其解包清除。
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user