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 說明。
This commit is contained in:
+9
-1
@@ -250,7 +250,15 @@ Nvim peek/Claude 模板等 v1 消費端之過渡期。**最後一批 v1 消費
|
|||||||
過濾 `insidePill`,不 charify/不插間隙/不切分/不下潛),禁則時作為原子鄰字
|
過濾 `insidePill`,不 charify/不插間隙/不切分/不下潛),禁則時作為原子鄰字
|
||||||
單位**整體綁入** `jz-jinze`。典型:`code`/`kbd`、行內公式 `.katex`。
|
單位**整體綁入** `jz-jinze`。典型:`code`/`kbd`、行內公式 `.katex`。
|
||||||
|
|
||||||
判定**以元素為準、非標籤名**(`isTransparent(el)`,先排除 pill/isolate/avoid
|
**單一分類 `finder.category(el)`(優先序)**:一元素恰屬六類之一,多選擇器同命中時依序短路
|
||||||
|
定案——`avoid > pill > isolate > block > transparent > opaque`(`opaque`=未在 styleInlines
|
||||||
|
之未知行內標籤,嚴格白名單下當隔離式邊界)。故帶 `data-jz-skip` 之 pill/isolate 判為 avoid;
|
||||||
|
同命中 pill 與 isolate 判為 pill(pill 覆蓋 isolate)。邏輯文本流之遍歷原語(`collectRuns`/
|
||||||
|
`jiyaAdjacency`/`isTransparent`)皆 `switch(category)` 分派,`canSplit` = `SPLITTABLE ∩ transparent`
|
||||||
|
——分類集中一處、優先序不再散落各原語各自短路(階段二後之清晰化)。
|
||||||
|
|
||||||
|
判定**以元素為準、非標籤名**(`isTransparent(el) = category(el)==="transparent"`,前置排除 pill/
|
||||||
|
isolate/avoid
|
||||||
再查標籤集):KaTeX 全樹皆 `<span>`,舊版按標籤名判透明曾令 `canSplit`/`descend`
|
再查標籤集):KaTeX 全樹皆 `<span>`,舊版按標籤名判透明曾令 `canSplit`/`descend`
|
||||||
整棵下潛——禁則把公式內側末字連同完整殼層逐層克隆切出(`splitoff`),上標漂移錯位
|
整棵下潛——禁則把公式內側末字連同完整殼層逐層克隆切出(`splitoff`),上標漂移錯位
|
||||||
(下游問題六);`pillSelector`/`skipAttribute`/`scope.avoid` 三層防護全被刺穿。
|
(下游問題六);`pillSelector`/`skipAttribute`/`scope.avoid` 三層防護全被刺穿。
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ web component 等)採**嚴格白名單**、一律當**邊界**(語義同隔
|
|||||||
|
|
||||||
要點:
|
要點:
|
||||||
|
|
||||||
|
- **分類優先序(單一判定 `finder.category`)**:一元素恰屬其一,多選擇器同命中時依序短路定案——
|
||||||
|
`avoid > pill > isolate > block > transparent > opaque`(`opaque`=未登記之未知行內標籤,
|
||||||
|
當隔離式邊界)。故帶 `data-jz-skip` 之 pill/isolate 判為 avoid;同命中 pill 與 isolate 判為 pill。
|
||||||
|
邏輯文本流各原語(`collectRuns`/`jiyaAdjacency`/`adjacentLogicalChar`)皆據此**同一分類**分派。
|
||||||
- **判定以元素為準、非標籤名**:KaTeX 全樹皆 `<span>`,但 `.katex` 命中 `pillSelector`
|
- **判定以元素為準、非標籤名**:KaTeX 全樹皆 `<span>`,但 `.katex` 命中 `pillSelector`
|
||||||
即為整體邊界——舊版按標籤名判透明曾整棵下潛、把公式內側末字連同殼層克隆切出(肢解)。
|
即為整體邊界——舊版按標籤名判透明曾整棵下潛、把公式內側末字連同殼層克隆切出(肢解)。
|
||||||
- **pill 行為覆蓋 isolate**:兩者邊界口徑完全相同(兩端補隙、斷 run/斷相鄰、不可切分,
|
- **pill 行為覆蓋 isolate**:兩者邊界口徑完全相同(兩端補隙、斷 run/斷相鄰、不可切分,
|
||||||
|
|||||||
Vendored
+19
-14
@@ -15,6 +15,8 @@ export interface AdjacentChar {
|
|||||||
offset: number;
|
offset: number;
|
||||||
ch: string;
|
ch: string;
|
||||||
}
|
}
|
||||||
|
/** 行內元素對排版之口徑分類(§4.1)——恰屬其一,優先序見 Finder.category。 */
|
||||||
|
export type InlineCategory = "avoid" | "pill" | "isolate" | "block" | "transparent" | "opaque";
|
||||||
export declare class Finder {
|
export declare class Finder {
|
||||||
private readonly opts;
|
private readonly opts;
|
||||||
private readonly styleSet;
|
private readonly styleSet;
|
||||||
@@ -39,20 +41,23 @@ export declare class Finder {
|
|||||||
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
||||||
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
||||||
insidePill(node: Node): boolean;
|
insidePill(node: Node): boolean;
|
||||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
/**
|
||||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
* 選擇器同命中時之定案規則):`avoid > pill > isolate > block > transparent > opaque`
|
||||||
* 鄰字仍須可達;I7)。
|
* (opaque=未在 styleInlines 之未知行內標籤,嚴格白名單下當隔離式邊界)。故:同時命中
|
||||||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
* pill 與 isolate → **pill**(pill 覆蓋 isolate);帶 data-jz-skip 之 pill/isolate → **avoid**
|
||||||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
* (自身排除最高)。**邏輯文本流之遍歷原語**(collectRuns/jiyaAdjacency/adjacentLogicalChar
|
||||||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
* 之 isTransparent)皆據此**單一分類**分派;core/split 之 canSplit = SPLITTABLE 白名單 ∩
|
||||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
* transparent(結構切分為獨立層,故更窄)。
|
||||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
*/
|
||||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
category(el: Element): InlineCategory;
|
||||||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
/** category 之透明判定末步:styleInlines 標籤,或聚珍自身之 wrap 類 jz-*(jz-hws 間隙、
|
||||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
* jz-jinze 禁則群組、**jz-char/jz-inner 標點原子**)。前四步已排除 avoid/pill/isolate/block。
|
||||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
* jz-char/jz-inner 必須列入:否則 adjacentLogicalChar 之 descend 遇標點原子既不進入亦不
|
||||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
* 終止、而**跳過**它續找更前之字(`汉字。<code>` 綁定後 pill 左鄰字應為「。」卻誤取「字」
|
||||||
|
* 補 jz-hws);jz-char 僅含標點(非 pillNeighbor),透明後 descend 回傳標點、不補隙。 */
|
||||||
|
private isTransparentTag;
|
||||||
|
/** el 是否為邏輯文本流可透明穿越者(= category "transparent")。 */
|
||||||
isTransparent(el: Element): boolean;
|
isTransparent(el: Element): boolean;
|
||||||
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
||||||
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||||||
|
|||||||
Vendored
+73
-42
@@ -65,27 +65,46 @@ var Juzhen = (() => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
/**
|
||||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
* 選擇器同命中時之定案規則):`avoid > pill > isolate > block > transparent > opaque`
|
||||||
* 鄰字仍須可達;I7)。
|
* (opaque=未在 styleInlines 之未知行內標籤,嚴格白名單下當隔離式邊界)。故:同時命中
|
||||||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
* pill 與 isolate → **pill**(pill 覆蓋 isolate);帶 data-jz-skip 之 pill/isolate → **avoid**
|
||||||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
* (自身排除最高)。**邏輯文本流之遍歷原語**(collectRuns/jiyaAdjacency/adjacentLogicalChar
|
||||||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
* 之 isTransparent)皆據此**單一分類**分派;core/split 之 canSplit = SPLITTABLE 白名單 ∩
|
||||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
* transparent(結構切分為獨立層,故更窄)。
|
||||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
*/
|
||||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
category(el) {
|
||||||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
if (this.avoidsSelf(el)) {
|
||||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
return "avoid";
|
||||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
|
||||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
|
||||||
isTransparent(el) {
|
|
||||||
if (this.pillMatches(el) || this.isolateMatches(el) || this.avoidsSelf(el)) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
if (this.pillMatches(el)) {
|
||||||
|
return "pill";
|
||||||
|
}
|
||||||
|
if (this.isolateMatches(el)) {
|
||||||
|
return "isolate";
|
||||||
|
}
|
||||||
|
if (this.blockSet.has(el.nodeName)) {
|
||||||
|
return "block";
|
||||||
|
}
|
||||||
|
if (this.isTransparentTag(el)) {
|
||||||
|
return "transparent";
|
||||||
|
}
|
||||||
|
return "opaque";
|
||||||
|
}
|
||||||
|
/** category 之透明判定末步:styleInlines 標籤,或聚珍自身之 wrap 類 jz-*(jz-hws 間隙、
|
||||||
|
* jz-jinze 禁則群組、**jz-char/jz-inner 標點原子**)。前四步已排除 avoid/pill/isolate/block。
|
||||||
|
* jz-char/jz-inner 必須列入:否則 adjacentLogicalChar 之 descend 遇標點原子既不進入亦不
|
||||||
|
* 終止、而**跳過**它續找更前之字(`汉字。<code>` 綁定後 pill 左鄰字應為「。」卻誤取「字」
|
||||||
|
* 補 jz-hws);jz-char 僅含標點(非 pillNeighbor),透明後 descend 回傳標點、不補隙。 */
|
||||||
|
isTransparentTag(el) {
|
||||||
const name = el.nodeName;
|
const name = el.nodeName;
|
||||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
||||||
}
|
}
|
||||||
|
/** el 是否為邏輯文本流可透明穿越者(= category "transparent")。 */
|
||||||
|
isTransparent(el) {
|
||||||
|
return this.category(el) === "transparent";
|
||||||
|
}
|
||||||
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
||||||
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||||||
* ③ SVG 命名空間 ④ contentEditable
|
* ③ SVG 命名空間 ④ contentEditable
|
||||||
@@ -224,29 +243,31 @@ var Juzhen = (() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const el = node;
|
const el = node;
|
||||||
if (this.avoidsSelf(el) || this.blockSet.has(el.nodeName)) {
|
const recurse = () => {
|
||||||
flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.pillMatches(el)) {
|
|
||||||
flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.isTransparent(el)) {
|
|
||||||
let c2 = el.firstChild;
|
let c2 = el.firstChild;
|
||||||
while (c2) {
|
while (c2) {
|
||||||
visit(c2);
|
visit(c2);
|
||||||
c2 = c2.nextSibling;
|
c2 = c2.nextSibling;
|
||||||
}
|
}
|
||||||
return;
|
};
|
||||||
|
switch (this.category(el)) {
|
||||||
|
case "avoid":
|
||||||
|
case "block":
|
||||||
|
case "pill": {
|
||||||
|
flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "transparent": {
|
||||||
|
recurse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
flush();
|
||||||
|
recurse();
|
||||||
|
flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flush();
|
|
||||||
let ic = el.firstChild;
|
|
||||||
while (ic) {
|
|
||||||
visit(ic);
|
|
||||||
ic = ic.nextSibling;
|
|
||||||
}
|
|
||||||
flush();
|
|
||||||
};
|
};
|
||||||
let c = block.firstChild;
|
let c = block.firstChild;
|
||||||
while (c) {
|
while (c) {
|
||||||
@@ -897,14 +918,24 @@ var Juzhen = (() => {
|
|||||||
squeeze(prev, el);
|
squeeze(prev, el);
|
||||||
}
|
}
|
||||||
prev = el;
|
prev = el;
|
||||||
} else if (finder.pillMatches(el) || finder.avoidsSelf(el)) {
|
|
||||||
prev = null;
|
|
||||||
} else if (finder.isTransparent(el)) {
|
|
||||||
visit(el);
|
|
||||||
} else {
|
} else {
|
||||||
prev = null;
|
switch (finder.category(el)) {
|
||||||
visit(el);
|
case "avoid":
|
||||||
prev = null;
|
case "pill": {
|
||||||
|
prev = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "transparent": {
|
||||||
|
visit(el);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
prev = null;
|
||||||
|
visit(el);
|
||||||
|
prev = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -932,7 +963,7 @@ var Juzhen = (() => {
|
|||||||
"SUP"
|
"SUP"
|
||||||
]);
|
]);
|
||||||
function canSplit(el, finder) {
|
function canSplit(el, finder) {
|
||||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && !finder.pillMatches(el) && !finder.isolateMatches(el) && !finder.avoidsSelf(el);
|
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && finder.category(el) === "transparent";
|
||||||
}
|
}
|
||||||
function firstText(el, finder) {
|
function firstText(el, finder) {
|
||||||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||||||
|
|||||||
Vendored
+73
-42
@@ -38,27 +38,46 @@ var Finder = class {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
/**
|
||||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
* 選擇器同命中時之定案規則):`avoid > pill > isolate > block > transparent > opaque`
|
||||||
* 鄰字仍須可達;I7)。
|
* (opaque=未在 styleInlines 之未知行內標籤,嚴格白名單下當隔離式邊界)。故:同時命中
|
||||||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
* pill 與 isolate → **pill**(pill 覆蓋 isolate);帶 data-jz-skip 之 pill/isolate → **avoid**
|
||||||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
* (自身排除最高)。**邏輯文本流之遍歷原語**(collectRuns/jiyaAdjacency/adjacentLogicalChar
|
||||||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
* 之 isTransparent)皆據此**單一分類**分派;core/split 之 canSplit = SPLITTABLE 白名單 ∩
|
||||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
* transparent(結構切分為獨立層,故更窄)。
|
||||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
*/
|
||||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
category(el) {
|
||||||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
if (this.avoidsSelf(el)) {
|
||||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
return "avoid";
|
||||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
|
||||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
|
||||||
isTransparent(el) {
|
|
||||||
if (this.pillMatches(el) || this.isolateMatches(el) || this.avoidsSelf(el)) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
if (this.pillMatches(el)) {
|
||||||
|
return "pill";
|
||||||
|
}
|
||||||
|
if (this.isolateMatches(el)) {
|
||||||
|
return "isolate";
|
||||||
|
}
|
||||||
|
if (this.blockSet.has(el.nodeName)) {
|
||||||
|
return "block";
|
||||||
|
}
|
||||||
|
if (this.isTransparentTag(el)) {
|
||||||
|
return "transparent";
|
||||||
|
}
|
||||||
|
return "opaque";
|
||||||
|
}
|
||||||
|
/** category 之透明判定末步:styleInlines 標籤,或聚珍自身之 wrap 類 jz-*(jz-hws 間隙、
|
||||||
|
* jz-jinze 禁則群組、**jz-char/jz-inner 標點原子**)。前四步已排除 avoid/pill/isolate/block。
|
||||||
|
* jz-char/jz-inner 必須列入:否則 adjacentLogicalChar 之 descend 遇標點原子既不進入亦不
|
||||||
|
* 終止、而**跳過**它續找更前之字(`汉字。<code>` 綁定後 pill 左鄰字應為「。」卻誤取「字」
|
||||||
|
* 補 jz-hws);jz-char 僅含標點(非 pillNeighbor),透明後 descend 回傳標點、不補隙。 */
|
||||||
|
isTransparentTag(el) {
|
||||||
const name = el.nodeName;
|
const name = el.nodeName;
|
||||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
||||||
}
|
}
|
||||||
|
/** el 是否為邏輯文本流可透明穿越者(= category "transparent")。 */
|
||||||
|
isTransparent(el) {
|
||||||
|
return this.category(el) === "transparent";
|
||||||
|
}
|
||||||
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
||||||
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||||||
* ③ SVG 命名空間 ④ contentEditable
|
* ③ SVG 命名空間 ④ contentEditable
|
||||||
@@ -197,29 +216,31 @@ var Finder = class {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const el = node;
|
const el = node;
|
||||||
if (this.avoidsSelf(el) || this.blockSet.has(el.nodeName)) {
|
const recurse = () => {
|
||||||
flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.pillMatches(el)) {
|
|
||||||
flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.isTransparent(el)) {
|
|
||||||
let c2 = el.firstChild;
|
let c2 = el.firstChild;
|
||||||
while (c2) {
|
while (c2) {
|
||||||
visit(c2);
|
visit(c2);
|
||||||
c2 = c2.nextSibling;
|
c2 = c2.nextSibling;
|
||||||
}
|
}
|
||||||
return;
|
};
|
||||||
|
switch (this.category(el)) {
|
||||||
|
case "avoid":
|
||||||
|
case "block":
|
||||||
|
case "pill": {
|
||||||
|
flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "transparent": {
|
||||||
|
recurse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
flush();
|
||||||
|
recurse();
|
||||||
|
flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flush();
|
|
||||||
let ic = el.firstChild;
|
|
||||||
while (ic) {
|
|
||||||
visit(ic);
|
|
||||||
ic = ic.nextSibling;
|
|
||||||
}
|
|
||||||
flush();
|
|
||||||
};
|
};
|
||||||
let c = block.firstChild;
|
let c = block.firstChild;
|
||||||
while (c) {
|
while (c) {
|
||||||
@@ -870,14 +891,24 @@ var jiyaAdjacencyPass = {
|
|||||||
squeeze(prev, el);
|
squeeze(prev, el);
|
||||||
}
|
}
|
||||||
prev = el;
|
prev = el;
|
||||||
} else if (finder.pillMatches(el) || finder.avoidsSelf(el)) {
|
|
||||||
prev = null;
|
|
||||||
} else if (finder.isTransparent(el)) {
|
|
||||||
visit(el);
|
|
||||||
} else {
|
} else {
|
||||||
prev = null;
|
switch (finder.category(el)) {
|
||||||
visit(el);
|
case "avoid":
|
||||||
prev = null;
|
case "pill": {
|
||||||
|
prev = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "transparent": {
|
||||||
|
visit(el);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
prev = null;
|
||||||
|
visit(el);
|
||||||
|
prev = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -905,7 +936,7 @@ var SPLITTABLE = /* @__PURE__ */ new Set([
|
|||||||
"SUP"
|
"SUP"
|
||||||
]);
|
]);
|
||||||
function canSplit(el, finder) {
|
function canSplit(el, finder) {
|
||||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && !finder.pillMatches(el) && !finder.isolateMatches(el) && !finder.avoidsSelf(el);
|
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && finder.category(el) === "transparent";
|
||||||
}
|
}
|
||||||
function firstText(el, finder) {
|
function firstText(el, finder) {
|
||||||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||||||
|
|||||||
+62
-33
@@ -42,6 +42,10 @@ export interface AdjacentChar
|
|||||||
ch: string;
|
ch: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 行內元素對排版之口徑分類(§4.1)——恰屬其一,優先序見 Finder.category。 */
|
||||||
|
export type InlineCategory =
|
||||||
|
| "avoid" | "pill" | "isolate" | "block" | "transparent" | "opaque";
|
||||||
|
|
||||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
export class Finder
|
export class Finder
|
||||||
@@ -104,32 +108,44 @@ export class Finder
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
/**
|
||||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
* 選擇器同命中時之定案規則):`avoid > pill > isolate > block > transparent > opaque`
|
||||||
* 鄰字仍須可達;I7)。
|
* (opaque=未在 styleInlines 之未知行內標籤,嚴格白名單下當隔離式邊界)。故:同時命中
|
||||||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
* pill 與 isolate → **pill**(pill 覆蓋 isolate);帶 data-jz-skip 之 pill/isolate → **avoid**
|
||||||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
* (自身排除最高)。**邏輯文本流之遍歷原語**(collectRuns/jiyaAdjacency/adjacentLogicalChar
|
||||||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
* 之 isTransparent)皆據此**單一分類**分派;core/split 之 canSplit = SPLITTABLE 白名單 ∩
|
||||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
* transparent(結構切分為獨立層,故更窄)。
|
||||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
*/
|
||||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
category(el: Element): InlineCategory
|
||||||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
{
|
||||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
if (this.avoidsSelf(el)) { return "avoid"; }
|
||||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
if (this.pillMatches(el)) { return "pill"; }
|
||||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
if (this.isolateMatches(el)) { return "isolate"; }
|
||||||
isTransparent(el: Element): boolean
|
if (this.blockSet.has(el.nodeName)) { return "block"; }
|
||||||
|
if (this.isTransparentTag(el)) { return "transparent"; }
|
||||||
|
return "opaque";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** category 之透明判定末步:styleInlines 標籤,或聚珍自身之 wrap 類 jz-*(jz-hws 間隙、
|
||||||
|
* jz-jinze 禁則群組、**jz-char/jz-inner 標點原子**)。前四步已排除 avoid/pill/isolate/block。
|
||||||
|
* jz-char/jz-inner 必須列入:否則 adjacentLogicalChar 之 descend 遇標點原子既不進入亦不
|
||||||
|
* 終止、而**跳過**它續找更前之字(`汉字。<code>` 綁定後 pill 左鄰字應為「。」卻誤取「字」
|
||||||
|
* 補 jz-hws);jz-char 僅含標點(非 pillNeighbor),透明後 descend 回傳標點、不補隙。 */
|
||||||
|
private isTransparentTag(el: Element): boolean
|
||||||
{
|
{
|
||||||
if (this.pillMatches(el) || this.isolateMatches(el) || this.avoidsSelf(el))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const name = el.nodeName;
|
const name = el.nodeName;
|
||||||
return this.styleSet.has(name)
|
return this.styleSet.has(name)
|
||||||
|| name === "JZ-HWS" || name === "JZ-JINZE"
|
|| name === "JZ-HWS" || name === "JZ-JINZE"
|
||||||
|| name === "JZ-CHAR" || name === "JZ-INNER";
|
|| name === "JZ-CHAR" || name === "JZ-INNER";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** el 是否為邏輯文本流可透明穿越者(= category "transparent")。 */
|
||||||
|
isTransparent(el: Element): boolean
|
||||||
|
{
|
||||||
|
return this.category(el) === "transparent";
|
||||||
|
}
|
||||||
|
|
||||||
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
/** 該元素**自身**是否為 avoid 邊界。avoid 之七種來源,依序短路判定、任一命中即是:
|
||||||
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
* ① isSkipped 謂詞(消費端函數) ② skipTags 標籤名單(PRE/SAMP/SCRIPT…,可覆寫)
|
||||||
* ③ SVG 命名空間 ④ contentEditable
|
* ③ SVG 命名空間 ④ contentEditable
|
||||||
@@ -266,24 +282,37 @@ export class Finder
|
|||||||
}
|
}
|
||||||
if (node.nodeType !== 1) { return; }
|
if (node.nodeType !== 1) { return; }
|
||||||
const el = node as Element;
|
const el = node as Element;
|
||||||
// avoid 自身 / block:斷 run、內部不入(avoid 內外皆不處理;block 另計)。
|
const recurse = (): void =>
|
||||||
if (this.avoidsSelf(el) || this.blockSet.has(el.nodeName)) { flush(); return; }
|
|
||||||
// 整體 pill:斷 run、內部完全不可見(不遞迴)。
|
|
||||||
if (this.pillMatches(el)) { flush(); return; }
|
|
||||||
// 透明(styleInlines + wrap 類 jz-*,如 jz-hws/jz-jinze/jz-char/jz-inner):
|
|
||||||
// 遞迴穿越、run 延續。jz-hws 以 margin-right 包裹左側邊界字,其內之字仍計入邏輯 run。
|
|
||||||
if (this.isTransparent(el))
|
|
||||||
{
|
{
|
||||||
let c = el.firstChild;
|
let c = el.firstChild;
|
||||||
while (c) { visit(c); c = c.nextSibling; }
|
while (c) { visit(c); c = c.nextSibling; }
|
||||||
return;
|
};
|
||||||
|
switch (this.category(el))
|
||||||
|
{
|
||||||
|
case "avoid":
|
||||||
|
case "block":
|
||||||
|
case "pill":
|
||||||
|
{
|
||||||
|
// 邊界、內部不入:avoid/pill 內部不可見;block 另由 eachBlock 處理。
|
||||||
|
flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "transparent":
|
||||||
|
{
|
||||||
|
// 透明:遞迴穿越、run 延續。jz-hws 以 margin-right 包裹左側邊界字,
|
||||||
|
// 其內之字仍計入邏輯 run。
|
||||||
|
recurse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: // "isolate" | "opaque"(未知行內,嚴格白名單)
|
||||||
|
{
|
||||||
|
// 斷 run,內部自成獨立 run(照常處理、不與外部相鄰)。
|
||||||
|
flush();
|
||||||
|
recurse();
|
||||||
|
flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 隔離,及嚴格白名單下之**未知行內標籤**:斷 run,內部自成獨立 run(照常處理、
|
|
||||||
// 不與外部相鄰)。透明須顯式登記 styleInlines(見檔頭三分類說明)。
|
|
||||||
flush();
|
|
||||||
let ic = el.firstChild;
|
|
||||||
while (ic) { visit(ic); ic = ic.nextSibling; }
|
|
||||||
flush();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let c = block.firstChild;
|
let c = block.firstChild;
|
||||||
|
|||||||
+5
-4
@@ -38,14 +38,15 @@ interface Point
|
|||||||
offset: number;
|
offset: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 元素是否可克隆切分(純樣式行內標籤、無 id、非 pill/isolate/avoid 邊界)。 */
|
/** 元素是否可克隆切分 = SPLITTABLE 白名單(結構切分安全,排除 <a>/ABBR/RUBY/帶 id 等)
|
||||||
|
* ∩ transparent(非 avoid/pill/isolate/block 邊界)。SPLITTABLE ⊂ styleInlines,故命中白名單
|
||||||
|
* 且 category 為 transparent 者即可切;為邊界者(含消費端自 styleInlines 移出之標籤)不可切、
|
||||||
|
* 退化整綁。結構切分為獨立層、白名單較邏輯透明更窄(見 finder.category)。 */
|
||||||
function canSplit(el: Element, finder: Finder): boolean
|
function canSplit(el: Element, finder: Finder): boolean
|
||||||
{
|
{
|
||||||
return SPLITTABLE.has(el.nodeName)
|
return SPLITTABLE.has(el.nodeName)
|
||||||
&& !el.hasAttribute("id")
|
&& !el.hasAttribute("id")
|
||||||
&& !finder.pillMatches(el)
|
&& finder.category(el) === "transparent";
|
||||||
&& !finder.isolateMatches(el)
|
|
||||||
&& !finder.avoidsSelf(el);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** el 內**首**個非空文本節點(僅下探可切標籤;遇非可切元素回 null=放棄切分)。 */
|
/** el 內**首**個非空文本節點(僅下探可切標籤;遇非可切元素回 null=放棄切分)。 */
|
||||||
|
|||||||
+19
-7
@@ -165,15 +165,27 @@ export const jiyaAdjacencyPass: StandalonePass = {
|
|||||||
if (prev) { squeeze(prev, el); }
|
if (prev) { squeeze(prev, el); }
|
||||||
prev = el; // jz-char 為原子,不下探(內含 jz-inner)
|
prev = el; // jz-char 為原子,不下探(內含 jz-inner)
|
||||||
}
|
}
|
||||||
else if (finder.pillMatches(el) || finder.avoidsSelf(el))
|
|
||||||
{
|
|
||||||
prev = null; // 整體 pill/avoid:斷相鄰、不下探(內容不可見/不處理)
|
|
||||||
}
|
|
||||||
else if (finder.isTransparent(el)) { visit(el); } // 透明:穿越、相鄰性延續
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 隔離/block/嚴格白名單下之未知行內:前後皆斷相鄰,內部自成脈絡(另計)。
|
switch (finder.category(el)) // 與 collectRuns 共用同一分類(§4.1)
|
||||||
prev = null; visit(el); prev = null;
|
{
|
||||||
|
case "avoid":
|
||||||
|
case "pill":
|
||||||
|
{
|
||||||
|
prev = null; // 斷相鄰、不下探(內容不可見/不處理)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "transparent":
|
||||||
|
{
|
||||||
|
visit(el); // 穿越、相鄰性延續
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: // isolate/block/opaque(未知行內)
|
||||||
|
{
|
||||||
|
prev = null; visit(el); prev = null; // 斷相鄰、內部自成脈絡
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ const HEAD_CLASSES = new Set([ "bd-open" ]);
|
|||||||
const TAIL_CLASSES = new Set([ "bd-close" ]);
|
const TAIL_CLASSES = new Set([ "bd-close" ]);
|
||||||
|
|
||||||
// 每 root 各 layout pass(以 key 區分)一個 ResizeObserver。
|
// 每 root 各 layout pass(以 key 區分)一個 ResizeObserver。
|
||||||
|
//
|
||||||
|
// **跨 observer 派發序不變量(已於 Chrome headless 實測確認)**:resize 時 ResizeObserver
|
||||||
|
// 之回調按 **observer 創建順序** 派發(ResizeObserver 規範遍歷 observers 列表之插入序,非
|
||||||
|
// observe 序)。lineEdgePass(order 90)先於 gapTrimPass(order 92)執行、故其 observer 先
|
||||||
|
// 建,於每次 resize 皆先派發——確保 gapTrim 之行末去隙判定恆基於 lineEdge **已套用**之半形
|
||||||
|
// 版面(非陳舊態)。**勿調換兩 pass 之 order 或創建序**,否則 gapTrim 可能以 resize 前之半形
|
||||||
|
// 狀態誤判行末(且 ResizeObserver 只觀 root 尺寸、不因內部 class 變化重觸發,錯誤會持續)。
|
||||||
const observers = new WeakMap<Element, Map<string, ResizeObserver>>();
|
const observers = new WeakMap<Element, Map<string, ResizeObserver>>();
|
||||||
|
|
||||||
function hasLayout(): boolean
|
function hasLayout(): boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user