feat: 聚珍切片 1 — 中西間隙/標點擠壓/禁則 + v1 相容層
從零重寫之第一垂直切片(依 ARCHITECTURE.md 契約),涵蓋並超越 v1 全部功能。 核心基座(src/core):unicode 字元表+標點子類+字形側位;locale 語言與 全角/開明政策(全域 lang.style +逐 lang policy);dom(jz-* 工廠+可還原 標記);finder 遍歷引擎(avoid/scope.include/邏輯 run/pill 鄰字/分塊閘); pass 聚合管線。 功能 pass(src/typeset): - spacing:中西間隙採 margin 模型——包裹邊界左側字、以 margin-right 留隙, 不注入空白字元(textContent 完全不變、複製緊湊、justify 不暴增、不撐框、 行首乾淨)。含跨樣式邊界、pill、原檔空格移除。 - gapTrim(lineedge):layout pass,行末去隙,修正 justify 右緣內縮。 - longword:長英文詞 <span lang> + hyphens。 - jiya:標點 charify + 全角/開明/半角(全域 + 逐 lang);半形以字型 OpenType halt 渲染(按字形正確定位、不重疊,居中字形 !? 與繁體 locl 句逗點號皆正確); 行內連續擠壓。 - jinze:行首行末避頭尾(閉類/點號綁前字、開類綁後字,以詞元為邊界、長引文 中段可斷);亦為 justify 綁定載體。 - lineedge:行端擠壓 layout pass(括號引號;ResizeObserver 重算,瀏覽器專用)。 §8 justify:jz-char inline-block 原子,句末標點單份間距。 v1 相容層(compat/v1):createCjkAutospace().apply 映射,IIFE 全域別名。 構建:esbuild → ESM/IIFE/CSS;tsc → d.ts。測試:39 個 jsdom 案例。 demo:簡繁並列+justify+強制繁體開明+標點寬度量測,載 Noto webfont 作參考 (擠壓依賴字型 halt)。間隔號 · 不觸發中西間隙;200% 等詞元不被禁則切斷。
This commit is contained in:
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
interface V1LongWord {
|
||||
minLength?: number;
|
||||
lang?: string;
|
||||
}
|
||||
interface V1Squeeze {
|
||||
squeezeClass?: string;
|
||||
marks?: string[];
|
||||
langWhitelist?: string[];
|
||||
}
|
||||
export interface V1Options {
|
||||
styleInlines?: string[];
|
||||
blockTags?: string[];
|
||||
skipTags?: string[];
|
||||
pillSelector?: string;
|
||||
autospaceClass?: string;
|
||||
skipAttribute?: string;
|
||||
isSkipped?: (node: Node) => boolean;
|
||||
longWordWrap?: boolean | V1LongWord;
|
||||
punctuationSqueeze?: boolean | V1Squeeze;
|
||||
}
|
||||
export interface V1Instance {
|
||||
apply(root?: Element): void;
|
||||
revert(root?: Element): void;
|
||||
}
|
||||
/**
|
||||
* v1 相容工廠。回傳 { apply, revert };apply→render、revert→revert。
|
||||
*/
|
||||
export declare function createCjkAutospace(options?: V1Options): V1Instance;
|
||||
export {};
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
/** jz-* 自訂元素名(皆含連字號,合法 custom element,預設 inline)。 */
|
||||
export type JzTag = "jz-hws" | "jz-char" | "jz-inner" | "jz-cs" | "jz-jinze" | "jz-em" | "jz-ruby" | "jz-rb" | "jz-rt";
|
||||
type JzKind = "marker" | "wrap";
|
||||
export interface CreateOptions {
|
||||
/** 追加之 class(空白分隔或陣列)。 */
|
||||
classes?: string | string[];
|
||||
/** 文本內容。 */
|
||||
text?: string;
|
||||
/** 其餘屬性。 */
|
||||
attrs?: Record<string, string>;
|
||||
}
|
||||
/** 造一個帶 pass 標記之 jz-* 元素。 */
|
||||
export declare function createJz(tag: JzTag, pass: string, kind: JzKind, opts?: CreateOptions): HTMLElement;
|
||||
/** 造一個 copy-clean 間隙標記(內含真實空白,user-select 由 CSS 關閉)。 */
|
||||
export declare function createMarker(tag: JzTag, pass: string): HTMLElement;
|
||||
/**
|
||||
* 還原某 pass 之全部產物(I6)。以 data-jz 定址,逆文件序處理(先內層後
|
||||
* 外層),marker 移除、wrap 解包,最後 normalize 受影響父節點合併文本。
|
||||
*/
|
||||
export declare function revertPass(pass: string, root: ParentNode): void;
|
||||
/** 讀取 jz-char 之 bd-* 子類(無則 null)。 */
|
||||
export declare function bdClassOf(el: Element): string | null;
|
||||
/** 該元素是否為某/任一 pass 之 jz 產物。 */
|
||||
export declare function isJz(node: Node | null, pass?: string): boolean;
|
||||
export {};
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
import type { ResolvedOptions } from "../types.js";
|
||||
/** 邏輯 run 中之單一字元來源位置。 */
|
||||
export interface CharPos {
|
||||
textNode: Text;
|
||||
offset: number;
|
||||
}
|
||||
/** 一段邏輯文本:剝離樣式後之連續字元,及其逐字回映射。 */
|
||||
export interface LogicalRun {
|
||||
text: string;
|
||||
map: CharPos[];
|
||||
}
|
||||
/** pill 鄰接之邏輯鄰字。 */
|
||||
export interface AdjacentChar {
|
||||
textNode: Text;
|
||||
offset: number;
|
||||
ch: string;
|
||||
}
|
||||
export declare class Finder {
|
||||
private readonly opts;
|
||||
private readonly styleSet;
|
||||
private readonly blockSet;
|
||||
private readonly skipSet;
|
||||
private readonly pillSelector;
|
||||
private readonly avoidSelector;
|
||||
private readonly includeSelector;
|
||||
private readonly skipAttr;
|
||||
private readonly userIsSkipped;
|
||||
private readonly featureCache;
|
||||
constructor(opts: ResolvedOptions);
|
||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||
pillMatches(el: Element): boolean;
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
isAvoided(node: Node): boolean;
|
||||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||||
inScope(node: Node): boolean;
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / 非作用域)。 */
|
||||
eachTextNode(root: Element, cb: (node: Text) => void): void;
|
||||
/** 走訪 root 下所有 block 元素(含 root 本身若為 block)。 */
|
||||
eachBlock(root: Element, cb: (block: Element) => void): void;
|
||||
/**
|
||||
* 收集 block 之邏輯文本 run(架構 §4.1,I7):樣式行內元素與 wrap 類
|
||||
* jz-* 透明穿越;block/pill/avoid/間隙 marker(jz-hws)中斷 run。
|
||||
* 回傳 text.length >= 2 之 run(短於 2 無間隙可言)。
|
||||
*/
|
||||
collectRuns(block: Element): LogicalRun[];
|
||||
/**
|
||||
* pill 之邏輯鄰字(架構 §4.1):經樣式鏈遞迴,遇 pill/block/avoid/間隙
|
||||
* marker 即止。direction = -1 取前鄰字,+1 取後鄰字。
|
||||
*/
|
||||
adjacentLogicalChar(el: Element, direction: -1 | 1): AdjacentChar | null;
|
||||
/**
|
||||
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
||||
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
||||
* blocks 選擇器),無則回退全域 features。結果以元素為鍵快取。
|
||||
*/
|
||||
featureEnabledFor(node: Node, name: string): boolean;
|
||||
private globalFeature;
|
||||
private resolveFeature;
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
/** 標點寬度政策。 */
|
||||
export type PunctStyle = "quanjiao" | "kaiming" | "banjiao";
|
||||
/** 語言分類。 */
|
||||
export type LangClass = "zh-Hans" | "zh-Hant" | "ja" | "other";
|
||||
export interface LocaleConfig {
|
||||
/** 無 lang 祖先時之預設語言。 */
|
||||
default: string;
|
||||
/** 全域標點擠壓預設(全角/開明/半角)。設則覆寫內建之逐語言預設,
|
||||
* 但仍可被 policy 之逐 lang 設定覆寫。未設則回退內建預設。 */
|
||||
style?: PunctStyle;
|
||||
/** 語言分類 → 標點寬度政策之逐 lang 覆寫(最高優先)。 */
|
||||
policy: Partial<Record<LangClass, PunctStyle>>;
|
||||
}
|
||||
/**
|
||||
* 由節點向上找最近帶 lang 屬性之祖先,回傳小寫值;無則回傳 fallback。
|
||||
*/
|
||||
export declare function effectiveLang(node: Node, fallback: string): string;
|
||||
/**
|
||||
* 將 BCP-47 語言標籤歸類。前綴以子標籤邊界比對('zh-tw' 命中 zh-Hant,
|
||||
* 'zhx' 不命中 zh)。香港/台灣繁體歸 zh-Hant;含 Hant/Hans 子標籤者優先。
|
||||
*/
|
||||
export declare function classify(lang: string): LangClass;
|
||||
/**
|
||||
* 回傳該語言分類在給定 config 下之標點寬度政策。優先序:
|
||||
* 逐 lang policy > 全域 style > 內建逐語言預設 DEFAULT_POLICY。
|
||||
*/
|
||||
export declare function styleFor(langClass: LangClass, config: LocaleConfig): PunctStyle;
|
||||
/** 解析節點之有效政策(lang → 分類 → 政策)。 */
|
||||
export declare function resolveStyle(node: Node, config: LocaleConfig): PunctStyle;
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import type { Pass, RenderContext } from "../types.js";
|
||||
/** 對 ctx.root 套用 passes 中已啟用者。 */
|
||||
export declare function runPasses(passes: Pass[], ctx: RenderContext): void;
|
||||
/** 還原 passes 之全部產物(逆 order;revert 對無產物者安全)。 */
|
||||
export declare function revertPasses(passes: Pass[], ctx: RenderContext): void;
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
/** CJK 表意文字/假名/注音等範圍(沿用並擴充 v1 之 PANGU_CJK)。 */
|
||||
export declare const CJK: string;
|
||||
/** 西文字母與數字。 */
|
||||
export declare const ALNUM = "A-Za-z0-9";
|
||||
/** 西文字母。 */
|
||||
export declare const ALPHA = "A-Za-z";
|
||||
/** 任一 CJK 字元測試。 */
|
||||
export declare const ANY_CJK: RegExp;
|
||||
/** CJK 之後可接間隙之西文集(含希臘字母、運算符、拉丁補充、箭號、裝飾符)。 */
|
||||
export declare const ANS_AFTER_CJK: string;
|
||||
/** CJK 之前可接間隙之西文集(同上,去 @)。 */
|
||||
export declare const ANS_BEFORE_CJK: string;
|
||||
/** 句末點號:。!?(開明式與全角式皆保持全形,不擠壓)。 */
|
||||
export declare const BD_STOP = "\u3002\uFF01\uFF1F";
|
||||
/** 句內點號:,、;:(開明式擠成半形;全角式保持全形)。 */
|
||||
export declare const BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
||||
/** 開始括號/引號:「『(《〈【〖〔[{“‘。 */
|
||||
export declare const BD_OPEN = "\u300C\u300E\uFF08\u300A\u3008\u3010\u3016\u3014\uFF3B\uFF5B\u201C\u2018";
|
||||
/** 結束括號/引號:」』)》〉】〗〕]}”’。 */
|
||||
export declare const BD_CLOSE = "\u300D\u300F\uFF09\u300B\u3009\u3011\u3017\u3015\uFF3D\uFF5D\u201D\u2019";
|
||||
/** 間隔/分隔號:·・‧。 */
|
||||
export declare const BD_MIDDLE = "\u00B7\u30FB\u2027";
|
||||
/** 連續省略/破折:…—。 */
|
||||
export declare const BD_LIGA = "\u2026\u2014";
|
||||
/** 全體標點字元(供 charify 走訪偵測)。 */
|
||||
export declare const BD_ALL: string;
|
||||
/**
|
||||
* 將單一標點字元歸入其 bd-* 子類;非標點回傳 null。
|
||||
* 供 jiya 之 charify 為 jz-char 標記 class。
|
||||
*/
|
||||
export declare function classifyBiaodian(ch: string): string | null;
|
||||
/** 全體 bd-* 子類名(DOM 端讀取 jz-char 之 bd class 用)。 */
|
||||
export declare const BD_CLASS_NAMES: readonly ["bd-stop", "bd-pause", "bd-open", "bd-close", "bd-middle", "bd-liga"];
|
||||
/**
|
||||
* 標點之「可壓縮空白」側(橫排)。字面偏左者空白在右(句內/句末/閉類),
|
||||
* 開類字面偏右、空白在左。間隔號/連接號不壓縮。擠壓即收掉對應側之半形空白。
|
||||
*/
|
||||
export declare function blankSides(bdClass: string | null): {
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
};
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import type { JuzhenOptions, Pass, ResolvedOptions } from "./types.js";
|
||||
export type { JuzhenOptions, ResolvedOptions, Pass };
|
||||
export { createCjkAutospace } from "./compat/v1.js";
|
||||
/** 正規化公開選項為內部 ResolvedOptions。 */
|
||||
export declare function normalizeOptions(opts?: JuzhenOptions): ResolvedOptions;
|
||||
export interface Juzhen {
|
||||
render(root?: Element | string): void;
|
||||
revert(root?: Element | string): void;
|
||||
readonly options: ResolvedOptions;
|
||||
}
|
||||
/** 建立一個聚珍實例。 */
|
||||
export declare function createJuzhen(opts?: JuzhenOptions): Juzhen;
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
/* src/css/_normalize.css */
|
||||
@supports (text-autospace: no-autospace) {
|
||||
.juzhen {
|
||||
text-autospace: no-autospace;
|
||||
}
|
||||
}
|
||||
jz-char,
|
||||
jz-inner,
|
||||
jz-hws,
|
||||
jz-cs,
|
||||
jz-jinze,
|
||||
jz-em {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* src/css/_lang.css */
|
||||
:where(.juzhen):lang(zh-Hans),
|
||||
:where(.juzhen) :lang(zh-Hans) {
|
||||
font-family:
|
||||
"Noto Serif SC",
|
||||
"Noto Serif CJK SC",
|
||||
"Source Han Serif SC",
|
||||
serif;
|
||||
}
|
||||
:where(.juzhen):lang(zh-Hant),
|
||||
:where(.juzhen) :lang(zh-Hant) {
|
||||
font-family:
|
||||
"Noto Serif TC",
|
||||
"Noto Serif CJK TC",
|
||||
"Source Han Serif TC",
|
||||
serif;
|
||||
}
|
||||
:where(.juzhen):lang(zh-HK),
|
||||
:where(.juzhen) :lang(zh-HK) {
|
||||
font-family:
|
||||
"Noto Serif HK",
|
||||
"Noto Serif CJK HK",
|
||||
"Noto Serif TC",
|
||||
serif;
|
||||
}
|
||||
:where(.juzhen):lang(ja),
|
||||
:where(.juzhen) :lang(ja) {
|
||||
font-family:
|
||||
"Noto Serif JP",
|
||||
"Noto Serif CJK JP",
|
||||
serif;
|
||||
}
|
||||
|
||||
/* src/css/_spacing.css */
|
||||
jz-hws {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
jz-hws.jz-hws-trim {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* src/css/_jiya.css */
|
||||
jz-char {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
jz-inner {
|
||||
display: inline;
|
||||
}
|
||||
jz-char.jz-half jz-inner,
|
||||
jz-char.jz-half-le jz-inner {
|
||||
font-feature-settings: "halt" 1;
|
||||
-webkit-font-feature-settings: "halt" 1;
|
||||
}
|
||||
|
||||
/* src/css/_jinze.css */
|
||||
jz-jinze {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* src/css/juzhen.css */
|
||||
Vendored
+1462
File diff suppressed because it is too large
Load Diff
Vendored
+1436
File diff suppressed because it is too large
Load Diff
Vendored
+119
@@ -0,0 +1,119 @@
|
||||
import type { Finder } from "./core/finder.js";
|
||||
import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js";
|
||||
export type { LangClass, PunctStyle };
|
||||
/** 長英文詞包裝設定。 */
|
||||
export interface LongWordOptions {
|
||||
minLength?: number;
|
||||
lang?: string;
|
||||
}
|
||||
/** 分塊功能選擇:選擇器 → 該塊內生效之功能集(覆寫全域)。 */
|
||||
export interface BlockRule {
|
||||
selector: string;
|
||||
features: string[];
|
||||
}
|
||||
/** createJuzhen 之公開選項。所有欄位皆可選;預設見 normalizeOptions。 */
|
||||
export interface JuzhenOptions {
|
||||
/** 語言與全角/開明政策。 */
|
||||
lang?: {
|
||||
/** 無 lang 祖先時之預設語言標籤(預設 "zh-Hant")。 */
|
||||
default?: string;
|
||||
/** 全域標點擠壓預設:quanjiao(全角式)|kaiming(開明式)|
|
||||
* banjiao(半角式)。覆寫內建逐語言預設,可被 policy 逐 lang 覆寫。 */
|
||||
style?: PunctStyle;
|
||||
/** 逐 lang 覆寫(最高優先)。 */
|
||||
policy?: Partial<Record<LangClass, PunctStyle>>;
|
||||
};
|
||||
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
||||
autospaceClass?: string;
|
||||
/** 作用域限定(§3.6)。 */
|
||||
scope?: {
|
||||
root?: Element | string;
|
||||
include?: string | null;
|
||||
avoid?: string;
|
||||
};
|
||||
spacing?: boolean;
|
||||
longWord?: boolean | LongWordOptions;
|
||||
jiya?: boolean;
|
||||
jinze?: boolean;
|
||||
hanging?: boolean;
|
||||
biaodian?: boolean;
|
||||
emphasis?: boolean;
|
||||
ruby?: boolean;
|
||||
/** 分塊功能選擇(§3.6)。 */
|
||||
blocks?: BlockRule[];
|
||||
styleInlines?: string[];
|
||||
blockTags?: string[];
|
||||
skipTags?: string[];
|
||||
pillSelector?: string;
|
||||
skipAttribute?: string;
|
||||
isSkipped?: (node: Node) => boolean;
|
||||
}
|
||||
/** 正規化後之內部選項。 */
|
||||
export interface ResolvedOptions {
|
||||
locale: LocaleConfig;
|
||||
autospaceClass: string;
|
||||
scope: {
|
||||
root: Element | string | null;
|
||||
include: string | null;
|
||||
avoid: string | null;
|
||||
};
|
||||
features: {
|
||||
spacing: boolean;
|
||||
longWord: boolean;
|
||||
jiya: boolean;
|
||||
jinze: boolean;
|
||||
hanging: boolean;
|
||||
biaodian: boolean;
|
||||
emphasis: boolean;
|
||||
ruby: boolean;
|
||||
};
|
||||
longWord: {
|
||||
minLength: number;
|
||||
lang: string;
|
||||
};
|
||||
blocks: BlockRule[];
|
||||
finder: {
|
||||
styleInlines: Set<string>;
|
||||
blockTags: Set<string>;
|
||||
skipTags: Set<string>;
|
||||
pillSelector: string;
|
||||
skipAttribute: string | null;
|
||||
isSkipped: ((node: Node) => boolean) | null;
|
||||
};
|
||||
}
|
||||
/** render/revert 之統一上下文(§3.3)。 */
|
||||
export interface RenderContext {
|
||||
root: Element;
|
||||
options: ResolvedOptions;
|
||||
finder: Finder;
|
||||
}
|
||||
/**
|
||||
* charify 階段之包裝請求:把 node.data[start, end) 包進 build() 造出之元素。
|
||||
* 同階段各 pass 之請求須互不重疊(標點/字母段天然不交集)。
|
||||
*/
|
||||
export interface WrapRequest {
|
||||
start: number;
|
||||
end: number;
|
||||
build: (text: string) => HTMLElement;
|
||||
pass: string;
|
||||
}
|
||||
/** pass 種類。 */
|
||||
export type PassKind = "charify" | "standalone";
|
||||
interface BasePass {
|
||||
name: string;
|
||||
order: number;
|
||||
kind: PassKind;
|
||||
enabled: (options: ResolvedOptions) => boolean;
|
||||
revert: (ctx: RenderContext) => void;
|
||||
}
|
||||
/** 共享 P1 走訪之字元包裝 pass。 */
|
||||
export interface CharifyPass extends BasePass {
|
||||
kind: "charify";
|
||||
collect: (node: Text, ctx: RenderContext) => WrapRequest[];
|
||||
}
|
||||
/** 自跑走訪/版面後處理之 pass。 */
|
||||
export interface StandalonePass extends BasePass {
|
||||
kind: "standalone";
|
||||
render: (ctx: RenderContext) => void;
|
||||
}
|
||||
export type Pass = CharifyPass | StandalonePass;
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { StandalonePass } from "../types.js";
|
||||
export declare const jinzePass: StandalonePass;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { CharifyPass, StandalonePass } from "../types.js";
|
||||
export declare const jiyaPass: CharifyPass;
|
||||
export declare const jiyaAdjacencyPass: StandalonePass;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { StandalonePass } from "../types.js";
|
||||
export declare const lineEdgePass: StandalonePass;
|
||||
export declare const gapTrimPass: StandalonePass;
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { CharifyPass } from "../types.js";
|
||||
export declare const longWordPass: CharifyPass;
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { StandalonePass } from "../types.js";
|
||||
export declare const spacingPass: StandalonePass;
|
||||
Reference in New Issue
Block a user