// 字元分類與規則字集(架構 §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 = [ "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; } /** 消費端字符分類覆寫(追加語義;見 README「自定義字符分類」)。 */ export interface CharClassOverride { /** 追加為中文(CJK)。 */ cjk?: string; /** 追加為西文(半寬標點亦歸此,按西文處理間隙);併入 ansAfter/ansBefore。 */ western?: string; /** 追加為全寬標點,按子類。 */ biaodian?: Partial>; } /** 內置默認規則集(即現狀硬編碼值)。 */ 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 = { ...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 = 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>>; // 括號/引號偏側依**字身墨色中心**(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 = (() => { const t: Record = {}; 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 = { ",": "left", "、": "left", "。": "left", ".": "left", ";": "left", ":": "left", "!": "left", "?": "left", }; const BIAS_HANT: Record = { ",": "center", "、": "center", "。": "center", ".": "center", ";": "center", ":": "center", "!": "center", "?": "center", }; const BIAS_DEFAULT: Record> = { "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; }