Files
cjk-autospace/src/typeset/jiya.ts
T
admin 4f84d31785 refactor: 元素分類單一入口 finder.category + ResizeObserver 派發序不變量確認
承階段二之兩件收尾:確認 lineedge 雙 ResizeObserver 之派發序(審計唯一未闭環項),並把散落
各遍歷原語之元素分類優先序收斂為單一 finder.category。

ResizeObserver 派發序——**確認為非缺陷**(Chrome headless 實測):resize 時回調按 observer
**創建順序**派發(ResizeObserver 規範遍歷 observers 之插入序,非 observe 序;實測 seq=12——
反向 observe 仍按創建序)。lineEdgePass(order 90)之 observer 先於 gapTrimPass(order 92)建、
故每次 resize 皆先派發,gapTrim 之行末去隙判定恆基於 lineEdge **已套用**之半形版面(非陳舊態)。
以註解鎖為不變量(勿調換兩 pass 之 order/創建序),不需改碼。

元素分類單一入口:新增 `Finder.category(el): InlineCategory`——一元素恰屬 avoid/pill/isolate/
block/transparent/opaque 之一,優先序 `avoid > pill > isolate > block > transparent > opaque`
集中一處(原散落 collectRuns/jiyaAdjacency/canSplit/isTransparent 各自短路、易漂移)。
・isTransparent = `category === "transparent"`;collectRuns/jiyaAdjacency 改 `switch(category)`;
  canSplit = `SPLITTABLE ∩ transparent`(結構切分為獨立層、白名單更窄)。
・行為等價(121 全綠);多命中優先序(data-jz-skip pill→avoid、pill+isolate→pill、未知行內
  →opaque 邊界)由既有行為測試覆蓋。
・README 三分類要點+ARCHITECTURE §4.1 補優先序與 category 說明。
2026-07-07 21:45:06 +08:00

199 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 標點擠壓(架構 §6.3)。完整 clreq 擠壓之「行內」部分——三條規則並集,
// 以方向性 jz-cl(收左半形空白)/jz-cr(收右)落地,CSS 以負 margin 渲染,
// 跨瀏覽器一致(I4),不依賴 halttext-spacing-trim。
//
// 機制 C baseline 寬度政策(依 langI3):
// 全角式 quanjiao — 一律不收;
// 開明式 kaiming — 句內點號(,、;:)+ 開類/閉類括號引號半形,
// 句末點號(。!?)保持全形;
// 半角式 banjiao — 全部標點半形。
// 機制 A 行內連續擠壓(jiyaAdjacencyPasslang 無關):相鄰標點之對向空白
// 對消一次(每相鄰對省半形),如 。」→ 。收右 → 1.5em。
// 機制 B 行端擠壓 → typeset/lineedge.tslayout pass,瀏覽器專用)。
//
// 同時為 §8:所有 jz-char 經 CSS 設 inline-block,成 justify 單一原子項(I5)。
import { bdClassOf, createJz, revertPass } from "../core/dom.js";
import { classify, effectiveLang, resolveStyle } from "../core/locale.js";
import type { LangClass, PunctStyle } from "../core/locale.js";
import { blankSides, classifyBiaodian, inkBias } 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,
level: "text",
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;
let langClass: LangClass | null = null;
const biasOverrides = ctx.options.jiyaConfig.bias;
const biaodian = ctx.options.ruleset.biaodian;
for (let i = 0; i < data.length; i += 1)
{
const ch = data.charAt(i);
const bdClass = classifyBiaodian(ch, biaodian);
if (!bdClass) { continue; }
if (style === null) { style = resolveStyle(node, ctx.options.locale); }
if (langClass === null)
{
langClass = classify(effectiveLang(node, ctx.options.locale.default));
}
const classes = baselineHalf(style, bdClass) ? [ bdClass, "jz-half" ] : [ bdClass ];
// 字形偏側 classjz-ink-*)— 供 margin 後備模式收對側半形空白;halt 模式
// 之 CSS 不讀,無害。一律於 charify 標記(偏側為字形固有屬性,與何時擠壓
// 無關),鄰接/行端 pass 後續加 jz-half 即可沿用。
const bias = inkBias(ch, langClass, biasOverrides);
if (bias) { classes.push("jz-ink-" + bias); }
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-----
//
// 對「邏輯相鄰」之標點對 (L, R)——其間只隔**透明行內邊界**(樣式行內 strong/em…),
// 無文本、無非透明元素、不跨 block——挤掉兩者「指向彼此」之空白:**兩側獨立判斷**
// (非二選一)——L 右側有空白則收 L(半形),R 左側有空白則收 R。故:
// ・,「(pause 右空白 + open 左空白):兩側皆收 → 合占 1em。
// ・。」(stop 右空白 + close 右空白,」左為墨):只收 。→ 全角式 1.5em/開明式
// 1em(」baseline 已半)。
// ・。「(句末點號 + 開類):此為**句界**——保留句末點號右側空白(不收 L),只收
// 開類左側 → 全角/開明皆 1.5em(對稱於 。」)。**唯一例外**:限句末點號(。.!?)
// 後接開類;句內點號 ,「 與雙括號 」「/》《 仍兩側皆收 1em(屬同句/貼合,非句界)。
// ・·—(中點/連接,皆無指向對方之空白):皆不收 → 2em(不宜挤之例外)。
// 機制依 clreq「相鄰空白重疊即挤」。**透明穿越**:以從 root 之遍歷取代原「同 parent
// 直接兄弟」掃描,使跨 strong/em 邊界之連續標點亦正確配對(下游回饋;block 邊界與
// avoid 子樹斷相鄰)。半形以 halt 渲染、冪等並集,與 baseline 疊加。class 掛於
// charify 之 jz-char,隨其解包清除,本 pass 無獨立產物。
export const jiyaAdjacencyPass: StandalonePass = {
name: "jiyaAdjacency",
kind: "standalone",
order: 25,
level: "text",
enabled: (o) => o.features.jiya,
render(ctx: RenderContext): void
{
const { finder } = ctx;
let prev: Element | null = null;
const squeeze = (L: Element, R: Element): void =>
{
if (!finder.featureEnabledFor(L, PASS)) { return; }
const lClass = bdClassOf(L);
const rClass = bdClassOf(R);
const lSides = blankSides(lClass);
const rSides = blankSides(rClass);
// 句界例外:句末點號後接開類(。「)保留句末點號右側空白,不收 L(→ 1.5em,
// 對稱於 。」)。僅此一例;句內點號 ,「 與雙括號 」「 仍照常兩側收。
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
if (lSides.right && !stopBeforeOpen) { L.classList.add("jz-half"); }
if (rSides.left) { R.classList.add("jz-half"); }
};
const visit = (node: Node): void =>
{
for (let c: Node | null = node.firstChild; c; c = c.nextSibling)
{
if (c.nodeType === 3)
{
if ((c as Text).data.length > 0) { prev = null; } // 文本斷相鄰
continue;
}
if (c.nodeType !== 1) { continue; } // 註解等:略過、不斷
const el = c as Element;
const nm = el.nodeName;
if (nm === "JZ-CHAR")
{
if (prev) { squeeze(prev, el); }
prev = el; // jz-char 為原子,不下探(內含 jz-inner)
}
else
{
switch (finder.category(el)) // 與 collectRuns 共用同一分類(§4.1
{
case "avoid":
case "pill":
{
prev = null; // 斷相鄰、不下探(內容不可見/不處理)
break;
}
case "transparent":
{
visit(el); // 穿越、相鄰性延續
break;
}
default: // isolateblockopaque(未知行內)
{
prev = null; visit(el); prev = null; // 斷相鄰、內部自成脈絡
break;
}
}
}
}
};
visit(ctx.root);
},
revert(): void
{
// 無獨立產物:class 掛於 jiya charify 之 jz-char,隨其解包清除。
},
};