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:
+179
-47
@@ -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>四路並行審計(core/typeset+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><code data-jz-skip></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><button></code>)採嚴格白名單、一律當邊界</strong>(用戶抉擇;透明須顯式登記)。分層澄清:結構切分安全 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 各有正當之不同邊界、刻意不併入。</li>
|
||||
<li><strong>跨透明元素邊界避頭尾</strong>(修 #3):避頭類標點落於 <code><strong></code> 邊緣(<code>你好<strong>。世界</strong></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><mark></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(違反 I6,3→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 去多餘 |null;gapTrim 補功能閘;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 = 100vw−4em): 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 = 100vw−4em, 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
@@ -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>四路並行審計(core/typeset+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><code data-jz-skip></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><button></code>)採嚴格白名單、一律當邊界</strong>(用戶抉擇;透明須顯式登記)。分層澄清:結構切分安全 <code>canSplit</code>、行端物理版面 <code>lineedge</code> 各有正當之不同邊界、刻意不併入。</li>
|
||||
<li><strong>跨透明元素邊界避頭尾</strong>(修 #3):避頭類標點落於 <code><strong></code> 邊緣(<code>你好<strong>。世界</strong></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><mark></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)— 聚珍(Juzhen)/cjk-autospace
|
||||
|
||||
## 專案整理:架構一致性 + 確認缺陷修復 + 文檔重整(2026-07-07)
|
||||
|
||||
四路並行審計後之兩階段整理。核心:消除「透明穿越」判定之兩套口徑分裂。測試 116→121 全綠。
|
||||
|
||||
### 階段一 · 確認缺陷 + 清理
|
||||
- jinze 冪等:二次 render 曾巢狀 jz-jinze(違反 I6,3→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 去多餘 |null;gapTrim 補功能閘;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
@@ -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+3002,Noto 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><span></code>,但 <code>.katex</code> 命中 <code>pillSelector</code> 即為整體邊界。</p>
|
||||
<p>行內元素對排版有且僅有三種口徑。<strong>邏輯文本流之遍歷原語共用單一判定</strong> <code>isTransparent</code>(中西間隙、擠壓相鄰、禁則綁定、垂懸字);<strong>未在 <code>styleInlines</code> 之未知行內標籤(自訂元素等)採嚴格白名單、一律當邊界</strong>,透明須顯式登記。判定<strong>以元素為準、非標籤名</strong>——KaTeX 全樹皆 <code><span></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><mark></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 API;v1 原始碼保存於 <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> 基線)須於 Chrome/Safari/Firefox 三引擎目視核對——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> 基線、間隙隨寬度之穩定性)須於 Chrome/Safari/Firefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。</p>
|
||||
|
||||
</main>
|
||||
|
||||
@@ -2774,7 +2783,7 @@ JS 只注入 `jz-*` 結構,所有視覺尺寸在 juzhen.css,缺一不可。
|
||||
|
||||
## 行內元素三分類
|
||||
|
||||
行內元素對排版三種口徑,所有遍歷原語共用同一判定(以元素為準、非標籤名):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。avoid(scope.avoid/skipAttribute/data-jz-skip)以自身判定,命中即邊界。行內公式/夾注引用宜列為 pill:`pillSelector: "code, kbd, .katex, .cite-ref"`。
|
||||
行內元素對排版三種口徑,邏輯文本流之遍歷原語共用單一判定 isTransparent(以元素為準、非標籤名);未在 styleInlines 之未知行內標籤採嚴格白名單、一律當邊界(透明須顯式登記):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。結構克隆切分 canSplit、行端物理版面 lineedge 為正當之獨立層。avoid 七源(isSkipped/skipTags/SVG/contentEditable/data-jz-skip/skipAttribute/scope.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 = 100vw−4em): 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 = 100vw−4em, 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
@@ -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+3002,Noto 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><span></code>,但 <code>.katex</code> 命中 <code>pillSelector</code> 即為整體邊界。</p>
|
||||
<p>行內元素對排版有且僅有三種口徑。<strong>邏輯文本流之遍歷原語共用單一判定</strong> <code>isTransparent</code>(中西間隙、擠壓相鄰、禁則綁定、垂懸字);<strong>未在 <code>styleInlines</code> 之未知行內標籤(自訂元素等)採嚴格白名單、一律當邊界</strong>,透明須顯式登記。判定<strong>以元素為準、非標籤名</strong>——KaTeX 全樹皆 <code><span></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><mark></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 API;v1 原始碼保存於 <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> 基線)須於 Chrome/Safari/Firefox 三引擎目視核對——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> 基線、間隙隨寬度之穩定性)須於 Chrome/Safari/Firefox 三引擎目視核對——jsdom 僅驗證 DOM 結構正確性。</p>
|
||||
|
||||
</main>
|
||||
|
||||
@@ -247,7 +247,7 @@ JS 只注入 `jz-*` 結構,所有視覺尺寸在 juzhen.css,缺一不可。
|
||||
|
||||
## 行內元素三分類
|
||||
|
||||
行內元素對排版三種口徑,所有遍歷原語共用同一判定(以元素為準、非標籤名):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。avoid(scope.avoid/skipAttribute/data-jz-skip)以自身判定,命中即邊界。行內公式/夾注引用宜列為 pill:`pillSelector: "code, kbd, .katex, .cite-ref"`。
|
||||
行內元素對排版三種口徑,邏輯文本流之遍歷原語共用單一判定 isTransparent(以元素為準、非標籤名);未在 styleInlines 之未知行內標籤採嚴格白名單、一律當邊界(透明須顯式登記):透明(styleInlines——穿越、run 延續、可克隆切分);隔離(isolateSelector,預設 "mark"——邊界斷 run/斷相鄰/不可切分、兩端補隙,內部照常處理);整體(pillSelector——內部對一切 pass 不可見,禁則時作為原子鄰字單位整體綁入 jz-jinze)。結構克隆切分 canSplit、行端物理版面 lineedge 為正當之獨立層。avoid 七源(isSkipped/skipTags/SVG/contentEditable/data-jz-skip/skipAttribute/scope.avoid)以自身判定、命中即邊界(一次性用組合 isAvoided);skipTags 預設不含 code/kbd(由 pill 涵蓋、三分類互斥)。行內公式/夾注引用宜列為 pill:`pillSelector: "code, kbd, .katex, .cite-ref"`。
|
||||
|
||||
## 分頁器相容
|
||||
|
||||
|
||||
Reference in New Issue
Block a user