30a11b5d6e
下游審計(4 路並行測繪 + 逐條源碼複驗)之階段一:落地已複驗確認之缺陷與低風險 清理;架構統一(透明判定單一真相源)與文檔重整留待階段二/三。 修復(複驗確認): ・jinze 二次 render(未 revert)產生巢狀 jz-jinze、違反 I6 冪等——守衛改判 parent 自身即本 pass 產物(isJz(parent, PASS)),已綁定內容不再重包(實測 jz-jinze 3→6 已修)。 ・processPills 漏檢 avoidsSelf——自身命中 avoid(如 <code data-jz-skip>)之 pill 仍被補 間隙(isAvoided 只查祖先、不含自身,同「下游問題六」型)。processPills + root 自處理 分支補 avoidsSelf 檢查。 ・三分類互斥:CODE/KBD 自 DEFAULT_SKIP_TAGS 移除——二者已由預設 pillSelector "code, kbd" 以 pill 機制涵蓋(內部不可見+兩端補隙+斷 run/相鄰),再列 skipTags 致 同元素既 pill 又 avoid、pill 兩端間隙被 avoid 抹掉(違反互斥,且令上一項修復誤傷普通 code)。移除後普通 code 為純 pill、正常補隙;唯自身 data-jz-skip 者不補(即上項修復)。 清理: ・刪零引用死代碼 createMarker/ANY_CJK/BD_ALL/BD_CLASS_NAMES/CreateOptions.attrs; isJz 由 jinze 冪等守衛啟用、脫離死代碼。 ・死旗標 biaodian/emphasis/ruby 補 @reserved 註記(比照 hanging;點明與 charClass.biaodian 撞名異義,避免混淆)。 ・JzKind 補註 data-jz-kind 第三值 "splitoff"(split.ts 直接設、不經 createJz); longWord/orphan 冗餘 ===false 特判簡化(feature() 已對 false 回 false)。 複驗後重新定性(不照單全收): ・jinze 跨透明元素邊界避頭尾漏綁(你好<strong>。世界</strong> 之句末點號漏綁)——**真 缺陷**,但正解須改 jinze 之父級處理模型(穿越透明包裹上攀評估跨層綁定),與階段二 「透明判定單一真相源」一併處理更穩;forbiddenBreak 下探草稿已還原、缺口以註解標記。 ・lineedge edgeRect 攀穿 isolate——**非缺陷**:edgeRect 為物理版面測量(getClientRects), isolate 斷的是邏輯 run/相鄰,其內部字元之物理行首/行尾判定須跨 <mark> 找真實相鄰 內容(否則「文字<mark>「引」</mark>」之 「 誤判行首而錯收左半)。初評誤判、已還原並補註。 新增 3 回歸測試(jinze 冪等、預設 code 為 pill 非 skipTags、自身 avoid 之 pill 不補隙); 119 測試全綠、tsc + esbuild 構建乾淨。
154 lines
5.2 KiB
TypeScript
154 lines
5.2 KiB
TypeScript
// jz-* 元素工廠與還原(架構 §4.4/§5)。
|
||
//
|
||
// 所有注入結構皆帶 data-jz="<pass>" 標記(冪等 + 可定址還原,I6),並以
|
||
// data-jz-kind 區分「marker(純注入,還原時移除)」與「wrap(包裝原內容,
|
||
// 還原時解包)」。視覺尺寸一律在 CSS(I2);本模組只造結構。
|
||
|
||
/** jz-* 自訂元素名(皆含連字號,合法 custom element,預設 inline)。 */
|
||
export type JzTag =
|
||
| "jz-hws"
|
||
| "jz-char"
|
||
| "jz-inner"
|
||
| "jz-cs"
|
||
| "jz-jinze"
|
||
| "jz-orphan"
|
||
| "jz-em"
|
||
| "jz-ruby"
|
||
| "jz-rb"
|
||
| "jz-rt";
|
||
|
||
// createJz 之 kind:marker(純注入,還原時移除)/wrap(包裝原內容,還原時解包)。
|
||
// data-jz-kind 另有第三種值 "splitoff"(克隆切分之邊界詞元),由 core/split.ts 直接以
|
||
// setAttribute 設定、不經 createJz;revertPass 於 wrap 全解包後之第二階段併回(見下)。
|
||
type JzKind = "marker" | "wrap";
|
||
|
||
export interface CreateOptions
|
||
{
|
||
/** 追加之 class(空白分隔或陣列)。 */
|
||
classes?: string | string[];
|
||
/** 文本內容。 */
|
||
text?: string;
|
||
}
|
||
|
||
const KIND_ATTR = "data-jz-kind";
|
||
const PASS_ATTR = "data-jz";
|
||
|
||
function doc(): Document
|
||
{
|
||
if (typeof document === "undefined")
|
||
{
|
||
throw new Error("juzhen: 無 document(需於瀏覽器或 jsdom 環境執行)。");
|
||
}
|
||
return document;
|
||
}
|
||
|
||
/** 造一個帶 pass 標記之 jz-* 元素。 */
|
||
export function createJz(
|
||
tag: JzTag,
|
||
pass: string,
|
||
kind: JzKind,
|
||
opts: CreateOptions = {},
|
||
): HTMLElement
|
||
{
|
||
const el = doc().createElement(tag);
|
||
el.setAttribute(PASS_ATTR, pass);
|
||
el.setAttribute(KIND_ATTR, kind);
|
||
if (opts.classes)
|
||
{
|
||
const list = Array.isArray(opts.classes) ? opts.classes : [ opts.classes ];
|
||
for (const c of list) { if (c) { el.classList.add(c); } }
|
||
}
|
||
if (opts.text !== undefined) { el.textContent = opts.text; }
|
||
return el;
|
||
}
|
||
|
||
/**
|
||
* 還原某 pass 之全部產物(I6)。以 data-jz 定址,逆文件序處理(先內層後
|
||
* 外層),marker 移除、wrap 解包,最後 normalize 受影響父節點合併文本。
|
||
*
|
||
* splitoff(克隆切分之邊界詞元,見 core/split.ts)須在 wrap 全部解包**之後**才併回:
|
||
* 否則其前兄弟可能仍是 wrap 殼(如 jz-jinze)而非原元素。故主迴圈跳過 splitoff,於
|
||
* 解包後第二階段以文件序逐一併回前兄弟(同標籤元素),還原為單一元素(I6 一致)。
|
||
*/
|
||
export function revertPass(pass: string, root: ParentNode): void
|
||
{
|
||
const nodes = Array.from(
|
||
root.querySelectorAll<HTMLElement>('[' + PASS_ATTR + '="' + pass + '"]'),
|
||
);
|
||
// 逆序:子元素先於父元素處理,避免解包父後子引用失效。
|
||
const touched = new Set<Node>();
|
||
for (let i = nodes.length - 1; i >= 0; i -= 1)
|
||
{
|
||
const el = nodes[i]!;
|
||
const parent = el.parentNode;
|
||
if (!parent) { continue; }
|
||
const kind = el.getAttribute(KIND_ATTR);
|
||
if (kind === "splitoff") { continue; } // 延後階段處理。
|
||
touched.add(parent);
|
||
if (kind === "marker")
|
||
{
|
||
parent.removeChild(el);
|
||
}
|
||
else
|
||
{
|
||
// wrap:把子節點搬出後移除外殼。
|
||
while (el.firstChild)
|
||
{
|
||
parent.insertBefore(el.firstChild, el);
|
||
}
|
||
parent.removeChild(el);
|
||
}
|
||
}
|
||
// 第二階段:splitoff 併回(此時 wrap 已全解包,前兄弟即原元素)。文件序,使同一
|
||
// 原元素之多個 splitoff 依序併入。
|
||
const splitoffs = Array.from(
|
||
root.querySelectorAll<HTMLElement>(
|
||
'[' + PASS_ATTR + '="' + pass + '"][' + KIND_ATTR + '="splitoff"]',
|
||
),
|
||
);
|
||
for (const so of splitoffs)
|
||
{
|
||
const parent = so.parentNode;
|
||
if (!parent) { continue; }
|
||
touched.add(parent);
|
||
const prev = so.previousSibling;
|
||
if (prev && prev.nodeType === 1 && (prev as Element).nodeName === so.nodeName)
|
||
{
|
||
// 併入前兄弟(同標籤原元素)尾部。
|
||
while (so.firstChild) { prev.appendChild(so.firstChild); }
|
||
parent.removeChild(so);
|
||
}
|
||
else
|
||
{
|
||
// 前兄弟非預期同標籤元素(罕見,結構曾被外力改動):原地解包,仍移除標記。
|
||
while (so.firstChild) { parent.insertBefore(so.firstChild, so); }
|
||
parent.removeChild(so);
|
||
}
|
||
}
|
||
for (const p of touched)
|
||
{
|
||
if ((p as Element).normalize) { (p as Element).normalize(); }
|
||
}
|
||
}
|
||
|
||
const BD_CLASSES = [ "bd-open", "bd-close", "bd-pause", "bd-stop", "bd-middle", "bd-liga" ];
|
||
|
||
/** 讀取 jz-char 之 bd-* 子類(無則 null)。 */
|
||
export function bdClassOf(el: Element): string | null
|
||
{
|
||
for (const c of BD_CLASSES)
|
||
{
|
||
if (el.classList.contains(c)) { return c; }
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 該元素是否為某/任一 pass 之 jz 產物。 */
|
||
export function isJz(node: Node | null, pass?: string): boolean
|
||
{
|
||
if (!node || node.nodeType !== 1) { return false; }
|
||
const el = node as Element;
|
||
if (!el.hasAttribute(PASS_ATTR)) { return false; }
|
||
return pass === undefined || el.getAttribute(PASS_ATTR) === pass;
|
||
}
|