feat: 功能分級(文本級 vs 段落級)+ data-jz-level 標記(§6.5.0a)
整體計劃 §6.5 之前置基礎設施。把已實作功能切為兩級:
- 文本級(spacing 中西間隙、jiya 標點擠壓/鄰接):任意文本片段皆適用。
- 段落級(jinze 禁則、longWord 斷詞、lineEdge 行端、gapTrim):需行/段布局。
標記 API:
- 預設塊級元素 → 段落級(全功能)。
- 元素屬性 data-jz-level="text"|"paragraph"(最近祖先勝)。
- 選項 level:{ text: "選擇器" }(如標題/按鈕一鍵降為文本級)。
實作:
- 每個 pass 加 level 元數據;Finder.levelFor/levelAllows(含快取)。
- charify 段落級 pass(longWord)於共享走訪受閘;standalone 段落級 pass
(jinze/lineEdge/gapTrim)逐元素受閘。文本級不受限。
- spacing 補單元素 root 支援:root 為行內/單一非塊元素時亦處理(標題等
之中西間隙、標點半形生效)。
6 項新單元測試(text-level 跳段落級、選擇器標記、巢狀最近勝、單元素 spacing、
預設段落級回歸)。共 76 測試全綠。
This commit is contained in:
Vendored
+15
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { ResolvedOptions } from "../types.js";
|
import type { FeatureLevel, ResolvedOptions } from "../types.js";
|
||||||
/** 邏輯 run 中之單一字元來源位置。 */
|
/** 邏輯 run 中之單一字元來源位置。 */
|
||||||
export interface CharPos {
|
export interface CharPos {
|
||||||
textNode: Text;
|
textNode: Text;
|
||||||
@@ -25,7 +25,9 @@ export declare class Finder {
|
|||||||
private readonly includeSelector;
|
private readonly includeSelector;
|
||||||
private readonly skipAttr;
|
private readonly skipAttr;
|
||||||
private readonly userIsSkipped;
|
private readonly userIsSkipped;
|
||||||
|
private readonly levelTextSelector;
|
||||||
private readonly featureCache;
|
private readonly featureCache;
|
||||||
|
private readonly levelCache;
|
||||||
constructor(opts: ResolvedOptions);
|
constructor(opts: ResolvedOptions);
|
||||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||||
pillMatches(el: Element): boolean;
|
pillMatches(el: Element): boolean;
|
||||||
@@ -53,6 +55,18 @@ export declare class Finder {
|
|||||||
* marker 即止。direction = -1 取前鄰字,+1 取後鄰字。
|
* marker 即止。direction = -1 取前鄰字,+1 取後鄰字。
|
||||||
*/
|
*/
|
||||||
adjacentLogicalChar(el: Element, direction: -1 | 1): AdjacentChar | null;
|
adjacentLogicalChar(el: Element, direction: -1 | 1): AdjacentChar | null;
|
||||||
|
/**
|
||||||
|
* 節點之功能級別(§6.5.0a)。自 node 向上找**最近**之顯式信號:元素屬性
|
||||||
|
* `data-jz-level="text|paragraph"`(最優先),或命中 `level.text` 選擇器(→ text);
|
||||||
|
* 皆無則預設 paragraph(全功能)。結果以解析後之元素為鍵快取。
|
||||||
|
*/
|
||||||
|
levelFor(node: Node): FeatureLevel;
|
||||||
|
/**
|
||||||
|
* 某級別之 pass 是否可作用於該節點(§6.5.0a gate)。文本級 pass 不受限(恆 true);
|
||||||
|
* 段落級 pass 僅作用於段落級元素(text-level 子樹跳過)。與 featureEnabledFor 疊加:
|
||||||
|
* level 為粗粒度語義組(先判),blocks/屬性為細粒度逐功能(後調)。
|
||||||
|
*/
|
||||||
|
levelAllows(node: Node, passLevel: FeatureLevel): boolean;
|
||||||
/**
|
/**
|
||||||
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
||||||
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
||||||
|
|||||||
Vendored
+4
-2
@@ -11,8 +11,10 @@ export declare const ANY_CJK: RegExp;
|
|||||||
export declare const ANS_AFTER_CJK: string;
|
export declare const ANS_AFTER_CJK: string;
|
||||||
/** CJK 之前可接間隙之西文集(去 @)。 */
|
/** CJK 之前可接間隙之西文集(去 @)。 */
|
||||||
export declare const ANS_BEFORE_CJK: string;
|
export declare const ANS_BEFORE_CJK: string;
|
||||||
/** 句末點號:。!?(開明式與全角式皆保持全形,不擠壓)。 */
|
/** 句末點號:。.!?(開明式與全角式皆保持全形,不擠壓;banjiao 擠半)。
|
||||||
export declare const BD_STOP = "\u3002\uFF01\uFF1F";
|
* 含全角句點 .(U+FF0E,異於表意句號 。U+3002)——實測 Noto CJK SC/TC 皆有 halt、
|
||||||
|
* 挤壓收 0.5em,故與 。!? 同列為句末點號(下游回饋)。 */
|
||||||
|
export declare const BD_STOP = "\u3002\uFF0E\uFF01\uFF1F";
|
||||||
/** 句內點號:,、;:(開明式擠成半形;全角式保持全形)。 */
|
/** 句內點號:,、;:(開明式擠成半形;全角式保持全形)。 */
|
||||||
export declare const BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
export declare const BD_PAUSE = "\uFF0C\u3001\uFF1B\uFF1A";
|
||||||
/** 開始括號/引號:「『(《〈【〖〔[{“‘。 */
|
/** 開始括號/引號:「『(《〈【〖〔[{“‘。 */
|
||||||
|
|||||||
Vendored
+65
-1
@@ -37,9 +37,11 @@ var Juzhen = (() => {
|
|||||||
this.pillSelector = opts.finder.pillSelector;
|
this.pillSelector = opts.finder.pillSelector;
|
||||||
this.skipAttr = opts.finder.skipAttribute;
|
this.skipAttr = opts.finder.skipAttribute;
|
||||||
this.userIsSkipped = opts.finder.isSkipped;
|
this.userIsSkipped = opts.finder.isSkipped;
|
||||||
|
this.levelTextSelector = opts.levelText;
|
||||||
this.avoidSelector = opts.scope.avoid;
|
this.avoidSelector = opts.scope.avoid;
|
||||||
this.includeSelector = opts.scope.include;
|
this.includeSelector = opts.scope.include;
|
||||||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||||||
|
this.levelCache = /* @__PURE__ */ new WeakMap();
|
||||||
}
|
}
|
||||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||||
pillMatches(el) {
|
pillMatches(el) {
|
||||||
@@ -259,6 +261,48 @@ var Juzhen = (() => {
|
|||||||
p = pe.parentNode;
|
p = pe.parentNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 節點之功能級別(§6.5.0a)。自 node 向上找**最近**之顯式信號:元素屬性
|
||||||
|
* `data-jz-level="text|paragraph"`(最優先),或命中 `level.text` 選擇器(→ text);
|
||||||
|
* 皆無則預設 paragraph(全功能)。結果以解析後之元素為鍵快取。
|
||||||
|
*/
|
||||||
|
levelFor(node) {
|
||||||
|
const start = node.nodeType === 1 ? node : node.parentElement;
|
||||||
|
if (!start) {
|
||||||
|
return "paragraph";
|
||||||
|
}
|
||||||
|
const cached = this.levelCache.get(start);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
let el = start;
|
||||||
|
let result = "paragraph";
|
||||||
|
while (el) {
|
||||||
|
const attr = el.getAttribute ? el.getAttribute("data-jz-level") : null;
|
||||||
|
if (attr === "text" || attr === "paragraph") {
|
||||||
|
result = attr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.levelTextSelector && el.matches && el.matches(this.levelTextSelector)) {
|
||||||
|
result = "text";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
this.levelCache.set(start, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 某級別之 pass 是否可作用於該節點(§6.5.0a gate)。文本級 pass 不受限(恆 true);
|
||||||
|
* 段落級 pass 僅作用於段落級元素(text-level 子樹跳過)。與 featureEnabledFor 疊加:
|
||||||
|
* level 為粗粒度語義組(先判),blocks/屬性為細粒度逐功能(後調)。
|
||||||
|
*/
|
||||||
|
levelAllows(node, passLevel) {
|
||||||
|
if (passLevel === "text") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.levelFor(node) === "paragraph";
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
||||||
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
||||||
@@ -331,6 +375,9 @@ var Juzhen = (() => {
|
|||||||
ctx.finder.eachTextNode(ctx.root, (node) => {
|
ctx.finder.eachTextNode(ctx.root, (node) => {
|
||||||
let requests = [];
|
let requests = [];
|
||||||
for (const p of passes) {
|
for (const p of passes) {
|
||||||
|
if (!ctx.finder.levelAllows(node, p.level)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const got = p.collect(node, ctx);
|
const got = p.collect(node, ctx);
|
||||||
if (got.length > 0) {
|
if (got.length > 0) {
|
||||||
requests = requests.concat(got);
|
requests = requests.concat(got);
|
||||||
@@ -665,6 +712,7 @@ var Juzhen = (() => {
|
|||||||
name: PASS,
|
name: PASS,
|
||||||
kind: "charify",
|
kind: "charify",
|
||||||
order: 20,
|
order: 20,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
collect(node, ctx) {
|
collect(node, ctx) {
|
||||||
if (inJzChar(node)) {
|
if (inJzChar(node)) {
|
||||||
@@ -718,6 +766,7 @@ var Juzhen = (() => {
|
|||||||
name: "jiyaAdjacency",
|
name: "jiyaAdjacency",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 25,
|
order: 25,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
const { finder, options } = ctx;
|
const { finder, options } = ctx;
|
||||||
@@ -890,11 +939,15 @@ var Juzhen = (() => {
|
|||||||
name: PASS2,
|
name: PASS2,
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 40,
|
order: 40,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.jinze,
|
enabled: (o) => o.features.jinze,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
|
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
|
||||||
const parents = /* @__PURE__ */ new Set();
|
const parents = /* @__PURE__ */ new Set();
|
||||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||||
|
if (!ctx.finder.levelAllows(c, "paragraph")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (c.parentNode) {
|
if (c.parentNode) {
|
||||||
parents.add(c.parentNode);
|
parents.add(c.parentNode);
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1057,9 @@ var Juzhen = (() => {
|
|||||||
(c) => finder.inScope(c) && !finder.isAvoided(c)
|
(c) => finder.inScope(c) && !finder.isAvoided(c)
|
||||||
);
|
);
|
||||||
for (const el of chars) {
|
for (const el of chars) {
|
||||||
|
if (!finder.levelAllows(el, "paragraph")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!finder.featureEnabledFor(el, "jiya")) {
|
if (!finder.featureEnabledFor(el, "jiya")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1032,6 +1088,7 @@ var Juzhen = (() => {
|
|||||||
name: "jiyaLineEdge",
|
name: "jiyaLineEdge",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 90,
|
order: 90,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
if (!hasLayout()) {
|
if (!hasLayout()) {
|
||||||
@@ -1047,7 +1104,7 @@ var Juzhen = (() => {
|
|||||||
var TRIM = "jz-hws-trim";
|
var TRIM = "jz-hws-trim";
|
||||||
function trimGaps(root, finder) {
|
function trimGaps(root, finder) {
|
||||||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||||||
(g) => finder.inScope(g) && !finder.isAvoided(g)
|
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph")
|
||||||
);
|
);
|
||||||
if (!gaps.length) {
|
if (!gaps.length) {
|
||||||
return;
|
return;
|
||||||
@@ -1066,6 +1123,7 @@ var Juzhen = (() => {
|
|||||||
name: "spacingGapTrim",
|
name: "spacingGapTrim",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 92,
|
order: 92,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.spacing,
|
enabled: (o) => o.features.spacing,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
if (!hasLayout()) {
|
if (!hasLayout()) {
|
||||||
@@ -1096,6 +1154,7 @@ var Juzhen = (() => {
|
|||||||
name: PASS3,
|
name: PASS3,
|
||||||
kind: "charify",
|
kind: "charify",
|
||||||
order: 60,
|
order: 60,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.longWord,
|
enabled: (o) => o.features.longWord,
|
||||||
collect(node, ctx) {
|
collect(node, ctx) {
|
||||||
if (inLangSpan(node)) {
|
if (inLangSpan(node)) {
|
||||||
@@ -1347,6 +1406,7 @@ var Juzhen = (() => {
|
|||||||
name: PASS4,
|
name: PASS4,
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 50,
|
order: 50,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.spacing,
|
enabled: (o) => o.features.spacing,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
const { finder, options } = ctx;
|
const { finder, options } = ctx;
|
||||||
@@ -1357,6 +1417,9 @@ var Juzhen = (() => {
|
|||||||
}
|
}
|
||||||
processBlock(block, finder, R);
|
processBlock(block, finder, R);
|
||||||
});
|
});
|
||||||
|
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS4) && finder.inScope(ctx.root) && !finder.isAvoided(ctx.root)) {
|
||||||
|
processBlock(ctx.root, finder, R);
|
||||||
|
}
|
||||||
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
||||||
},
|
},
|
||||||
revert(ctx) {
|
revert(ctx) {
|
||||||
@@ -1551,6 +1614,7 @@ var Juzhen = (() => {
|
|||||||
ruleset: resolveRuleset(opts.charClass),
|
ruleset: resolveRuleset(opts.charClass),
|
||||||
autospaceClass: opts.autospaceClass || "",
|
autospaceClass: opts.autospaceClass || "",
|
||||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||||
|
levelText: opts.level && opts.level.text || null,
|
||||||
scope: {
|
scope: {
|
||||||
root: scope.root || null,
|
root: scope.root || null,
|
||||||
include: scope.include || null,
|
include: scope.include || null,
|
||||||
|
|||||||
Vendored
+65
-1
@@ -9,9 +9,11 @@ var Finder = class {
|
|||||||
this.pillSelector = opts.finder.pillSelector;
|
this.pillSelector = opts.finder.pillSelector;
|
||||||
this.skipAttr = opts.finder.skipAttribute;
|
this.skipAttr = opts.finder.skipAttribute;
|
||||||
this.userIsSkipped = opts.finder.isSkipped;
|
this.userIsSkipped = opts.finder.isSkipped;
|
||||||
|
this.levelTextSelector = opts.levelText;
|
||||||
this.avoidSelector = opts.scope.avoid;
|
this.avoidSelector = opts.scope.avoid;
|
||||||
this.includeSelector = opts.scope.include;
|
this.includeSelector = opts.scope.include;
|
||||||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||||||
|
this.levelCache = /* @__PURE__ */ new WeakMap();
|
||||||
}
|
}
|
||||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||||
pillMatches(el) {
|
pillMatches(el) {
|
||||||
@@ -231,6 +233,48 @@ var Finder = class {
|
|||||||
p = pe.parentNode;
|
p = pe.parentNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 節點之功能級別(§6.5.0a)。自 node 向上找**最近**之顯式信號:元素屬性
|
||||||
|
* `data-jz-level="text|paragraph"`(最優先),或命中 `level.text` 選擇器(→ text);
|
||||||
|
* 皆無則預設 paragraph(全功能)。結果以解析後之元素為鍵快取。
|
||||||
|
*/
|
||||||
|
levelFor(node) {
|
||||||
|
const start = node.nodeType === 1 ? node : node.parentElement;
|
||||||
|
if (!start) {
|
||||||
|
return "paragraph";
|
||||||
|
}
|
||||||
|
const cached = this.levelCache.get(start);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
let el = start;
|
||||||
|
let result = "paragraph";
|
||||||
|
while (el) {
|
||||||
|
const attr = el.getAttribute ? el.getAttribute("data-jz-level") : null;
|
||||||
|
if (attr === "text" || attr === "paragraph") {
|
||||||
|
result = attr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.levelTextSelector && el.matches && el.matches(this.levelTextSelector)) {
|
||||||
|
result = "text";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
this.levelCache.set(start, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 某級別之 pass 是否可作用於該節點(§6.5.0a gate)。文本級 pass 不受限(恆 true);
|
||||||
|
* 段落級 pass 僅作用於段落級元素(text-level 子樹跳過)。與 featureEnabledFor 疊加:
|
||||||
|
* level 為粗粒度語義組(先判),blocks/屬性為細粒度逐功能(後調)。
|
||||||
|
*/
|
||||||
|
levelAllows(node, passLevel) {
|
||||||
|
if (passLevel === "text") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.levelFor(node) === "paragraph";
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
||||||
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
||||||
@@ -303,6 +347,9 @@ function runCharify(passes, ctx) {
|
|||||||
ctx.finder.eachTextNode(ctx.root, (node) => {
|
ctx.finder.eachTextNode(ctx.root, (node) => {
|
||||||
let requests = [];
|
let requests = [];
|
||||||
for (const p of passes) {
|
for (const p of passes) {
|
||||||
|
if (!ctx.finder.levelAllows(node, p.level)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const got = p.collect(node, ctx);
|
const got = p.collect(node, ctx);
|
||||||
if (got.length > 0) {
|
if (got.length > 0) {
|
||||||
requests = requests.concat(got);
|
requests = requests.concat(got);
|
||||||
@@ -637,6 +684,7 @@ var jiyaPass = {
|
|||||||
name: PASS,
|
name: PASS,
|
||||||
kind: "charify",
|
kind: "charify",
|
||||||
order: 20,
|
order: 20,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
collect(node, ctx) {
|
collect(node, ctx) {
|
||||||
if (inJzChar(node)) {
|
if (inJzChar(node)) {
|
||||||
@@ -690,6 +738,7 @@ var jiyaAdjacencyPass = {
|
|||||||
name: "jiyaAdjacency",
|
name: "jiyaAdjacency",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 25,
|
order: 25,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
const { finder, options } = ctx;
|
const { finder, options } = ctx;
|
||||||
@@ -862,11 +911,15 @@ var jinzePass = {
|
|||||||
name: PASS2,
|
name: PASS2,
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 40,
|
order: 40,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.jinze,
|
enabled: (o) => o.features.jinze,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
|
const anyCjk = new RegExp("[" + ctx.options.ruleset.cjk + "]");
|
||||||
const parents = /* @__PURE__ */ new Set();
|
const parents = /* @__PURE__ */ new Set();
|
||||||
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
ctx.root.querySelectorAll("jz-char").forEach((c) => {
|
||||||
|
if (!ctx.finder.levelAllows(c, "paragraph")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (c.parentNode) {
|
if (c.parentNode) {
|
||||||
parents.add(c.parentNode);
|
parents.add(c.parentNode);
|
||||||
}
|
}
|
||||||
@@ -976,6 +1029,9 @@ function relayout(root, finder) {
|
|||||||
(c) => finder.inScope(c) && !finder.isAvoided(c)
|
(c) => finder.inScope(c) && !finder.isAvoided(c)
|
||||||
);
|
);
|
||||||
for (const el of chars) {
|
for (const el of chars) {
|
||||||
|
if (!finder.levelAllows(el, "paragraph")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!finder.featureEnabledFor(el, "jiya")) {
|
if (!finder.featureEnabledFor(el, "jiya")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1060,7 @@ var lineEdgePass = {
|
|||||||
name: "jiyaLineEdge",
|
name: "jiyaLineEdge",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 90,
|
order: 90,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
if (!hasLayout()) {
|
if (!hasLayout()) {
|
||||||
@@ -1019,7 +1076,7 @@ var lineEdgePass = {
|
|||||||
var TRIM = "jz-hws-trim";
|
var TRIM = "jz-hws-trim";
|
||||||
function trimGaps(root, finder) {
|
function trimGaps(root, finder) {
|
||||||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||||||
(g) => finder.inScope(g) && !finder.isAvoided(g)
|
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph")
|
||||||
);
|
);
|
||||||
if (!gaps.length) {
|
if (!gaps.length) {
|
||||||
return;
|
return;
|
||||||
@@ -1038,6 +1095,7 @@ var gapTrimPass = {
|
|||||||
name: "spacingGapTrim",
|
name: "spacingGapTrim",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 92,
|
order: 92,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.spacing,
|
enabled: (o) => o.features.spacing,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
if (!hasLayout()) {
|
if (!hasLayout()) {
|
||||||
@@ -1068,6 +1126,7 @@ var longWordPass = {
|
|||||||
name: PASS3,
|
name: PASS3,
|
||||||
kind: "charify",
|
kind: "charify",
|
||||||
order: 60,
|
order: 60,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.longWord,
|
enabled: (o) => o.features.longWord,
|
||||||
collect(node, ctx) {
|
collect(node, ctx) {
|
||||||
if (inLangSpan(node)) {
|
if (inLangSpan(node)) {
|
||||||
@@ -1319,6 +1378,7 @@ var spacingPass = {
|
|||||||
name: PASS4,
|
name: PASS4,
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 50,
|
order: 50,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.spacing,
|
enabled: (o) => o.features.spacing,
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
const { finder, options } = ctx;
|
const { finder, options } = ctx;
|
||||||
@@ -1329,6 +1389,9 @@ var spacingPass = {
|
|||||||
}
|
}
|
||||||
processBlock(block, finder, R);
|
processBlock(block, finder, R);
|
||||||
});
|
});
|
||||||
|
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS4) && finder.inScope(ctx.root) && !finder.isAvoided(ctx.root)) {
|
||||||
|
processBlock(ctx.root, finder, R);
|
||||||
|
}
|
||||||
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
||||||
},
|
},
|
||||||
revert(ctx) {
|
revert(ctx) {
|
||||||
@@ -1523,6 +1586,7 @@ function normalizeOptions(opts = {}) {
|
|||||||
ruleset: resolveRuleset(opts.charClass),
|
ruleset: resolveRuleset(opts.charClass),
|
||||||
autospaceClass: opts.autospaceClass || "",
|
autospaceClass: opts.autospaceClass || "",
|
||||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||||
|
levelText: opts.level && opts.level.text || null,
|
||||||
scope: {
|
scope: {
|
||||||
root: scope.root || null,
|
root: scope.root || null,
|
||||||
include: scope.include || null,
|
include: scope.include || null,
|
||||||
|
|||||||
Vendored
+14
@@ -37,6 +37,14 @@ export interface JuzhenOptions {
|
|||||||
/** 自定義字符分類(擴充內置規則集)。把未分類字符或新符號歸入中文/西文/
|
/** 自定義字符分類(擴充內置規則集)。把未分類字符或新符號歸入中文/西文/
|
||||||
* 全寬標點各子類,以覆寫 spacing/jiya 等規則之分類判定(追加語義)。 */
|
* 全寬標點各子類,以覆寫 spacing/jiya 等規則之分類判定(追加語義)。 */
|
||||||
charClass?: CharClassOverride;
|
charClass?: CharClassOverride;
|
||||||
|
/** 功能分級標記(§6.5.0a)。段落級功能(禁則/行端/懸掛/垂懸字/長詞斷字)
|
||||||
|
* 僅作用於段落級元素;文本級功能(中西間隙/標點擠壓)任意文本皆適用。
|
||||||
|
* - `text`:選擇器,命中之子樹**僅文本級**(標題/按鈕/UI 文字典型);亦可
|
||||||
|
* 經元素屬性 `data-jz-level="text"`/`"paragraph"` 標記(屬性優先、最近祖先勝)。
|
||||||
|
* 預設:塊級元素(blockTags)→ 段落級(全功能)。 */
|
||||||
|
level?: {
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
||||||
autospaceClass?: string;
|
autospaceClass?: string;
|
||||||
/** justify 原子化:jz-char/jz-jinze 是否以 inline-block 作對齊單一原子
|
/** justify 原子化:jz-char/jz-jinze 是否以 inline-block 作對齊單一原子
|
||||||
@@ -74,6 +82,8 @@ export interface ResolvedOptions {
|
|||||||
ruleset: Ruleset;
|
ruleset: Ruleset;
|
||||||
autospaceClass: string;
|
autospaceClass: string;
|
||||||
justifyAtoms: boolean;
|
justifyAtoms: boolean;
|
||||||
|
/** 功能分級之 text-level 選擇器(§6.5.0a);null 表示未設。 */
|
||||||
|
levelText: string | null;
|
||||||
scope: {
|
scope: {
|
||||||
root: Element | string | null;
|
root: Element | string | null;
|
||||||
include: string | null;
|
include: string | null;
|
||||||
@@ -126,10 +136,14 @@ export interface WrapRequest {
|
|||||||
}
|
}
|
||||||
/** pass 種類。 */
|
/** pass 種類。 */
|
||||||
export type PassKind = "charify" | "standalone";
|
export type PassKind = "charify" | "standalone";
|
||||||
|
/** 功能分級(§6.5.0a):text=任意文本片段皆適用;paragraph=需行/段布局。 */
|
||||||
|
export type FeatureLevel = "text" | "paragraph";
|
||||||
interface BasePass {
|
interface BasePass {
|
||||||
name: string;
|
name: string;
|
||||||
order: number;
|
order: number;
|
||||||
kind: PassKind;
|
kind: PassKind;
|
||||||
|
/** 功能分級(§6.5.0a)。paragraph 級 pass 對 text-level 元素自動跳過。 */
|
||||||
|
level: FeatureLevel;
|
||||||
enabled: (options: ResolvedOptions) => boolean;
|
enabled: (options: ResolvedOptions) => boolean;
|
||||||
revert: (ctx: RenderContext) => void;
|
revert: (ctx: RenderContext) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-1
@@ -4,7 +4,7 @@
|
|||||||
// 行內元素透明、pill/block/avoid 中斷)、pill 鄰字查找、per-node 功能閘
|
// 行內元素透明、pill/block/avoid 中斷)、pill 鄰字查找、per-node 功能閘
|
||||||
// (featureEnabledFor)。finder 不知具體排版規則,只提供遍歷與安全變更原語。
|
// (featureEnabledFor)。finder 不知具體排版規則,只提供遍歷與安全變更原語。
|
||||||
|
|
||||||
import type { ResolvedOptions } from "../types.js";
|
import type { FeatureLevel, ResolvedOptions } from "../types.js";
|
||||||
|
|
||||||
/** 邏輯 run 中之單一字元來源位置。 */
|
/** 邏輯 run 中之單一字元來源位置。 */
|
||||||
export interface CharPos
|
export interface CharPos
|
||||||
@@ -41,7 +41,9 @@ export class Finder
|
|||||||
private readonly includeSelector: string | null;
|
private readonly includeSelector: string | null;
|
||||||
private readonly skipAttr: string | null;
|
private readonly skipAttr: string | null;
|
||||||
private readonly userIsSkipped: ((node: Node) => boolean) | null;
|
private readonly userIsSkipped: ((node: Node) => boolean) | null;
|
||||||
|
private readonly levelTextSelector: string | null;
|
||||||
private readonly featureCache: WeakMap<Element, Record<string, boolean>>;
|
private readonly featureCache: WeakMap<Element, Record<string, boolean>>;
|
||||||
|
private readonly levelCache: WeakMap<Element, FeatureLevel>;
|
||||||
|
|
||||||
constructor(opts: ResolvedOptions)
|
constructor(opts: ResolvedOptions)
|
||||||
{
|
{
|
||||||
@@ -52,9 +54,11 @@ export class Finder
|
|||||||
this.pillSelector = opts.finder.pillSelector;
|
this.pillSelector = opts.finder.pillSelector;
|
||||||
this.skipAttr = opts.finder.skipAttribute;
|
this.skipAttr = opts.finder.skipAttribute;
|
||||||
this.userIsSkipped = opts.finder.isSkipped;
|
this.userIsSkipped = opts.finder.isSkipped;
|
||||||
|
this.levelTextSelector = opts.levelText;
|
||||||
this.avoidSelector = opts.scope.avoid;
|
this.avoidSelector = opts.scope.avoid;
|
||||||
this.includeSelector = opts.scope.include;
|
this.includeSelector = opts.scope.include;
|
||||||
this.featureCache = new WeakMap();
|
this.featureCache = new WeakMap();
|
||||||
|
this.levelCache = new WeakMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||||
@@ -278,6 +282,46 @@ export class Finder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 節點之功能級別(§6.5.0a)。自 node 向上找**最近**之顯式信號:元素屬性
|
||||||
|
* `data-jz-level="text|paragraph"`(最優先),或命中 `level.text` 選擇器(→ text);
|
||||||
|
* 皆無則預設 paragraph(全功能)。結果以解析後之元素為鍵快取。
|
||||||
|
*/
|
||||||
|
levelFor(node: Node): FeatureLevel
|
||||||
|
{
|
||||||
|
const start = node.nodeType === 1 ? (node as Element) : node.parentElement;
|
||||||
|
if (!start) { return "paragraph"; }
|
||||||
|
const cached = this.levelCache.get(start);
|
||||||
|
if (cached) { return cached; }
|
||||||
|
|
||||||
|
let el: Element | null = start;
|
||||||
|
let result: FeatureLevel = "paragraph";
|
||||||
|
while (el)
|
||||||
|
{
|
||||||
|
const attr = el.getAttribute ? el.getAttribute("data-jz-level") : null;
|
||||||
|
if (attr === "text" || attr === "paragraph") { result = attr; break; }
|
||||||
|
if (this.levelTextSelector && el.matches && el.matches(this.levelTextSelector))
|
||||||
|
{
|
||||||
|
result = "text";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
this.levelCache.set(start, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 某級別之 pass 是否可作用於該節點(§6.5.0a gate)。文本級 pass 不受限(恆 true);
|
||||||
|
* 段落級 pass 僅作用於段落級元素(text-level 子樹跳過)。與 featureEnabledFor 疊加:
|
||||||
|
* level 為粗粒度語義組(先判),blocks/屬性為細粒度逐功能(後調)。
|
||||||
|
*/
|
||||||
|
levelAllows(node: Node, passLevel: FeatureLevel): boolean
|
||||||
|
{
|
||||||
|
if (passLevel === "text") { return true; }
|
||||||
|
return this.levelFor(node) === "paragraph";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
* 解析節點對某功能之有效啟用狀態(§3.6,per-node 閘)。向上找最近帶
|
||||||
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
* 功能指令之祖先(data-juzhen 白名單 / data-juzhen-off 黑名單 / 命中
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ function runCharify(passes: CharifyPass[], ctx: RenderContext): void
|
|||||||
let requests: WrapRequest[] = [];
|
let requests: WrapRequest[] = [];
|
||||||
for (const p of passes)
|
for (const p of passes)
|
||||||
{
|
{
|
||||||
|
// 功能分級閘(§6.5.0a):段落級 charify pass(如 longWord)對 text-level
|
||||||
|
// 子樹跳過;文本級(如 jiya)不受限。
|
||||||
|
if (!ctx.finder.levelAllows(node, p.level)) { continue; }
|
||||||
const got = p.collect(node, ctx);
|
const got = p.collect(node, ctx);
|
||||||
if (got.length > 0) { requests = requests.concat(got); }
|
if (got.length > 0) { requests = requests.concat(got); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export function normalizeOptions(opts: JuzhenOptions = {}): ResolvedOptions
|
|||||||
ruleset: resolveRuleset(opts.charClass),
|
ruleset: resolveRuleset(opts.charClass),
|
||||||
autospaceClass: opts.autospaceClass || "",
|
autospaceClass: opts.autospaceClass || "",
|
||||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||||
|
levelText: (opts.level && opts.level.text) || null,
|
||||||
scope: {
|
scope: {
|
||||||
root: scope.root || null,
|
root: scope.root || null,
|
||||||
include: scope.include || null,
|
include: scope.include || null,
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ export interface JuzhenOptions
|
|||||||
* 全寬標點各子類,以覆寫 spacing/jiya 等規則之分類判定(追加語義)。 */
|
* 全寬標點各子類,以覆寫 spacing/jiya 等規則之分類判定(追加語義)。 */
|
||||||
charClass?: CharClassOverride;
|
charClass?: CharClassOverride;
|
||||||
|
|
||||||
|
/** 功能分級標記(§6.5.0a)。段落級功能(禁則/行端/懸掛/垂懸字/長詞斷字)
|
||||||
|
* 僅作用於段落級元素;文本級功能(中西間隙/標點擠壓)任意文本皆適用。
|
||||||
|
* - `text`:選擇器,命中之子樹**僅文本級**(標題/按鈕/UI 文字典型);亦可
|
||||||
|
* 經元素屬性 `data-jz-level="text"`/`"paragraph"` 標記(屬性優先、最近祖先勝)。
|
||||||
|
* 預設:塊級元素(blockTags)→ 段落級(全功能)。 */
|
||||||
|
level?: {
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
||||||
autospaceClass?: string;
|
autospaceClass?: string;
|
||||||
|
|
||||||
@@ -96,6 +105,8 @@ export interface ResolvedOptions
|
|||||||
ruleset: Ruleset;
|
ruleset: Ruleset;
|
||||||
autospaceClass: string;
|
autospaceClass: string;
|
||||||
justifyAtoms: boolean;
|
justifyAtoms: boolean;
|
||||||
|
/** 功能分級之 text-level 選擇器(§6.5.0a);null 表示未設。 */
|
||||||
|
levelText: string | null;
|
||||||
scope: {
|
scope: {
|
||||||
root: Element | string | null;
|
root: Element | string | null;
|
||||||
include: string | null;
|
include: string | null;
|
||||||
@@ -148,11 +159,16 @@ export interface WrapRequest
|
|||||||
/** pass 種類。 */
|
/** pass 種類。 */
|
||||||
export type PassKind = "charify" | "standalone";
|
export type PassKind = "charify" | "standalone";
|
||||||
|
|
||||||
|
/** 功能分級(§6.5.0a):text=任意文本片段皆適用;paragraph=需行/段布局。 */
|
||||||
|
export type FeatureLevel = "text" | "paragraph";
|
||||||
|
|
||||||
interface BasePass
|
interface BasePass
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
order: number;
|
order: number;
|
||||||
kind: PassKind;
|
kind: PassKind;
|
||||||
|
/** 功能分級(§6.5.0a)。paragraph 級 pass 對 text-level 元素自動跳過。 */
|
||||||
|
level: FeatureLevel;
|
||||||
enabled: (options: ResolvedOptions) => boolean;
|
enabled: (options: ResolvedOptions) => boolean;
|
||||||
revert: (ctx: RenderContext) => void;
|
revert: (ctx: RenderContext) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export const jinzePass: StandalonePass = {
|
|||||||
name: PASS,
|
name: PASS,
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 40,
|
order: 40,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.jinze,
|
enabled: (o) => o.features.jinze,
|
||||||
render(ctx: RenderContext): void
|
render(ctx: RenderContext): void
|
||||||
{
|
{
|
||||||
@@ -151,6 +152,8 @@ export const jinzePass: StandalonePass = {
|
|||||||
const parents = new Set<Node>();
|
const parents = new Set<Node>();
|
||||||
ctx.root.querySelectorAll("jz-char").forEach((c) =>
|
ctx.root.querySelectorAll("jz-char").forEach((c) =>
|
||||||
{
|
{
|
||||||
|
// 功能分級閘(§6.5.0a):禁則為段落級,text-level 子樹(標題/按鈕)跳過。
|
||||||
|
if (!ctx.finder.levelAllows(c, "paragraph")) { return; }
|
||||||
if (c.parentNode) { parents.add(c.parentNode); }
|
if (c.parentNode) { parents.add(c.parentNode); }
|
||||||
});
|
});
|
||||||
for (const parent of parents) { processParent(parent, ctx.finder, anyCjk); }
|
for (const parent of parents) { processParent(parent, ctx.finder, anyCjk); }
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const jiyaPass: CharifyPass = {
|
|||||||
name: PASS,
|
name: PASS,
|
||||||
kind: "charify",
|
kind: "charify",
|
||||||
order: 20,
|
order: 20,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
collect(node, ctx): WrapRequest[]
|
collect(node, ctx): WrapRequest[]
|
||||||
{
|
{
|
||||||
@@ -123,6 +124,7 @@ export const jiyaAdjacencyPass: StandalonePass = {
|
|||||||
name: "jiyaAdjacency",
|
name: "jiyaAdjacency",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 25,
|
order: 25,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
render(ctx: RenderContext): void
|
render(ctx: RenderContext): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ function relayout(root: Element, finder: Finder): void
|
|||||||
|
|
||||||
for (const el of chars)
|
for (const el of chars)
|
||||||
{
|
{
|
||||||
|
// 行端為段落級(§6.5.0a):text-level 子樹(單行標題等)不收行端半形。
|
||||||
|
if (!finder.levelAllows(el, "paragraph")) { continue; }
|
||||||
if (!finder.featureEnabledFor(el, "jiya")) { continue; }
|
if (!finder.featureEnabledFor(el, "jiya")) { continue; }
|
||||||
// 行端僅處理括號引號:開類於行首收左、閉類於行尾收右。
|
// 行端僅處理括號引號:開類於行首收左、閉類於行尾收右。
|
||||||
// **不**處理句末/句內點號(。!?,等)——行尾/行首之點號保持全形,
|
// **不**處理句末/句內點號(。!?,等)——行尾/行首之點號保持全形,
|
||||||
@@ -149,6 +151,7 @@ export const lineEdgePass: StandalonePass = {
|
|||||||
name: "jiyaLineEdge",
|
name: "jiyaLineEdge",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 90,
|
order: 90,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.jiya,
|
enabled: (o) => o.features.jiya,
|
||||||
render(ctx: RenderContext): void
|
render(ctx: RenderContext): void
|
||||||
{
|
{
|
||||||
@@ -174,7 +177,7 @@ const TRIM = "jz-hws-trim";
|
|||||||
function trimGaps(root: Element, finder: Finder): void
|
function trimGaps(root: Element, finder: Finder): void
|
||||||
{
|
{
|
||||||
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
const gaps = Array.from(root.querySelectorAll("jz-hws")).filter(
|
||||||
(g) => finder.inScope(g) && !finder.isAvoided(g),
|
(g) => finder.inScope(g) && !finder.isAvoided(g) && finder.levelAllows(g, "paragraph"),
|
||||||
);
|
);
|
||||||
if (!gaps.length) { return; }
|
if (!gaps.length) { return; }
|
||||||
|
|
||||||
@@ -207,6 +210,7 @@ export const gapTrimPass: StandalonePass = {
|
|||||||
name: "spacingGapTrim",
|
name: "spacingGapTrim",
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 92,
|
order: 92,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.spacing,
|
enabled: (o) => o.features.spacing,
|
||||||
render(ctx: RenderContext): void
|
render(ctx: RenderContext): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const longWordPass: CharifyPass = {
|
|||||||
name: PASS,
|
name: PASS,
|
||||||
kind: "charify",
|
kind: "charify",
|
||||||
order: 60,
|
order: 60,
|
||||||
|
level: "paragraph",
|
||||||
enabled: (o) => o.features.longWord,
|
enabled: (o) => o.features.longWord,
|
||||||
collect(node, ctx): WrapRequest[]
|
collect(node, ctx): WrapRequest[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ export const spacingPass: StandalonePass = {
|
|||||||
name: PASS,
|
name: PASS,
|
||||||
kind: "standalone",
|
kind: "standalone",
|
||||||
order: 50,
|
order: 50,
|
||||||
|
level: "text",
|
||||||
enabled: (o) => o.features.spacing,
|
enabled: (o) => o.features.spacing,
|
||||||
render(ctx: RenderContext): void
|
render(ctx: RenderContext): void
|
||||||
{
|
{
|
||||||
@@ -318,6 +319,15 @@ export const spacingPass: StandalonePass = {
|
|||||||
if (!finder.featureEnabledFor(block, PASS)) { return; }
|
if (!finder.featureEnabledFor(block, PASS)) { return; }
|
||||||
processBlock(block, finder, R);
|
processBlock(block, finder, R);
|
||||||
});
|
});
|
||||||
|
// 文本級功能須支持單元素(§6.5.0a):root 為行內/單一非塊元素(如消費端對
|
||||||
|
// <span> 標題呼叫 render)時 eachBlock 不含之,補處理 root 自身(collectRuns
|
||||||
|
// 遇巢狀 block 仍會 flush,故與上面逐 block 不重複;行內 root 通常無 block 子)。
|
||||||
|
if (!options.finder.blockTags.has(ctx.root.nodeName)
|
||||||
|
&& finder.featureEnabledFor(ctx.root, PASS)
|
||||||
|
&& finder.inScope(ctx.root) && !finder.isAvoided(ctx.root))
|
||||||
|
{
|
||||||
|
processBlock(ctx.root, finder, R);
|
||||||
|
}
|
||||||
// Pass B:pill 邊界。
|
// Pass B:pill 邊界。
|
||||||
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -642,3 +642,63 @@ test("charClass.biaodian:多子類同傳,各自生效(stop/pause)", ()
|
|||||||
const pause = p.querySelector("jz-char.bd-pause");
|
const pause = p.querySelector("jz-char.bd-pause");
|
||||||
assert.ok(pause && pause.classList.contains("jz-half"), "• → bd-pause 且開明式挤半形");
|
assert.ok(pause && pause.classList.contains("jz-half"), "• → bd-pause 且開明式挤半形");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- 功能分級(§6.5.0a,text-level vs paragraph-level)-----
|
||||||
|
|
||||||
|
test("分級:data-jz-level=text 之子樹只跑文本級——jiya/spacing 生效、jinze 跳過", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p data-jz-level="text" lang="zh-Hant">說「你好」用Neovim寫</p>');
|
||||||
|
createJuzhen().render(document.body);
|
||||||
|
const p = document.querySelector("p");
|
||||||
|
// 文本級:標點 charify(jz-char)、中西間隙(jz-hws)照常。
|
||||||
|
assert.ok(countTag(p, "jz-char") >= 1, "text-level 仍跑 jiya(jz-char)");
|
||||||
|
assert.ok(countTag(p, "jz-hws") >= 1, "text-level 仍跑 spacing(jz-hws)");
|
||||||
|
// 段落級:禁則跳過——無 jz-jinze(「」本會綁鄰字)。
|
||||||
|
assert.equal(countTag(p, "jz-jinze"), 0, "text-level 段落級禁則跳過(無 jz-jinze)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("分級:data-jz-level=text 跳過段落級 longWord(charify 亦受閘)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p data-jz-level="text">用 configuration 設定</p>');
|
||||||
|
createJuzhen().render(document.body);
|
||||||
|
const p = document.querySelector("p");
|
||||||
|
assert.equal(p.querySelectorAll('[data-jz="longWord"]').length, 0, "text-level 不跑 longWord(無 span[data-jz=longWord])");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("分級:選項 level.text 選擇器標記(h1 → text-level,禁則跳過)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<h1 lang="zh-Hant">說「你好」結束</h1><p lang="zh-Hant">說「你好」結束</p>');
|
||||||
|
createJuzhen({ level: { text: "h1" } }).render(document.body);
|
||||||
|
const h1 = document.querySelector("h1");
|
||||||
|
const p = document.querySelector("p");
|
||||||
|
assert.equal(countTag(h1, "jz-jinze"), 0, "h1 命中 level.text → 段落級禁則跳過");
|
||||||
|
assert.ok(countTag(h1, "jz-char") >= 1, "h1 文本級 jiya 仍生效");
|
||||||
|
assert.ok(countTag(p, "jz-jinze") >= 1, "未命中之 p 仍跑禁則");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("分級:data-jz-level=paragraph 巢狀於 text 子樹內,最近祖先勝(恢復全功能)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom(
|
||||||
|
'<div data-jz-level="text" lang="zh-Hant">'
|
||||||
|
+ '<p data-jz-level="paragraph">說「你好」結束</p></div>',
|
||||||
|
);
|
||||||
|
createJuzhen().render(document.body);
|
||||||
|
const p = document.querySelector("p");
|
||||||
|
assert.ok(countTag(p, "jz-jinze") >= 1, "最近 data-jz-level=paragraph 勝 → 禁則恢復");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("分級:spacing 文本級支持單元素 root(render 於 <span>)", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom("<span>我用Neovim寫程式</span>");
|
||||||
|
const span = document.querySelector("span");
|
||||||
|
createJuzhen({ jiya: false, jinze: false, longWord: false }).render(span);
|
||||||
|
assert.ok(countTag(span, "jz-hws") >= 1, "行內單元素 root 亦補中西間隙");
|
||||||
|
assert.equal(span.textContent, "我用Neovim寫程式", "textContent 不變");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("分級:預設(無標記)塊級元素為段落級——禁則照常", () =>
|
||||||
|
{
|
||||||
|
const { document } = setupDom('<p lang="zh-Hant">說「你好」結束</p>');
|
||||||
|
createJuzhen().render(document.body);
|
||||||
|
assert.ok(countTag(document.querySelector("p"), "jz-jinze") >= 1, "預設段落級,禁則綁定");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user