docs: 標點挤压一致性(同步 main fdf071c)
changelog 增「標點挤压一致性(2026-06-04)」:全角句點 . 納入句末標點、連續標點雙側 獨立挤压+透明穿越;並把前批 charClass 條目之 . 例改為 ‥(. 今已內建)。使用文檔 標點半形節增句末標點與連續挤压規則(涉長度處標明全角式/開明式)。HTML 與 Markdown 孿生同步、manifest 重建。
This commit is contained in:
+233
-131
@@ -1,4 +1,4 @@
|
||||
<!-- built with theme=dracula, mode=report, at=2026-06-04T08:24:26Z -->
|
||||
<!-- built with theme=dracula, mode=report, at=2026-06-09T07:22:49Z -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
@@ -92,9 +92,11 @@
|
||||
:root
|
||||
{
|
||||
/* Sans = Orbit Gothic CJK SC → Orbit Gothic (Latin) → sans-serif.
|
||||
CJK SC is the only CJK variant string we declare — Orbit Gothic
|
||||
carries the full pan-CJK set in one file and selects SC / TC /
|
||||
JP / KR glyph shapes via OpenType `locl` based on `<html lang>`. */
|
||||
The regional glyph cut is selected by NAME per `<html lang>` in the
|
||||
`:lang()` blocks below (SC default; TC / JP / KR overrides) — NOT via
|
||||
OpenType `locl`, which Juzhen v2 can defeat by splitting runs into
|
||||
per-glyph atoms. No Noto fallback: Orbit Gothic CJK already covers the
|
||||
same range, so a Noto entry would never be reached. */
|
||||
--font-sans: "Orbit Gothic CJK SC", "Orbit Gothic", sans-serif;
|
||||
|
||||
/* Alucard (light) — palette.md */
|
||||
@@ -131,6 +133,26 @@
|
||||
--del-bg: color-mix(in srgb, var(--error) 10%, transparent);
|
||||
}
|
||||
|
||||
/* Region-matched sans cut by NAME (locl-independent — see note above).
|
||||
Mirrors tokens.css's per-locale --font-serif reordering. The matching
|
||||
Orbit Gothic CJK cut leads; Orbit Gothic (Latin) then generic sans
|
||||
follow. (--font-serif / --font-mono are reordered in tokens.css.) */
|
||||
:root:lang(zh-TW),
|
||||
:root:lang(zh-Hant),
|
||||
:root:lang(zh-HK),
|
||||
:root:lang(zh-MO)
|
||||
{
|
||||
--font-sans: "Orbit Gothic CJK TC", "Orbit Gothic", sans-serif;
|
||||
}
|
||||
:root:lang(ja)
|
||||
{
|
||||
--font-sans: "Orbit Gothic CJK JP", "Orbit Gothic", sans-serif;
|
||||
}
|
||||
:root:lang(ko)
|
||||
{
|
||||
--font-sans: "Orbit Gothic CJK KR", "Orbit Gothic", sans-serif;
|
||||
}
|
||||
|
||||
/* === Pro (dark) =====================================================
|
||||
Applied when OS prefers dark AND mode isn't manually pinned to
|
||||
light. The `:not()` chain lets a manually-set light mode survive
|
||||
@@ -2509,10 +2531,17 @@ 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-06-04</span></h2>
|
||||
<h3>新增/修復</h3>
|
||||
<ul>
|
||||
<li><strong>全角句點 <code>.</code> 納入句末標點</strong>:<code>BD_STOP</code> 由 <code>。!?</code> 擴為 <code>。.!?</code>——納入全角句點 <code>.</code>(U+FF0E,異於表意句號 <code>。</code> U+3002)。實測 Noto CJK SC/TC 之 <code>halt</code> 皆覆蓋、挤压收半形 advance 0.5em,與 <code>。</code> 一致;<code>inkBias</code> 同步補(簡 left/繁 center)。政策行為同 <code>。!?</code>:<strong>全角式與開明式句末保持全角 1em</strong>,banjiao(或鄰接被收)挤半。設計原則:<code>halt</code> 即字型之「挤压意願」,字型不提供某標點 <code>halt</code>(字形占滿、不宜挤)則信任字型、保持全角不強挤。</li>
|
||||
<li><strong>連續標點挤压規則:雙側獨立 + 透明穿越</strong>(修正既有不一致)。規則:相鄰兩標點,<strong>對每一標點</strong>獨立判斷——若其「有空白之側」緊鄰另一標點即收半。原為「二選一只收一側」,致 <code>,「</code> 全角式誤為 1.5em;修後 <code>,「</code> <strong>全角式與開明式皆 1em</strong>、<code>。」</code> <strong>全角式 1.5em/開明式 1em</strong>、<code>·—</code> 兩式皆 2em(不宜挤)。並改「同 parent 直接兄弟」掃描為<strong>從 root 之透明遍歷</strong>(穿越 <code>strong</code>/<code>em</code> 等行內邊界、block/avoid 斷相鄰),使跨 inline 邊界之連續標點亦正確配對(原漏致達 2em)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>字符分類規則集 <span class="badge accent">2026-06-04</span></h2>
|
||||
<h3>新增</h3>
|
||||
<ul>
|
||||
<li><strong>可配置字符分類(<code>charClass</code>)</strong>:把散落各 pass 之硬編碼分類(中文 CJK/西文 ANS/全寬標點 BD_* 子類)統一抽象為<strong>規則集</strong>,內置默認即現狀值,消費端經 <code>options.charClass</code>(<code>{ cjk?, western?, biaodian?:{stop,pause,open,close,middle,liga} }</code>)<strong>追加擴充</strong>,覆寫 <code>spacing</code>/<code>jiya</code>/<code>jinze</code> 之分類判定。動機:英文全角句號 <code>.</code>(U+FF0E,異於中文句號 <code>。</code>)實證為未分類——不 charify/不挤/不補隙;以 <code>charClass:{biaodian:{stop:"."}}</code> 即比照 <code>。</code>。<strong>向下兼容</strong>:不傳則行為與內置完全一致、各實例隔離(覆寫不污染默認)。v1 為追加語義(不重分類/移除內置);結構性 ASCII 不開放。<code>spacing</code> 之間隙正則改由規則集 per-instance 編譯。測試 66 通過(含 9 條 charClass)。</li>
|
||||
<li><strong>可配置字符分類(<code>charClass</code>)</strong>:把散落各 pass 之硬編碼分類(中文 CJK/西文 ANS/全寬標點 BD_* 子類)統一抽象為<strong>規則集</strong>,內置默認即現狀值,消費端經 <code>options.charClass</code>(<code>{ cjk?, western?, biaodian?:{stop,pause,open,close,middle,liga} }</code>)<strong>追加擴充</strong>,覆寫 <code>spacing</code>/<code>jiya</code>/<code>jinze</code> 之分類判定。動機:未分類符號(如二點省略 <code>‥</code> U+2025)——不 charify/不挤;以 <code>charClass:{biaodian:{stop:"‥"}}</code> 即可歸入對應類。<strong>向下兼容</strong>:不傳則行為與內置完全一致、各實例隔離(覆寫不污染默認)。v1 為追加語義(不重分類/移除內置);結構性 ASCII 不開放。<code>spacing</code> 之間隙正則改由規則集 per-instance 編譯。測試 66 通過(含 9 條 charClass)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>下游回饋修復批次 <span class="badge accent">2026-06-03</span></h2>
|
||||
@@ -2618,10 +2647,16 @@ main > *:nth-child(n+6) { animation-delay: 0.46s; }
|
||||
<script type="text/markdown" id="md-source">
|
||||
# 變更紀錄(Changelog)— 聚珍(Juzhen)/cjk-autospace
|
||||
|
||||
## 標點挤压一致性(2026-06-04)
|
||||
|
||||
### 新增/修復
|
||||
- 全角句點 . 納入句末標點:BD_STOP 由 。!? 擴為 。.!?(. U+FF0E 異於 。 U+3002)。Noto CJK 有 halt、挤压收 0.5em,與 。一致;inkBias 補(簡 left/繁 center)。政策同 。!?:全角式與開明式句末保持全角 1em,banjiao 挤半。原則:halt 即字型挤压意願,字型不給某標點 halt(不宜挤)則信任字型不強挤。
|
||||
- 連續標點挤压:雙側獨立 + 透明穿越。規則:相鄰兩標點,對每一標點獨立判斷,若其有空白之側緊鄰另一標點即收半。修後 ,「 全角式與開明式皆 1em、。」 全角式 1.5em/開明式 1em、·— 兩式皆 2em。並改同 parent 掃描為從 root 透明遍歷,跨 strong/em 邊界之連續標點亦正確挤(原漏致 2em)。
|
||||
|
||||
## 字符分類規則集(2026-06-04)
|
||||
|
||||
### 新增
|
||||
- 可配置字符分類 charClass:硬編碼分類(中文/西文/全寬標點子類)統一抽象為規則集,內置默認,消費端經 options.charClass(cjk/western/biaodian{stop,pause,open,close,middle,liga})追加擴充,覆寫 spacing/jiya/jinze 判定。例:英文全角句號 .(U+FF0E)預設未分類,以 biaodian.stop 歸全寬句末標點即比照 。。向下兼容(不傳則行為不變、實例隔離);v1 追加語義、不重分類內置;結構性 ASCII 不開放。spacing 間隙正則改由規則集 per-instance 編譯。測試 66 通過。
|
||||
- 可配置字符分類 charClass:硬編碼分類(中文/西文/全寬標點子類)統一抽象為規則集,內置默認,消費端經 options.charClass(cjk/western/biaodian{stop,pause,open,close,middle,liga})追加擴充,覆寫 spacing/jiya/jinze 判定。例:未分類符號(如 ‥ U+2025)可經 biaodian.stop 歸全寬句末標點。向下兼容(不傳則行為不變、實例隔離);v1 追加語義、不重分類內置;結構性 ASCII 不開放。spacing 間隙正則改由規則集 per-instance 編譯。測試 66 通過。
|
||||
|
||||
## 下游回饋修復批次(2026-06-03)
|
||||
|
||||
@@ -3187,25 +3222,59 @@ var Juzhen = (() => {
|
||||
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
|
||||
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
|
||||
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
|
||||
var BD_STOP = "\u3002\uFF01\uFF1F";
|
||||
var BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
|
||||
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
||||
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
|
||||
var BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
|
||||
var BD_MIDDLE = "\xB7\u30FB\u2027";
|
||||
var BD_LIGA = "\u2026\u2014";
|
||||
var BD_ALL = BD_STOP + BD_PAUSE + BD_OPEN + BD_CLOSE + BD_MIDDLE + BD_LIGA;
|
||||
var BD_SETS = [
|
||||
["bd-stop", BD_STOP],
|
||||
["bd-pause", BD_PAUSE],
|
||||
["bd-open", BD_OPEN],
|
||||
["bd-close", BD_CLOSE],
|
||||
["bd-middle", BD_MIDDLE],
|
||||
["bd-liga", BD_LIGA]
|
||||
var BD_ORDER = [
|
||||
"stop",
|
||||
"pause",
|
||||
"open",
|
||||
"close",
|
||||
"middle",
|
||||
"liga"
|
||||
];
|
||||
function classifyBiaodian(ch) {
|
||||
for (const [name, set] of BD_SETS) {
|
||||
if (set.indexOf(ch) >= 0) {
|
||||
return name;
|
||||
var DEFAULT_RULESET = {
|
||||
cjk: CJK,
|
||||
ansAfter: ANS_AFTER_CJK,
|
||||
ansBefore: ANS_BEFORE_CJK,
|
||||
biaodian: {
|
||||
stop: BD_STOP,
|
||||
pause: BD_PAUSE,
|
||||
open: BD_OPEN,
|
||||
close: BD_CLOSE,
|
||||
middle: BD_MIDDLE,
|
||||
liga: BD_LIGA
|
||||
}
|
||||
};
|
||||
function resolveRuleset(override) {
|
||||
if (!override) {
|
||||
return DEFAULT_RULESET;
|
||||
}
|
||||
const west = override.western || "";
|
||||
const bd = { ...DEFAULT_RULESET.biaodian };
|
||||
if (override.biaodian) {
|
||||
for (const sub of BD_ORDER) {
|
||||
const extra = override.biaodian[sub];
|
||||
if (extra) {
|
||||
bd[sub] = bd[sub] + extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
cjk: CJK + (override.cjk || ""),
|
||||
ansAfter: ANS_AFTER_CJK + west,
|
||||
ansBefore: ANS_BEFORE_CJK + west,
|
||||
biaodian: bd
|
||||
};
|
||||
}
|
||||
function classifyBiaodian(ch, biaodian = DEFAULT_RULESET.biaodian) {
|
||||
for (const sub of BD_ORDER) {
|
||||
if (biaodian[sub].indexOf(ch) >= 0) {
|
||||
return "bd-" + sub;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -3241,6 +3310,7 @@ var Juzhen = (() => {
|
||||
"\uFF0C": "left",
|
||||
"\u3001": "left",
|
||||
"\u3002": "left",
|
||||
"\uFF0E": "left",
|
||||
"\uFF1B": "left",
|
||||
"\uFF1A": "left",
|
||||
"\uFF01": "left",
|
||||
@@ -3250,6 +3320,7 @@ var Juzhen = (() => {
|
||||
"\uFF0C": "center",
|
||||
"\u3001": "center",
|
||||
"\u3002": "center",
|
||||
"\uFF0E": "center",
|
||||
"\uFF1B": "center",
|
||||
"\uFF1A": "center",
|
||||
"\uFF01": "center",
|
||||
@@ -3314,9 +3385,10 @@ var Juzhen = (() => {
|
||||
let style = null;
|
||||
let langClass = null;
|
||||
const biasOverrides = ctx.options.jiyaConfig.bias;
|
||||
const biaodian = ctx.options.ruleset.biaodian;
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const ch = data.charAt(i);
|
||||
const bdClass = classifyBiaodian(ch);
|
||||
const bdClass = classifyBiaodian(ch, biaodian);
|
||||
if (!bdClass) {
|
||||
continue;
|
||||
}
|
||||
@@ -3355,33 +3427,52 @@ var Juzhen = (() => {
|
||||
order: 25,
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx) {
|
||||
const { finder } = ctx;
|
||||
const parents = /* @__PURE__ */ new Set();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||
if (c.parentNode) {
|
||||
parents.add(c.parentNode);
|
||||
const { finder, options } = ctx;
|
||||
const blockSet = options.finder.blockTags;
|
||||
let prev = null;
|
||||
const squeeze = (L, R) => {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
let child = parent.firstChild;
|
||||
while (child) {
|
||||
const next = child.nextSibling;
|
||||
if (child.nodeType === 1 && child.nodeName === "JZ-CHAR" && next && next.nodeType === 1 && next.nodeName === "JZ-CHAR") {
|
||||
const L = child;
|
||||
const R = next;
|
||||
if (finder.featureEnabledFor(L, PASS)) {
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
} else if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
}
|
||||
if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
};
|
||||
const visit = (node) => {
|
||||
for (let c = node.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3) {
|
||||
if (c.data.length > 0) {
|
||||
prev = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c.nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
const el = c;
|
||||
const nm = el.nodeName;
|
||||
if (nm === "JZ-CHAR") {
|
||||
if (prev) {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else {
|
||||
visit(el);
|
||||
}
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(ctx.root);
|
||||
},
|
||||
revert() {
|
||||
}
|
||||
@@ -3409,46 +3500,46 @@ var Juzhen = (() => {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isTokenChar(ch) {
|
||||
return !ANY_CJK.test(ch) && !/\s/.test(ch);
|
||||
function isTokenChar(ch, anyCjk) {
|
||||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||||
}
|
||||
function leadingTokenEnd(d) {
|
||||
if (ANY_CJK.test(d.charAt(0))) {
|
||||
function leadingTokenEnd(d, anyCjk) {
|
||||
if (anyCjk.test(d.charAt(0))) {
|
||||
return 1;
|
||||
}
|
||||
let e = 1;
|
||||
while (e < d.length && isTokenChar(d.charAt(e))) {
|
||||
while (e < d.length && isTokenChar(d.charAt(e), anyCjk)) {
|
||||
e += 1;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
function trailingTokenStart(d) {
|
||||
function trailingTokenStart(d, anyCjk) {
|
||||
const last = d.length - 1;
|
||||
if (ANY_CJK.test(d.charAt(last))) {
|
||||
if (anyCjk.test(d.charAt(last))) {
|
||||
return last;
|
||||
}
|
||||
let s = last;
|
||||
while (s > 0 && isTokenChar(d.charAt(s - 1))) {
|
||||
while (s > 0 && isTokenChar(d.charAt(s - 1), anyCjk)) {
|
||||
s -= 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function splitBoundaries(t, needFirst, needLast) {
|
||||
function splitBoundaries(t, needFirst, needLast, anyCjk) {
|
||||
let node = t;
|
||||
if (needFirst) {
|
||||
const e = leadingTokenEnd(node.data);
|
||||
const e = leadingTokenEnd(node.data, anyCjk);
|
||||
if (e < node.data.length) {
|
||||
node = node.splitText(e);
|
||||
}
|
||||
}
|
||||
if (needLast) {
|
||||
const s = trailingTokenStart(node.data);
|
||||
const s = trailingTokenStart(node.data, anyCjk);
|
||||
if (s > 0) {
|
||||
node.splitText(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
function processParent(parent, finder) {
|
||||
function processParent(parent, finder, anyCjk) {
|
||||
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 1 && c.nodeName === "JZ-JINZE") {
|
||||
return;
|
||||
@@ -3478,7 +3569,7 @@ var Juzhen = (() => {
|
||||
const needFirst = i2 > 0 && forbidden[i2 - 1];
|
||||
const needLast = i2 < n - 1 && forbidden[i2];
|
||||
if (needFirst || needLast) {
|
||||
splitBoundaries(u, needFirst, needLast);
|
||||
splitBoundaries(u, needFirst, needLast, anyCjk);
|
||||
}
|
||||
}
|
||||
const kids = [];
|
||||
@@ -3508,6 +3599,7 @@ var Juzhen = (() => {
|
||||
order: 40,
|
||||
enabled: (o) => o.features.jinze,
|
||||
render(ctx) {
|
||||
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
|
||||
const parents = /* @__PURE__ */ new Set();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||
if (c.parentNode) {
|
||||
@@ -3515,7 +3607,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
processParent(parent, ctx.finder);
|
||||
processParent(parent, ctx.finder, anyCjk);
|
||||
}
|
||||
},
|
||||
revert(ctx) {
|
||||
@@ -3761,79 +3853,88 @@ var Juzhen = (() => {
|
||||
var LBR_EXT = "\\(\\[\\{<>";
|
||||
var RBR_EXT = "\\)\\]\\}<>";
|
||||
var DEL = String.fromCharCode(57347);
|
||||
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
||||
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
||||
var STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
|
||||
var DOTS_CJK = new RegExp("([\\.]{2,})([" + CJK + "])", "g");
|
||||
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
||||
var CJK_TILDE = new RegExp("([" + CJK + "])(~+)(?!=)(?=[" + CJK + ALNUM + "])", "g");
|
||||
var CJK_TILDE_EQUALS = new RegExp("([" + CJK + "])(~=)", "g");
|
||||
var CJK_PERIOD = new RegExp("([" + CJK + "])(\\.)(?![" + ALNUM + "\\./])(?=[" + CJK + ALNUM + "])", "g");
|
||||
var AN_PERIOD_CJK = new RegExp("([" + ALNUM + "])(\\.)([" + CJK + "])", "g");
|
||||
var AN_COLON_CJK = new RegExp("([" + ALNUM + "])(:)([" + CJK + "])", "g");
|
||||
var CJK_QUOTE = new RegExp("([" + CJK + "])([" + QUOTES_CLS + "])", "g");
|
||||
var QUOTE_CJK = new RegExp("([" + QUOTES_CLS + "])([" + CJK + "])", "g");
|
||||
var CJK_QUOTE_AN = new RegExp("([" + CJK + '])(")([' + ALNUM + "])", "g");
|
||||
var CJK_HASH = new RegExp("([" + CJK + "])(#([^ ]))", "g");
|
||||
var HASH_CJK = new RegExp("(([^ ])#)([" + CJK + "])", "g");
|
||||
var SINGLE_LETTER_GRADE = new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])([" + CJK + "])", "g");
|
||||
var CJK_OPERATOR_ANS = new RegExp("([" + CJK + "])([" + OPS_HYPHEN + "])([" + ALNUM + "])", "g");
|
||||
var ANS_OPERATOR_CJK = new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])([" + CJK + "])", "g");
|
||||
var CJK_LESS_THAN = new RegExp("([" + CJK + "])(<)([" + ALNUM + "])", "g");
|
||||
var LESS_THAN_CJK = new RegExp("([" + ALNUM + "])(<)([" + CJK + "])", "g");
|
||||
var CJK_GREATER_THAN = new RegExp("([" + CJK + "])(>)([" + ALNUM + "])", "g");
|
||||
var GREATER_THAN_CJK = new RegExp("([" + ALNUM + "])(>)([" + CJK + "])", "g");
|
||||
var CJK_LEFT_BRACKET = new RegExp("([" + CJK + "])([" + LBR_EXT + "])", "g");
|
||||
var RIGHT_BRACKET_CJK = new RegExp("([" + RBR_EXT + "])([" + CJK + "])", "g");
|
||||
var AN_LEFT_BRACKET = new RegExp("([" + ALNUM + "])(?<!\\.[" + ALNUM + "]*)([" + LBR_BASIC + "])", "g");
|
||||
var RIGHT_BRACKET_AN = new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g");
|
||||
var CJK_ANS = new RegExp("([" + CJK + "])([" + ANS_AFTER_CJK + "])", "g");
|
||||
var ANS_CJK = new RegExp("([" + ANS_BEFORE_CJK + "])([" + CJK + "])", "g");
|
||||
var S_A = new RegExp("(%)([" + ALPHA + "])", "g");
|
||||
function makeRules(rs) {
|
||||
const CJK2 = rs.cjk;
|
||||
const ANS_AFTER = rs.ansAfter;
|
||||
const ANS_BEFORE = rs.ansBefore;
|
||||
return {
|
||||
anyCjk: new RegExp("[" + CJK2 + "]"),
|
||||
pillNeighbor: new RegExp("[" + ALNUM + CJK2 + "]"),
|
||||
STRIP_CJK_SPACE_ANS: new RegExp("([" + CJK2 + "]) ([" + ANS_AFTER + "])", "g"),
|
||||
STRIP_ANS_SPACE_CJK: new RegExp("([" + ANS_BEFORE + "]) ([" + CJK2 + "])", "g"),
|
||||
STRIP_CJK_SPACE_CJK: new RegExp("([" + CJK2 + "]) (?=[" + CJK2 + "])", "g"),
|
||||
DOTS_CJK: new RegExp("([\\.]{2,})([" + CJK2 + "])", "g"),
|
||||
CJK_PUNCTUATION: new RegExp("([" + CJK2 + "])([!;,\\?:]+)(?=[" + CJK2 + ALNUM + "])", "g"),
|
||||
AN_PUNCTUATION_CJK: new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK2 + "])", "g"),
|
||||
CJK_TILDE: new RegExp("([" + CJK2 + "])(~+)(?!=)(?=[" + CJK2 + ALNUM + "])", "g"),
|
||||
CJK_TILDE_EQUALS: new RegExp("([" + CJK2 + "])(~=)", "g"),
|
||||
CJK_PERIOD: new RegExp("([" + CJK2 + "])(\\.)(?![" + ALNUM + "\\./])(?=[" + CJK2 + ALNUM + "])", "g"),
|
||||
AN_PERIOD_CJK: new RegExp("([" + ALNUM + "])(\\.)([" + CJK2 + "])", "g"),
|
||||
AN_COLON_CJK: new RegExp("([" + ALNUM + "])(:)([" + CJK2 + "])", "g"),
|
||||
CJK_QUOTE: new RegExp("([" + CJK2 + "])([" + QUOTES_CLS + "])", "g"),
|
||||
QUOTE_CJK: new RegExp("([" + QUOTES_CLS + "])([" + CJK2 + "])", "g"),
|
||||
CJK_QUOTE_AN: new RegExp("([" + CJK2 + '])(")([' + ALNUM + "])", "g"),
|
||||
CJK_HASH: new RegExp("([" + CJK2 + "])(#([^ ]))", "g"),
|
||||
HASH_CJK: new RegExp("(([^ ])#)([" + CJK2 + "])", "g"),
|
||||
SINGLE_LETTER_GRADE: new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])([" + CJK2 + "])", "g"),
|
||||
CJK_OPERATOR_ANS: new RegExp("([" + CJK2 + "])([" + OPS_HYPHEN + "])([" + ALNUM + "])", "g"),
|
||||
ANS_OPERATOR_CJK: new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])([" + CJK2 + "])", "g"),
|
||||
CJK_LESS_THAN: new RegExp("([" + CJK2 + "])(<)([" + ALNUM + "])", "g"),
|
||||
LESS_THAN_CJK: new RegExp("([" + ALNUM + "])(<)([" + CJK2 + "])", "g"),
|
||||
CJK_GREATER_THAN: new RegExp("([" + CJK2 + "])(>)([" + ALNUM + "])", "g"),
|
||||
GREATER_THAN_CJK: new RegExp("([" + ALNUM + "])(>)([" + CJK2 + "])", "g"),
|
||||
CJK_LEFT_BRACKET: new RegExp("([" + CJK2 + "])([" + LBR_EXT + "])", "g"),
|
||||
RIGHT_BRACKET_CJK: new RegExp("([" + RBR_EXT + "])([" + CJK2 + "])", "g"),
|
||||
AN_LEFT_BRACKET: new RegExp("([" + ALNUM + "])(?<!\\.[" + ALNUM + "]*)([" + LBR_BASIC + "])", "g"),
|
||||
RIGHT_BRACKET_AN: new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g"),
|
||||
CJK_ANS: new RegExp("([" + CJK2 + "])([" + ANS_AFTER + "])", "g"),
|
||||
ANS_CJK: new RegExp("([" + ANS_BEFORE + "])([" + CJK2 + "])", "g"),
|
||||
S_A: new RegExp("(%)([" + ALPHA + "])", "g")
|
||||
};
|
||||
}
|
||||
var COMPOUND_WORD = /\b(?:[A-Za-z0-9]*[a-z][A-Za-z0-9]*-[A-Za-z0-9]+|[A-Za-z0-9]+-[A-Za-z0-9]*[a-z][A-Za-z0-9]*|[A-Za-z]+-[0-9]+|[A-Za-z]+[0-9]+-[A-Za-z0-9]+)(?:-[A-Za-z0-9]+)*\b/g;
|
||||
var PH_PATTERN = new RegExp(PH_OPEN + "(\\d+)" + PH_CLOSE, "g");
|
||||
function panguSpace(text) {
|
||||
if (text.length <= 1 || !ANY_CJK.test(text)) {
|
||||
function panguSpace(text, R) {
|
||||
if (text.length <= 1 || !R.anyCjk.test(text)) {
|
||||
return text;
|
||||
}
|
||||
let s = text;
|
||||
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
||||
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
||||
s = s.replace(STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||||
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
||||
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_TILDE, "$1$2" + MARK);
|
||||
s = s.replace(CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
|
||||
s = s.replace(CJK_PERIOD, "$1$2" + MARK);
|
||||
s = s.replace(AN_PERIOD_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(AN_COLON_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_QUOTE, "$1" + MARK + "$2");
|
||||
s = s.replace(QUOTE_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_HASH, "$1" + MARK + "$2");
|
||||
s = s.replace(HASH_CJK, "$1" + MARK + "$3");
|
||||
s = s.replace(R.STRIP_CJK_SPACE_ANS, "$1$2");
|
||||
s = s.replace(R.STRIP_ANS_SPACE_CJK, "$1$2");
|
||||
s = s.replace(R.STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||||
s = s.replace(R.DOTS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_PUNCTUATION, "$1$2" + MARK);
|
||||
s = s.replace(R.AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_TILDE, "$1$2" + MARK);
|
||||
s = s.replace(R.CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
|
||||
s = s.replace(R.CJK_PERIOD, "$1$2" + MARK);
|
||||
s = s.replace(R.AN_PERIOD_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.AN_COLON_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_QUOTE, "$1" + MARK + "$2");
|
||||
s = s.replace(R.QUOTE_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_HASH, "$1" + MARK + "$2");
|
||||
s = s.replace(R.HASH_CJK, "$1" + MARK + "$3");
|
||||
const compounds = [];
|
||||
s = s.replace(COMPOUND_WORD, (m) => {
|
||||
compounds.push(m);
|
||||
return PH_OPEN + (compounds.length - 1) + PH_CLOSE;
|
||||
});
|
||||
s = s.replace(SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(PH_PATTERN, (_m, idx) => compounds[Number(idx)] || "");
|
||||
s = s.replace(CJK_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(AN_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(RIGHT_BRACKET_AN, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_ANS, "$1" + MARK + "$2");
|
||||
s = s.replace(ANS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(S_A, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(R.RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.AN_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(R.RIGHT_BRACKET_AN, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_ANS, "$1" + MARK + "$2");
|
||||
s = s.replace(R.ANS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.S_A, "$1" + MARK + "$2");
|
||||
return s;
|
||||
}
|
||||
function wrapNodeInGap(node) {
|
||||
@@ -3858,11 +3959,11 @@ var Juzhen = (() => {
|
||||
}
|
||||
wrapNodeInGap(tn);
|
||||
}
|
||||
function processBlock(block, finder) {
|
||||
function processBlock(block, finder, R) {
|
||||
const runs = finder.collectRuns(block);
|
||||
for (const run of runs) {
|
||||
const { text, map } = run;
|
||||
const spaced = panguSpace(text);
|
||||
const spaced = panguSpace(text, R);
|
||||
if (spaced === text) {
|
||||
continue;
|
||||
}
|
||||
@@ -3903,8 +4004,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
var PILL_NEIGHBOR = new RegExp("[A-Za-z0-9" + CJK + "]");
|
||||
function processPills(root, finder, pillSelector) {
|
||||
function processPills(root, finder, pillSelector, pillNeighbor) {
|
||||
root.querySelectorAll(pillSelector).forEach((pill) => {
|
||||
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
@@ -3914,36 +4014,36 @@ var Juzhen = (() => {
|
||||
}
|
||||
const prev = finder.adjacentLogicalChar(pill, -1);
|
||||
if (prev) {
|
||||
handlePillSide(prev, pill, -1);
|
||||
handlePillSide(prev, pill, -1, pillNeighbor);
|
||||
}
|
||||
const next = finder.adjacentLogicalChar(pill, 1);
|
||||
if (next) {
|
||||
handlePillSide(next, pill, 1);
|
||||
handlePillSide(next, pill, 1, pillNeighbor);
|
||||
}
|
||||
});
|
||||
}
|
||||
function handlePillSide(adj, pill, dir) {
|
||||
function handlePillSide(adj, pill, dir, pillNeighbor) {
|
||||
if (dir === -1) {
|
||||
if (PILL_NEIGHBOR.test(adj.ch)) {
|
||||
if (pillNeighbor.test(adj.ch)) {
|
||||
wrapGapLeft({ textNode: adj.textNode, offset: adj.offset });
|
||||
return;
|
||||
}
|
||||
if (adj.ch === " " && adj.offset >= 1) {
|
||||
const tn = adj.textNode;
|
||||
const bc = tn.data.charAt(adj.offset - 1);
|
||||
if (bc !== " " && PILL_NEIGHBOR.test(bc)) {
|
||||
if (bc !== " " && pillNeighbor.test(bc)) {
|
||||
tn.data = tn.data.slice(0, adj.offset) + tn.data.slice(adj.offset + 1);
|
||||
wrapGapLeft({ textNode: tn, offset: adj.offset - 1 });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (PILL_NEIGHBOR.test(adj.ch)) {
|
||||
if (pillNeighbor.test(adj.ch)) {
|
||||
wrapNodeInGap(pill);
|
||||
return;
|
||||
}
|
||||
if (adj.ch === " " && adj.textNode.data.length >= 2) {
|
||||
const ac = adj.textNode.data.charAt(1);
|
||||
if (ac !== " " && PILL_NEIGHBOR.test(ac)) {
|
||||
if (ac !== " " && pillNeighbor.test(ac)) {
|
||||
adj.textNode.data = adj.textNode.data.slice(1);
|
||||
wrapNodeInGap(pill);
|
||||
}
|
||||
@@ -3957,13 +4057,14 @@ var Juzhen = (() => {
|
||||
enabled: (o) => o.features.spacing,
|
||||
render(ctx) {
|
||||
const { finder, options } = ctx;
|
||||
const R = makeRules(options.ruleset);
|
||||
ctx.finder.eachBlock(ctx.root, (block) => {
|
||||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||||
return;
|
||||
}
|
||||
processBlock(block, finder);
|
||||
processBlock(block, finder, R);
|
||||
});
|
||||
processPills(ctx.root, finder, options.finder.pillSelector);
|
||||
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
||||
},
|
||||
revert(ctx) {
|
||||
revertPass(PASS4, ctx.root);
|
||||
@@ -4154,6 +4255,7 @@ var Juzhen = (() => {
|
||||
...langOpt.style ? { style: langOpt.style } : {},
|
||||
policy: langOpt.policy || {}
|
||||
},
|
||||
ruleset: resolveRuleset(opts.charClass),
|
||||
autospaceClass: opts.autospaceClass || "",
|
||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||
scope: {
|
||||
|
||||
+15
-2
@@ -25,10 +25,17 @@
|
||||
<!-- BODY -->
|
||||
<p class="lede">聚珍(Juzhen)/cjk-autospace 變更紀錄。<a href="./index.html">← 回文檔</a></p>
|
||||
|
||||
<h2>標點挤压一致性 <span class="badge accent">2026-06-04</span></h2>
|
||||
<h3>新增/修復</h3>
|
||||
<ul>
|
||||
<li><strong>全角句點 <code>.</code> 納入句末標點</strong>:<code>BD_STOP</code> 由 <code>。!?</code> 擴為 <code>。.!?</code>——納入全角句點 <code>.</code>(U+FF0E,異於表意句號 <code>。</code> U+3002)。實測 Noto CJK SC/TC 之 <code>halt</code> 皆覆蓋、挤压收半形 advance 0.5em,與 <code>。</code> 一致;<code>inkBias</code> 同步補(簡 left/繁 center)。政策行為同 <code>。!?</code>:<strong>全角式與開明式句末保持全角 1em</strong>,banjiao(或鄰接被收)挤半。設計原則:<code>halt</code> 即字型之「挤压意願」,字型不提供某標點 <code>halt</code>(字形占滿、不宜挤)則信任字型、保持全角不強挤。</li>
|
||||
<li><strong>連續標點挤压規則:雙側獨立 + 透明穿越</strong>(修正既有不一致)。規則:相鄰兩標點,<strong>對每一標點</strong>獨立判斷——若其「有空白之側」緊鄰另一標點即收半。原為「二選一只收一側」,致 <code>,「</code> 全角式誤為 1.5em;修後 <code>,「</code> <strong>全角式與開明式皆 1em</strong>、<code>。」</code> <strong>全角式 1.5em/開明式 1em</strong>、<code>·—</code> 兩式皆 2em(不宜挤)。並改「同 parent 直接兄弟」掃描為<strong>從 root 之透明遍歷</strong>(穿越 <code>strong</code>/<code>em</code> 等行內邊界、block/avoid 斷相鄰),使跨 inline 邊界之連續標點亦正確配對(原漏致達 2em)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>字符分類規則集 <span class="badge accent">2026-06-04</span></h2>
|
||||
<h3>新增</h3>
|
||||
<ul>
|
||||
<li><strong>可配置字符分類(<code>charClass</code>)</strong>:把散落各 pass 之硬編碼分類(中文 CJK/西文 ANS/全寬標點 BD_* 子類)統一抽象為<strong>規則集</strong>,內置默認即現狀值,消費端經 <code>options.charClass</code>(<code>{ cjk?, western?, biaodian?:{stop,pause,open,close,middle,liga} }</code>)<strong>追加擴充</strong>,覆寫 <code>spacing</code>/<code>jiya</code>/<code>jinze</code> 之分類判定。動機:英文全角句號 <code>.</code>(U+FF0E,異於中文句號 <code>。</code>)實證為未分類——不 charify/不挤/不補隙;以 <code>charClass:{biaodian:{stop:"."}}</code> 即比照 <code>。</code>。<strong>向下兼容</strong>:不傳則行為與內置完全一致、各實例隔離(覆寫不污染默認)。v1 為追加語義(不重分類/移除內置);結構性 ASCII 不開放。<code>spacing</code> 之間隙正則改由規則集 per-instance 編譯。測試 66 通過(含 9 條 charClass)。</li>
|
||||
<li><strong>可配置字符分類(<code>charClass</code>)</strong>:把散落各 pass 之硬編碼分類(中文 CJK/西文 ANS/全寬標點 BD_* 子類)統一抽象為<strong>規則集</strong>,內置默認即現狀值,消費端經 <code>options.charClass</code>(<code>{ cjk?, western?, biaodian?:{stop,pause,open,close,middle,liga} }</code>)<strong>追加擴充</strong>,覆寫 <code>spacing</code>/<code>jiya</code>/<code>jinze</code> 之分類判定。動機:未分類符號(如二點省略 <code>‥</code> U+2025)——不 charify/不挤;以 <code>charClass:{biaodian:{stop:"‥"}}</code> 即可歸入對應類。<strong>向下兼容</strong>:不傳則行為與內置完全一致、各實例隔離(覆寫不污染默認)。v1 為追加語義(不重分類/移除內置);結構性 ASCII 不開放。<code>spacing</code> 之間隙正則改由規則集 per-instance 編譯。測試 66 通過(含 9 條 charClass)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>下游回饋修復批次 <span class="badge accent">2026-06-03</span></h2>
|
||||
@@ -118,10 +125,16 @@
|
||||
<script type="text/markdown" id="md-source">
|
||||
# 變更紀錄(Changelog)— 聚珍(Juzhen)/cjk-autospace
|
||||
|
||||
## 標點挤压一致性(2026-06-04)
|
||||
|
||||
### 新增/修復
|
||||
- 全角句點 . 納入句末標點:BD_STOP 由 。!? 擴為 。.!?(. U+FF0E 異於 。 U+3002)。Noto CJK 有 halt、挤压收 0.5em,與 。一致;inkBias 補(簡 left/繁 center)。政策同 。!?:全角式與開明式句末保持全角 1em,banjiao 挤半。原則:halt 即字型挤压意願,字型不給某標點 halt(不宜挤)則信任字型不強挤。
|
||||
- 連續標點挤压:雙側獨立 + 透明穿越。規則:相鄰兩標點,對每一標點獨立判斷,若其有空白之側緊鄰另一標點即收半。修後 ,「 全角式與開明式皆 1em、。」 全角式 1.5em/開明式 1em、·— 兩式皆 2em。並改同 parent 掃描為從 root 透明遍歷,跨 strong/em 邊界之連續標點亦正確挤(原漏致 2em)。
|
||||
|
||||
## 字符分類規則集(2026-06-04)
|
||||
|
||||
### 新增
|
||||
- 可配置字符分類 charClass:硬編碼分類(中文/西文/全寬標點子類)統一抽象為規則集,內置默認,消費端經 options.charClass(cjk/western/biaodian{stop,pause,open,close,middle,liga})追加擴充,覆寫 spacing/jiya/jinze 判定。例:英文全角句號 .(U+FF0E)預設未分類,以 biaodian.stop 歸全寬句末標點即比照 。。向下兼容(不傳則行為不變、實例隔離);v1 追加語義、不重分類內置;結構性 ASCII 不開放。spacing 間隙正則改由規則集 per-instance 編譯。測試 66 通過。
|
||||
- 可配置字符分類 charClass:硬編碼分類(中文/西文/全寬標點子類)統一抽象為規則集,內置默認,消費端經 options.charClass(cjk/western/biaodian{stop,pause,open,close,middle,liga})追加擴充,覆寫 spacing/jiya/jinze 判定。例:未分類符號(如 ‥ U+2025)可經 biaodian.stop 歸全寬句末標點。向下兼容(不傳則行為不變、實例隔離);v1 追加語義、不重分類內置;結構性 ASCII 不開放。spacing 間隙正則改由規則集 per-instance 編譯。測試 66 通過。
|
||||
|
||||
## 下游回饋修復批次(2026-06-03)
|
||||
|
||||
|
||||
+222
-130
@@ -1,4 +1,4 @@
|
||||
<!-- built with theme=dracula, mode=report, at=2026-06-04T08:24:26Z -->
|
||||
<!-- built with theme=dracula, mode=report, at=2026-06-09T07:22:49Z -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
@@ -92,9 +92,11 @@
|
||||
:root
|
||||
{
|
||||
/* Sans = Orbit Gothic CJK SC → Orbit Gothic (Latin) → sans-serif.
|
||||
CJK SC is the only CJK variant string we declare — Orbit Gothic
|
||||
carries the full pan-CJK set in one file and selects SC / TC /
|
||||
JP / KR glyph shapes via OpenType `locl` based on `<html lang>`. */
|
||||
The regional glyph cut is selected by NAME per `<html lang>` in the
|
||||
`:lang()` blocks below (SC default; TC / JP / KR overrides) — NOT via
|
||||
OpenType `locl`, which Juzhen v2 can defeat by splitting runs into
|
||||
per-glyph atoms. No Noto fallback: Orbit Gothic CJK already covers the
|
||||
same range, so a Noto entry would never be reached. */
|
||||
--font-sans: "Orbit Gothic CJK SC", "Orbit Gothic", sans-serif;
|
||||
|
||||
/* Alucard (light) — palette.md */
|
||||
@@ -131,6 +133,26 @@
|
||||
--del-bg: color-mix(in srgb, var(--error) 10%, transparent);
|
||||
}
|
||||
|
||||
/* Region-matched sans cut by NAME (locl-independent — see note above).
|
||||
Mirrors tokens.css's per-locale --font-serif reordering. The matching
|
||||
Orbit Gothic CJK cut leads; Orbit Gothic (Latin) then generic sans
|
||||
follow. (--font-serif / --font-mono are reordered in tokens.css.) */
|
||||
:root:lang(zh-TW),
|
||||
:root:lang(zh-Hant),
|
||||
:root:lang(zh-HK),
|
||||
:root:lang(zh-MO)
|
||||
{
|
||||
--font-sans: "Orbit Gothic CJK TC", "Orbit Gothic", sans-serif;
|
||||
}
|
||||
:root:lang(ja)
|
||||
{
|
||||
--font-sans: "Orbit Gothic CJK JP", "Orbit Gothic", sans-serif;
|
||||
}
|
||||
:root:lang(ko)
|
||||
{
|
||||
--font-sans: "Orbit Gothic CJK KR", "Orbit Gothic", sans-serif;
|
||||
}
|
||||
|
||||
/* === Pro (dark) =====================================================
|
||||
Applied when OS prefers dark AND mode isn't manually pinned to
|
||||
light. The `:not()` chain lets a manually-set light mode survive
|
||||
@@ -2576,7 +2598,8 @@ 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> 後備後,同句行寬 <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.5em</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> 等行內邊界。</div>
|
||||
|
||||
<h3>激活與粒度</h3>
|
||||
<p>模式由 <strong><code>data-jz-halfwidth</code> 屬性所在子樹</strong>決定(CSS 後代選擇器),故粒度極靈活:</p>
|
||||
@@ -2711,6 +2734,8 @@ JS 只注入 `jz-*` 結構,所有視覺尺寸在 juzhen.css,缺一不可。
|
||||
- margin(`jiya: { halfWidth: "margin" }`):width:0.5em+字身依墨色偏側溢出定位,以 CSS 重現 halt;供缺 halt 字型(如方正書宋 GBK,僅含 vert)。
|
||||
- 模式由 `data-jz-halfwidth` 子樹屬性決定,可逐區塊/lang。
|
||||
- 偏側 inkBias 給 left/center/right/full,可 `jiya.bias` 逐 lang 逐字覆寫。
|
||||
- 句末點號 。.!?(含全角句點 . U+FF0E,異於 。 U+3002):全角式與開明式句末皆保持全角 1em,banjiao 挤半。
|
||||
- 連續標點:對每一標點獨立判斷,若其有空白之側緊鄰另一標點即收半。,「 全角式與開明式皆 1em;。」 全角式 1.5em/開明式 1em;透明穿越 strong/em 等行內邊界。
|
||||
|
||||
### 字型適配工作流
|
||||
|
||||
@@ -3255,25 +3280,59 @@ var Juzhen = (() => {
|
||||
var WEST_SHARED = "0-9\\$%\\^&\\*\\-\\+\\\\=\\u0370-\\u03ff\\u0100-\\u024f\\u0400-\\u04ff\\u00a1-\\u00b6\\u00b8-\\u00ff\\u20a0-\\u20bf\\u2150-\\u21ff\\u2200-\\u22ff\\u2700-\\u27bf";
|
||||
var ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
|
||||
var ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
|
||||
var BD_STOP = "\u3002\uFF01\uFF1F";
|
||||
var BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
|
||||
var BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
||||
var BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
|
||||
var BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
|
||||
var BD_MIDDLE = "\xB7\u30FB\u2027";
|
||||
var BD_LIGA = "\u2026\u2014";
|
||||
var BD_ALL = BD_STOP + BD_PAUSE + BD_OPEN + BD_CLOSE + BD_MIDDLE + BD_LIGA;
|
||||
var BD_SETS = [
|
||||
["bd-stop", BD_STOP],
|
||||
["bd-pause", BD_PAUSE],
|
||||
["bd-open", BD_OPEN],
|
||||
["bd-close", BD_CLOSE],
|
||||
["bd-middle", BD_MIDDLE],
|
||||
["bd-liga", BD_LIGA]
|
||||
var BD_ORDER = [
|
||||
"stop",
|
||||
"pause",
|
||||
"open",
|
||||
"close",
|
||||
"middle",
|
||||
"liga"
|
||||
];
|
||||
function classifyBiaodian(ch) {
|
||||
for (const [name, set] of BD_SETS) {
|
||||
if (set.indexOf(ch) >= 0) {
|
||||
return name;
|
||||
var DEFAULT_RULESET = {
|
||||
cjk: CJK,
|
||||
ansAfter: ANS_AFTER_CJK,
|
||||
ansBefore: ANS_BEFORE_CJK,
|
||||
biaodian: {
|
||||
stop: BD_STOP,
|
||||
pause: BD_PAUSE,
|
||||
open: BD_OPEN,
|
||||
close: BD_CLOSE,
|
||||
middle: BD_MIDDLE,
|
||||
liga: BD_LIGA
|
||||
}
|
||||
};
|
||||
function resolveRuleset(override) {
|
||||
if (!override) {
|
||||
return DEFAULT_RULESET;
|
||||
}
|
||||
const west = override.western || "";
|
||||
const bd = { ...DEFAULT_RULESET.biaodian };
|
||||
if (override.biaodian) {
|
||||
for (const sub of BD_ORDER) {
|
||||
const extra = override.biaodian[sub];
|
||||
if (extra) {
|
||||
bd[sub] = bd[sub] + extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
cjk: CJK + (override.cjk || ""),
|
||||
ansAfter: ANS_AFTER_CJK + west,
|
||||
ansBefore: ANS_BEFORE_CJK + west,
|
||||
biaodian: bd
|
||||
};
|
||||
}
|
||||
function classifyBiaodian(ch, biaodian = DEFAULT_RULESET.biaodian) {
|
||||
for (const sub of BD_ORDER) {
|
||||
if (biaodian[sub].indexOf(ch) >= 0) {
|
||||
return "bd-" + sub;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -3309,6 +3368,7 @@ var Juzhen = (() => {
|
||||
"\uFF0C": "left",
|
||||
"\u3001": "left",
|
||||
"\u3002": "left",
|
||||
"\uFF0E": "left",
|
||||
"\uFF1B": "left",
|
||||
"\uFF1A": "left",
|
||||
"\uFF01": "left",
|
||||
@@ -3318,6 +3378,7 @@ var Juzhen = (() => {
|
||||
"\uFF0C": "center",
|
||||
"\u3001": "center",
|
||||
"\u3002": "center",
|
||||
"\uFF0E": "center",
|
||||
"\uFF1B": "center",
|
||||
"\uFF1A": "center",
|
||||
"\uFF01": "center",
|
||||
@@ -3382,9 +3443,10 @@ var Juzhen = (() => {
|
||||
let style = null;
|
||||
let langClass = null;
|
||||
const biasOverrides = ctx.options.jiyaConfig.bias;
|
||||
const biaodian = ctx.options.ruleset.biaodian;
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const ch = data.charAt(i);
|
||||
const bdClass = classifyBiaodian(ch);
|
||||
const bdClass = classifyBiaodian(ch, biaodian);
|
||||
if (!bdClass) {
|
||||
continue;
|
||||
}
|
||||
@@ -3423,33 +3485,52 @@ var Juzhen = (() => {
|
||||
order: 25,
|
||||
enabled: (o) => o.features.jiya,
|
||||
render(ctx) {
|
||||
const { finder } = ctx;
|
||||
const parents = /* @__PURE__ */ new Set();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||
if (c.parentNode) {
|
||||
parents.add(c.parentNode);
|
||||
const { finder, options } = ctx;
|
||||
const blockSet = options.finder.blockTags;
|
||||
let prev = null;
|
||||
const squeeze = (L, R) => {
|
||||
if (!finder.featureEnabledFor(L, PASS)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
let child = parent.firstChild;
|
||||
while (child) {
|
||||
const next = child.nextSibling;
|
||||
if (child.nodeType === 1 && child.nodeName === "JZ-CHAR" && next && next.nodeType === 1 && next.nodeName === "JZ-CHAR") {
|
||||
const L = child;
|
||||
const R = next;
|
||||
if (finder.featureEnabledFor(L, PASS)) {
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
} else if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
const lSides = blankSides(bdClassOf(L));
|
||||
const rSides = blankSides(bdClassOf(R));
|
||||
if (lSides.right) {
|
||||
L.classList.add("jz-half");
|
||||
}
|
||||
if (rSides.left) {
|
||||
R.classList.add("jz-half");
|
||||
}
|
||||
};
|
||||
const visit = (node) => {
|
||||
for (let c = node.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3) {
|
||||
if (c.data.length > 0) {
|
||||
prev = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c.nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
const el = c;
|
||||
const nm = el.nodeName;
|
||||
if (nm === "JZ-CHAR") {
|
||||
if (prev) {
|
||||
squeeze(prev, el);
|
||||
}
|
||||
prev = el;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else {
|
||||
visit(el);
|
||||
}
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(ctx.root);
|
||||
},
|
||||
revert() {
|
||||
}
|
||||
@@ -3477,46 +3558,46 @@ var Juzhen = (() => {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isTokenChar(ch) {
|
||||
return !ANY_CJK.test(ch) && !/\s/.test(ch);
|
||||
function isTokenChar(ch, anyCjk) {
|
||||
return !anyCjk.test(ch) && !/\s/.test(ch);
|
||||
}
|
||||
function leadingTokenEnd(d) {
|
||||
if (ANY_CJK.test(d.charAt(0))) {
|
||||
function leadingTokenEnd(d, anyCjk) {
|
||||
if (anyCjk.test(d.charAt(0))) {
|
||||
return 1;
|
||||
}
|
||||
let e = 1;
|
||||
while (e < d.length && isTokenChar(d.charAt(e))) {
|
||||
while (e < d.length && isTokenChar(d.charAt(e), anyCjk)) {
|
||||
e += 1;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
function trailingTokenStart(d) {
|
||||
function trailingTokenStart(d, anyCjk) {
|
||||
const last = d.length - 1;
|
||||
if (ANY_CJK.test(d.charAt(last))) {
|
||||
if (anyCjk.test(d.charAt(last))) {
|
||||
return last;
|
||||
}
|
||||
let s = last;
|
||||
while (s > 0 && isTokenChar(d.charAt(s - 1))) {
|
||||
while (s > 0 && isTokenChar(d.charAt(s - 1), anyCjk)) {
|
||||
s -= 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function splitBoundaries(t, needFirst, needLast) {
|
||||
function splitBoundaries(t, needFirst, needLast, anyCjk) {
|
||||
let node = t;
|
||||
if (needFirst) {
|
||||
const e = leadingTokenEnd(node.data);
|
||||
const e = leadingTokenEnd(node.data, anyCjk);
|
||||
if (e < node.data.length) {
|
||||
node = node.splitText(e);
|
||||
}
|
||||
}
|
||||
if (needLast) {
|
||||
const s = trailingTokenStart(node.data);
|
||||
const s = trailingTokenStart(node.data, anyCjk);
|
||||
if (s > 0) {
|
||||
node.splitText(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
function processParent(parent, finder) {
|
||||
function processParent(parent, finder, anyCjk) {
|
||||
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 1 && c.nodeName === "JZ-JINZE") {
|
||||
return;
|
||||
@@ -3546,7 +3627,7 @@ var Juzhen = (() => {
|
||||
const needFirst = i2 > 0 && forbidden[i2 - 1];
|
||||
const needLast = i2 < n - 1 && forbidden[i2];
|
||||
if (needFirst || needLast) {
|
||||
splitBoundaries(u, needFirst, needLast);
|
||||
splitBoundaries(u, needFirst, needLast, anyCjk);
|
||||
}
|
||||
}
|
||||
const kids = [];
|
||||
@@ -3576,6 +3657,7 @@ var Juzhen = (() => {
|
||||
order: 40,
|
||||
enabled: (o) => o.features.jinze,
|
||||
render(ctx) {
|
||||
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
|
||||
const parents = /* @__PURE__ */ new Set();
|
||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||
if (c.parentNode) {
|
||||
@@ -3583,7 +3665,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
});
|
||||
for (const parent of parents) {
|
||||
processParent(parent, ctx.finder);
|
||||
processParent(parent, ctx.finder, anyCjk);
|
||||
}
|
||||
},
|
||||
revert(ctx) {
|
||||
@@ -3829,79 +3911,88 @@ var Juzhen = (() => {
|
||||
var LBR_EXT = "\\(\\[\\{<>";
|
||||
var RBR_EXT = "\\)\\]\\}<>";
|
||||
var DEL = String.fromCharCode(57347);
|
||||
var STRIP_CJK_SPACE_ANS = new RegExp("([" + CJK + "]) ([" + ANS_AFTER_CJK + "])", "g");
|
||||
var STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + CJK + "])", "g");
|
||||
var STRIP_CJK_SPACE_CJK = new RegExp("([" + CJK + "]) (?=[" + CJK + "])", "g");
|
||||
var DOTS_CJK = new RegExp("([\\.]{2,})([" + CJK + "])", "g");
|
||||
var CJK_PUNCTUATION = new RegExp("([" + CJK + "])([!;,\\?:]+)(?=[" + CJK + ALNUM + "])", "g");
|
||||
var AN_PUNCTUATION_CJK = new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK + "])", "g");
|
||||
var CJK_TILDE = new RegExp("([" + CJK + "])(~+)(?!=)(?=[" + CJK + ALNUM + "])", "g");
|
||||
var CJK_TILDE_EQUALS = new RegExp("([" + CJK + "])(~=)", "g");
|
||||
var CJK_PERIOD = new RegExp("([" + CJK + "])(\\.)(?![" + ALNUM + "\\./])(?=[" + CJK + ALNUM + "])", "g");
|
||||
var AN_PERIOD_CJK = new RegExp("([" + ALNUM + "])(\\.)([" + CJK + "])", "g");
|
||||
var AN_COLON_CJK = new RegExp("([" + ALNUM + "])(:)([" + CJK + "])", "g");
|
||||
var CJK_QUOTE = new RegExp("([" + CJK + "])([" + QUOTES_CLS + "])", "g");
|
||||
var QUOTE_CJK = new RegExp("([" + QUOTES_CLS + "])([" + CJK + "])", "g");
|
||||
var CJK_QUOTE_AN = new RegExp("([" + CJK + '])(")([' + ALNUM + "])", "g");
|
||||
var CJK_HASH = new RegExp("([" + CJK + "])(#([^ ]))", "g");
|
||||
var HASH_CJK = new RegExp("(([^ ])#)([" + CJK + "])", "g");
|
||||
var SINGLE_LETTER_GRADE = new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])([" + CJK + "])", "g");
|
||||
var CJK_OPERATOR_ANS = new RegExp("([" + CJK + "])([" + OPS_HYPHEN + "])([" + ALNUM + "])", "g");
|
||||
var ANS_OPERATOR_CJK = new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])([" + CJK + "])", "g");
|
||||
var CJK_LESS_THAN = new RegExp("([" + CJK + "])(<)([" + ALNUM + "])", "g");
|
||||
var LESS_THAN_CJK = new RegExp("([" + ALNUM + "])(<)([" + CJK + "])", "g");
|
||||
var CJK_GREATER_THAN = new RegExp("([" + CJK + "])(>)([" + ALNUM + "])", "g");
|
||||
var GREATER_THAN_CJK = new RegExp("([" + ALNUM + "])(>)([" + CJK + "])", "g");
|
||||
var CJK_LEFT_BRACKET = new RegExp("([" + CJK + "])([" + LBR_EXT + "])", "g");
|
||||
var RIGHT_BRACKET_CJK = new RegExp("([" + RBR_EXT + "])([" + CJK + "])", "g");
|
||||
var AN_LEFT_BRACKET = new RegExp("([" + ALNUM + "])(?<!\\.[" + ALNUM + "]*)([" + LBR_BASIC + "])", "g");
|
||||
var RIGHT_BRACKET_AN = new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g");
|
||||
var CJK_ANS = new RegExp("([" + CJK + "])([" + ANS_AFTER_CJK + "])", "g");
|
||||
var ANS_CJK = new RegExp("([" + ANS_BEFORE_CJK + "])([" + CJK + "])", "g");
|
||||
var S_A = new RegExp("(%)([" + ALPHA + "])", "g");
|
||||
function makeRules(rs) {
|
||||
const CJK2 = rs.cjk;
|
||||
const ANS_AFTER = rs.ansAfter;
|
||||
const ANS_BEFORE = rs.ansBefore;
|
||||
return {
|
||||
anyCjk: new RegExp("[" + CJK2 + "]"),
|
||||
pillNeighbor: new RegExp("[" + ALNUM + CJK2 + "]"),
|
||||
STRIP_CJK_SPACE_ANS: new RegExp("([" + CJK2 + "]) ([" + ANS_AFTER + "])", "g"),
|
||||
STRIP_ANS_SPACE_CJK: new RegExp("([" + ANS_BEFORE + "]) ([" + CJK2 + "])", "g"),
|
||||
STRIP_CJK_SPACE_CJK: new RegExp("([" + CJK2 + "]) (?=[" + CJK2 + "])", "g"),
|
||||
DOTS_CJK: new RegExp("([\\.]{2,})([" + CJK2 + "])", "g"),
|
||||
CJK_PUNCTUATION: new RegExp("([" + CJK2 + "])([!;,\\?:]+)(?=[" + CJK2 + ALNUM + "])", "g"),
|
||||
AN_PUNCTUATION_CJK: new RegExp("([" + ALNUM + "])([!;,\\?]+)([" + CJK2 + "])", "g"),
|
||||
CJK_TILDE: new RegExp("([" + CJK2 + "])(~+)(?!=)(?=[" + CJK2 + ALNUM + "])", "g"),
|
||||
CJK_TILDE_EQUALS: new RegExp("([" + CJK2 + "])(~=)", "g"),
|
||||
CJK_PERIOD: new RegExp("([" + CJK2 + "])(\\.)(?![" + ALNUM + "\\./])(?=[" + CJK2 + ALNUM + "])", "g"),
|
||||
AN_PERIOD_CJK: new RegExp("([" + ALNUM + "])(\\.)([" + CJK2 + "])", "g"),
|
||||
AN_COLON_CJK: new RegExp("([" + ALNUM + "])(:)([" + CJK2 + "])", "g"),
|
||||
CJK_QUOTE: new RegExp("([" + CJK2 + "])([" + QUOTES_CLS + "])", "g"),
|
||||
QUOTE_CJK: new RegExp("([" + QUOTES_CLS + "])([" + CJK2 + "])", "g"),
|
||||
CJK_QUOTE_AN: new RegExp("([" + CJK2 + '])(")([' + ALNUM + "])", "g"),
|
||||
CJK_HASH: new RegExp("([" + CJK2 + "])(#([^ ]))", "g"),
|
||||
HASH_CJK: new RegExp("(([^ ])#)([" + CJK2 + "])", "g"),
|
||||
SINGLE_LETTER_GRADE: new RegExp("\\b([" + ALPHA + "])([" + OPS_GRADE + "])([" + CJK2 + "])", "g"),
|
||||
CJK_OPERATOR_ANS: new RegExp("([" + CJK2 + "])([" + OPS_HYPHEN + "])([" + ALNUM + "])", "g"),
|
||||
ANS_OPERATOR_CJK: new RegExp("([" + ALNUM + "])([" + OPS_HYPHEN + "])([" + CJK2 + "])", "g"),
|
||||
CJK_LESS_THAN: new RegExp("([" + CJK2 + "])(<)([" + ALNUM + "])", "g"),
|
||||
LESS_THAN_CJK: new RegExp("([" + ALNUM + "])(<)([" + CJK2 + "])", "g"),
|
||||
CJK_GREATER_THAN: new RegExp("([" + CJK2 + "])(>)([" + ALNUM + "])", "g"),
|
||||
GREATER_THAN_CJK: new RegExp("([" + ALNUM + "])(>)([" + CJK2 + "])", "g"),
|
||||
CJK_LEFT_BRACKET: new RegExp("([" + CJK2 + "])([" + LBR_EXT + "])", "g"),
|
||||
RIGHT_BRACKET_CJK: new RegExp("([" + RBR_EXT + "])([" + CJK2 + "])", "g"),
|
||||
AN_LEFT_BRACKET: new RegExp("([" + ALNUM + "])(?<!\\.[" + ALNUM + "]*)([" + LBR_BASIC + "])", "g"),
|
||||
RIGHT_BRACKET_AN: new RegExp("([" + RBR_BASIC + "])([" + ALNUM + "])", "g"),
|
||||
CJK_ANS: new RegExp("([" + CJK2 + "])([" + ANS_AFTER + "])", "g"),
|
||||
ANS_CJK: new RegExp("([" + ANS_BEFORE + "])([" + CJK2 + "])", "g"),
|
||||
S_A: new RegExp("(%)([" + ALPHA + "])", "g")
|
||||
};
|
||||
}
|
||||
var COMPOUND_WORD = /\b(?:[A-Za-z0-9]*[a-z][A-Za-z0-9]*-[A-Za-z0-9]+|[A-Za-z0-9]+-[A-Za-z0-9]*[a-z][A-Za-z0-9]*|[A-Za-z]+-[0-9]+|[A-Za-z]+[0-9]+-[A-Za-z0-9]+)(?:-[A-Za-z0-9]+)*\b/g;
|
||||
var PH_PATTERN = new RegExp(PH_OPEN + "(\\d+)" + PH_CLOSE, "g");
|
||||
function panguSpace(text) {
|
||||
if (text.length <= 1 || !ANY_CJK.test(text)) {
|
||||
function panguSpace(text, R) {
|
||||
if (text.length <= 1 || !R.anyCjk.test(text)) {
|
||||
return text;
|
||||
}
|
||||
let s = text;
|
||||
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
||||
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
||||
s = s.replace(STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||||
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
||||
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_TILDE, "$1$2" + MARK);
|
||||
s = s.replace(CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
|
||||
s = s.replace(CJK_PERIOD, "$1$2" + MARK);
|
||||
s = s.replace(AN_PERIOD_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(AN_COLON_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_QUOTE, "$1" + MARK + "$2");
|
||||
s = s.replace(QUOTE_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_HASH, "$1" + MARK + "$2");
|
||||
s = s.replace(HASH_CJK, "$1" + MARK + "$3");
|
||||
s = s.replace(R.STRIP_CJK_SPACE_ANS, "$1$2");
|
||||
s = s.replace(R.STRIP_ANS_SPACE_CJK, "$1$2");
|
||||
s = s.replace(R.STRIP_CJK_SPACE_CJK, "$1" + DEL);
|
||||
s = s.replace(R.DOTS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_PUNCTUATION, "$1$2" + MARK);
|
||||
s = s.replace(R.AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_TILDE, "$1$2" + MARK);
|
||||
s = s.replace(R.CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
|
||||
s = s.replace(R.CJK_PERIOD, "$1$2" + MARK);
|
||||
s = s.replace(R.AN_PERIOD_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.AN_COLON_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_QUOTE, "$1" + MARK + "$2");
|
||||
s = s.replace(R.QUOTE_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_HASH, "$1" + MARK + "$2");
|
||||
s = s.replace(R.HASH_CJK, "$1" + MARK + "$3");
|
||||
const compounds = [];
|
||||
s = s.replace(COMPOUND_WORD, (m) => {
|
||||
compounds.push(m);
|
||||
return PH_OPEN + (compounds.length - 1) + PH_CLOSE;
|
||||
});
|
||||
s = s.replace(SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(R.GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(PH_PATTERN, (_m, idx) => compounds[Number(idx)] || "");
|
||||
s = s.replace(CJK_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(AN_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(RIGHT_BRACKET_AN, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_ANS, "$1" + MARK + "$2");
|
||||
s = s.replace(ANS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(S_A, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(R.RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.AN_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(R.RIGHT_BRACKET_AN, "$1" + MARK + "$2");
|
||||
s = s.replace(R.CJK_ANS, "$1" + MARK + "$2");
|
||||
s = s.replace(R.ANS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(R.S_A, "$1" + MARK + "$2");
|
||||
return s;
|
||||
}
|
||||
function wrapNodeInGap(node) {
|
||||
@@ -3926,11 +4017,11 @@ var Juzhen = (() => {
|
||||
}
|
||||
wrapNodeInGap(tn);
|
||||
}
|
||||
function processBlock(block, finder) {
|
||||
function processBlock(block, finder, R) {
|
||||
const runs = finder.collectRuns(block);
|
||||
for (const run of runs) {
|
||||
const { text, map } = run;
|
||||
const spaced = panguSpace(text);
|
||||
const spaced = panguSpace(text, R);
|
||||
if (spaced === text) {
|
||||
continue;
|
||||
}
|
||||
@@ -3971,8 +4062,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
var PILL_NEIGHBOR = new RegExp("[A-Za-z0-9" + CJK + "]");
|
||||
function processPills(root, finder, pillSelector) {
|
||||
function processPills(root, finder, pillSelector, pillNeighbor) {
|
||||
root.querySelectorAll(pillSelector).forEach((pill) => {
|
||||
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
@@ -3982,36 +4072,36 @@ var Juzhen = (() => {
|
||||
}
|
||||
const prev = finder.adjacentLogicalChar(pill, -1);
|
||||
if (prev) {
|
||||
handlePillSide(prev, pill, -1);
|
||||
handlePillSide(prev, pill, -1, pillNeighbor);
|
||||
}
|
||||
const next = finder.adjacentLogicalChar(pill, 1);
|
||||
if (next) {
|
||||
handlePillSide(next, pill, 1);
|
||||
handlePillSide(next, pill, 1, pillNeighbor);
|
||||
}
|
||||
});
|
||||
}
|
||||
function handlePillSide(adj, pill, dir) {
|
||||
function handlePillSide(adj, pill, dir, pillNeighbor) {
|
||||
if (dir === -1) {
|
||||
if (PILL_NEIGHBOR.test(adj.ch)) {
|
||||
if (pillNeighbor.test(adj.ch)) {
|
||||
wrapGapLeft({ textNode: adj.textNode, offset: adj.offset });
|
||||
return;
|
||||
}
|
||||
if (adj.ch === " " && adj.offset >= 1) {
|
||||
const tn = adj.textNode;
|
||||
const bc = tn.data.charAt(adj.offset - 1);
|
||||
if (bc !== " " && PILL_NEIGHBOR.test(bc)) {
|
||||
if (bc !== " " && pillNeighbor.test(bc)) {
|
||||
tn.data = tn.data.slice(0, adj.offset) + tn.data.slice(adj.offset + 1);
|
||||
wrapGapLeft({ textNode: tn, offset: adj.offset - 1 });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (PILL_NEIGHBOR.test(adj.ch)) {
|
||||
if (pillNeighbor.test(adj.ch)) {
|
||||
wrapNodeInGap(pill);
|
||||
return;
|
||||
}
|
||||
if (adj.ch === " " && adj.textNode.data.length >= 2) {
|
||||
const ac = adj.textNode.data.charAt(1);
|
||||
if (ac !== " " && PILL_NEIGHBOR.test(ac)) {
|
||||
if (ac !== " " && pillNeighbor.test(ac)) {
|
||||
adj.textNode.data = adj.textNode.data.slice(1);
|
||||
wrapNodeInGap(pill);
|
||||
}
|
||||
@@ -4025,13 +4115,14 @@ var Juzhen = (() => {
|
||||
enabled: (o) => o.features.spacing,
|
||||
render(ctx) {
|
||||
const { finder, options } = ctx;
|
||||
const R = makeRules(options.ruleset);
|
||||
ctx.finder.eachBlock(ctx.root, (block) => {
|
||||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||||
return;
|
||||
}
|
||||
processBlock(block, finder);
|
||||
processBlock(block, finder, R);
|
||||
});
|
||||
processPills(ctx.root, finder, options.finder.pillSelector);
|
||||
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
||||
},
|
||||
revert(ctx) {
|
||||
revertPass(PASS4, ctx.root);
|
||||
@@ -4222,6 +4313,7 @@ var Juzhen = (() => {
|
||||
...langOpt.style ? { style: langOpt.style } : {},
|
||||
policy: langOpt.policy || {}
|
||||
},
|
||||
ruleset: resolveRuleset(opts.charClass),
|
||||
autospaceClass: opts.autospaceClass || "",
|
||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||
scope: {
|
||||
|
||||
+4
-1
@@ -92,7 +92,8 @@ 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> 後備後,同句行寬 <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.5em</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> 等行內邊界。</div>
|
||||
|
||||
<h3>激活與粒度</h3>
|
||||
<p>模式由 <strong><code>data-jz-halfwidth</code> 屬性所在子樹</strong>決定(CSS 後代選擇器),故粒度極靈活:</p>
|
||||
@@ -211,6 +212,8 @@ JS 只注入 `jz-*` 結構,所有視覺尺寸在 juzhen.css,缺一不可。
|
||||
- margin(`jiya: { halfWidth: "margin" }`):width:0.5em+字身依墨色偏側溢出定位,以 CSS 重現 halt;供缺 halt 字型(如方正書宋 GBK,僅含 vert)。
|
||||
- 模式由 `data-jz-halfwidth` 子樹屬性決定,可逐區塊/lang。
|
||||
- 偏側 inkBias 給 left/center/right/full,可 `jiya.bias` 逐 lang 逐字覆寫。
|
||||
- 句末點號 。.!?(含全角句點 . U+FF0E,異於 。 U+3002):全角式與開明式句末皆保持全角 1em,banjiao 挤半。
|
||||
- 連續標點:對每一標點獨立判斷,若其有空白之側緊鄰另一標點即收半。,「 全角式與開明式皆 1em;。」 全角式 1.5em/開明式 1em;透明穿越 strong/em 等行內邊界。
|
||||
|
||||
### 字型適配工作流
|
||||
|
||||
|
||||
Reference in New Issue
Block a user