docs: changelog 2026-07-07 專案整理(階段一/二)+ index 三分類分層/avoid 七源/demo/19.7em

新增 changelog 2026-07-07「專案整理」條目(架構一致性:透明判定單一真相源+跨透明邊界避頭尾
+avoid 排除歸一;確認缺陷修復+清理;復驗後 lineedge 非缺陷/五處幂等不做之定性;文檔重整)
+md-twin 同步。

index 同步:三分類「共用同一判定」改分層表述(isTransparent 為邏輯流單一真相源+未知行內採
嚴格白名單邊界;canSplit/lineedge 為獨立層)、透明列標「未列=邊界」、avoid 改七源+isAvoided
組合入口+skipTags 不含 code/kbd;修 19.5em→19.7em(與 main 一致);目視驗證補 demo/resize-check
與 demo/showcase 引用。changelog/index HTML 經 build.sh 重建。
This commit is contained in:
2026-07-07 21:21:52 +08:00
parent f75382c9b5
commit 648605ac53
4 changed files with 370 additions and 106 deletions
+179 -47
View File
@@ -1,11 +1,11 @@
<!-- built with theme=dracula, mode=report, at=2026-06-13T07:41:09Z -->
<!-- built with theme=dracula, mode=report, at=2026-07-07T13:21:19Z -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>變更紀錄(Changelog</title>
<meta name="generated-at" content="2026-06-13">
<meta name="generated-at" content="2026-07-07">
<meta name="source" content="cjk-autospace · 變更紀錄">
<style>
/* ---------- Core tokens ----------
@@ -706,6 +706,14 @@ hr.line
}
hr.line::before { content: none; }
.lede
{
/* 首字下沉為 float,本段行數不足以填滿下沉字高度時,浮標會探出段底,
令次段首數行繞排、向右擠貼下沉字。將本段下沿托至下沉字下沿(高度=
4.2em × line-height 0.85 頂距 8px,與下方 ::first-letter 同步),
使次段恆起於下沉字之下。本段自然行高足夠時 min-height 不生效,無副作用。 */
min-height: calc(4.2em * 0.85 + 8px);
}
.lede::first-letter
{
font-family: var(--font-serif);
@@ -2323,9 +2331,10 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
/* ---------- Mode: report — Print / PDF ----------
Dynamic single-page PDF: runtime.js setupDynamicPrintPage() measures
content height before print and injects @page sized to fit. Slides
mode uses per-slide pagination (page-break-after: always) instead. */
Dynamic single-page PDF: report/behavior.js setupDynamicPrintPage()
measures the printed height (off-screen iframe at the print column width)
and injects @page sized to fit. Slides mode uses per-slide pagination
(page-break-after: always) instead. */
/* ---------- Print ---------- */
/* ---------- Print / PDF export ----------
@@ -2357,9 +2366,9 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
@page
{
/* A4 fallback. Real page size is set dynamically at runtime by
`setupDynamicPrintPage()` in runtime.js — it measures the rendered
height before print and injects a fresh `@page` rule sized to fit
content exactly.
`setupDynamicPrintPage()` in report/behavior.js — it measures the
printed height (off-screen iframe at the print column width) and
injects a fresh `@page` rule sized to fit content exactly.
Margin: 0 here. The real margin lives in `body` padding (see the
@media print rule below), which the user's print dialog cannot
@@ -2521,7 +2530,7 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
<div class="meta">
<h1>變更紀錄(Changelog</h1>
<p class="subtitle">聚珍(Juzhen)/cjk-autospace 之版本變更紀錄。</p>
<p class="timestamp"><time>2026-06-13</time> · <span>cjk-autospace · 變更紀錄</span></p>
<p class="timestamp"><time>2026-07-07</time> · <span>cjk-autospace · 變更紀錄</span></p>
</div>
<div class="actions">
<button class="btn" type="button" data-action="export-md" title="Copy Markdown to clipboard">Copy MD</button>
@@ -2536,6 +2545,32 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
<!-- BODY -->
<p class="lede">聚珍(Juzhen)/cjk-autospace 變更紀錄。<a href="./index.html">← 回文檔</a></p>
<h2>專案整理:架構一致性 + 確認缺陷修復 + 文檔重整 <span class="badge accent">2026-07-07</span></h2>
<p>四路並行審計(coretypeset+CSS/設置 API/文檔)後之兩階段整理。核心:消除「透明穿越」判定之兩套口徑分裂,並修四個審計缺陷中之確認項(另一證偽、一延後)。單元測試 116→121 全綠。</p>
<h3>階段一 · 確認缺陷修復 清理</h3>
<ul>
<li><strong>jinze 冪等</strong>:二次 render(未 revert)曾產生巢狀 <code>jz-jinze</code>(違反 I6,實測 3→6)——守衛改判 <code>isJz(parent, PASS)</code></li>
<li><strong>pill 自身 avoid 漏檢</strong><code>processPills</code><code>avoidsSelf</code>——自身命中 avoid(如 <code>&lt;code data-jz-skip&gt;</code>)之 pill 曾仍被補間隙。連帶:<strong><code>CODE</code><code>KBD</code><code>skipTags</code> 預設移除</strong>(已由 <code>pillSelector</code> 以 pill 涵蓋;再列 skipTags 違反三分類互斥、且會抹掉普通 <code>code</code> 之 pill 兩端間隙)。若把 <code>code</code> 移出 <code>pillSelector</code>,須自行加入 <code>skipTags</code><code>scope.avoid</code></li>
<li>刪零引用死代碼(<code>createMarker</code><code>ANY_CJK</code><code>BD_ALL</code><code>BD_CLASS_NAMES</code><code>CreateOptions.attrs</code>);死旗標 <code>biaodian</code><code>emphasis</code><code>ruby</code><code>@reserved</code> 註記。</li>
</ul>
<h3>階段二 · 架構一致性</h3>
<ul>
<li><strong>透明判定單一真相源</strong><code>transparentInline</code> 提升為公開 <code>isTransparent</code>,邏輯文本流之遍歷原語(<code>collectRuns</code><code>adjacentLogicalChar</code><code>jiyaAdjacency</code>)共用之。<strong>未在 <code>styleInlines</code> 之未知行內標籤(如 <code>&lt;button&gt;</code>)採嚴格白名單、一律當邊界</strong>(用戶抉擇;透明須顯式登記)。分層澄清:結構切分安全 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 各有正當之不同邊界、刻意不併入。</li>
<li><strong>跨透明元素邊界避頭尾</strong>(修 #3):避頭類標點落於 <code>&lt;strong&gt;</code> 邊緣(<code>你好&lt;strong&gt;。世界&lt;/strong&gt;</code> 之句末點號位於 strong 首位)時,<code>forbiddenBreak</code><code>edgeCharEl</code> 穿越透明元素下探定位、render 沿透明邊緣上攀處理外層 parent,不再漏綁、誤置行首。</li>
<li><strong>avoid 排除歸一</strong><code>isAvoided</code> 改單一組合入口(自身或祖先),免除「<code>avoidsSelf ‖ isAvoided</code>」易漏一半之誤用(階段一 pill 漏檢即此類)。</li>
<li>小項:<code>scope.include</code> 型別去多餘 <code>| null</code><code>gapTrim</code> 補逐節點功能閘;<code>PASSES</code> 陣列補註執行序。</li>
</ul>
<h3>復驗後重新定性(不照單全收)</h3>
<ul>
<li><strong>lineedge 攀穿 isolate——非缺陷</strong><code>edgeRect</code> 為物理版面測量(<code>getClientRects</code>),isolate 斷的是邏輯 run/相鄰,物理行首行尾判定須跨 <code>&lt;mark&gt;</code> 找真實相鄰內容;初評誤判、已還原。</li>
<li><strong>五處幂等收斂——評估後不做</strong>:各檢查不同位置關係(祖先/父/子),無單一 <code>isJz</code> 形狀可全覆蓋,強行歸一徒增風險。</li>
</ul>
<h3>文檔(本次)</h3>
<ul>
<li>README 新增「全部選項速查」表——突出被埋沒之 <code>lang.style</code><code>styleInlines</code><code>blockTags</code><code>skipTags</code>(標籤名陣列)/<code>skipAttribute</code><code>isSkipped</code>;三分類改分層表述、avoid 改七源+組合入口。</li>
<li>ARCHITECTURE §3.1 <code>scope.avoid</code> 過時示例、§4.1 透明分層與 avoid 七源、§4.2 標點六子類(舊列 <code>bd-cop</code><code>bd-sep</code><code>quote</code> 與實作不符)修正。</li>
</ul>
<h2>標點擠壓句界例外(句末點號接開類保留右空白) <span class="badge accent">2026-06-13</span></h2>
<p>行內連續擠壓(機制 A)新增<strong>句界例外</strong>:句末點號(<code>。.!?</code>)後接開類引號/括號(<code>。「</code><code>?「</code>)時保留句末點號右側空白、僅收開類左側,全角式與開明式皆合占 <strong>1.5em</strong>(對稱於 <code>。」</code>)——下游回饋之語感修正。單元測試 113→116 全綠。</p>
<h3>變更</h3>
@@ -2724,6 +2759,29 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
<script type="text/markdown" id="md-source">
# 變更紀錄(Changelog)— 聚珍(Juzhen)/cjk-autospace
## 專案整理:架構一致性 + 確認缺陷修復 + 文檔重整(2026-07-07
四路並行審計後之兩階段整理。核心:消除「透明穿越」判定之兩套口徑分裂。測試 116→121 全綠。
### 階段一 · 確認缺陷 清理
- jinze 冪等:二次 render 曾巢狀 jz-jinze(違反 I63→6)——守衛改判 isJz(parent, PASS)。
- pill 自身 avoid 漏檢:processPills 補 avoidsSelf。連帶 CODE/KBD 自 skipTags 預設移除(已由 pillSelector 以 pill 涵蓋;再列 skipTags 違反三分類互斥、會抹掉普通 code 之 pill 間隙)。若把 code 移出 pillSelector 須自行加入 skipTags/scope.avoid。
- 刪死代碼 createMarker/ANY_CJK/BD_ALL/BD_CLASS_NAMES/CreateOptions.attrs;死旗標 biaodian/emphasis/ruby 補 @reserved。
### 階段二 · 架構一致性
- 透明判定單一真相源:transparentInline 提升為公開 isTransparent,邏輯文本流三原語(collectRuns/adjacentLogicalChar/jiyaAdjacency)共用之。未在 styleInlines 之未知行內標籤採嚴格白名單、一律當邊界(用戶抉擇)。分層:canSplit(切分安全)、lineedge(物理版面)各有正當之不同邊界、不併入。
- 跨透明元素邊界避頭尾(修 #3):句末點號落於 <strong> 首位(你好<strong>。世界</strong>)時 forbiddenBreak 經 edgeCharEl 下探、render 沿透明邊緣上攀處理外層 parent,不再漏綁。
- avoid 排除歸一:isAvoided 改組合入口(自身或祖先),免「avoidsSelf ‖ isAvoided」漏一半。
- 小項:scope.include 去多餘 |nullgapTrim 補功能閘;PASSES 補註執行序。
### 復驗後重新定性(不照單全收)
- lineedge 攀穿 isolate——非缺陷:edgeRect 為物理測量,須跨 mark 找真實相鄰;初評誤判、已還原。
- 五處幂等收斂——不做:各檢查不同位置關係,無單一 isJz 形狀可全覆蓋,強行歸一徒增風險。
### 文檔
- README 新增「全部選項速查」表(突出 lang.style/styleInlines/skipTags/skipAttribute/isSkipped);三分類改分層、avoid 改七源。
- ARCHITECTURE §3.1 scope.avoid 過時示例、§4.1 透明分層、§4.2 標點六子類修正。
## 標點擠壓句界例外(句末點號接開類保留右空白)(2026-06-13)
### 變更
@@ -3682,9 +3740,12 @@ var Juzhen = (() => {
if (!finder.featureEnabledFor(L, PASS)) {
return;
}
const lSides = blankSides(bdClassOf(L));
const rSides = blankSides(bdClassOf(R));
if (lSides.right) {
const lClass = bdClassOf(L);
const rClass = bdClassOf(R);
const lSides = blankSides(lClass);
const rSides = blankSides(rClass);
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
if (lSides.right && !stopBeforeOpen) {
L.classList.add("jz-half");
}
if (rSides.left) {
@@ -5122,7 +5183,9 @@ window.__JUZHEN_PUNCT__="quanjiao";window.__JUZHEN_JUSTIFY_ATOMS__=true;window._
.lede.marginalia.references.appendix……)不命中、維持全功能。最近祖先勝,
故標題與 chrome 容器一旦命中,其後代(如 .slide-title 內的 .section-name
.slide-dot、.title-page 內的作者欄)一併降級。選擇器涵蓋三模式;某模式不存在之
class 自然不命中。 */
class 自然不命中。**切記只列「非段落 chrome 本身」,切勿納入包裹正文的容器**
(如 .deck-stage.deck.cols-2)——否則「最近祖先勝」會把整個子樹正文一併降級、
喪失禁則/垂懸/長詞(曾誤納 .deck-stage 致 slides 全 deck 正文無禁則、標點落行首)。 */
level: {
text: [
"h1, h2, h3, h4, h5, h6", // 全部標題(含 deck-section-slide-tp-toc-title
@@ -5130,7 +5193,7 @@ window.__JUZHEN_PUNCT__="quanjiao";window.__JUZHEN_JUSTIFY_ATOMS__=true;window._
".subtitle, .section-subtitle, .deck-subtitle", // 副標題
".meta, .timestamp, .actions, .page", // report chrome
".cover-block, .slide-foot, .toc-list", // slides 封面/頁尾/目錄條目
".deck-nav, .deck-counter, .deck-author, .deck-institute, .deck-date, .deck-actions, .deck-stage",
".deck-nav, .deck-counter, .deck-author, .deck-institute, .deck-date, .deck-actions",
".title-page, .toc-page, .simple-title", // academic 前置 chrome(含 tp-st- 欄位)
].join(", "),
},
@@ -5161,9 +5224,10 @@ window.__JUZHEN_PUNCT__="quanjiao";window.__JUZHEN_JUSTIFY_ATOMS__=true;window._
renderMath();
wrapFlexTitleText();
/* 一般模式於 init 立即排版。__JUZHEN_DEFER__academic 由 build.sh 設)令此處
跳過,改由該模式 behavior.js Paged.js preview() 完成後對成品頁呼叫
__juzhenRun__——關鍵:Paged.js chunker 對 inline-block 原子量測失準(分頁前
排版會致窄欄散架),故先以純文字分頁、再於定版頁上加排版原子。 */
跳過,改由該模式 behavior.js 在編號/目錄/正文標記完成後、Paged.js preview()
**之前**手動呼叫 __juzhenRun__——使 Paged.js 直接以聚珍後的最終幾何分頁(每頁
自然填滿、末行齊滿、無需分頁後斷縫修復)。整頁寬單欄下 chunker 正確量度聚珍
inline-block 原子,無散架。 */
if (!window.__JUZHEN_DEFER__) { runJuzhen(); }
/* ========== Feature: code-block — behavior =================================
@@ -6072,30 +6136,28 @@ if (!window.__copyDelegated__)
/* ---------- Dynamic print page sizing ----------
Measure the document's rendered height and inject an @page rule
sized to fit content exactly. Produces a single-page PDF on "Save
as PDF" with no trailing blank space.
Measure the document's true printed height and inject an @page rule
sized to fit it exactly a single-page PDF on "Save as PDF" with no
trailing blank space and no overflow onto a second page.
Page margin: 0. Real margins live in `body` padding (runtime.css
@media print). That keeps content width stable regardless of the
print dialog's Margins selector — and stable width is what makes
the screen-to-print height heuristic reliable.
Measurement: an off-screen iframe at the print page width (210mm) with
the document's own @media print rules un-gated reproduces the printed
layout; its content height is the answer (see measurePrintMm). This is
window-independent.
Page-height heuristic: with horizontal padding fixed (1cm each
side), print rendering height roughly matches screen rendering
height. Ratio = 1.0 (was 0.85 — too aggressive an under-estimate;
caused tail clipping). The 50mm buffer absorbs the residual
variance plus whatever vertical margin a dialog like Chrome's
Default might add on top.
Earlier this used `documentElement.scrollHeight` × a ratio. That broke
because report has NO max-width (content width = 100vw4em): on a wide
window the screen wraps less and measures short, but print always lays
out at the fixed 190mm column and wraps more, so the page came out too
short and content overflowed. The wider the window, the worse it got.
Vertical padding (body padding-top 1.5cm + padding-bottom 1cm =
25mm total) is reserved inside the page so the breathing room
above the title and below the footer is preserved.
Page margin: 0. Real margins live in `body` padding (the @media print
rule), so they survive the print dialog's "Margins: None" and stay
inside the measured height.
Escape hatch: set `window.disablePrintAutoSize = true` (in the
DevTools console or before page load) to disable. The injected
rule is cleared and printing falls back to the static A4 in
runtime.css (multi-page). */
Escape hatch: set `window.disablePrintAutoSize = true` (DevTools console
or before load) to clear the injected rule and fall back to the static
A4 @page in runtime.css (multi-page). */
function setupDynamicPrintPage()
{
let styleEl = null;
@@ -6109,25 +6171,95 @@ if (!window.__copyDelegated__)
}
return styleEl;
}
/* Collect this document's own `@media print` rules as unconditional
CSS, so an off-screen iframe can reproduce the printed-page layout. */
function collectPrintCss()
{
let css = "";
Array.from(document.styleSheets).forEach((sheet) =>
{
let rules;
try { rules = sheet.cssRules; }
catch (e) { return; } /* cross-origin sheet — unreadable */
Array.from(rules).forEach((rule) =>
{
if (rule.type === CSSRule.MEDIA_RULE && /print/i.test(rule.media.mediaText))
{
Array.from(rule.cssRules).forEach((inner) => { css += inner.cssText + "\n"; });
}
});
});
return css;
}
/* Measure the true printed height: clone the already-rendered DOM (KaTeX
+ juzhen baked in) into a hidden iframe sized to the print page width
(210mm) with the print rules un-gated, then read its content height.
innerHTML insertion never runs <script>, so the runtime IIFE / KaTeX /
juzhen do NOT re-execute. scrollHeight already includes the print
`body` padding (the un-gated @media print rule). Resolves to mm.
Why an iframe and not `documentElement.scrollHeight`: report mode has
NO max-width — on-screen content width = 100vw4em, so screen height
tracks the window and diverged from the fixed 190mm print column
(wide windows under-sized the page → overflow). The iframe pins the
measurement to the real print width, making it window-independent. */
function measurePrintMm()
{
return new Promise((resolve) =>
{
const iframe = document.createElement("iframe");
iframe.setAttribute("aria-hidden", "true");
iframe.style.cssText =
"position:fixed;left:-10000px;top:0;width:210mm;height:50px;"
+ "border:0;visibility:hidden;";
document.body.appendChild(iframe);
const idoc = iframe.contentDocument;
idoc.open();
idoc.write("<!DOCTYPE html><html><head></head><body></body></html>");
idoc.close();
idoc.documentElement.lang = document.documentElement.lang;
idoc.head.innerHTML = document.head.innerHTML;
idoc.body.innerHTML = document.body.innerHTML;
const ps = idoc.createElement("style");
ps.textContent = collectPrintCss();
idoc.head.appendChild(ps);
function done()
{
const h = Math.max(idoc.documentElement.scrollHeight, idoc.body.scrollHeight);
const mm = Math.ceil(h * 25.4 / 96);
iframe.remove();
resolve(mm);
}
/* Wait for the iframe's fonts (same @font-face, HTTP-cached from
the parent) so glyph metrics match; rAF lets layout settle. */
const fontsReady = (idoc.fonts && idoc.fonts.ready) || Promise.resolve();
fontsReady.then(() => requestAnimationFrame(() => requestAnimationFrame(done)));
});
}
function update()
{
const el = ensureStyle();
if (window.disablePrintAutoSize)
{
el.textContent = ""; /* fall back to runtime.css @page */
el.textContent = ""; /* fall back to runtime.css static @page */
return;
}
const PX_TO_MM = 25.4 / 96;
const RATIO = 1.0;
const PAD_V_MM = 25; /* body padding-top 15 + padding-bottom 10 */
const BUFFER_MM = 0; /* absorbs dialog margin + heuristic variance */
const screenH = document.documentElement.scrollHeight;
const printContentH = Math.ceil(screenH * PX_TO_MM * RATIO);
const pageH = printContentH + PAD_V_MM + BUFFER_MM;
el.textContent = "@page { size: 210mm " + pageH + "mm; margin: 0; }";
measurePrintMm().then((contentMm) =>
{
if (window.disablePrintAutoSize) { el.textContent = ""; return; }
const BUFFER_MM = 10; /* dialog margin + sub-mm rounding slack */
const pageH = contentMm + BUFFER_MM;
el.textContent = "@page { size: 210mm " + pageH + "mm; margin: 0; }";
});
}
update();
window.addEventListener("resize", update);
/* Print column width is fixed (190mm), so page height no longer depends
on the window — no resize re-measure. Re-measure before printing to
pick up dynamically expanded/collapsed content (async; the init rule
already covers static content for the current print). */
window.addEventListener("beforeprint", update);
}
+51 -2
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>變更紀錄(Changelog</title>
<meta name="generated-at" content="2026-06-13">
<meta name="generated-at" content="2026-07-07">
<meta name="source" content="cjk-autospace · 變更紀錄">
<!-- RUNTIME_CSS -->
</head>
@@ -14,7 +14,7 @@
<div class="meta">
<h1>變更紀錄(Changelog</h1>
<p class="subtitle">聚珍(Juzhen)/cjk-autospace 之版本變更紀錄。</p>
<p class="timestamp"><time>2026-06-13</time> · <span>cjk-autospace · 變更紀錄</span></p>
<p class="timestamp"><time>2026-07-07</time> · <span>cjk-autospace · 變更紀錄</span></p>
</div>
<div class="actions">
<!-- ACTIONS -->
@@ -25,6 +25,32 @@
<!-- BODY -->
<p class="lede">聚珍(Juzhen)/cjk-autospace 變更紀錄。<a href="./index.html">← 回文檔</a></p>
<h2>專案整理:架構一致性 + 確認缺陷修復 + 文檔重整 <span class="badge accent">2026-07-07</span></h2>
<p>四路並行審計(coretypeset+CSS/設置 API/文檔)後之兩階段整理。核心:消除「透明穿越」判定之兩套口徑分裂,並修四個審計缺陷中之確認項(另一證偽、一延後)。單元測試 116→121 全綠。</p>
<h3>階段一 · 確認缺陷修復 清理</h3>
<ul>
<li><strong>jinze 冪等</strong>:二次 render(未 revert)曾產生巢狀 <code>jz-jinze</code>(違反 I6,實測 3→6)——守衛改判 <code>isJz(parent, PASS)</code></li>
<li><strong>pill 自身 avoid 漏檢</strong><code>processPills</code><code>avoidsSelf</code>——自身命中 avoid(如 <code>&lt;code data-jz-skip&gt;</code>)之 pill 曾仍被補間隙。連帶:<strong><code>CODE</code><code>KBD</code><code>skipTags</code> 預設移除</strong>(已由 <code>pillSelector</code> 以 pill 涵蓋;再列 skipTags 違反三分類互斥、且會抹掉普通 <code>code</code> 之 pill 兩端間隙)。若把 <code>code</code> 移出 <code>pillSelector</code>,須自行加入 <code>skipTags</code><code>scope.avoid</code></li>
<li>刪零引用死代碼(<code>createMarker</code><code>ANY_CJK</code><code>BD_ALL</code><code>BD_CLASS_NAMES</code><code>CreateOptions.attrs</code>);死旗標 <code>biaodian</code><code>emphasis</code><code>ruby</code><code>@reserved</code> 註記。</li>
</ul>
<h3>階段二 · 架構一致性</h3>
<ul>
<li><strong>透明判定單一真相源</strong><code>transparentInline</code> 提升為公開 <code>isTransparent</code>,邏輯文本流之遍歷原語(<code>collectRuns</code><code>adjacentLogicalChar</code><code>jiyaAdjacency</code>)共用之。<strong>未在 <code>styleInlines</code> 之未知行內標籤(如 <code>&lt;button&gt;</code>)採嚴格白名單、一律當邊界</strong>(用戶抉擇;透明須顯式登記)。分層澄清:結構切分安全 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 各有正當之不同邊界、刻意不併入。</li>
<li><strong>跨透明元素邊界避頭尾</strong>(修 #3):避頭類標點落於 <code>&lt;strong&gt;</code> 邊緣(<code>你好&lt;strong&gt;。世界&lt;/strong&gt;</code> 之句末點號位於 strong 首位)時,<code>forbiddenBreak</code><code>edgeCharEl</code> 穿越透明元素下探定位、render 沿透明邊緣上攀處理外層 parent,不再漏綁、誤置行首。</li>
<li><strong>avoid 排除歸一</strong><code>isAvoided</code> 改單一組合入口(自身或祖先),免除「<code>avoidsSelf ‖ isAvoided</code>」易漏一半之誤用(階段一 pill 漏檢即此類)。</li>
<li>小項:<code>scope.include</code> 型別去多餘 <code>| null</code><code>gapTrim</code> 補逐節點功能閘;<code>PASSES</code> 陣列補註執行序。</li>
</ul>
<h3>復驗後重新定性(不照單全收)</h3>
<ul>
<li><strong>lineedge 攀穿 isolate——非缺陷</strong><code>edgeRect</code> 為物理版面測量(<code>getClientRects</code>),isolate 斷的是邏輯 run/相鄰,物理行首行尾判定須跨 <code>&lt;mark&gt;</code> 找真實相鄰內容;初評誤判、已還原。</li>
<li><strong>五處幂等收斂——評估後不做</strong>:各檢查不同位置關係(祖先/父/子),無單一 <code>isJz</code> 形狀可全覆蓋,強行歸一徒增風險。</li>
</ul>
<h3>文檔(本次)</h3>
<ul>
<li>README 新增「全部選項速查」表——突出被埋沒之 <code>lang.style</code><code>styleInlines</code><code>blockTags</code><code>skipTags</code>(標籤名陣列)/<code>skipAttribute</code><code>isSkipped</code>;三分類改分層表述、avoid 改七源+組合入口。</li>
<li>ARCHITECTURE §3.1 <code>scope.avoid</code> 過時示例、§4.1 透明分層與 avoid 七源、§4.2 標點六子類(舊列 <code>bd-cop</code><code>bd-sep</code><code>quote</code> 與實作不符)修正。</li>
</ul>
<h2>標點擠壓句界例外(句末點號接開類保留右空白) <span class="badge accent">2026-06-13</span></h2>
<p>行內連續擠壓(機制 A)新增<strong>句界例外</strong>:句末點號(<code>。.!?</code>)後接開類引號/括號(<code>。「</code><code>?「</code>)時保留句末點號右側空白、僅收開類左側,全角式與開明式皆合占 <strong>1.5em</strong>(對稱於 <code>。」</code>)——下游回饋之語感修正。單元測試 113→116 全綠。</p>
<h3>變更</h3>
@@ -197,6 +223,29 @@
<script type="text/markdown" id="md-source">
# 變更紀錄Changelog 聚珍Juzhencjk-autospace
## 專案整理架構一致性 確認缺陷修復 文檔重整2026-07-07
四路並行審計後之兩階段整理核心消除透明穿越判定之兩套口徑分裂測試 116121 全綠
### 階段一 · 確認缺陷 清理
- jinze 冪等二次 render 曾巢狀 jz-jinze違反 I636守衛改判 isJz(parent, PASS)
- pill 自身 avoid 漏檢processPills avoidsSelf連帶 CODE/KBD skipTags 預設移除已由 pillSelector pill 涵蓋再列 skipTags 違反三分類互斥會抹掉普通 code pill 間隙若把 code 移出 pillSelector 須自行加入 skipTags/scope.avoid
- 刪死代碼 createMarker/ANY_CJK/BD_ALL/BD_CLASS_NAMES/CreateOptions.attrs死旗標 biaodian/emphasis/ruby @reserved
### 階段二 · 架構一致性
- 透明判定單一真相源transparentInline 提升為公開 isTransparent邏輯文本流三原語collectRuns/adjacentLogicalChar/jiyaAdjacency共用之未在 styleInlines 之未知行內標籤採嚴格白名單一律當邊界用戶抉擇分層canSplit切分安全lineedge物理版面各有正當之不同邊界不併入
- 跨透明元素邊界避頭尾 #3句末點號落於 <strong> 首位你好<strong>世界</strong> forbiddenBreak edgeCharEl render 沿 parent
- avoid 排除歸一isAvoided 改組合入口自身或祖先avoidsSelf isAvoided漏一半
- 小項scope.include 去多餘 |nullgapTrim 補功能閘PASSES 補註執行序
### 復驗後重新定性不照單全收
- lineedge 攀穿 isolate非缺陷edgeRect 為物理測量須跨 mark 找真實相鄰初評誤判已還原
- 五處幂等收斂不做各檢查不同位置關係無單一 isJz 形狀可全覆蓋強行歸一徒增風險
### 文檔
- README 新增全部選項速查突出 lang.style/styleInlines/skipTags/skipAttribute/isSkipped三分類改分層avoid 改七源
- ARCHITECTURE §3.1 scope.avoid 過時示例§4.1 透明分層§4.2 標點六子類修正
## 標點擠壓句界例外句末點號接開類保留右空白2026-06-13
### 變更
+134 -51
View File
@@ -1,4 +1,4 @@
<!-- built with theme=dracula, mode=report, at=2026-06-13T07:41:09Z -->
<!-- built with theme=dracula, mode=report, at=2026-07-07T13:21:19Z -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
@@ -706,6 +706,14 @@ hr.line
}
hr.line::before { content: none; }
.lede
{
/* 首字下沉為 float,本段行數不足以填滿下沉字高度時,浮標會探出段底,
令次段首數行繞排、向右擠貼下沉字。將本段下沿托至下沉字下沿(高度=
4.2em × line-height 0.85 頂距 8px,與下方 ::first-letter 同步),
使次段恆起於下沉字之下。本段自然行高足夠時 min-height 不生效,無副作用。 */
min-height: calc(4.2em * 0.85 + 8px);
}
.lede::first-letter
{
font-family: var(--font-serif);
@@ -2323,9 +2331,10 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
/* ---------- Mode: report — Print / PDF ----------
Dynamic single-page PDF: runtime.js setupDynamicPrintPage() measures
content height before print and injects @page sized to fit. Slides
mode uses per-slide pagination (page-break-after: always) instead. */
Dynamic single-page PDF: report/behavior.js setupDynamicPrintPage()
measures the printed height (off-screen iframe at the print column width)
and injects @page sized to fit. Slides mode uses per-slide pagination
(page-break-after: always) instead. */
/* ---------- Print ---------- */
/* ---------- Print / PDF export ----------
@@ -2357,9 +2366,9 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
@page
{
/* A4 fallback. Real page size is set dynamically at runtime by
`setupDynamicPrintPage()` in runtime.js — it measures the rendered
height before print and injects a fresh `@page` rule sized to fit
content exactly.
`setupDynamicPrintPage()` in report/behavior.js — it measures the
printed height (off-screen iframe at the print column width) and
injects a fresh `@page` rule sized to fit content exactly.
Margin: 0 here. The real margin lives in `body` padding (see the
@media print rule below), which the user's print dialog cannot
@@ -2609,7 +2618,7 @@ createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("ma
<tr><td><code>margin</code>(後備)</td><td><code>jiya: { halfWidth: "margin" }</code></td><td><code>width:0.5em</code> +字身依墨色偏側溢出定位(以 CSS 重現 halt)</td><td>字型<strong></strong> <code>halt</code>(多數系統字型、GBK 老字型如方正書宋 GBK、PingFang</td></tr>
</tbody>
</table>
<div class="callout warn"><span class="label">為何需要後備</span> <code>halt</code> 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測<strong>方正書宋 GBK 僅含 <code>vert</code>、無 <code>halt</code></strong> → halt 模式不擠;切 <code>margin</code> 後備後(開明式 zh-Hans),同句行寬 <code>24.0em → 19.5em</code>、單一句內點號 <code>1.0em → 0.5em</code>,半形生效。</div>
<div class="callout warn"><span class="label">為何需要後備</span> <code>halt</code> 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測<strong>方正書宋 GBK 僅含 <code>vert</code>、無 <code>halt</code></strong> → halt 模式不擠;切 <code>margin</code> 後備後(開明式 zh-Hans),同句行寬 <code>24.0em → 19.7em</code>、單一句內點號 <code>1.0em → 0.5em</code>,半形生效。</div>
<div class="callout"><span class="label">句末標點與連續挤压</span> 句末點號為 <code>。.!?</code>——含<strong>全角句點 <code></code></strong>U+FF0E,異於表意句號 <code></code> U+3002Noto CJK 有 <code>halt</code>);全角式與開明式句末皆<strong>保持全角 1em</strong>banjiao 挤半。<strong>連續標點</strong>對每一標點獨立判斷——若其有空白之側緊鄰另一標點即收半形:故 <code>,「</code> <strong>全角式與開明式皆 1em</strong><code>。」</code> <strong>全角式 1.5em/開明式 1em</strong>;並透明穿越 <code>strong</code><code>em</code> 等行內邊界。<strong>句界例外</strong>:句末點號接開類(<code>。「</code><code>?「</code>)為句界,保留句末點號右側空白、僅收開類左側 → 全角式與開明式皆 <strong>1.5em</strong>(對稱於 <code>。」</code>);例外僅限句末點號+開類,句內點號 <code>,「</code> 與雙括號 <code>」「</code><code>》《</code> 仍兩側皆收 1em。半角式句末點號由 baseline 已半形,故仍 1em(不回歸全寬)。</div>
<h3>激活與粒度</h3>
@@ -2664,16 +2673,16 @@ createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("ma
</ol>
<h2>行內元素三分類:透明/隔離/整體</h2>
<p>行內元素對排版有且僅有三種口徑<strong>所有遍歷原語共用一判定</strong>(中西間隙、擠壓相鄰、禁則綁定、垂懸字、克隆切分皆一致);判定<strong>以元素為準、非標籤名</strong>——KaTeX 全樹皆 <code>&lt;span&gt;</code>,但 <code>.katex</code> 命中 <code>pillSelector</code> 即為整體邊界。</p>
<p>行內元素對排版有且僅有三種口徑<strong>邏輯文本流之遍歷原語共用一判定</strong> <code>isTransparent</code>(中西間隙、擠壓相鄰、禁則綁定、垂懸字);<strong>未在 <code>styleInlines</code> 之未知行內標籤(自訂元素等)採嚴格白名單、一律當邊界</strong>,透明須顯式登記。判定<strong>以元素為準、非標籤名</strong>——KaTeX 全樹皆 <code>&lt;span&gt;</code>,但 <code>.katex</code> 命中 <code>pillSelector</code> 即為整體邊界。結構克隆切分 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 為正當之獨立層(邊界各異,見架構)。</p>
<table>
<thead><tr><th>分類</th><th>選擇器</th><th>邊界</th><th>內部</th><th>典型</th></tr></thead>
<tbody>
<tr><td><strong>透明</strong></td><td><code>styleInlines</code></td><td>穿越——間隙/相鄰/run 皆延續</td><td>照常處理;禁則可<strong>克隆切分</strong>(最小綁定)</td><td><code>strong</code><code>em</code><code>a</code><code>span</code></td></tr>
<tr><td><strong>透明</strong></td><td><code>styleInlines</code><strong>未列之行內標籤=邊界</strong></td><td>穿越——間隙/相鄰/run 皆延續</td><td>照常處理;禁則可<strong>克隆切分</strong>(最小綁定)</td><td><code>strong</code><code>em</code><code>a</code><code>span</code></td></tr>
<tr><td><strong>隔離</strong></td><td><code>isolateSelector</code>(預設 <code>"mark"</code><code>""</code> 停用)</td><td>斷 run/斷相鄰/不可切分;<strong>兩端</strong><code>jz-hws</code> 間隙</td><td><strong>照常處理</strong>(自成脈絡)</td><td>帶背景色之 <code>&lt;mark&gt;</code>、badge 類</td></tr>
<tr><td><strong>整體</strong></td><td><code>pillSelector</code>(預設 <code>"code, kbd"</code></td><td>同隔離</td><td><strong>完全不可見</strong>——不 charify、不插間隙、不切分、不下潛</td><td><code>code</code><code>kbd</code>、行內公式 <code>.katex</code>、引用標 <code>.cite-ref</code></td></tr>
</tbody>
</table>
<p>禁則遇 pill<code>…$公式$</code>):整個 pill 作為原子鄰字單位<strong>整體綁入</strong> <code>jz-jinze</code>(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。<code>scope.avoid</code><code>skipAttribute</code><code>data-jz-skip</code> <strong>自身</strong>判定命中元素自身即邊界、內外皆不處理</p>
<p>禁則遇 pill<code>…$公式$</code>):整個 pill 作為原子鄰字單位<strong>整體綁入</strong> <code>jz-jinze</code>(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。<strong>avoid 七源</strong><code>isSkipped</code><code>skipTags</code>SVG<code>contentEditable</code><code>data-jz-skip</code><code>skipAttribute</code><code>scope.avoid</code><strong>自身</strong>判定命中即邊界;一次性檢查用組合入口 <code>isAvoided</code>(自身或祖先)。<strong><code>skipTags</code> 預設不含 <code>code</code><code>kbd</code></strong>——二者由 <code>pillSelector</code> 以 pill 涵蓋(三分類互斥)</p>
<div class="callout"><span class="label">建議</span> 行內公式/夾注引用等獨立行內單位宜列為 pill:<code>pillSelector: "code, kbd, .katex, .cite-ref"</code>——兩側補隙、吸收作者空格、整體綁定,統一處理。</div>
<h2>分頁器(Paged.js 等)相容:<code>justifyAtoms</code></h2>
@@ -2699,7 +2708,7 @@ createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("ma
<p>v1 相容層(<code>createCjkAutospace</code> 導出、IIFE 之全域別名、<code>autospaceClass</code> 選項)<strong>已隨最後一批 v1 消費端遷移完成而移除</strong>。請改用 <code>createJuzhen(options).render(root)</code> v2 APIv1 原始碼保存於 <code>v1-autospace</code> 分支。</p>
<h2>目視驗證</h2>
<p><code>demo/index.html</code> 載入 <code>dist/</code> 產物,簡繁並列、可切 <code>justify</code>、附 copy-clean 測試區。<code>demo/fallback.html</code> 以方正書宋 GBK(無 <code>halt</code>)對照 <code>halt</code> 失效 vs <code>margin</code> 後備生效。版面相關結論(justify 單份間距、擠壓視覺量、行端、<code>inline-block</code> 基線)須於 ChromeSafariFirefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。</p>
<p><code>demo/index.html</code> 載入 <code>dist/</code> 產物,簡繁並列、可切 <code>justify</code>、附 copy-clean 測試區。<code>demo/fallback.html</code> 以方正書宋 GBK(無 <code>halt</code>)對照 <code>halt</code> 失效 vs <code>margin</code> 後備生效。<code>demo/resize-check.html</code> 為可拖曳容器檢查頁(驗中西間隙在各寬度恆穩、行末去隙、pill 跨行右隙)。<code>demo/showcase.html</code> 以定寬文本框並列,逐場景演示功能分級與垂懸字。版面相關結論(justify 單份間距、擠壓視覺量、行端、<code>inline-block</code> 基線、間隙隨寬度之穩定性)須於 ChromeSafariFirefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。</p>
</main>
@@ -2774,7 +2783,7 @@ JS 只注入 `jz-*` 結構,所有視覺尺寸在 juzhen.css,缺一不可。
## 行內元素三分類
行內元素對排版三種口徑,所有遍歷原語共用一判定(以元素為準、非標籤名):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。avoidscope.avoidskipAttributedata-jz-skip)以自身判定命中即邊界。行內公式/夾注引用宜列為 pill`pillSelector: "code, kbd, .katex, .cite-ref"`。
行內元素對排版三種口徑,邏輯文本流之遍歷原語共用一判定 isTransparent(以元素為準、非標籤名);未在 styleInlines 之未知行內標籤採嚴格白名單、一律當邊界(透明須顯式登記):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。結構克隆切分 canSplit、行端物理版面 lineedge 為正當之獨立層。avoid 七源(isSkippedskipTagsSVGcontentEditabledata-jz-skipskipAttributescope.avoid)以自身判定命中即邊界(一次性用組合 isAvoided);skipTags 預設不含 code/kbd(由 pill 涵蓋、三分類互斥)。行內公式/夾注引用宜列為 pill`pillSelector: "code, kbd, .katex, .cite-ref"`。
## 分頁器相容
@@ -3652,9 +3661,12 @@ var Juzhen = (() => {
if (!finder.featureEnabledFor(L, PASS)) {
return;
}
const lSides = blankSides(bdClassOf(L));
const rSides = blankSides(bdClassOf(R));
if (lSides.right) {
const lClass = bdClassOf(L);
const rClass = bdClassOf(R);
const lSides = blankSides(lClass);
const rSides = blankSides(rClass);
const stopBeforeOpen = lClass === "bd-stop" && rClass === "bd-open";
if (lSides.right && !stopBeforeOpen) {
L.classList.add("jz-half");
}
if (rSides.left) {
@@ -5092,7 +5104,9 @@ window.__JUZHEN_PUNCT__="quanjiao";window.__JUZHEN_JUSTIFY_ATOMS__=true;window._
.lede.marginalia.references.appendix……)不命中、維持全功能。最近祖先勝,
故標題與 chrome 容器一旦命中,其後代(如 .slide-title 內的 .section-name
.slide-dot、.title-page 內的作者欄)一併降級。選擇器涵蓋三模式;某模式不存在之
class 自然不命中。 */
class 自然不命中。**切記只列「非段落 chrome 本身」,切勿納入包裹正文的容器**
(如 .deck-stage.deck.cols-2)——否則「最近祖先勝」會把整個子樹正文一併降級、
喪失禁則/垂懸/長詞(曾誤納 .deck-stage 致 slides 全 deck 正文無禁則、標點落行首)。 */
level: {
text: [
"h1, h2, h3, h4, h5, h6", // 全部標題(含 deck-section-slide-tp-toc-title
@@ -5100,7 +5114,7 @@ window.__JUZHEN_PUNCT__="quanjiao";window.__JUZHEN_JUSTIFY_ATOMS__=true;window._
".subtitle, .section-subtitle, .deck-subtitle", // 副標題
".meta, .timestamp, .actions, .page", // report chrome
".cover-block, .slide-foot, .toc-list", // slides 封面/頁尾/目錄條目
".deck-nav, .deck-counter, .deck-author, .deck-institute, .deck-date, .deck-actions, .deck-stage",
".deck-nav, .deck-counter, .deck-author, .deck-institute, .deck-date, .deck-actions",
".title-page, .toc-page, .simple-title", // academic 前置 chrome(含 tp-st- 欄位)
].join(", "),
},
@@ -5131,9 +5145,10 @@ window.__JUZHEN_PUNCT__="quanjiao";window.__JUZHEN_JUSTIFY_ATOMS__=true;window._
renderMath();
wrapFlexTitleText();
/* 一般模式於 init 立即排版。__JUZHEN_DEFER__academic 由 build.sh 設)令此處
跳過,改由該模式 behavior.js Paged.js preview() 完成後對成品頁呼叫
__juzhenRun__——關鍵:Paged.js chunker 對 inline-block 原子量測失準(分頁前
排版會致窄欄散架),故先以純文字分頁、再於定版頁上加排版原子。 */
跳過,改由該模式 behavior.js 在編號/目錄/正文標記完成後、Paged.js preview()
**之前**手動呼叫 __juzhenRun__——使 Paged.js 直接以聚珍後的最終幾何分頁(每頁
自然填滿、末行齊滿、無需分頁後斷縫修復)。整頁寬單欄下 chunker 正確量度聚珍
inline-block 原子,無散架。 */
if (!window.__JUZHEN_DEFER__) { runJuzhen(); }
/* ========== Feature: code-block — behavior =================================
@@ -6042,30 +6057,28 @@ if (!window.__copyDelegated__)
/* ---------- Dynamic print page sizing ----------
Measure the document's rendered height and inject an @page rule
sized to fit content exactly. Produces a single-page PDF on "Save
as PDF" with no trailing blank space.
Measure the document's true printed height and inject an @page rule
sized to fit it exactly a single-page PDF on "Save as PDF" with no
trailing blank space and no overflow onto a second page.
Page margin: 0. Real margins live in `body` padding (runtime.css
@media print). That keeps content width stable regardless of the
print dialog's Margins selector — and stable width is what makes
the screen-to-print height heuristic reliable.
Measurement: an off-screen iframe at the print page width (210mm) with
the document's own @media print rules un-gated reproduces the printed
layout; its content height is the answer (see measurePrintMm). This is
window-independent.
Page-height heuristic: with horizontal padding fixed (1cm each
side), print rendering height roughly matches screen rendering
height. Ratio = 1.0 (was 0.85 — too aggressive an under-estimate;
caused tail clipping). The 50mm buffer absorbs the residual
variance plus whatever vertical margin a dialog like Chrome's
Default might add on top.
Earlier this used `documentElement.scrollHeight` × a ratio. That broke
because report has NO max-width (content width = 100vw4em): on a wide
window the screen wraps less and measures short, but print always lays
out at the fixed 190mm column and wraps more, so the page came out too
short and content overflowed. The wider the window, the worse it got.
Vertical padding (body padding-top 1.5cm + padding-bottom 1cm =
25mm total) is reserved inside the page so the breathing room
above the title and below the footer is preserved.
Page margin: 0. Real margins live in `body` padding (the @media print
rule), so they survive the print dialog's "Margins: None" and stay
inside the measured height.
Escape hatch: set `window.disablePrintAutoSize = true` (in the
DevTools console or before page load) to disable. The injected
rule is cleared and printing falls back to the static A4 in
runtime.css (multi-page). */
Escape hatch: set `window.disablePrintAutoSize = true` (DevTools console
or before load) to clear the injected rule and fall back to the static
A4 @page in runtime.css (multi-page). */
function setupDynamicPrintPage()
{
let styleEl = null;
@@ -6079,25 +6092,95 @@ if (!window.__copyDelegated__)
}
return styleEl;
}
/* Collect this document's own `@media print` rules as unconditional
CSS, so an off-screen iframe can reproduce the printed-page layout. */
function collectPrintCss()
{
let css = "";
Array.from(document.styleSheets).forEach((sheet) =>
{
let rules;
try { rules = sheet.cssRules; }
catch (e) { return; } /* cross-origin sheet — unreadable */
Array.from(rules).forEach((rule) =>
{
if (rule.type === CSSRule.MEDIA_RULE && /print/i.test(rule.media.mediaText))
{
Array.from(rule.cssRules).forEach((inner) => { css += inner.cssText + "\n"; });
}
});
});
return css;
}
/* Measure the true printed height: clone the already-rendered DOM (KaTeX
+ juzhen baked in) into a hidden iframe sized to the print page width
(210mm) with the print rules un-gated, then read its content height.
innerHTML insertion never runs <script>, so the runtime IIFE / KaTeX /
juzhen do NOT re-execute. scrollHeight already includes the print
`body` padding (the un-gated @media print rule). Resolves to mm.
Why an iframe and not `documentElement.scrollHeight`: report mode has
NO max-width — on-screen content width = 100vw4em, so screen height
tracks the window and diverged from the fixed 190mm print column
(wide windows under-sized the page → overflow). The iframe pins the
measurement to the real print width, making it window-independent. */
function measurePrintMm()
{
return new Promise((resolve) =>
{
const iframe = document.createElement("iframe");
iframe.setAttribute("aria-hidden", "true");
iframe.style.cssText =
"position:fixed;left:-10000px;top:0;width:210mm;height:50px;"
+ "border:0;visibility:hidden;";
document.body.appendChild(iframe);
const idoc = iframe.contentDocument;
idoc.open();
idoc.write("<!DOCTYPE html><html><head></head><body></body></html>");
idoc.close();
idoc.documentElement.lang = document.documentElement.lang;
idoc.head.innerHTML = document.head.innerHTML;
idoc.body.innerHTML = document.body.innerHTML;
const ps = idoc.createElement("style");
ps.textContent = collectPrintCss();
idoc.head.appendChild(ps);
function done()
{
const h = Math.max(idoc.documentElement.scrollHeight, idoc.body.scrollHeight);
const mm = Math.ceil(h * 25.4 / 96);
iframe.remove();
resolve(mm);
}
/* Wait for the iframe's fonts (same @font-face, HTTP-cached from
the parent) so glyph metrics match; rAF lets layout settle. */
const fontsReady = (idoc.fonts && idoc.fonts.ready) || Promise.resolve();
fontsReady.then(() => requestAnimationFrame(() => requestAnimationFrame(done)));
});
}
function update()
{
const el = ensureStyle();
if (window.disablePrintAutoSize)
{
el.textContent = ""; /* fall back to runtime.css @page */
el.textContent = ""; /* fall back to runtime.css static @page */
return;
}
const PX_TO_MM = 25.4 / 96;
const RATIO = 1.0;
const PAD_V_MM = 25; /* body padding-top 15 + padding-bottom 10 */
const BUFFER_MM = 0; /* absorbs dialog margin + heuristic variance */
const screenH = document.documentElement.scrollHeight;
const printContentH = Math.ceil(screenH * PX_TO_MM * RATIO);
const pageH = printContentH + PAD_V_MM + BUFFER_MM;
el.textContent = "@page { size: 210mm " + pageH + "mm; margin: 0; }";
measurePrintMm().then((contentMm) =>
{
if (window.disablePrintAutoSize) { el.textContent = ""; return; }
const BUFFER_MM = 10; /* dialog margin + sub-mm rounding slack */
const pageH = contentMm + BUFFER_MM;
el.textContent = "@page { size: 210mm " + pageH + "mm; margin: 0; }";
});
}
update();
window.addEventListener("resize", update);
/* Print column width is fixed (190mm), so page height no longer depends
on the window — no resize re-measure. Re-measure before printing to
pick up dynamically expanded/collapsed content (async; the init rule
already covers static content for the current print). */
window.addEventListener("beforeprint", update);
}
+6 -6
View File
@@ -98,7 +98,7 @@ createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("ma
<tr><td><code>margin</code>(後備)</td><td><code>jiya: { halfWidth: "margin" }</code></td><td><code>width:0.5em</code> +字身依墨色偏側溢出定位(以 CSS 重現 halt)</td><td>字型<strong></strong> <code>halt</code>(多數系統字型、GBK 老字型如方正書宋 GBK、PingFang</td></tr>
</tbody>
</table>
<div class="callout warn"><span class="label">為何需要後備</span> <code>halt</code> 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測<strong>方正書宋 GBK 僅含 <code>vert</code>、無 <code>halt</code></strong> → halt 模式不擠;切 <code>margin</code> 後備後(開明式 zh-Hans),同句行寬 <code>24.0em → 19.5em</code>、單一句內點號 <code>1.0em → 0.5em</code>,半形生效。</div>
<div class="callout warn"><span class="label">為何需要後備</span> <code>halt</code> 缺席時開明式擠壓「完全無效」(標點仍佔全形)。實測<strong>方正書宋 GBK 僅含 <code>vert</code>、無 <code>halt</code></strong> → halt 模式不擠;切 <code>margin</code> 後備後(開明式 zh-Hans),同句行寬 <code>24.0em → 19.7em</code>、單一句內點號 <code>1.0em → 0.5em</code>,半形生效。</div>
<div class="callout"><span class="label">句末標點與連續挤压</span> 句末點號為 <code>。.!?</code>——含<strong>全角句點 <code></code></strong>U+FF0E,異於表意句號 <code></code> U+3002Noto CJK 有 <code>halt</code>);全角式與開明式句末皆<strong>保持全角 1em</strong>banjiao 挤半。<strong>連續標點</strong>對每一標點獨立判斷——若其有空白之側緊鄰另一標點即收半形:故 <code>,「</code> <strong>全角式與開明式皆 1em</strong><code>。」</code> <strong>全角式 1.5em/開明式 1em</strong>;並透明穿越 <code>strong</code><code>em</code> 等行內邊界。<strong>句界例外</strong>:句末點號接開類(<code>。「</code><code>?「</code>)為句界,保留句末點號右側空白、僅收開類左側 → 全角式與開明式皆 <strong>1.5em</strong>(對稱於 <code>。」</code>);例外僅限句末點號+開類,句內點號 <code>,「</code> 與雙括號 <code>」「</code><code>》《</code> 仍兩側皆收 1em。半角式句末點號由 baseline 已半形,故仍 1em(不回歸全寬)。</div>
<h3>激活與粒度</h3>
@@ -153,16 +153,16 @@ createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("ma
</ol>
<h2>行內元素三分類:透明/隔離/整體</h2>
<p>行內元素對排版有且僅有三種口徑<strong>所有遍歷原語共用一判定</strong>(中西間隙、擠壓相鄰、禁則綁定、垂懸字、克隆切分皆一致);判定<strong>以元素為準、非標籤名</strong>——KaTeX 全樹皆 <code>&lt;span&gt;</code>,但 <code>.katex</code> 命中 <code>pillSelector</code> 即為整體邊界。</p>
<p>行內元素對排版有且僅有三種口徑<strong>邏輯文本流之遍歷原語共用一判定</strong> <code>isTransparent</code>(中西間隙、擠壓相鄰、禁則綁定、垂懸字);<strong>未在 <code>styleInlines</code> 之未知行內標籤(自訂元素等)採嚴格白名單、一律當邊界</strong>,透明須顯式登記。判定<strong>以元素為準、非標籤名</strong>——KaTeX 全樹皆 <code>&lt;span&gt;</code>,但 <code>.katex</code> 命中 <code>pillSelector</code> 即為整體邊界。結構克隆切分 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 為正當之獨立層(邊界各異,見架構)。</p>
<table>
<thead><tr><th>分類</th><th>選擇器</th><th>邊界</th><th>內部</th><th>典型</th></tr></thead>
<tbody>
<tr><td><strong>透明</strong></td><td><code>styleInlines</code></td><td>穿越——間隙/相鄰/run 皆延續</td><td>照常處理;禁則可<strong>克隆切分</strong>(最小綁定)</td><td><code>strong</code><code>em</code><code>a</code><code>span</code></td></tr>
<tr><td><strong>透明</strong></td><td><code>styleInlines</code><strong>未列之行內標籤=邊界</strong></td><td>穿越——間隙/相鄰/run 皆延續</td><td>照常處理;禁則可<strong>克隆切分</strong>(最小綁定)</td><td><code>strong</code><code>em</code><code>a</code><code>span</code></td></tr>
<tr><td><strong>隔離</strong></td><td><code>isolateSelector</code>(預設 <code>"mark"</code><code>""</code> 停用)</td><td>斷 run/斷相鄰/不可切分;<strong>兩端</strong><code>jz-hws</code> 間隙</td><td><strong>照常處理</strong>(自成脈絡)</td><td>帶背景色之 <code>&lt;mark&gt;</code>、badge 類</td></tr>
<tr><td><strong>整體</strong></td><td><code>pillSelector</code>(預設 <code>"code, kbd"</code></td><td>同隔離</td><td><strong>完全不可見</strong>——不 charify、不插間隙、不切分、不下潛</td><td><code>code</code><code>kbd</code>、行內公式 <code>.katex</code>、引用標 <code>.cite-ref</code></td></tr>
</tbody>
</table>
<p>禁則遇 pill<code>…$公式$</code>):整個 pill 作為原子鄰字單位<strong>整體綁入</strong> <code>jz-jinze</code>(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。<code>scope.avoid</code><code>skipAttribute</code><code>data-jz-skip</code> <strong>自身</strong>判定命中元素自身即邊界、內外皆不處理</p>
<p>禁則遇 pill<code>…$公式$</code>):整個 pill 作為原子鄰字單位<strong>整體綁入</strong> <code>jz-jinze</code>(公式本不可斷,無額外代價);隔離元素同樣整綁(切分背景盒有可見接縫)。<strong>avoid 七源</strong><code>isSkipped</code><code>skipTags</code>SVG<code>contentEditable</code><code>data-jz-skip</code><code>skipAttribute</code><code>scope.avoid</code><strong>自身</strong>判定命中即邊界;一次性檢查用組合入口 <code>isAvoided</code>(自身或祖先)。<strong><code>skipTags</code> 預設不含 <code>code</code><code>kbd</code></strong>——二者由 <code>pillSelector</code> 以 pill 涵蓋(三分類互斥)</p>
<div class="callout"><span class="label">建議</span> 行內公式/夾注引用等獨立行內單位宜列為 pill:<code>pillSelector: "code, kbd, .katex, .cite-ref"</code>——兩側補隙、吸收作者空格、整體綁定,統一處理。</div>
<h2>分頁器(Paged.js 等)相容:<code>justifyAtoms</code></h2>
@@ -188,7 +188,7 @@ createJuzhen({ lang: { default: "zh-Hant" } }).render(document.querySelector("ma
<p>v1 相容層(<code>createCjkAutospace</code> 導出、IIFE 之全域別名、<code>autospaceClass</code> 選項)<strong>已隨最後一批 v1 消費端遷移完成而移除</strong>。請改用 <code>createJuzhen(options).render(root)</code> v2 APIv1 原始碼保存於 <code>v1-autospace</code> 分支。</p>
<h2>目視驗證</h2>
<p><code>demo/index.html</code> 載入 <code>dist/</code> 產物,簡繁並列、可切 <code>justify</code>、附 copy-clean 測試區。<code>demo/fallback.html</code> 以方正書宋 GBK(無 <code>halt</code>)對照 <code>halt</code> 失效 vs <code>margin</code> 後備生效。版面相關結論(justify 單份間距、擠壓視覺量、行端、<code>inline-block</code> 基線)須於 ChromeSafariFirefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。</p>
<p><code>demo/index.html</code> 載入 <code>dist/</code> 產物,簡繁並列、可切 <code>justify</code>、附 copy-clean 測試區。<code>demo/fallback.html</code> 以方正書宋 GBK(無 <code>halt</code>)對照 <code>halt</code> 失效 vs <code>margin</code> 後備生效。<code>demo/resize-check.html</code> 為可拖曳容器檢查頁(驗中西間隙在各寬度恆穩、行末去隙、pill 跨行右隙)。<code>demo/showcase.html</code> 以定寬文本框並列,逐場景演示功能分級與垂懸字。版面相關結論(justify 單份間距、擠壓視覺量、行端、<code>inline-block</code> 基線、間隙隨寬度之穩定性)須於 ChromeSafariFirefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。</p>
</main>
@@ -247,7 +247,7 @@ JS 只注入 `jz-*` 結構,所有視覺尺寸在 juzhen.css,缺一不可。
## 行內元素三分類
行內元素對排版三種口徑,所有遍歷原語共用一判定(以元素為準、非標籤名):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。avoidscope.avoidskipAttributedata-jz-skip)以自身判定命中即邊界。行內公式/夾注引用宜列為 pill`pillSelector: "code, kbd, .katex, .cite-ref"`。
行內元素對排版三種口徑,邏輯文本流之遍歷原語共用一判定 isTransparent(以元素為準、非標籤名);未在 styleInlines 之未知行內標籤採嚴格白名單、一律當邊界(透明須顯式登記):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。結構克隆切分 canSplit、行端物理版面 lineedge 為正當之獨立層。avoid 七源(isSkippedskipTagsSVGcontentEditabledata-jz-skipskipAttributescope.avoid)以自身判定命中即邊界(一次性用組合 isAvoided);skipTags 預設不含 code/kbd(由 pill 涵蓋、三分類互斥)。行內公式/夾注引用宜列為 pill`pillSelector: "code, kbd, .katex, .cite-ref"`。
## 分頁器相容