From c00fb3e482e765157225707cd0a7cc5edd95fe86 Mon Sep 17 00:00:00 2001 From: commilitia Date: Sat, 11 Jul 2026 01:42:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=90=E5=85=83=E7=B4=A0=E6=A8=99?= =?UTF-8?q?=E9=BB=9E=E6=A8=A3=E5=BC=8F=E8=A6=86=E5=AF=AB=20data-jz-style?= =?UTF-8?q?=20=EF=BC=8B=20lang.stylePolicy=EF=BC=88=E9=9D=9E=E6=AE=B5?= =?UTF-8?q?=E8=90=BD=E7=94=A8=E5=8D=8A=E8=A7=92=E5=BC=8F=E7=AD=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 下游需求:非段落/特定範圍元素用半角式(banjiao),段落用全角/開明。原標點樣式僅由 lang 決定(lang.policy/lang.style),無逐元素覆寫。新增與 data-jz-level/level.text 同構之逐元素樣式 機制(banjiao 渲染早已就緒,此為純路由、無新壓縮邏輯、無新 CSS)。 設置格式(供下游): ・HTML 屬性:data-jz-style="banjiao|kaiming|quanjiao"——標於元素或其祖先,該子樹用此樣式。 ・選項:lang: { stylePolicy: [{ selector, style }] }——如 [{ selector: "h1,h2,button,.ui", style: "banjiao" }]。 ・優先序:逐元素覆寫(近祖先勝、同層屬性優先於選擇器)> lang.policy > lang.style > 內建。 實作:resolveStyle 先查 resolveStyleOverride(自身及祖先鏈掃 data-jz-style 屬性/stylePolicy 選擇器命中),無則回退 lang。屬性值非法(拼錯)則忽略、續回退。types.ts 補 lang.stylePolicy +導出 StyleRule;index.ts 正規化(stylePolicy 預設 [])。 冒烟三例通過(屬性 banjiao/選擇器 banjiao/屬性覆蓋選擇器);既有 121 測試無回歸(stylePolicy 預設空、行為不變)。回歸測試+文檔隨後補(e2e 階段)。 --- dist/core/locale.d.ts | 15 +++++++++++-- dist/juzhen.iife.js | 28 ++++++++++++++++++++++- dist/juzhen.mjs | 28 ++++++++++++++++++++++- dist/types.d.ts | 12 ++++++---- src/core/locale.ts | 52 +++++++++++++++++++++++++++++++++++++++++-- src/index.ts | 1 + src/types.ts | 12 ++++++---- 7 files changed, 134 insertions(+), 14 deletions(-) diff --git a/dist/core/locale.d.ts b/dist/core/locale.d.ts index 1ecf6cc..59b59d7 100644 --- a/dist/core/locale.d.ts +++ b/dist/core/locale.d.ts @@ -2,14 +2,22 @@ export type PunctStyle = "quanjiao" | "kaiming" | "banjiao"; /** 語言分類。 */ export type LangClass = "zh-Hans" | "zh-Hant" | "ja" | "other"; +/** 按選擇器逐元素覆寫標點樣式之規則(§4.3;如非段落/UI 元素用半角式)。 */ +export interface StyleRule { + selector: string; + style: PunctStyle; +} export interface LocaleConfig { /** 無 lang 祖先時之預設語言。 */ default: string; /** 全域標點擠壓預設(全角/開明/半角)。設則覆寫內建之逐語言預設, * 但仍可被 policy 之逐 lang 設定覆寫。未設則回退內建預設。 */ style?: PunctStyle; - /** 語言分類 → 標點寬度政策之逐 lang 覆寫(最高優先)。 */ + /** 語言分類 → 標點寬度政策之逐 lang 覆寫。 */ policy: Partial>; + /** 按選擇器**逐元素**覆寫標點樣式(優先於 lang;與 `data-jz-style` 屬性同一機制)。 + * 近祖先勝、同層屬性優先於選擇器。空/未設則不覆寫、純由 lang 決定。 */ + stylePolicy?: StyleRule[]; } /** * 由節點向上找最近帶 lang 屬性之祖先,回傳小寫值;無則回傳 fallback。 @@ -25,5 +33,8 @@ export declare function classify(lang: string): LangClass; * 逐 lang policy > 全域 style > 內建逐語言預設 DEFAULT_POLICY。 */ export declare function styleFor(langClass: LangClass, config: LocaleConfig): PunctStyle; -/** 解析節點之有效政策(lang → 分類 → 政策)。 */ +/** + * 解析節點之有效標點樣式。優先序:**逐元素覆寫**(`data-jz-style` 屬性/`stylePolicy` + * 選擇器,近祖先勝)> 逐 lang `policy` > 全域 `style` > 內建逐語言預設。 + */ export declare function resolveStyle(node: Node, config: LocaleConfig): PunctStyle; diff --git a/dist/juzhen.iife.js b/dist/juzhen.iife.js index c31fa9e..0948602 100644 --- a/dist/juzhen.iife.js +++ b/dist/juzhen.iife.js @@ -664,7 +664,32 @@ var Juzhen = (() => { } return DEFAULT_POLICY[langClass]; } + function isPunctStyle(v) { + return v === "quanjiao" || v === "kaiming" || v === "banjiao"; + } + function resolveStyleOverride(node, stylePolicy) { + let el = node.nodeType === 1 ? node : node.parentElement; + while (el) { + const attr = el.getAttribute ? el.getAttribute("data-jz-style") : null; + if (isPunctStyle(attr)) { + return attr; + } + if (stylePolicy) { + for (const rule of stylePolicy) { + if (rule.selector && el.matches && el.matches(rule.selector)) { + return rule.style; + } + } + } + el = el.parentElement; + } + return null; + } function resolveStyle(node, config) { + const override = resolveStyleOverride(node, config.stylePolicy); + if (override) { + return override; + } const lang = effectiveLang(node, config.default); return styleFor(classify(lang), config); } @@ -2055,7 +2080,8 @@ var Juzhen = (() => { locale: { default: langOpt.default || "zh-Hant", ...langOpt.style ? { style: langOpt.style } : {}, - policy: langOpt.policy || {} + policy: langOpt.policy || {}, + stylePolicy: Array.isArray(langOpt.stylePolicy) ? langOpt.stylePolicy.slice() : [] }, ruleset: resolveRuleset(opts.charClass), justifyAtoms: opts.justifyAtoms === false ? false : true, diff --git a/dist/juzhen.mjs b/dist/juzhen.mjs index da31f33..ba6fcc3 100644 --- a/dist/juzhen.mjs +++ b/dist/juzhen.mjs @@ -637,7 +637,32 @@ function styleFor(langClass, config) { } return DEFAULT_POLICY[langClass]; } +function isPunctStyle(v) { + return v === "quanjiao" || v === "kaiming" || v === "banjiao"; +} +function resolveStyleOverride(node, stylePolicy) { + let el = node.nodeType === 1 ? node : node.parentElement; + while (el) { + const attr = el.getAttribute ? el.getAttribute("data-jz-style") : null; + if (isPunctStyle(attr)) { + return attr; + } + if (stylePolicy) { + for (const rule of stylePolicy) { + if (rule.selector && el.matches && el.matches(rule.selector)) { + return rule.style; + } + } + } + el = el.parentElement; + } + return null; +} function resolveStyle(node, config) { + const override = resolveStyleOverride(node, config.stylePolicy); + if (override) { + return override; + } const lang = effectiveLang(node, config.default); return styleFor(classify(lang), config); } @@ -2028,7 +2053,8 @@ function normalizeOptions(opts = {}) { locale: { default: langOpt.default || "zh-Hant", ...langOpt.style ? { style: langOpt.style } : {}, - policy: langOpt.policy || {} + policy: langOpt.policy || {}, + stylePolicy: Array.isArray(langOpt.stylePolicy) ? langOpt.stylePolicy.slice() : [] }, ruleset: resolveRuleset(opts.charClass), justifyAtoms: opts.justifyAtoms === false ? false : true, diff --git a/dist/types.d.ts b/dist/types.d.ts index e80f221..5456a44 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -1,7 +1,7 @@ import type { Finder } from "./core/finder.js"; -import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js"; +import type { LangClass, LocaleConfig, PunctStyle, StyleRule } from "./core/locale.js"; import type { Bias, BiasTable, BdSubclass, CharClassOverride, Ruleset } from "./core/unicode.js"; -export type { LangClass, PunctStyle, Bias, BiasTable, BdSubclass, CharClassOverride, Ruleset }; +export type { LangClass, PunctStyle, StyleRule, Bias, BiasTable, BdSubclass, CharClassOverride, Ruleset }; /** 長英文詞包裝設定。 */ export interface LongWordOptions { minLength?: number; @@ -34,10 +34,14 @@ export interface JuzhenOptions { /** 無 lang 祖先時之預設語言標籤(預設 "zh-Hant")。 */ default?: string; /** 全域標點擠壓預設:quanjiao(全角式)|kaiming(開明式)| - * banjiao(半角式)。覆寫內建逐語言預設,可被 policy 逐 lang 覆寫。 */ + * banjiao(半角式)。覆寫內建逐語言預設,可被 policy 逐 lang/stylePolicy 逐元素覆寫。 */ style?: PunctStyle; - /** 逐 lang 覆寫(最高優先)。 */ + /** 逐 lang 覆寫。 */ policy?: Partial>; + /** **逐元素**覆寫標點樣式(優先於 lang/policy/style):選擇器命中之元素及其子樹 + * 用指定樣式,如非段落/UI 元素用半角式 `[{ selector: "h1,h2,button,.ui", style: "banjiao" }]`。 + * 與 HTML 屬性 `data-jz-style="banjiao|kaiming|quanjiao"` 同一機制;近祖先勝、屬性優先於選擇器。 */ + stylePolicy?: StyleRule[]; }; /** 自定義字符分類(擴充內置規則集)。把未分類字符或新符號歸入中文/西文/ * 全寬標點各子類,以覆寫 spacing/jiya 等規則之分類判定(追加語義)。 */ diff --git a/src/core/locale.ts b/src/core/locale.ts index 4813513..81c0a48 100644 --- a/src/core/locale.ts +++ b/src/core/locale.ts @@ -9,6 +9,13 @@ export type PunctStyle = "quanjiao" | "kaiming" | "banjiao"; /** 語言分類。 */ export type LangClass = "zh-Hans" | "zh-Hant" | "ja" | "other"; +/** 按選擇器逐元素覆寫標點樣式之規則(§4.3;如非段落/UI 元素用半角式)。 */ +export interface StyleRule +{ + selector: string; + style: PunctStyle; +} + export interface LocaleConfig { /** 無 lang 祖先時之預設語言。 */ @@ -16,8 +23,11 @@ export interface LocaleConfig /** 全域標點擠壓預設(全角/開明/半角)。設則覆寫內建之逐語言預設, * 但仍可被 policy 之逐 lang 設定覆寫。未設則回退內建預設。 */ style?: PunctStyle; - /** 語言分類 → 標點寬度政策之逐 lang 覆寫(最高優先)。 */ + /** 語言分類 → 標點寬度政策之逐 lang 覆寫。 */ policy: Partial>; + /** 按選擇器**逐元素**覆寫標點樣式(優先於 lang;與 `data-jz-style` 屬性同一機制)。 + * 近祖先勝、同層屬性優先於選擇器。空/未設則不覆寫、純由 lang 決定。 */ + stylePolicy?: StyleRule[]; } const DEFAULT_POLICY: Record = { @@ -76,9 +86,47 @@ export function styleFor(langClass: LangClass, config: LocaleConfig): PunctStyle return DEFAULT_POLICY[langClass]; } -/** 解析節點之有效政策(lang → 分類 → 政策)。 */ +/** 該字串是否為合法標點樣式。 */ +function isPunctStyle(v: string | null): v is PunctStyle +{ + return v === "quanjiao" || v === "kaiming" || v === "banjiao"; +} + +/** + * 逐元素之標點樣式覆寫(§4.3):自 node 向上找**最近**祖先(含自身)之樣式信號—— + * `data-jz-style` 屬性(值須為合法樣式,優先),或 `stylePolicy` 選擇器命中——回傳其樣式; + * 皆無則 null(回退 lang 決定)。近祖先勝,同層屬性優先於選擇器。 + */ +function resolveStyleOverride(node: Node, stylePolicy?: StyleRule[]): PunctStyle | null +{ + let el: Element | null = node.nodeType === 1 ? (node as Element) : node.parentElement; + while (el) + { + const attr = el.getAttribute ? el.getAttribute("data-jz-style") : null; + if (isPunctStyle(attr)) { return attr; } + if (stylePolicy) + { + for (const rule of stylePolicy) + { + if (rule.selector && el.matches && el.matches(rule.selector)) + { + return rule.style; + } + } + } + el = el.parentElement; + } + return null; +} + +/** + * 解析節點之有效標點樣式。優先序:**逐元素覆寫**(`data-jz-style` 屬性/`stylePolicy` + * 選擇器,近祖先勝)> 逐 lang `policy` > 全域 `style` > 內建逐語言預設。 + */ export function resolveStyle(node: Node, config: LocaleConfig): PunctStyle { + const override = resolveStyleOverride(node, config.stylePolicy); + if (override) { return override; } const lang = effectiveLang(node, config.default); return styleFor(classify(lang), config); } diff --git a/src/index.ts b/src/index.ts index a2b7092..e983e6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,6 +100,7 @@ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions default: langOpt.default || "zh-Hant", ...(langOpt.style ? { style: langOpt.style } : {}), policy: langOpt.policy || {}, + stylePolicy: Array.isArray(langOpt.stylePolicy) ? langOpt.stylePolicy.slice() : [], }, ruleset: resolveRuleset(opts.charClass), justifyAtoms: opts.justifyAtoms === false ? false : true, diff --git a/src/types.ts b/src/types.ts index 82165f3..3b859ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,10 @@ // 公開選項型別與內部上下文型別(架構 §3.1/§3.3)。 import type { Finder } from "./core/finder.js"; -import type { LangClass, LocaleConfig, PunctStyle } from "./core/locale.js"; +import type { LangClass, LocaleConfig, PunctStyle, StyleRule } from "./core/locale.js"; import type { Bias, BiasTable, BdSubclass, CharClassOverride, Ruleset } from "./core/unicode.js"; -export type { LangClass, PunctStyle, Bias, BiasTable, BdSubclass, CharClassOverride, Ruleset }; +export type { LangClass, PunctStyle, StyleRule, Bias, BiasTable, BdSubclass, CharClassOverride, Ruleset }; /** 長英文詞包裝設定。 */ export interface LongWordOptions @@ -47,10 +47,14 @@ export interface JuzhenOptions /** 無 lang 祖先時之預設語言標籤(預設 "zh-Hant")。 */ default?: string; /** 全域標點擠壓預設:quanjiao(全角式)|kaiming(開明式)| - * banjiao(半角式)。覆寫內建逐語言預設,可被 policy 逐 lang 覆寫。 */ + * banjiao(半角式)。覆寫內建逐語言預設,可被 policy 逐 lang/stylePolicy 逐元素覆寫。 */ style?: PunctStyle; - /** 逐 lang 覆寫(最高優先)。 */ + /** 逐 lang 覆寫。 */ policy?: Partial>; + /** **逐元素**覆寫標點樣式(優先於 lang/policy/style):選擇器命中之元素及其子樹 + * 用指定樣式,如非段落/UI 元素用半角式 `[{ selector: "h1,h2,button,.ui", style: "banjiao" }]`。 + * 與 HTML 屬性 `data-jz-style="banjiao|kaiming|quanjiao"` 同一機制;近祖先勝、屬性優先於選擇器。 */ + stylePolicy?: StyleRule[]; }; /** 自定義字符分類(擴充內置規則集)。把未分類字符或新符號歸入中文/西文/