perf+feat: 掃描/佈局提速(中間/S4+S1)+增量恢復(Tier3 C1/C3/C2)
承 Tier1(S2+S3,已於 main 168e9e7)。本批為效能第二層+第三層,全程獨立複驗(差分/往返/ 增量≡全量/進程隔離測速/真瀏覽器)+4 份真人撰寫真實文檔驗收,行為保真、提速真實。 掃描層(無狀態、保行為): - 中間發現+S4:insidePill/isAvoided 之「逐文本節點沿祖先鏈」以 ancestorOrSelfPill/…Avoided 迭代回填記憶化(每元素恰算一次、兄弟共用祖先鏈快取),O(文本×depth)→O(元素)。實測掃描層 el.matches() 累計約降五成;深嵌套 render 進程隔離墻鐘 pre-Tier1 707ms→254ms(2.8×)。 佈局層(僅瀏覽器、正確性敏感): - S1 gapTrim 對齊門:尾隨 margin 僅在右緣 flush 之對齊可見,唯 left/start(ltr) 之 ragged 右緣 可整批跳過,省其 O(gaps) 次同步重排。實測非兩端對齊內容 gapTrim 數千 ms→數十 ms;justify/ center/right/RTL 保留完整逐一定案、行為不變(真瀏覽器差分:justify trim 集指紋逐位相同)。 增量恢復(第三層,向下兼容 API): - C1 持久化 Finder:跨 render 復用元素鍵快取(category/level/feature/祖先游走)。正確性靠 失效:新增 finder.invalidate(el) 清整棵子樹快取(祖先屬性變則後代快取變脏),revert 自動失效 其子樹(安全預設)。 - C3 rerender(root)=revert+render;局部恢復(實測編輯 400 段中一段 766×、matches 1441×)。 - C2 observe/disconnect:MutationObserver 自動增量——內容/分類屬性變化重處理最近塊、新增之塊 直接 render;render/revert 期間暫停觀察,自身注入絕不自觸發。 - append 修復(IV-3 差分複驗發現、單測漏掉):向葉塊(帶 jz-orphan)追加子塊使其轉非葉,須撤 容器陳舊 orphan——追加塊時若塊級父有 jz-orphan 直接子則改重處理該父。 測試 130→140(+S2 非葉塊、C1/C3、C2 觀察/失效/append、不自觸發)。基準設施:bench-incremental (增量恢復)、bench-layout(版面層,headless Chrome)。dist 重建。
This commit is contained in:
Vendored
+17
@@ -32,7 +32,18 @@ export declare class Finder {
|
||||
private readonly featureCache;
|
||||
private readonly levelCache;
|
||||
private readonly categoryCache;
|
||||
private readonly pillAncestorCache;
|
||||
private readonly avoidAncestorCache;
|
||||
constructor(opts: ResolvedOptions);
|
||||
/**
|
||||
* 清除 el 及其**整棵子樹**之元素鍵快取(C1 失效原語)。持久化 Finder(跨 render 復用快取)
|
||||
* 下,元素之屬性或內容變化會令其分類(category)/級別(level)/功能(feature)/祖先游走
|
||||
* (pill/avoid ancestor)快取失效。**須清整棵子樹、非僅該元素**:level/feature/祖先游走
|
||||
* 之結果依賴祖先鏈,某祖先之 data-jz-level/data-juzhen/class 變化會令其**後代**之快取值變脏,
|
||||
* 即便後代自身未動。querySelectorAll('*') 走子樹(O(子樹),native、無遞迴棧風險)。移除之元素
|
||||
* 由 WeakMap 自動回收,無需顯式清。 */
|
||||
invalidate(el: Element): void;
|
||||
private forget;
|
||||
/** 該元素是否匹配 pill 選擇器(三分類之「整體」:code/kbd/行內公式等)。 */
|
||||
pillMatches(el: Element): boolean;
|
||||
/** 該元素是否匹配 isolate 選擇器(三分類之「隔離」:帶盒樣式之行內,如背景色
|
||||
@@ -42,6 +53,10 @@ export declare class Finder {
|
||||
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
||||
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
||||
insidePill(node: Node): boolean;
|
||||
/** el 或其祖先鏈任一為 pill(記憶化)。沿祖先鏈迭代(非遞迴,任意深度棧安全)至最近之
|
||||
* 已快取/pill 命中/鏈頂,途中非 pill 元素入棧、以斷點之值一次回填——每元素恰算一次,
|
||||
* 兄弟共用祖先鏈快取。insidePill 為 pillMatches 之最大來源(S3 延伸)。 */
|
||||
private ancestorOrSelfPill;
|
||||
/**
|
||||
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||
* 選擇器同命中時之定案規則):`avoid > pill > isolate > block > transparent > opaque`
|
||||
@@ -83,6 +98,8 @@ export declare class Finder {
|
||||
* 預設)**:一次性檢查用此,免除「avoidsSelf ‖ isAvoided」易漏一半之誤用。遍歷原語逐
|
||||
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
|
||||
isAvoided(node: Node): boolean;
|
||||
/** el 或其祖先鏈任一為 avoid 邊界(記憶化,迭代回填,同 ancestorOrSelfPill)。 */
|
||||
private ancestorOrSelfAvoided;
|
||||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||||
inScope(node: Node): boolean;
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / pill 內部 / 非作用域)。 */
|
||||
|
||||
Vendored
+15
@@ -6,6 +6,21 @@ export declare function normalizeOptions(opts?: JuzhenOptions): ResolvedOptions;
|
||||
export interface Juzhen {
|
||||
render(root?: Element | string): void;
|
||||
revert(root?: Element | string): void;
|
||||
/** revert 後重新 render 同一(子)樹(C3 增量便利);等價於 revert(root) + render(root)。
|
||||
* 頁面局部變化後,對受影響之最近塊調用即可只重處理該子樹、免全樹重掃。 */
|
||||
rerender(root?: Element | string): void;
|
||||
/** 清除該(子)樹之內部元素快取(C1)。**revert 已自動失效其子樹**,故僅在「改了既有元素之
|
||||
* 分類相關屬性(class/data-jz-*/lang 等)但不走 revert」時才需顯式調用,以免下次 render
|
||||
* 沿用陳舊分類。內容(子節點/文本)變化不改分類、無需 invalidate(但須 revert+render 該塊
|
||||
* 方能重處理)。 */
|
||||
invalidate(root?: Element | string): void;
|
||||
/** 開始觀察 root 子樹之 DOM 變化(C2 增量模式,opt-in):內容/分類屬性變化時自動只重處理
|
||||
* 受影響之最近塊(revert+render),新增之塊直接 render——免消費端手動 rerender 或全樹重掃。
|
||||
* **juzhen 自身之變更**(jz-* 注入、data-jz* 屬性)於處理期間暫停觀察、絕不自觸發。需
|
||||
* MutationObserver(瀏覽器/jsdom 有;無則 no-op)。重複 observe 前自動 disconnect 前者。 */
|
||||
observe(root?: Element | string): void;
|
||||
/** 停止 C2 觀察。 */
|
||||
disconnect(): void;
|
||||
readonly options: ResolvedOptions;
|
||||
}
|
||||
/** 建立一個聚珍實例。 */
|
||||
|
||||
Vendored
+264
-28
@@ -43,6 +43,26 @@ var Juzhen = (() => {
|
||||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||||
this.levelCache = /* @__PURE__ */ new WeakMap();
|
||||
this.categoryCache = /* @__PURE__ */ new WeakMap();
|
||||
this.pillAncestorCache = /* @__PURE__ */ new WeakMap();
|
||||
this.avoidAncestorCache = /* @__PURE__ */ new WeakMap();
|
||||
}
|
||||
/**
|
||||
* 清除 el 及其**整棵子樹**之元素鍵快取(C1 失效原語)。持久化 Finder(跨 render 復用快取)
|
||||
* 下,元素之屬性或內容變化會令其分類(category)/級別(level)/功能(feature)/祖先游走
|
||||
* (pill/avoid ancestor)快取失效。**須清整棵子樹、非僅該元素**:level/feature/祖先游走
|
||||
* 之結果依賴祖先鏈,某祖先之 data-jz-level/data-juzhen/class 變化會令其**後代**之快取值變脏,
|
||||
* 即便後代自身未動。querySelectorAll('*') 走子樹(O(子樹),native、無遞迴棧風險)。移除之元素
|
||||
* 由 WeakMap 自動回收,無需顯式清。 */
|
||||
invalidate(el) {
|
||||
this.forget(el);
|
||||
el.querySelectorAll("*").forEach((c) => this.forget(c));
|
||||
}
|
||||
forget(e) {
|
||||
this.categoryCache.delete(e);
|
||||
this.pillAncestorCache.delete(e);
|
||||
this.avoidAncestorCache.delete(e);
|
||||
this.levelCache.delete(e);
|
||||
this.featureCache.delete(e);
|
||||
}
|
||||
/** 該元素是否匹配 pill 選擇器(三分類之「整體」:code/kbd/行內公式等)。 */
|
||||
pillMatches(el) {
|
||||
@@ -57,14 +77,34 @@ var Juzhen = (() => {
|
||||
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
||||
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
||||
insidePill(node) {
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
if (this.pillMatches(p)) {
|
||||
return true;
|
||||
const p = node.parentElement;
|
||||
return p ? this.ancestorOrSelfPill(p) : false;
|
||||
}
|
||||
/** el 或其祖先鏈任一為 pill(記憶化)。沿祖先鏈迭代(非遞迴,任意深度棧安全)至最近之
|
||||
* 已快取/pill 命中/鏈頂,途中非 pill 元素入棧、以斷點之值一次回填——每元素恰算一次,
|
||||
* 兄弟共用祖先鏈快取。insidePill 為 pillMatches 之最大來源(S3 延伸)。 */
|
||||
ancestorOrSelfPill(el) {
|
||||
const chain = [];
|
||||
let cur = el;
|
||||
let base = false;
|
||||
while (cur) {
|
||||
const c = this.pillAncestorCache.get(cur);
|
||||
if (c !== void 0) {
|
||||
base = c;
|
||||
break;
|
||||
}
|
||||
p = p.parentNode;
|
||||
if (this.pillMatches(cur)) {
|
||||
this.pillAncestorCache.set(cur, true);
|
||||
base = true;
|
||||
break;
|
||||
}
|
||||
chain.push(cur);
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return false;
|
||||
for (const e of chain) {
|
||||
this.pillAncestorCache.set(e, base);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
/**
|
||||
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||
@@ -161,20 +201,37 @@ var Juzhen = (() => {
|
||||
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
|
||||
isAvoided(node) {
|
||||
if (node.nodeType === 1) {
|
||||
if (this.avoidsSelf(node)) {
|
||||
return true;
|
||||
}
|
||||
} else if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return this.ancestorOrSelfAvoided(node);
|
||||
}
|
||||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return true;
|
||||
}
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
if (this.avoidsSelf(p)) {
|
||||
return true;
|
||||
const p = node.parentElement;
|
||||
return p ? this.ancestorOrSelfAvoided(p) : false;
|
||||
}
|
||||
/** el 或其祖先鏈任一為 avoid 邊界(記憶化,迭代回填,同 ancestorOrSelfPill)。 */
|
||||
ancestorOrSelfAvoided(el) {
|
||||
const chain = [];
|
||||
let cur = el;
|
||||
let base = false;
|
||||
while (cur) {
|
||||
const c = this.avoidAncestorCache.get(cur);
|
||||
if (c !== void 0) {
|
||||
base = c;
|
||||
break;
|
||||
}
|
||||
p = p.parentNode;
|
||||
if (this.avoidsSelf(cur)) {
|
||||
this.avoidAncestorCache.set(cur, true);
|
||||
base = true;
|
||||
break;
|
||||
}
|
||||
chain.push(cur);
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return false;
|
||||
for (const e of chain) {
|
||||
this.avoidAncestorCache.set(e, base);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||||
inScope(node) {
|
||||
@@ -1528,9 +1585,29 @@ var Juzhen = (() => {
|
||||
}
|
||||
};
|
||||
var TRIM = "jz-hws-trim";
|
||||
function skippableAlign(g, cache) {
|
||||
const key = g.parentElement;
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
const cached = cache.get(key);
|
||||
if (cached !== void 0) {
|
||||
return cached;
|
||||
}
|
||||
let result = false;
|
||||
const view = g.ownerDocument ? g.ownerDocument.defaultView : null;
|
||||
if (view) {
|
||||
const cs = view.getComputedStyle(g);
|
||||
const ltr = cs.direction !== "rtl";
|
||||
result = cs.textAlign === "left" || cs.textAlign === "start" && ltr;
|
||||
}
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
}
|
||||
function trimGaps(root, finder, wrapSet) {
|
||||
const alignCache = /* @__PURE__ */ new Map();
|
||||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||||
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph") && finder.featureEnabledFor(g, "spacing")
|
||||
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph") && finder.featureEnabledFor(g, "spacing") && !skippableAlign(g, alignCache)
|
||||
);
|
||||
if (!gaps.length) {
|
||||
return;
|
||||
@@ -2170,10 +2247,135 @@ var Juzhen = (() => {
|
||||
}
|
||||
return typeof document !== "undefined" ? document.body : null;
|
||||
}
|
||||
var OBSERVE_ATTRS = [
|
||||
"class",
|
||||
"lang",
|
||||
"id",
|
||||
"contenteditable",
|
||||
"data-jz-skip",
|
||||
"data-jz-level",
|
||||
"data-jz-style",
|
||||
"data-juzhen",
|
||||
"data-juzhen-off"
|
||||
];
|
||||
function createJuzhen(opts = {}) {
|
||||
const options = normalizeOptions(opts);
|
||||
const finder = new Finder(options);
|
||||
function makeCtx(root) {
|
||||
return { root, options, finder: new Finder(options) };
|
||||
return { root, options, finder };
|
||||
}
|
||||
function renderImpl(el) {
|
||||
runPasses(PASSES, makeCtx(el));
|
||||
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||||
el.setAttribute("data-jz-halfwidth", "margin");
|
||||
}
|
||||
if (!options.justifyAtoms) {
|
||||
el.setAttribute("data-jz-atoms", "inline");
|
||||
}
|
||||
}
|
||||
function revertImpl(el) {
|
||||
revertPasses(PASSES, makeCtx(el));
|
||||
el.removeAttribute("data-jz-halfwidth");
|
||||
el.removeAttribute("data-jz-atoms");
|
||||
finder.invalidate(el);
|
||||
}
|
||||
let observer = null;
|
||||
let observeRoot = null;
|
||||
const observeConfig = {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: OBSERVE_ATTRS.slice()
|
||||
};
|
||||
function pauseObserver() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
function resumeObserver() {
|
||||
if (observer && observeRoot) {
|
||||
observer.takeRecords();
|
||||
observer.observe(observeRoot, observeConfig);
|
||||
}
|
||||
}
|
||||
function nearestBlock(node) {
|
||||
let e = node.nodeType === 1 ? node : node.parentElement;
|
||||
while (e) {
|
||||
if (options.finder.blockTags.has(e.nodeName)) {
|
||||
return e;
|
||||
}
|
||||
if (e === observeRoot) {
|
||||
break;
|
||||
}
|
||||
e = e.parentElement;
|
||||
}
|
||||
return observeRoot;
|
||||
}
|
||||
function onMutations(records) {
|
||||
const rerenderBlocks = /* @__PURE__ */ new Set();
|
||||
const renderNew = [];
|
||||
for (const m of records) {
|
||||
if (m.type === "childList") {
|
||||
m.addedNodes.forEach((n) => {
|
||||
if (n.nodeType === 1 && options.finder.blockTags.has(n.nodeName)) {
|
||||
const np = n;
|
||||
const P = np.parentElement;
|
||||
const pIsBlock = !!P && options.finder.blockTags.has(P.nodeName);
|
||||
const pHadLeafOrphan = !!P && Array.prototype.some.call(
|
||||
P.children,
|
||||
(c) => c.nodeName === "JZ-ORPHAN"
|
||||
);
|
||||
if (pIsBlock && pHadLeafOrphan) {
|
||||
rerenderBlocks.add(P);
|
||||
} else if (!pIsBlock && P) {
|
||||
const b = nearestBlock(P);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
} else {
|
||||
renderNew.push(np);
|
||||
}
|
||||
} else if (n.nodeType === 1 || n.nodeType === 3) {
|
||||
const b = nearestBlock(m.target);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (m.removedNodes.length) {
|
||||
const b = nearestBlock(m.target);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const b = nearestBlock(m.target);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rerenderBlocks.size && !renderNew.length) {
|
||||
return;
|
||||
}
|
||||
pauseObserver();
|
||||
const all = Array.from(rerenderBlocks).filter((b) => b.isConnected);
|
||||
const blocks = all.filter((b) => !all.some((o) => o !== b && o.contains(b)));
|
||||
for (const b of blocks) {
|
||||
revertImpl(b);
|
||||
renderImpl(b);
|
||||
}
|
||||
for (const el of renderNew) {
|
||||
if (!el.isConnected) {
|
||||
continue;
|
||||
}
|
||||
if (blocks.some((b) => b.contains(el))) {
|
||||
continue;
|
||||
}
|
||||
renderImpl(el);
|
||||
}
|
||||
resumeObserver();
|
||||
}
|
||||
return {
|
||||
options,
|
||||
@@ -2182,22 +2384,56 @@ var Juzhen = (() => {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
runPasses(PASSES, makeCtx(el));
|
||||
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||||
el.setAttribute("data-jz-halfwidth", "margin");
|
||||
}
|
||||
if (!options.justifyAtoms) {
|
||||
el.setAttribute("data-jz-atoms", "inline");
|
||||
}
|
||||
pauseObserver();
|
||||
renderImpl(el);
|
||||
resumeObserver();
|
||||
},
|
||||
revert(root) {
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
revertPasses(PASSES, makeCtx(el));
|
||||
el.removeAttribute("data-jz-halfwidth");
|
||||
el.removeAttribute("data-jz-atoms");
|
||||
pauseObserver();
|
||||
revertImpl(el);
|
||||
resumeObserver();
|
||||
},
|
||||
rerender(root) {
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
pauseObserver();
|
||||
revertImpl(el);
|
||||
renderImpl(el);
|
||||
resumeObserver();
|
||||
},
|
||||
invalidate(root) {
|
||||
const el = resolveRoot(root, options);
|
||||
if (el) {
|
||||
finder.invalidate(el);
|
||||
}
|
||||
},
|
||||
observe(root) {
|
||||
if (typeof MutationObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
observeRoot = el;
|
||||
observer = new MutationObserver(onMutations);
|
||||
observer.observe(el, observeConfig);
|
||||
},
|
||||
disconnect() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
observeRoot = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+264
-28
@@ -16,6 +16,26 @@ var Finder = class {
|
||||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||||
this.levelCache = /* @__PURE__ */ new WeakMap();
|
||||
this.categoryCache = /* @__PURE__ */ new WeakMap();
|
||||
this.pillAncestorCache = /* @__PURE__ */ new WeakMap();
|
||||
this.avoidAncestorCache = /* @__PURE__ */ new WeakMap();
|
||||
}
|
||||
/**
|
||||
* 清除 el 及其**整棵子樹**之元素鍵快取(C1 失效原語)。持久化 Finder(跨 render 復用快取)
|
||||
* 下,元素之屬性或內容變化會令其分類(category)/級別(level)/功能(feature)/祖先游走
|
||||
* (pill/avoid ancestor)快取失效。**須清整棵子樹、非僅該元素**:level/feature/祖先游走
|
||||
* 之結果依賴祖先鏈,某祖先之 data-jz-level/data-juzhen/class 變化會令其**後代**之快取值變脏,
|
||||
* 即便後代自身未動。querySelectorAll('*') 走子樹(O(子樹),native、無遞迴棧風險)。移除之元素
|
||||
* 由 WeakMap 自動回收,無需顯式清。 */
|
||||
invalidate(el) {
|
||||
this.forget(el);
|
||||
el.querySelectorAll("*").forEach((c) => this.forget(c));
|
||||
}
|
||||
forget(e) {
|
||||
this.categoryCache.delete(e);
|
||||
this.pillAncestorCache.delete(e);
|
||||
this.avoidAncestorCache.delete(e);
|
||||
this.levelCache.delete(e);
|
||||
this.featureCache.delete(e);
|
||||
}
|
||||
/** 該元素是否匹配 pill 選擇器(三分類之「整體」:code/kbd/行內公式等)。 */
|
||||
pillMatches(el) {
|
||||
@@ -30,14 +50,34 @@ var Finder = class {
|
||||
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
||||
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
||||
insidePill(node) {
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
if (this.pillMatches(p)) {
|
||||
return true;
|
||||
const p = node.parentElement;
|
||||
return p ? this.ancestorOrSelfPill(p) : false;
|
||||
}
|
||||
/** el 或其祖先鏈任一為 pill(記憶化)。沿祖先鏈迭代(非遞迴,任意深度棧安全)至最近之
|
||||
* 已快取/pill 命中/鏈頂,途中非 pill 元素入棧、以斷點之值一次回填——每元素恰算一次,
|
||||
* 兄弟共用祖先鏈快取。insidePill 為 pillMatches 之最大來源(S3 延伸)。 */
|
||||
ancestorOrSelfPill(el) {
|
||||
const chain = [];
|
||||
let cur = el;
|
||||
let base = false;
|
||||
while (cur) {
|
||||
const c = this.pillAncestorCache.get(cur);
|
||||
if (c !== void 0) {
|
||||
base = c;
|
||||
break;
|
||||
}
|
||||
p = p.parentNode;
|
||||
if (this.pillMatches(cur)) {
|
||||
this.pillAncestorCache.set(cur, true);
|
||||
base = true;
|
||||
break;
|
||||
}
|
||||
chain.push(cur);
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return false;
|
||||
for (const e of chain) {
|
||||
this.pillAncestorCache.set(e, base);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
/**
|
||||
* 行內元素分類(§4.1)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||
@@ -134,20 +174,37 @@ var Finder = class {
|
||||
* 元素下探時則用 avoidsSelf(僅自身,祖先由遞迴涵蓋)。 */
|
||||
isAvoided(node) {
|
||||
if (node.nodeType === 1) {
|
||||
if (this.avoidsSelf(node)) {
|
||||
return true;
|
||||
}
|
||||
} else if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return this.ancestorOrSelfAvoided(node);
|
||||
}
|
||||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return true;
|
||||
}
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
if (this.avoidsSelf(p)) {
|
||||
return true;
|
||||
const p = node.parentElement;
|
||||
return p ? this.ancestorOrSelfAvoided(p) : false;
|
||||
}
|
||||
/** el 或其祖先鏈任一為 avoid 邊界(記憶化,迭代回填,同 ancestorOrSelfPill)。 */
|
||||
ancestorOrSelfAvoided(el) {
|
||||
const chain = [];
|
||||
let cur = el;
|
||||
let base = false;
|
||||
while (cur) {
|
||||
const c = this.avoidAncestorCache.get(cur);
|
||||
if (c !== void 0) {
|
||||
base = c;
|
||||
break;
|
||||
}
|
||||
p = p.parentNode;
|
||||
if (this.avoidsSelf(cur)) {
|
||||
this.avoidAncestorCache.set(cur, true);
|
||||
base = true;
|
||||
break;
|
||||
}
|
||||
chain.push(cur);
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return false;
|
||||
for (const e of chain) {
|
||||
this.avoidAncestorCache.set(e, base);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||||
inScope(node) {
|
||||
@@ -1501,9 +1558,29 @@ var lineEdgePass = {
|
||||
}
|
||||
};
|
||||
var TRIM = "jz-hws-trim";
|
||||
function skippableAlign(g, cache) {
|
||||
const key = g.parentElement;
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
const cached = cache.get(key);
|
||||
if (cached !== void 0) {
|
||||
return cached;
|
||||
}
|
||||
let result = false;
|
||||
const view = g.ownerDocument ? g.ownerDocument.defaultView : null;
|
||||
if (view) {
|
||||
const cs = view.getComputedStyle(g);
|
||||
const ltr = cs.direction !== "rtl";
|
||||
result = cs.textAlign === "left" || cs.textAlign === "start" && ltr;
|
||||
}
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
}
|
||||
function trimGaps(root, finder, wrapSet) {
|
||||
const alignCache = /* @__PURE__ */ new Map();
|
||||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||||
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph") && finder.featureEnabledFor(g, "spacing")
|
||||
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph") && finder.featureEnabledFor(g, "spacing") && !skippableAlign(g, alignCache)
|
||||
);
|
||||
if (!gaps.length) {
|
||||
return;
|
||||
@@ -2143,10 +2220,135 @@ function resolveRoot(arg, options) {
|
||||
}
|
||||
return typeof document !== "undefined" ? document.body : null;
|
||||
}
|
||||
var OBSERVE_ATTRS = [
|
||||
"class",
|
||||
"lang",
|
||||
"id",
|
||||
"contenteditable",
|
||||
"data-jz-skip",
|
||||
"data-jz-level",
|
||||
"data-jz-style",
|
||||
"data-juzhen",
|
||||
"data-juzhen-off"
|
||||
];
|
||||
function createJuzhen(opts = {}) {
|
||||
const options = normalizeOptions(opts);
|
||||
const finder = new Finder(options);
|
||||
function makeCtx(root) {
|
||||
return { root, options, finder: new Finder(options) };
|
||||
return { root, options, finder };
|
||||
}
|
||||
function renderImpl(el) {
|
||||
runPasses(PASSES, makeCtx(el));
|
||||
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||||
el.setAttribute("data-jz-halfwidth", "margin");
|
||||
}
|
||||
if (!options.justifyAtoms) {
|
||||
el.setAttribute("data-jz-atoms", "inline");
|
||||
}
|
||||
}
|
||||
function revertImpl(el) {
|
||||
revertPasses(PASSES, makeCtx(el));
|
||||
el.removeAttribute("data-jz-halfwidth");
|
||||
el.removeAttribute("data-jz-atoms");
|
||||
finder.invalidate(el);
|
||||
}
|
||||
let observer = null;
|
||||
let observeRoot = null;
|
||||
const observeConfig = {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: OBSERVE_ATTRS.slice()
|
||||
};
|
||||
function pauseObserver() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
function resumeObserver() {
|
||||
if (observer && observeRoot) {
|
||||
observer.takeRecords();
|
||||
observer.observe(observeRoot, observeConfig);
|
||||
}
|
||||
}
|
||||
function nearestBlock(node) {
|
||||
let e = node.nodeType === 1 ? node : node.parentElement;
|
||||
while (e) {
|
||||
if (options.finder.blockTags.has(e.nodeName)) {
|
||||
return e;
|
||||
}
|
||||
if (e === observeRoot) {
|
||||
break;
|
||||
}
|
||||
e = e.parentElement;
|
||||
}
|
||||
return observeRoot;
|
||||
}
|
||||
function onMutations(records) {
|
||||
const rerenderBlocks = /* @__PURE__ */ new Set();
|
||||
const renderNew = [];
|
||||
for (const m of records) {
|
||||
if (m.type === "childList") {
|
||||
m.addedNodes.forEach((n) => {
|
||||
if (n.nodeType === 1 && options.finder.blockTags.has(n.nodeName)) {
|
||||
const np = n;
|
||||
const P = np.parentElement;
|
||||
const pIsBlock = !!P && options.finder.blockTags.has(P.nodeName);
|
||||
const pHadLeafOrphan = !!P && Array.prototype.some.call(
|
||||
P.children,
|
||||
(c) => c.nodeName === "JZ-ORPHAN"
|
||||
);
|
||||
if (pIsBlock && pHadLeafOrphan) {
|
||||
rerenderBlocks.add(P);
|
||||
} else if (!pIsBlock && P) {
|
||||
const b = nearestBlock(P);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
} else {
|
||||
renderNew.push(np);
|
||||
}
|
||||
} else if (n.nodeType === 1 || n.nodeType === 3) {
|
||||
const b = nearestBlock(m.target);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (m.removedNodes.length) {
|
||||
const b = nearestBlock(m.target);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const b = nearestBlock(m.target);
|
||||
if (b) {
|
||||
rerenderBlocks.add(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rerenderBlocks.size && !renderNew.length) {
|
||||
return;
|
||||
}
|
||||
pauseObserver();
|
||||
const all = Array.from(rerenderBlocks).filter((b) => b.isConnected);
|
||||
const blocks = all.filter((b) => !all.some((o) => o !== b && o.contains(b)));
|
||||
for (const b of blocks) {
|
||||
revertImpl(b);
|
||||
renderImpl(b);
|
||||
}
|
||||
for (const el of renderNew) {
|
||||
if (!el.isConnected) {
|
||||
continue;
|
||||
}
|
||||
if (blocks.some((b) => b.contains(el))) {
|
||||
continue;
|
||||
}
|
||||
renderImpl(el);
|
||||
}
|
||||
resumeObserver();
|
||||
}
|
||||
return {
|
||||
options,
|
||||
@@ -2155,22 +2357,56 @@ function createJuzhen(opts = {}) {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
runPasses(PASSES, makeCtx(el));
|
||||
if (options.features.jiya && options.jiyaConfig.halfWidth === "margin") {
|
||||
el.setAttribute("data-jz-halfwidth", "margin");
|
||||
}
|
||||
if (!options.justifyAtoms) {
|
||||
el.setAttribute("data-jz-atoms", "inline");
|
||||
}
|
||||
pauseObserver();
|
||||
renderImpl(el);
|
||||
resumeObserver();
|
||||
},
|
||||
revert(root) {
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
revertPasses(PASSES, makeCtx(el));
|
||||
el.removeAttribute("data-jz-halfwidth");
|
||||
el.removeAttribute("data-jz-atoms");
|
||||
pauseObserver();
|
||||
revertImpl(el);
|
||||
resumeObserver();
|
||||
},
|
||||
rerender(root) {
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
pauseObserver();
|
||||
revertImpl(el);
|
||||
renderImpl(el);
|
||||
resumeObserver();
|
||||
},
|
||||
invalidate(root) {
|
||||
const el = resolveRoot(root, options);
|
||||
if (el) {
|
||||
finder.invalidate(el);
|
||||
}
|
||||
},
|
||||
observe(root) {
|
||||
if (typeof MutationObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
const el = resolveRoot(root, options);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
observeRoot = el;
|
||||
observer = new MutationObserver(onMutations);
|
||||
observer.observe(el, observeConfig);
|
||||
},
|
||||
disconnect() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
observeRoot = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user