docs: changelog 2026-07-16 塊級分類 CSS 鉤子 data-jz-block-level + 增量塊粒度契約(下游建議七/說明八)
This commit is contained in:
+338
-33
@@ -1,4 +1,4 @@
|
||||
<!-- built with theme=dracula, mode=report, at=2026-07-11T18:24:25Z -->
|
||||
<!-- built with theme=dracula, mode=report, at=2026-07-16T08:46:00Z -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
@@ -374,9 +374,21 @@
|
||||
the stretch, so Latin words stay intact and gaps stay even.
|
||||
|
||||
Code / mono content (`<pre>`, `<code>`, `<kbd>`, `<samp>`) opts out
|
||||
via the mono rule far above (text-align: revert added there). */
|
||||
via the mono rule far above (text-align: revert added there).
|
||||
|
||||
`.callout`, `.block-body` are listed by CLASS: they are block prose
|
||||
containers that hold DIRECT text in a `<div>` (not `<p>`), so the tag
|
||||
list can't reach them. The juzhen typesetter DOES treat them as
|
||||
paragraphs (kinsoku / widow binding applies via `level`), so without
|
||||
this they'd bind punctuation yet render ragged-right — a visible
|
||||
justify/kinsoku mismatch. `.marginalia` is DELIBERATELY excluded: it
|
||||
opts into `text-align: start` via the mono rule below (narrow side
|
||||
notes justify badly). Any new prose container carrying direct text in
|
||||
a bare `<div>` must wrap its text in `<p>`, be added here, or carry
|
||||
`.justify`. */
|
||||
:where(:lang(zh), :lang(ja), :lang(ko))
|
||||
:is(p, li, blockquote, td, dd, summary, figcaption, caption, .justify, .justify *)
|
||||
:is(p, li, blockquote, td, dd, summary, figcaption, caption,
|
||||
.callout, .block-body, .justify, .justify *)
|
||||
{
|
||||
text-align: justify;
|
||||
text-justify: auto;
|
||||
@@ -2545,6 +2557,20 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
|
||||
<!-- BODY -->
|
||||
<p class="lede">聚珍(Juzhen)/cjk-autospace 變更紀錄。<a href="./index.html">← 回文檔</a></p>
|
||||
|
||||
<h2>塊級分類 CSS 鉤子 data-jz-block-level + 增量塊粒度契約 <span class="badge accent">2026-07-16</span></h2>
|
||||
<p>下游 HTML 模板消費端新一輪回饋兩項(建議七/說明八):resolved 分類之 CSS 可查鉤子(新功能、向下兼容),與增量 API 使用契約之文檔化。單元測試 140→145 全綠。</p>
|
||||
<h3>建議七:resolved 分類寫回 DOM(levelMark pass)</h3>
|
||||
<ul>
|
||||
<li><strong>動機</strong>:消費端之段落級 CSS(首要 CJK 兩端對齊 <code>text-align:justify</code>)語義上應與段落級 pass(禁則/垂懸)作用於同一集合;level 判定原僅存內部快取、不寫回 DOM,消費端只能另立一份必然漂移之選擇器白名單(實例:<code>.callout</code> 綁了避頭尾卻仍散尾)。</li>
|
||||
<li><strong>機制</strong>:新增 standalone pass <code>levelMark</code>(order 10,恆開)——render 時對每個「直接承載可處理文本」之塊級元素寫 <code>data-jz-block-level="paragraph|text"</code>,revert 全數移除(I6 之 <code>data-jz-*</code> 清單涵蓋)。消費端 <code>[data-jz-block-level="paragraph"]{ text-align: justify }</code> 即與聚珍共用單一真相源、永不漂移。</li>
|
||||
<li><strong>純容器不標</strong>(body/ul/table…——直接文本僅空白):否則 body 恆被判 paragraph、justify 鉤子經繼承污染全頁。標記集合恰為段落級 pass 實際作用之塊集(collectRuns 對純容器收不到 run);avoid 子樹/pill 內部文本不計,未被處理之塊一致不標。</li>
|
||||
<li><strong>輸入/輸出分離</strong>:作者輸入 <code>data-jz-level</code> 只讀不寫;輸出屬性本庫絕不讀取、亦不在 C2 觀察白名單(外部改動不觸發重處理);增量路徑 revert 即清、render 即重標,分類屬性變化自動刷新。是否對齊仍由消費端 CSS 決定(窄欄邊注可 opt-out),聚珍只供分類、不介入樣式。</li>
|
||||
</ul>
|
||||
<h3>說明八:增量「塊粒度」契約(純文檔)</h3>
|
||||
<ul>
|
||||
<li>README/ARCHITECTURE §3.1 明載:增量更新須以<strong>塊</strong>為粒度——換塊內容、增刪塊、改分類屬性皆與全量重繪一致;<strong>不支持</strong>對聚珍已切分文本節點之外科式 <code>characterData</code> 直改(charify 以 fragment 替換原節點,render 前之舊引用已脫離文檔)——後處理排版器之固有限制、非缺陷,以塊為單位替換內容即可。</li>
|
||||
</ul>
|
||||
|
||||
<h2>效能第二/三層:掃描/佈局提速 + 增量恢復(Tier3) <span class="badge accent">2026-07-12</span></h2>
|
||||
<p>承第一層(S2+S3)。本批含掃描/佈局提速與**增量恢復**(動態頁面局部變化免全樹重掃),全程獨立複驗+4 份真人撰寫真實文檔驗收。單元測試 130→140 全綠。</p>
|
||||
<h3>掃描 / 佈局提速</h3>
|
||||
@@ -2816,6 +2842,19 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
|
||||
<script type="text/markdown" id="md-source">
|
||||
# 變更紀錄(Changelog)— 聚珍(Juzhen)/cjk-autospace
|
||||
|
||||
## 塊級分類 CSS 鉤子 data-jz-block-level + 增量塊粒度契約(2026-07-16)
|
||||
|
||||
下游 HTML 模板消費端回饋兩項(建議七/說明八):resolved 分類之 CSS 可查鉤子(新功能、向下兼容),與增量 API 使用契約之文檔化。測試 140→145 全綠。
|
||||
|
||||
### 建議七:resolved 分類寫回 DOM(levelMark pass)
|
||||
- 動機:消費端段落級 CSS(text-align:justify)應與段落級 pass(禁則/垂懸)作用於同一集合;level 原僅存內部快取,消費端另立選擇器白名單必然漂移(.callout 綁了避頭尾卻仍散尾)。
|
||||
- 機制:新增 standalone pass levelMark(order 10,恆開)——render 對每個「直接承載可處理文本」之塊寫 data-jz-block-level="paragraph|text",revert 全數移除(I6)。CSS:[data-jz-block-level="paragraph"]{ text-align: justify } 即與聚珍共用單一真相源。
|
||||
- 純容器不標(body/ul/table…直接文本僅空白):否則 body 恆判 paragraph、justify 經繼承污染全頁;標記集合恰為段落級 pass 實際作用之塊集;avoid 子樹/pill 內部文本不計。
|
||||
- 輸入/輸出分離:作者輸入 data-jz-level 只讀不寫;輸出屬性本庫絕不讀取、不在 C2 觀察白名單;增量路徑 revert 即清、render 即重標,自動刷新。
|
||||
|
||||
### 說明八:增量「塊粒度」契約(純文檔)
|
||||
- README/ARCHITECTURE §3.1 明載:增量須以塊為粒度(換塊內容、增刪塊、改分類屬性皆與全量一致);不支持對已切分文本節點之外科式 characterData 直改(charify 以 fragment 替換原節點,舊引用已脫離文檔)——後處理排版器固有限制、非缺陷。
|
||||
|
||||
## 效能第二/三層:掃描/佈局提速 + 增量恢復(Tier3)(2026-07-12)
|
||||
|
||||
承第一層(S2+S3)。含掃描/佈局提速與增量恢復(動態頁面局部變化免全樹重掃),全程獨立複驗+4 份真人撰寫真實文檔驗收。測試 130→140 全綠。
|
||||
@@ -3038,6 +3077,27 @@ var Juzhen = (() => {
|
||||
this.includeSelector = opts.scope.include;
|
||||
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) {
|
||||
@@ -3052,14 +3112,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)——一元素對排版之口徑,**恰屬其一**。**優先序**(依序短路,即多
|
||||
@@ -3069,8 +3149,24 @@ var Juzhen = (() => {
|
||||
* (自身排除最高)。**邏輯文本流之遍歷原語**(collectRuns/jiyaAdjacency/adjacentLogicalChar
|
||||
* 之 isTransparent)皆據此**單一分類**分派;core/split 之 canSplit = SPLITTABLE 白名單 ∩
|
||||
* transparent(結構切分為獨立層,故更窄)。
|
||||
*
|
||||
* **記憶化**(S3):分類含至多三次 el.matches()(pill/isolate/avoid 選擇器),且被
|
||||
* collectRuns/jiyaAdjacency/jinze/orphan/split 各 pass 逐元素重算——以 categoryCache
|
||||
* 記憶化砍除跨 pass 重複。安全性:單次 render 內一元素之分類**恆定**(class/skip 屬性/
|
||||
* nodeName 皆不因 render 變更;jz-* 包裹為新建元素、按 nodeName 分類亦恆定),且 Finder
|
||||
* 隨 makeCtx 每次 render 重建 → 快取僅存活單次 render,無跨 render 失效問題(持久化屬 C1,
|
||||
* 另議)。
|
||||
*/
|
||||
category(el) {
|
||||
const cached = this.categoryCache.get(el);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const result = this.computeCategory(el);
|
||||
this.categoryCache.set(el, result);
|
||||
return result;
|
||||
}
|
||||
computeCategory(el) {
|
||||
if (this.avoidsSelf(el)) {
|
||||
return "avoid";
|
||||
}
|
||||
@@ -3140,20 +3236,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) {
|
||||
@@ -4507,9 +4620,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;
|
||||
@@ -4688,6 +4821,20 @@ var Juzhen = (() => {
|
||||
const anyCjk = new RegExp("[" + options.ruleset.cjk + "]");
|
||||
const biaodian = options.ruleset.biaodian;
|
||||
const chars = options.orphan.chars;
|
||||
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
|
||||
const nonLeaf = /* @__PURE__ */ new Set();
|
||||
if (sel) {
|
||||
ctx.root.querySelectorAll(sel).forEach((b) => {
|
||||
let p = b.parentElement;
|
||||
while (p) {
|
||||
if (blockSet.has(p.nodeName)) {
|
||||
nonLeaf.add(p);
|
||||
break;
|
||||
}
|
||||
p = p.parentElement;
|
||||
}
|
||||
});
|
||||
}
|
||||
finder.eachBlock(ctx.root, (block) => {
|
||||
if (!finder.levelAllows(block, "paragraph")) {
|
||||
return;
|
||||
@@ -4698,8 +4845,7 @@ var Juzhen = (() => {
|
||||
if (finder.isAvoided(block)) {
|
||||
return;
|
||||
}
|
||||
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
|
||||
if (sel && block.querySelector(sel)) {
|
||||
if (nonLeaf.has(block)) {
|
||||
return;
|
||||
}
|
||||
processBlock(block, finder, blockSet, anyCjk, biaodian, chars);
|
||||
@@ -5136,10 +5282,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,
|
||||
@@ -5148,22 +5419,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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,20 @@
|
||||
<!-- BODY -->
|
||||
<p class="lede">聚珍(Juzhen)/cjk-autospace 變更紀錄。<a href="./index.html">← 回文檔</a></p>
|
||||
|
||||
<h2>塊級分類 CSS 鉤子 data-jz-block-level + 增量塊粒度契約 <span class="badge accent">2026-07-16</span></h2>
|
||||
<p>下游 HTML 模板消費端新一輪回饋兩項(建議七/說明八):resolved 分類之 CSS 可查鉤子(新功能、向下兼容),與增量 API 使用契約之文檔化。單元測試 140→145 全綠。</p>
|
||||
<h3>建議七:resolved 分類寫回 DOM(levelMark pass)</h3>
|
||||
<ul>
|
||||
<li><strong>動機</strong>:消費端之段落級 CSS(首要 CJK 兩端對齊 <code>text-align:justify</code>)語義上應與段落級 pass(禁則/垂懸)作用於同一集合;level 判定原僅存內部快取、不寫回 DOM,消費端只能另立一份必然漂移之選擇器白名單(實例:<code>.callout</code> 綁了避頭尾卻仍散尾)。</li>
|
||||
<li><strong>機制</strong>:新增 standalone pass <code>levelMark</code>(order 10,恆開)——render 時對每個「直接承載可處理文本」之塊級元素寫 <code>data-jz-block-level="paragraph|text"</code>,revert 全數移除(I6 之 <code>data-jz-*</code> 清單涵蓋)。消費端 <code>[data-jz-block-level="paragraph"]{ text-align: justify }</code> 即與聚珍共用單一真相源、永不漂移。</li>
|
||||
<li><strong>純容器不標</strong>(body/ul/table…——直接文本僅空白):否則 body 恆被判 paragraph、justify 鉤子經繼承污染全頁。標記集合恰為段落級 pass 實際作用之塊集(collectRuns 對純容器收不到 run);avoid 子樹/pill 內部文本不計,未被處理之塊一致不標。</li>
|
||||
<li><strong>輸入/輸出分離</strong>:作者輸入 <code>data-jz-level</code> 只讀不寫;輸出屬性本庫絕不讀取、亦不在 C2 觀察白名單(外部改動不觸發重處理);增量路徑 revert 即清、render 即重標,分類屬性變化自動刷新。是否對齊仍由消費端 CSS 決定(窄欄邊注可 opt-out),聚珍只供分類、不介入樣式。</li>
|
||||
</ul>
|
||||
<h3>說明八:增量「塊粒度」契約(純文檔)</h3>
|
||||
<ul>
|
||||
<li>README/ARCHITECTURE §3.1 明載:增量更新須以<strong>塊</strong>為粒度——換塊內容、增刪塊、改分類屬性皆與全量重繪一致;<strong>不支持</strong>對聚珍已切分文本節點之外科式 <code>characterData</code> 直改(charify 以 fragment 替換原節點,render 前之舊引用已脫離文檔)——後處理排版器之固有限制、非缺陷,以塊為單位替換內容即可。</li>
|
||||
</ul>
|
||||
|
||||
<h2>效能第二/三層:掃描/佈局提速 + 增量恢復(Tier3) <span class="badge accent">2026-07-12</span></h2>
|
||||
<p>承第一層(S2+S3)。本批含掃描/佈局提速與**增量恢復**(動態頁面局部變化免全樹重掃),全程獨立複驗+4 份真人撰寫真實文檔驗收。單元測試 130→140 全綠。</p>
|
||||
<h3>掃描 / 佈局提速</h3>
|
||||
@@ -280,6 +294,19 @@
|
||||
<script type="text/markdown" id="md-source">
|
||||
# 變更紀錄(Changelog)— 聚珍(Juzhen)/cjk-autospace
|
||||
|
||||
## 塊級分類 CSS 鉤子 data-jz-block-level + 增量塊粒度契約(2026-07-16)
|
||||
|
||||
下游 HTML 模板消費端回饋兩項(建議七/說明八):resolved 分類之 CSS 可查鉤子(新功能、向下兼容),與增量 API 使用契約之文檔化。測試 140→145 全綠。
|
||||
|
||||
### 建議七:resolved 分類寫回 DOM(levelMark pass)
|
||||
- 動機:消費端段落級 CSS(text-align:justify)應與段落級 pass(禁則/垂懸)作用於同一集合;level 原僅存內部快取,消費端另立選擇器白名單必然漂移(.callout 綁了避頭尾卻仍散尾)。
|
||||
- 機制:新增 standalone pass levelMark(order 10,恆開)——render 對每個「直接承載可處理文本」之塊寫 data-jz-block-level="paragraph|text",revert 全數移除(I6)。CSS:[data-jz-block-level="paragraph"]{ text-align: justify } 即與聚珍共用單一真相源。
|
||||
- 純容器不標(body/ul/table…直接文本僅空白):否則 body 恆判 paragraph、justify 經繼承污染全頁;標記集合恰為段落級 pass 實際作用之塊集;avoid 子樹/pill 內部文本不計。
|
||||
- 輸入/輸出分離:作者輸入 data-jz-level 只讀不寫;輸出屬性本庫絕不讀取、不在 C2 觀察白名單;增量路徑 revert 即清、render 即重標,自動刷新。
|
||||
|
||||
### 說明八:增量「塊粒度」契約(純文檔)
|
||||
- README/ARCHITECTURE §3.1 明載:增量須以塊為粒度(換塊內容、增刪塊、改分類屬性皆與全量一致);不支持對已切分文本節點之外科式 characterData 直改(charify 以 fragment 替換原節點,舊引用已脫離文檔)——後處理排版器固有限制、非缺陷。
|
||||
|
||||
## 效能第二/三層:掃描/佈局提速 + 增量恢復(Tier3)(2026-07-12)
|
||||
|
||||
承第一層(S2+S3)。含掃描/佈局提速與增量恢復(動態頁面局部變化免全樹重掃),全程獨立複驗+4 份真人撰寫真實文檔驗收。測試 130→140 全綠。
|
||||
|
||||
Reference in New Issue
Block a user