30a11b5d6e
下游審計(4 路並行測繪 + 逐條源碼複驗)之階段一:落地已複驗確認之缺陷與低風險 清理;架構統一(透明判定單一真相源)與文檔重整留待階段二/三。 修復(複驗確認): ・jinze 二次 render(未 revert)產生巢狀 jz-jinze、違反 I6 冪等——守衛改判 parent 自身即本 pass 產物(isJz(parent, PASS)),已綁定內容不再重包(實測 jz-jinze 3→6 已修)。 ・processPills 漏檢 avoidsSelf——自身命中 avoid(如 <code data-jz-skip>)之 pill 仍被補 間隙(isAvoided 只查祖先、不含自身,同「下游問題六」型)。processPills + root 自處理 分支補 avoidsSelf 檢查。 ・三分類互斥:CODE/KBD 自 DEFAULT_SKIP_TAGS 移除——二者已由預設 pillSelector "code, kbd" 以 pill 機制涵蓋(內部不可見+兩端補隙+斷 run/相鄰),再列 skipTags 致 同元素既 pill 又 avoid、pill 兩端間隙被 avoid 抹掉(違反互斥,且令上一項修復誤傷普通 code)。移除後普通 code 為純 pill、正常補隙;唯自身 data-jz-skip 者不補(即上項修復)。 清理: ・刪零引用死代碼 createMarker/ANY_CJK/BD_ALL/BD_CLASS_NAMES/CreateOptions.attrs; isJz 由 jinze 冪等守衛啟用、脫離死代碼。 ・死旗標 biaodian/emphasis/ruby 補 @reserved 註記(比照 hanging;點明與 charClass.biaodian 撞名異義,避免混淆)。 ・JzKind 補註 data-jz-kind 第三值 "splitoff"(split.ts 直接設、不經 createJz); longWord/orphan 冗餘 ===false 特判簡化(feature() 已對 false 回 false)。 複驗後重新定性(不照單全收): ・jinze 跨透明元素邊界避頭尾漏綁(你好<strong>。世界</strong> 之句末點號漏綁)——**真 缺陷**,但正解須改 jinze 之父級處理模型(穿越透明包裹上攀評估跨層綁定),與階段二 「透明判定單一真相源」一併處理更穩;forbiddenBreak 下探草稿已還原、缺口以註解標記。 ・lineedge edgeRect 攀穿 isolate——**非缺陷**:edgeRect 為物理版面測量(getClientRects), isolate 斷的是邏輯 run/相鄰,其內部字元之物理行首/行尾判定須跨 <mark> 找真實相鄰 內容(否則「文字<mark>「引」</mark>」之 「 誤判行首而錯收左半)。初評誤判、已還原並補註。 新增 3 回歸測試(jinze 冪等、預設 code 為 pill 非 skipTags、自身 avoid 之 pill 不補隙); 119 測試全綠、tsc + esbuild 構建乾淨。
270 lines
12 KiB
TypeScript
270 lines
12 KiB
TypeScript
// 字元分類與規則字集(架構 §4.2)。
|
||
//
|
||
// 集中所有 Unicode 範圍與標點子類,供各 pass 共用,杜絕分類散落不一致。
|
||
// CJK/西文字集沿用 v1(cjk-autospace)已驗證之 Pangu 範圍;標點子類為新增,
|
||
// 對應 CSS class(jz-char 之 bd-* 標記)。
|
||
//
|
||
// 這些常數以「regex 字元類片段」形式給出(已就 RegExp 字元類做必要轉義),
|
||
// 故可直接 `new RegExp("[" + X + "]")`。
|
||
|
||
import type { LangClass } from "./locale.js";
|
||
|
||
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
||
export const CJK = (
|
||
"⺀-⼀-" // CJK 部首補充 + 康熙部首
|
||
+ "-ゟ゠-ヺー-ヿ" // 平假名 + 片假名
|
||
+ "-ㄯ㈀-㋿" // 注音符號 + 圈號 CJK
|
||
+ "㐀-䶿一-鿿豈-" // 擴展 A + 統一表意 + 相容
|
||
);
|
||
|
||
/** 西文字母與數字。 */
|
||
export const ALNUM = "A-Za-z0-9";
|
||
|
||
/** 西文字母。 */
|
||
export const ALPHA = "A-Za-z";
|
||
|
||
// ----- Pangu 中西邊界用之擴充西文字集(沿用 v1)-----
|
||
|
||
// 西文(外文)字集——CJK 與其相鄰時補中西間隙。以 regex 字元類片段表示,供
|
||
// STRIP_*/CJK_ANS/ANS_CJK 等規則拼接。**列舉法(whitelist)**:逐區塊以
|
||
// \uXXXX 明列並附名稱;未來新區塊於此補入即可。範圍皆 BMP(≤U+FFFF),\uXXXX
|
||
// 於 regex 字元類內**無需 u flag** 即生效,故相容舊環境(2016/Windows 10 時代)。
|
||
// 相對 v1 補入:拉丁擴充 A/B(拼音聲調)、西里爾、貨幣符號(下游回報之 €)、數學
|
||
// 運算符;並把希臘由原 U+0370–037D 補全至整塊(原漏 Α-Ω α-ω 本身,致 `中文α`
|
||
// 不補隙)。刻意跳過 U+00B7 中間點 `·`(CJK 間隔號,如 卡爾·馬克思,屬 CJK 標點,
|
||
// 不觸發中西間隙)。
|
||
const WEST_SHARED = (
|
||
"0-9"
|
||
+ "\\$%\\^&\\*\\-\\+\\\\=" // 常用運算符(字面:$ % ^ & * - + \ =)
|
||
+ "\\u0370-\\u03ff" // 希臘+科普特(含 Α-Ω α-ω)
|
||
+ "\\u0100-\\u024f" // 拉丁擴充 A/B(拼音聲調 ā ǎ ē ǐ ō ǔ)
|
||
+ "\\u0400-\\u04ff" // 西里爾(俄文等)
|
||
+ "\\u00a1-\\u00b6\\u00b8-\\u00ff" // 拉丁-1 補充(跳過 U+00B7 間隔號 ·)
|
||
+ "\\u20a0-\\u20bf" // 貨幣符號(€ ₩ ₹ ₿ 等)
|
||
+ "\\u2150-\\u21ff" // 數字形式+箭號
|
||
+ "\\u2200-\\u22ff" // 數學運算符(∑ ∫ ≠ ≈ ± × ÷ 等)
|
||
+ "\\u2700-\\u27bf" // dingbats 裝飾符
|
||
);
|
||
|
||
/** CJK 之後可接間隙之西文集(含 @)。 */
|
||
export const ANS_AFTER_CJK = ALPHA + "@" + WEST_SHARED;
|
||
|
||
/** CJK 之前可接間隙之西文集(去 @)。 */
|
||
export const ANS_BEFORE_CJK = ALPHA + WEST_SHARED;
|
||
|
||
// ----- 標點(biaodian)子類。對應 jz-char 之 bd-* class -----
|
||
//
|
||
// 注意:分類依「字形角色」,而開明式/全角式之別依「句法角色」。slice 1 之
|
||
// 擠壓集(句內點號)由 BD_PAUSE 表達——此即 v1 Pass D 所擠之 ,、;:。
|
||
|
||
/** 句末點號:。.!?(開明式與全角式皆保持全形,不擠壓;banjiao 擠半)。
|
||
* 含全角句點 .(U+FF0E,異於表意句號 。U+3002)——實測 Noto CJK SC/TC 皆有 halt、
|
||
* 挤壓收 0.5em,故與 。!? 同列為句末點號(下游回饋)。 */
|
||
export const BD_STOP = "。.!?";
|
||
|
||
/** 句內點號:,、;:(開明式擠成半形;全角式保持全形)。 */
|
||
export const BD_PAUSE = ",、;:";
|
||
|
||
/** 開始括號/引號:「『(《〈【〖〔[{“‘。 */
|
||
export const BD_OPEN = "「『(《〈【〖〔[{“‘";
|
||
|
||
/** 結束括號/引號:」』)》〉】〗〕]}”’。 */
|
||
export const BD_CLOSE = "」』)》〉】〗〕]}”’";
|
||
|
||
/** 間隔/分隔號:·・‧。 */
|
||
export const BD_MIDDLE = "·・‧";
|
||
|
||
/** 連續省略/破折:…—。 */
|
||
export const BD_LIGA = "…—";
|
||
|
||
/** 全寬標點之六子類鍵。 */
|
||
export type BdSubclass = "stop" | "pause" | "open" | "close" | "middle" | "liga";
|
||
|
||
const BD_ORDER: ReadonlyArray<BdSubclass> = [
|
||
"stop", "pause", "open", "close", "middle", "liga",
|
||
];
|
||
|
||
/**
|
||
* 字符分類規則集(§4.2):把「中文/西文/全寬標點各子類」各表為 regex 字元類片段。
|
||
* 各 pass(spacing 之 CJK/ANS 正則、jiya 之 classifyBiaodian、jinze 之 anyCjk)皆由此
|
||
* 取分類,不再各自硬編碼。內置默認見 DEFAULT_RULESET;消費端經 options.charClass 擴充
|
||
* (resolveRuleset)。
|
||
*/
|
||
export interface Ruleset
|
||
{
|
||
/** 中文(CJK 表意/假名/注音等)——中西間隙之「中」側、禁則/justify 原子。 */
|
||
cjk: string;
|
||
/** 西文,CJK 之後可接隙者(含 @)。 */
|
||
ansAfter: string;
|
||
/** 西文,CJK 之前可接隙者(去 @)。 */
|
||
ansBefore: string;
|
||
/** 全寬標點,按子類——jiya charify/擠壓、禁則避頭尾、行端。 */
|
||
biaodian: Record<BdSubclass, string>;
|
||
}
|
||
|
||
/** 消費端字符分類覆寫(追加語義;見 README「自定義字符分類」)。 */
|
||
export interface CharClassOverride
|
||
{
|
||
/** 追加為中文(CJK)。 */
|
||
cjk?: string;
|
||
/** 追加為西文(半寬標點亦歸此,按西文處理間隙);併入 ansAfter/ansBefore。 */
|
||
western?: string;
|
||
/** 追加為全寬標點,按子類。 */
|
||
biaodian?: Partial<Record<BdSubclass, string>>;
|
||
}
|
||
|
||
/** 內置默認規則集(即現狀硬編碼值)。 */
|
||
export const DEFAULT_RULESET: 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,
|
||
},
|
||
};
|
||
|
||
/**
|
||
* 合併消費端覆寫,回傳最終規則集。v1 為**追加**語義:把 override 字元併入內置對應類
|
||
* (未分類字符→歸類、新符號→納入)。不傳則回傳 DEFAULT_RULESET(零行為變更)。
|
||
*/
|
||
export function resolveRuleset(override?: CharClassOverride): Ruleset
|
||
{
|
||
if (!override) { return DEFAULT_RULESET; }
|
||
const west = override.western || "";
|
||
const bd: Record<BdSubclass, string> = { ...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,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 將單一標點字元歸入其 bd-* 子類;非標點回傳 null。供 jiya charify 標記 class。
|
||
* biaodian 取自規則集(預設 DEFAULT_RULESET.biaodian,向下兼容無參數呼叫)。
|
||
*/
|
||
export function classifyBiaodian(
|
||
ch: string,
|
||
biaodian: Record<BdSubclass, string> = DEFAULT_RULESET.biaodian,
|
||
): string | null
|
||
{
|
||
for (const sub of BD_ORDER)
|
||
{
|
||
if (biaodian[sub].indexOf(ch) >= 0) { return "bd-" + sub; }
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 標點之「可壓縮空白」側(橫排)。字面偏左者空白在右(句內/句末/閉類),
|
||
* 開類字面偏右、空白在左。間隔號/連接號不壓縮。擠壓即收掉對應側之半形空白。
|
||
*/
|
||
export function blankSides(bdClass: string | null): { left: boolean; right: boolean }
|
||
{
|
||
switch (bdClass)
|
||
{
|
||
case "bd-open":
|
||
{
|
||
return { left: true, right: false };
|
||
}
|
||
case "bd-close":
|
||
case "bd-pause":
|
||
case "bd-stop":
|
||
{
|
||
return { left: false, right: true };
|
||
}
|
||
default:
|
||
{
|
||
return { left: false, right: false };
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----- 字形偏側(ink bias)參考表 — margin 後備模式用(架構 §6.3/.feedback)-----
|
||
//
|
||
// 預設半形渲染用字型 OpenType `halt`:由字型按字形正確定位,無需知道字面偏側。
|
||
// 但消費端字型未必含 halt(部分系統字型、特製字型)。此時改用「margin 後備模式」:
|
||
// 以負 margin 收半形空白,**須知道每個標點字形之墨色偏側**才能收對側、不重疊:
|
||
//
|
||
// left 墨色偏左(空白在右) → 收右:margin-right: -0.5em
|
||
// right 墨色偏右(空白在左) → 收左:margin-left: -0.5em
|
||
// center 墨色居中(兩側各半空白) → 兩側各收:margin-left/right: -0.25em
|
||
// full 無可壓縮空白(字面已佔滿全形) → 不收(維持全形)
|
||
//
|
||
// 下表為**參考預設**,依簡/繁字形慣例給出(簡體點號偏左下、繁體居中;!?恆
|
||
// 居中;開類偏右、閉類偏左)。因實際字型字形多樣,消費端可經 options.jiya.bias
|
||
// 逐 lang 逐字覆寫(見 README「margin 後備模式」一節)。
|
||
//
|
||
// 注意:此偏側表只在 margin 後備模式生效;halt 模式(預設)不讀它。
|
||
|
||
/** 字形墨色偏側。 */
|
||
export type Bias = "left" | "center" | "right" | "full";
|
||
|
||
/** 逐 lang 逐字之偏側覆寫表(消費端傳入,覆寫參考預設)。 */
|
||
export type BiasTable = Partial<Record<LangClass, Record<string, Bias>>>;
|
||
|
||
// 括號/引號偏側依**字身墨色中心**(margin 後備模式之 width:0.5em 盒內定位用)。
|
||
// 經方正書宋 GBK 實測 glyph ink bbox:彎引號 “”‘’、角括號 「」、尖括號 〈〉 之墨色
|
||
// 強偏一側(開類 inkcenter≈0.64–0.75 偏右、閉類≈0.25–0.37 偏左)→ 開 right、閉 left;
|
||
// 圓/方/書名/六角括號 ()【】《》[]{}〔〕〖〗『』 墨色近中(≈0.38–0.62)→ center。
|
||
// 偏側決定字身於 0.5em 半形盒內之左移量(right 移 0.5、center 移 0.25、left 不移),
|
||
// 使墨色落於盒內、空白溢向外側而不與鄰字重疊。字型字身分布各異,消費端可經
|
||
// options.jiya.bias 逐字覆寫(見 README「字型適配工作流」)。
|
||
const BRACKET_OPEN_RIGHT = "「〈“‘"; // 開類中墨色強偏右者
|
||
const BRACKET_CLOSE_LEFT = "」〉”’"; // 閉類中墨色強偏左者
|
||
const BIAS_BRACKETS: Record<string, Bias> = (() =>
|
||
{
|
||
const t: Record<string, Bias> = {};
|
||
for (const ch of BD_OPEN) { t[ch] = BRACKET_OPEN_RIGHT.indexOf(ch) >= 0 ? "right" : "center"; }
|
||
for (const ch of BD_CLOSE) { t[ch] = BRACKET_CLOSE_LEFT.indexOf(ch) >= 0 ? "left" : "center"; }
|
||
return t;
|
||
})();
|
||
|
||
// 點號偏側因簡繁字形而異。簡體(GBK/宋體系)點號皆沉於左下角、墨色偏左、空白
|
||
// 在右 → 全判 left(經方正書宋 GBK 實測:,、。;:!? 墨色 inkL≈0.1–0.18、
|
||
// inkR≈0.58–0.68,一致偏左)。繁體(Noto/教育部字形)點號置中 → center。
|
||
const BIAS_HANS: Record<string, Bias> = {
|
||
",": "left", "、": "left", "。": "left", ".": "left",
|
||
";": "left", ":": "left", "!": "left", "?": "left",
|
||
};
|
||
const BIAS_HANT: Record<string, Bias> = {
|
||
",": "center", "、": "center", "。": "center", ".": "center",
|
||
";": "center", ":": "center", "!": "center", "?": "center",
|
||
};
|
||
|
||
const BIAS_DEFAULT: Record<LangClass, Record<string, Bias>> = {
|
||
"zh-Hans": BIAS_HANS,
|
||
"zh-Hant": BIAS_HANT,
|
||
"ja": BIAS_HANT, // 日文點號慣例居中,沿用繁體表
|
||
"other": BIAS_HANT,
|
||
};
|
||
|
||
/**
|
||
* 解析標點字元在某語言下之墨色偏側(margin 後備模式用)。
|
||
* 優先序:消費端 overrides[langClass][ch] > 括號表 > 內建點號參考表。
|
||
* 非可壓縮標點(間隔/連接號等)回傳 null(margin 模式不收)。
|
||
*/
|
||
export function inkBias(
|
||
ch: string,
|
||
langClass: LangClass,
|
||
overrides?: BiasTable,
|
||
): Bias | null
|
||
{
|
||
const ov = overrides ? overrides[langClass] : undefined;
|
||
if (ov && ov[ch]) { return ov[ch]; }
|
||
const bracket = BIAS_BRACKETS[ch];
|
||
if (bracket) { return bracket; }
|
||
const t = BIAS_DEFAULT[langClass] || BIAS_DEFAULT.other;
|
||
return t[ch] || null;
|
||
}
|