4f84d31785
承階段二之兩件收尾:確認 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 說明。
199 lines
8.9 KiB
TypeScript
199 lines
8.9 KiB
TypeScript
// 標點擠壓(架構 §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 { 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 ];
|
||
// 字形偏側 class(jz-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: // isolate/block/opaque(未知行內)
|
||
{
|
||
prev = null; visit(el); prev = null; // 斷相鄰、內部自成脈絡
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
visit(ctx.root);
|
||
},
|
||
revert(): void
|
||
{
|
||
// 無獨立產物:class 掛於 jiya charify 之 jz-char,隨其解包清除。
|
||
},
|
||
};
|