fix: 行內元素三分類(透明/隔離/整體)——jinze 下潛肢解 KaTeX 修復(下游問題六);v1 相容層移除
下游問題六:行內公式緊鄰禁則標點時,jinze 為綁鄰字透明下潛 .katex(全樹皆 SPAN,
僅憑標籤名判透明),把公式內側末字連同完整殼層逐層克隆切出(splitoff),上標漂移
錯位;pillSelector/skipAttribute/scope.avoid 三層防護全被刺穿。復現另揭兩向量:
相鄰 pill 時 descend 刺穿致 jz-hws 插入公式內部;pill 內部標點被 jiya charify。
確立三分類(§4.1,所有遍歷原語共用同一判定):
透明(styleInlines+jz-*)— 穿越/run 延續/可克隆切分;
隔離(isolateSelector,預設 "mark",新增選項)— 邊界斷 run/斷相鄰/不可切分、
兩端補隙(同 pill),內部照常處理(自成脈絡);
整體(pillSelector)— 內部對一切 pass 不可見(insidePill 過濾),禁則時作為
原子鄰字單位整體綁入 jz-jinze。
機制修復:
finder — transparentInline 改以元素判定(先排除 pill/isolate/avoid 再查標籤集);
avoidsSelf 自身判定(isAvoided 僅查祖先,自身命中 avoid 曾被穿越);descend 三態
(邊界字/無內容續找兄弟/blocked 終止——不可越界跳找更遠之字);eachTextNode
過濾 pill 內部;collectRuns 隔離元素內部自成 run。
core/split — canSplit 諮詢 finder(pill/isolate/avoid 不可切 → 退化整綁);
MARK 移出 SPLITTABLE(背景盒切分有可見接縫)。
jiyaAdjacency — pill/avoid(自身判定)斷相鄰不下探;isolate 斷而內部自成脈絡。
spacing Pass B — 邊界選擇器擴為 pill ∪ isolate;巢於 pill 內者跳過。
orphan — avoidsSelf 邊界;avoid 塊不綁(舊漏判:avoid 子樹內裸文本曾被計數包綁)。
v1 相容層移除(消費端已全數遷移 v2 API):src/compat/v1.ts、IIFE 全域別名
createCjkAutospace、死選項 autospaceClass(經查從未被任何 pass 讀取)一併刪;
I8 退役。dist/compat 殘留聲明清除、dist/core/split.d.ts 補入庫(前次遺漏)。
測試 112 全綠(問題六 ×7、isolate ×6、compat 移除 ×1);headless Chrome 實證
公式完整(splitoff=0、內部無 jz-hws/jz-char)、mark 與公式整體入 jz-jinze。
文檔:README 三分類節+選項示例+功能表(hanging 行補「已移除」);ARCHITECTURE
§4.1 三分類/§3.5 相容層移除/§6.1/§6.3/§6.4/I8 退役。
This commit is contained in:
Vendored
-29
@@ -1,29 +0,0 @@
|
||||
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
+4
@@ -16,6 +16,10 @@ export declare function createMarker(tag: JzTag, pass: string): HTMLElement;
|
||||
/**
|
||||
* 還原某 pass 之全部產物(I6)。以 data-jz 定址,逆文件序處理(先內層後
|
||||
* 外層),marker 移除、wrap 解包,最後 normalize 受影響父節點合併文本。
|
||||
*
|
||||
* splitoff(克隆切分之邊界詞元,見 core/split.ts)須在 wrap 全部解包**之後**才併回:
|
||||
* 否則其前兄弟可能仍是 wrap 殼(如 jz-jinze)而非原元素。故主迴圈跳過 splitoff,於
|
||||
* 解包後第二階段以文件序逐一併回前兄弟(同標籤元素),還原為單一元素(I6 一致)。
|
||||
*/
|
||||
export declare function revertPass(pass: string, root: ParentNode): void;
|
||||
/** 讀取 jz-char 之 bd-* 子類(無則 null)。 */
|
||||
|
||||
Vendored
+32
-7
@@ -21,6 +21,7 @@ export declare class Finder {
|
||||
private readonly blockSet;
|
||||
private readonly skipSet;
|
||||
private readonly pillSelector;
|
||||
private readonly isolateSelector;
|
||||
private readonly avoidSelector;
|
||||
private readonly includeSelector;
|
||||
private readonly skipAttr;
|
||||
@@ -29,18 +30,42 @@ export declare class Finder {
|
||||
private readonly featureCache;
|
||||
private readonly levelCache;
|
||||
constructor(opts: ResolvedOptions);
|
||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||
/** 該元素是否匹配 pill 選擇器(三分類之「整體」:code/kbd/行內公式等)。 */
|
||||
pillMatches(el: Element): boolean;
|
||||
/** 該元素是否匹配 isolate 選擇器(三分類之「隔離」:帶盒樣式之行內,如背景色
|
||||
* <mark>)。邊界口徑同 pill(斷 run/斷相鄰/不可切分/兩端補隙),內部照常處理。 */
|
||||
isolateMatches(el: Element): boolean;
|
||||
/** 節點是否位於 pill **內部**(僅查祖先,不含自身)。pill 為整體元素:內部對
|
||||
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
||||
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
||||
insidePill(node: Node): boolean;
|
||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組)。穿越這些才能正確找到跨包裝之邏輯
|
||||
* 鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左鄰字仍須可達;I7)。 */
|
||||
private isTransparentInline;
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
||||
* 鄰字仍須可達;I7)。
|
||||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
||||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
||||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
||||
* 為「。」、不補間隙;舊邏輯卻越過 jz-char 取到「字」→ 誤包 jz-hws 於「字。」之間)。
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
private transparentInline;
|
||||
/** 該元素**自身**是否為 avoid 邊界(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / skipAttribute / avoid 選擇器 / isSkipped)。isAvoided 沿祖先
|
||||
* 鏈逐層呼叫本判定;遍歷原語另須對「正要進入」之元素直接判自身——舊版僅查
|
||||
* 祖先,元素自身命中 avoid 時被當透明穿越(下游問題六:scope.avoid 對 .katex
|
||||
* 形同虛設)。 */
|
||||
avoidsSelf(el: Element): boolean;
|
||||
/** 節點是否處於 avoid 子樹(祖先鏈上任一元素 avoidsSelf;自身不計——元素
|
||||
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
|
||||
isAvoided(node: Node): boolean;
|
||||
/** scope.include 啟用判定:未設則恆 true;設則須有命中祖先(opt-in)。 */
|
||||
inScope(node: Node): boolean;
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / 非作用域)。 */
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / pill 內部 / 非作用域)。 */
|
||||
eachTextNode(root: Element, cb: (node: Text) => void): void;
|
||||
/** 走訪 root 下所有 block 元素(含 root 本身若為 block)。 */
|
||||
eachBlock(root: Element, cb: (block: Element) => void): void;
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import type { Finder } from "./finder.js";
|
||||
/**
|
||||
* 隔離 el 之邊界詞元供綁定(最小綁定)。side="tail" 取末詞元、="head" 取首詞元。
|
||||
* 回傳**應綁定之節點**:
|
||||
* · 可切且確實切分 → tail 回克隆(末詞元)、head 回原 el(已只剩首詞元,其餘入克隆);
|
||||
* · 不可切/無法定位切點/整元素即該詞元 → 回 el 本身(綁整個元素,退化但安全)。
|
||||
* 切出之克隆標 splitoff,由 revertPass 併回;故 render→revert 還原一致(I6)。
|
||||
*/
|
||||
export declare function isolateBoundaryToken(el: Element, side: "head" | "tail", anyCjk: RegExp, pass: string, finder: Finder): Element;
|
||||
/**
|
||||
* 把 el 之**指定切點**(精確字位)起至末尾剝入克隆並回傳(供 orphan:自實義字綁至塊末)。
|
||||
* 不可切或切點在最起點 → 回 null(呼叫端綁整個直接子)。
|
||||
*/
|
||||
export declare function splitElementAt(el: Element, textNode: Text, offset: number, pass: string, finder: Finder): Element | null;
|
||||
Vendored
-1
@@ -1,7 +1,6 @@
|
||||
import type { JuzhenOptions, Pass, ResolvedOptions } from "./types.js";
|
||||
export type { JuzhenOptions, ResolvedOptions, Pass };
|
||||
export type { CharClassOverride, BdSubclass, Ruleset, Bias, BiasTable } from "./types.js";
|
||||
export { createCjkAutospace } from "./compat/v1.js";
|
||||
/** 正規化公開選項為內部 ResolvedOptions。 */
|
||||
export declare function normalizeOptions(opts?: JuzhenOptions): ResolvedOptions;
|
||||
export interface Juzhen {
|
||||
|
||||
Vendored
+128
-124
@@ -21,7 +21,6 @@ var Juzhen = (() => {
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
createCjkAutospace: () => createCjkAutospace,
|
||||
createJuzhen: () => createJuzhen,
|
||||
normalizeOptions: () => normalizeOptions
|
||||
});
|
||||
@@ -35,6 +34,7 @@ var Juzhen = (() => {
|
||||
this.blockSet = opts.finder.blockTags;
|
||||
this.skipSet = opts.finder.skipTags;
|
||||
this.pillSelector = opts.finder.pillSelector;
|
||||
this.isolateSelector = opts.finder.isolateSelector;
|
||||
this.skipAttr = opts.finder.skipAttribute;
|
||||
this.userIsSkipped = opts.finder.isSkipped;
|
||||
this.levelTextSelector = opts.levelText;
|
||||
@@ -43,14 +43,35 @@ var Juzhen = (() => {
|
||||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||||
this.levelCache = /* @__PURE__ */ new WeakMap();
|
||||
}
|
||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||
/** 該元素是否匹配 pill 選擇器(三分類之「整體」:code/kbd/行內公式等)。 */
|
||||
pillMatches(el) {
|
||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||
}
|
||||
/** 該元素是否匹配 isolate 選擇器(三分類之「隔離」:帶盒樣式之行內,如背景色
|
||||
* <mark>)。邊界口徑同 pill(斷 run/斷相鄰/不可切分/兩端補隙),內部照常處理。 */
|
||||
isolateMatches(el) {
|
||||
return !!(el.matches && this.isolateSelector && el.matches(this.isolateSelector));
|
||||
}
|
||||
/** 節點是否位於 pill **內部**(僅查祖先,不含自身)。pill 為整體元素:內部對
|
||||
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
||||
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
||||
insidePill(node) {
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
if (this.pillMatches(p)) {
|
||||
return true;
|
||||
}
|
||||
p = p.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
||||
* 鄰字仍須可達;I7)。
|
||||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
||||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
||||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
||||
@@ -58,34 +79,51 @@ var Juzhen = (() => {
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
isTransparentInline(name) {
|
||||
transparentInline(el) {
|
||||
if (this.pillMatches(el) || this.isolateMatches(el) || this.avoidsSelf(el)) {
|
||||
return false;
|
||||
}
|
||||
const name = el.nodeName;
|
||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
/** 該元素**自身**是否為 avoid 邊界(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / skipAttribute / avoid 選擇器 / isSkipped)。isAvoided 沿祖先
|
||||
* 鏈逐層呼叫本判定;遍歷原語另須對「正要進入」之元素直接判自身——舊版僅查
|
||||
* 祖先,元素自身命中 avoid 時被當透明穿越(下游問題六:scope.avoid 對 .katex
|
||||
* 形同虛設)。 */
|
||||
avoidsSelf(el) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(el)) {
|
||||
return true;
|
||||
}
|
||||
if (this.skipSet.has(el.nodeName)) {
|
||||
return true;
|
||||
}
|
||||
if (el.namespaceURI === SVG_NS) {
|
||||
return true;
|
||||
}
|
||||
if (el.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
if (el.hasAttribute("data-jz-skip")) {
|
||||
return true;
|
||||
}
|
||||
if (this.skipAttr && el.hasAttribute(this.skipAttr)) {
|
||||
return true;
|
||||
}
|
||||
if (this.avoidSelector && el.matches && el.matches(this.avoidSelector)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(祖先鏈上任一元素 avoidsSelf;自身不計——元素
|
||||
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
|
||||
isAvoided(node) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return true;
|
||||
}
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
const el = p;
|
||||
if (this.skipSet.has(el.nodeName)) {
|
||||
return true;
|
||||
}
|
||||
if (el.namespaceURI === SVG_NS) {
|
||||
return true;
|
||||
}
|
||||
if (el.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
if (el.hasAttribute("data-jz-skip")) {
|
||||
return true;
|
||||
}
|
||||
if (this.skipAttr && el.hasAttribute(this.skipAttr)) {
|
||||
return true;
|
||||
}
|
||||
if (this.avoidSelector && el.matches && el.matches(this.avoidSelector)) {
|
||||
if (this.avoidsSelf(p)) {
|
||||
return true;
|
||||
}
|
||||
p = p.parentNode;
|
||||
@@ -103,7 +141,7 @@ var Juzhen = (() => {
|
||||
}
|
||||
return !!(el.closest && el.closest(this.includeSelector));
|
||||
}
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / 非作用域)。 */
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / pill 內部 / 非作用域)。 */
|
||||
eachTextNode(root, cb) {
|
||||
const ownerDoc = root.ownerDocument || (typeof document !== "undefined" ? document : null);
|
||||
if (!ownerDoc) {
|
||||
@@ -114,7 +152,7 @@ var Juzhen = (() => {
|
||||
let n = walker.nextNode();
|
||||
while (n) {
|
||||
const t = n;
|
||||
if (!this.isAvoided(t) && this.inScope(t)) {
|
||||
if (!this.isAvoided(t) && !this.insidePill(t) && this.inScope(t)) {
|
||||
nodes.push(t);
|
||||
}
|
||||
n = walker.nextNode();
|
||||
@@ -176,11 +214,7 @@ var Juzhen = (() => {
|
||||
return;
|
||||
}
|
||||
const el = node;
|
||||
if (this.isAvoided(el)) {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
if (this.skipSet.has(el.nodeName) || this.blockSet.has(el.nodeName)) {
|
||||
if (this.avoidsSelf(el) || this.blockSet.has(el.nodeName)) {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
@@ -188,6 +222,16 @@ var Juzhen = (() => {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
if (this.isolateMatches(el)) {
|
||||
flush();
|
||||
let ic = el.firstChild;
|
||||
while (ic) {
|
||||
visit(ic);
|
||||
ic = ic.nextSibling;
|
||||
}
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
let c2 = el.firstChild;
|
||||
while (c2) {
|
||||
visit(c2);
|
||||
@@ -223,7 +267,10 @@ var Juzhen = (() => {
|
||||
if (n2.nodeType === 3 && n2.data.length > 0) {
|
||||
return pickEnd(n2);
|
||||
}
|
||||
if (n2.nodeType === 1 && this.isTransparentInline(n2.nodeName)) {
|
||||
if (n2.nodeType === 1) {
|
||||
if (!this.transparentInline(n2)) {
|
||||
return "blocked";
|
||||
}
|
||||
const r = descend(n2);
|
||||
if (r) {
|
||||
return r;
|
||||
@@ -246,22 +293,22 @@ var Juzhen = (() => {
|
||||
}
|
||||
if (n.nodeType === 1) {
|
||||
const e = n;
|
||||
if (this.isAvoided(e)) {
|
||||
if (!this.transparentInline(e)) {
|
||||
return null;
|
||||
}
|
||||
if (this.isTransparentInline(e.nodeName)) {
|
||||
const r = descend(e);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
n = direction === -1 ? e.previousSibling : e.nextSibling;
|
||||
continue;
|
||||
const r = descend(e);
|
||||
if (r === "blocked") {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
n = direction === -1 ? e.previousSibling : e.nextSibling;
|
||||
continue;
|
||||
}
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
}
|
||||
if (!p || p.nodeType !== 1 || !this.isTransparentInline(p.nodeName)) {
|
||||
if (!p || p.nodeType !== 1 || !this.transparentInline(p)) {
|
||||
return null;
|
||||
}
|
||||
const pe = p;
|
||||
@@ -840,7 +887,11 @@ var Juzhen = (() => {
|
||||
prev = el;
|
||||
} else if (finder.pillMatches(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
} else if (finder.avoidsSelf(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isolateMatches(el)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
@@ -868,26 +919,25 @@ var Juzhen = (() => {
|
||||
"U",
|
||||
"S",
|
||||
"SPAN",
|
||||
"MARK",
|
||||
"SMALL",
|
||||
"INS",
|
||||
"DEL",
|
||||
"SUB",
|
||||
"SUP"
|
||||
]);
|
||||
function canSplit(el) {
|
||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id");
|
||||
function canSplit(el, finder) {
|
||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && !finder.pillMatches(el) && !finder.isolateMatches(el) && !finder.avoidsSelf(el);
|
||||
}
|
||||
function firstText(el) {
|
||||
function firstText(el, finder) {
|
||||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3 && c.data.length > 0) {
|
||||
return c;
|
||||
}
|
||||
if (c.nodeType === 1) {
|
||||
if (!canSplit(c)) {
|
||||
if (!canSplit(c, finder)) {
|
||||
return null;
|
||||
}
|
||||
const r = firstText(c);
|
||||
const r = firstText(c, finder);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
@@ -895,16 +945,16 @@ var Juzhen = (() => {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function lastText(el) {
|
||||
function lastText(el, finder) {
|
||||
for (let c = el.lastChild; c; c = c.previousSibling) {
|
||||
if (c.nodeType === 3 && c.data.length > 0) {
|
||||
return c;
|
||||
}
|
||||
if (c.nodeType === 1) {
|
||||
if (!canSplit(c)) {
|
||||
if (!canSplit(c, finder)) {
|
||||
return null;
|
||||
}
|
||||
const r = lastText(c);
|
||||
const r = lastText(c, finder);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
@@ -936,8 +986,8 @@ var Juzhen = (() => {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function splitTreeAfter(top, textNode, offset, pass) {
|
||||
if (offset <= 0 && firstText(top) === textNode) {
|
||||
function splitTreeAfter(top, textNode, offset, pass, finder) {
|
||||
if (offset <= 0 && firstText(top, finder) === textNode) {
|
||||
return null;
|
||||
}
|
||||
let rightAtLevel;
|
||||
@@ -987,26 +1037,26 @@ var Juzhen = (() => {
|
||||
}
|
||||
return topClone;
|
||||
}
|
||||
function isolateBoundaryToken(el, side, anyCjk, pass) {
|
||||
if (!canSplit(el)) {
|
||||
function isolateBoundaryToken(el, side, anyCjk, pass, finder) {
|
||||
if (!canSplit(el, finder)) {
|
||||
return el;
|
||||
}
|
||||
const text = side === "tail" ? lastText(el) : firstText(el);
|
||||
const text = side === "tail" ? lastText(el, finder) : firstText(el, finder);
|
||||
if (!text) {
|
||||
return el;
|
||||
}
|
||||
const point = side === "tail" ? { textNode: text, offset: trailingTokenStart(text.data, anyCjk) } : { textNode: text, offset: leadingTokenEnd(text.data, anyCjk) };
|
||||
const clone = splitTreeAfter(el, point.textNode, point.offset, pass);
|
||||
const clone = splitTreeAfter(el, point.textNode, point.offset, pass, finder);
|
||||
if (!clone) {
|
||||
return el;
|
||||
}
|
||||
return side === "tail" ? clone : el;
|
||||
}
|
||||
function splitElementAt(el, textNode, offset, pass) {
|
||||
if (!canSplit(el)) {
|
||||
function splitElementAt(el, textNode, offset, pass, finder) {
|
||||
if (!canSplit(el, finder)) {
|
||||
return null;
|
||||
}
|
||||
return splitTreeAfter(el, textNode, offset, pass);
|
||||
return splitTreeAfter(el, textNode, offset, pass, finder);
|
||||
}
|
||||
|
||||
// src/typeset/jinze.ts
|
||||
@@ -1104,10 +1154,10 @@ var Juzhen = (() => {
|
||||
} else if (u.nodeType === 1) {
|
||||
const el = u;
|
||||
if (needLast) {
|
||||
isolateBoundaryToken(el, "tail", anyCjk, PASS2);
|
||||
isolateBoundaryToken(el, "tail", anyCjk, PASS2, finder);
|
||||
}
|
||||
if (needFirst) {
|
||||
isolateBoundaryToken(el, "head", anyCjk, PASS2);
|
||||
isolateBoundaryToken(el, "head", anyCjk, PASS2, finder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1449,7 +1499,7 @@ var Juzhen = (() => {
|
||||
return;
|
||||
}
|
||||
const el = node;
|
||||
if (finder.isAvoided(el)) {
|
||||
if (finder.avoidsSelf(el)) {
|
||||
return;
|
||||
}
|
||||
if (blockSet.has(el.nodeName) || finder.pillMatches(el)) {
|
||||
@@ -1483,7 +1533,8 @@ var Juzhen = (() => {
|
||||
startChild,
|
||||
boundary.textNode,
|
||||
boundary.offset,
|
||||
PASS4
|
||||
PASS4,
|
||||
finder
|
||||
);
|
||||
if (clone) {
|
||||
startChild = clone;
|
||||
@@ -1517,6 +1568,9 @@ var Juzhen = (() => {
|
||||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||||
return;
|
||||
}
|
||||
if (finder.avoidsSelf(block) || finder.isAvoided(block)) {
|
||||
return;
|
||||
}
|
||||
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
|
||||
if (sel && block.querySelector(sel)) {
|
||||
return;
|
||||
@@ -1693,9 +1747,9 @@ var Juzhen = (() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
function processPills(root, finder, pillSelector, pillNeighbor) {
|
||||
root.querySelectorAll(pillSelector).forEach((pill) => {
|
||||
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
|
||||
function processPills(root, finder, boundarySelector, pillNeighbor) {
|
||||
root.querySelectorAll(boundarySelector).forEach((pill) => {
|
||||
if (finder.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
}
|
||||
if (!finder.featureEnabledFor(pill, PASS5)) {
|
||||
@@ -1760,68 +1814,16 @@ var Juzhen = (() => {
|
||||
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS5) && finder.inScope(ctx.root) && !finder.isAvoided(ctx.root)) {
|
||||
processBlock2(ctx.root, finder, R);
|
||||
}
|
||||
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
||||
const boundarySelector = [options.finder.pillSelector, options.finder.isolateSelector].filter(Boolean).join(", ");
|
||||
if (boundarySelector) {
|
||||
processPills(ctx.root, finder, boundarySelector, R.pillNeighbor);
|
||||
}
|
||||
},
|
||||
revert(ctx) {
|
||||
revertPass(PASS5, ctx.root);
|
||||
}
|
||||
};
|
||||
|
||||
// src/compat/v1.ts
|
||||
function mapLongWord(v) {
|
||||
if (v === false) {
|
||||
return false;
|
||||
}
|
||||
if (v === void 0 || v === true || v === null) {
|
||||
return true;
|
||||
}
|
||||
const out = {};
|
||||
if (typeof v.minLength === "number") {
|
||||
out.minLength = v.minLength;
|
||||
}
|
||||
if (typeof v.lang === "string") {
|
||||
out.lang = v.lang;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function createCjkAutospace(options = {}) {
|
||||
const o = options || {};
|
||||
const mapped = {
|
||||
// finder 設定直通。
|
||||
...o.styleInlines ? { styleInlines: o.styleInlines } : {},
|
||||
...o.blockTags ? { blockTags: o.blockTags } : {},
|
||||
...o.skipTags ? { skipTags: o.skipTags } : {},
|
||||
pillSelector: o.pillSelector || "code, kbd",
|
||||
autospaceClass: o.autospaceClass || "cjk-autospace",
|
||||
skipAttribute: o.skipAttribute || "data-md-key",
|
||||
...o.isSkipped ? { isSkipped: o.isSkipped } : {},
|
||||
spacing: true,
|
||||
longWord: mapLongWord(o.longWordWrap),
|
||||
// v1 Pass D 不分 lang 地擠句內點號(開明式);compat 強制 kaiming
|
||||
// 政策以避免功能倒退(預設 zh-Hant→quanjiao 不擠壓)。
|
||||
jiya: o.punctuationSqueeze === false ? false : true,
|
||||
// v1 無連續標點綁定,保持 DOM 與 v1 一致:關閉 jinze。
|
||||
jinze: false,
|
||||
lang: {
|
||||
policy: {
|
||||
"zh-Hant": "kaiming",
|
||||
"zh-Hans": "kaiming",
|
||||
"ja": "kaiming",
|
||||
"other": "kaiming"
|
||||
}
|
||||
}
|
||||
};
|
||||
const jz = createJuzhen(mapped);
|
||||
return {
|
||||
apply(root) {
|
||||
jz.render(root);
|
||||
},
|
||||
revert(root) {
|
||||
jz.revert(root);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// src/index.ts
|
||||
var DEFAULT_STYLE_INLINES = [
|
||||
"STRONG",
|
||||
@@ -1960,7 +1962,6 @@ var Juzhen = (() => {
|
||||
policy: langOpt.policy || {}
|
||||
},
|
||||
ruleset: resolveRuleset(opts.charClass),
|
||||
autospaceClass: opts.autospaceClass || "",
|
||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||
levelText: opts.level && opts.level.text || null,
|
||||
scope: {
|
||||
@@ -1990,6 +1991,9 @@ var Juzhen = (() => {
|
||||
blockTags: new Set(opts.blockTags || DEFAULT_BLOCK_TAGS),
|
||||
skipTags: new Set(opts.skipTags || DEFAULT_SKIP_TAGS),
|
||||
pillSelector: opts.pillSelector || "code, kbd",
|
||||
// 內外隔離(§4.1 三分類):預設 <mark>(瀏覽器預設帶背景,切分有可見
|
||||
// 接縫、邊界需留隙)。顯式判 undefined,使 "" 可停用。
|
||||
isolateSelector: opts.isolateSelector !== void 0 ? opts.isolateSelector : "mark",
|
||||
skipAttribute: opts.skipAttribute || null,
|
||||
isSkipped: typeof opts.isSkipped === "function" ? opts.isSkipped : null
|
||||
}
|
||||
@@ -2038,4 +2042,4 @@ var Juzhen = (() => {
|
||||
}
|
||||
return __toCommonJS(index_exports);
|
||||
})();
|
||||
if(typeof globalThis!=='undefined'){globalThis.Juzhen=Juzhen;if(Juzhen&&Juzhen.createCjkAutospace&&!globalThis.createCjkAutospace){globalThis.createCjkAutospace=Juzhen.createCjkAutospace;}}
|
||||
if(typeof globalThis!=='undefined'){globalThis.Juzhen=Juzhen;}
|
||||
|
||||
Vendored
+127
-123
@@ -7,6 +7,7 @@ var Finder = class {
|
||||
this.blockSet = opts.finder.blockTags;
|
||||
this.skipSet = opts.finder.skipTags;
|
||||
this.pillSelector = opts.finder.pillSelector;
|
||||
this.isolateSelector = opts.finder.isolateSelector;
|
||||
this.skipAttr = opts.finder.skipAttribute;
|
||||
this.userIsSkipped = opts.finder.isSkipped;
|
||||
this.levelTextSelector = opts.levelText;
|
||||
@@ -15,14 +16,35 @@ var Finder = class {
|
||||
this.featureCache = /* @__PURE__ */ new WeakMap();
|
||||
this.levelCache = /* @__PURE__ */ new WeakMap();
|
||||
}
|
||||
/** 該元素是否匹配 pill 選擇器(行內塊,如 code/kbd)。 */
|
||||
/** 該元素是否匹配 pill 選擇器(三分類之「整體」:code/kbd/行內公式等)。 */
|
||||
pillMatches(el) {
|
||||
return !!(el.matches && this.pillSelector && el.matches(this.pillSelector));
|
||||
}
|
||||
/** 該元素是否匹配 isolate 選擇器(三分類之「隔離」:帶盒樣式之行內,如背景色
|
||||
* <mark>)。邊界口徑同 pill(斷 run/斷相鄰/不可切分/兩端補隙),內部照常處理。 */
|
||||
isolateMatches(el) {
|
||||
return !!(el.matches && this.isolateSelector && el.matches(this.isolateSelector));
|
||||
}
|
||||
/** 節點是否位於 pill **內部**(僅查祖先,不含自身)。pill 為整體元素:內部對
|
||||
* 一切 pass 不可見——不 charify、不收 run、不切分、不於其內插間隙(下游問題六:
|
||||
* .katex 內部曾被 jiya/spacing 直接變更)。 */
|
||||
insidePill(node) {
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
if (this.pillMatches(p)) {
|
||||
return true;
|
||||
}
|
||||
p = p.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** 邏輯文本應**透明穿越**之行內元素:樣式行內標籤,以及聚珍自身之 wrap 類
|
||||
* jz-*(jz-hws 間隙、jz-jinze 禁則群組、jz-char/jz-inner 標點原子)。穿越這些
|
||||
* 才能正確找到跨包裝之邏輯鄰字(如 jinze 已把 pill 納入 jz-jinze 後,pill 之左
|
||||
* 鄰字仍須可達;I7)。
|
||||
* ⚠ 以**元素**而非標籤名判定:pill/isolate/avoid 縱使標籤名在樣式集亦為邊界、
|
||||
* 不可穿越(下游問題六:KaTeX 全樹皆 SPAN,名字判定令整棵公式被當透明下潛而
|
||||
* 肢解;scope.avoid/skipAttribute 命中自身者同理)。
|
||||
* ⚠ jz-char/jz-inner 必須透明:否則 adjacentLogicalChar 之 descend 遇標點原子既
|
||||
* 不進入亦不終止、而是**跳過**它續找更前之兄弟(下游 Bug:`汉字。<code>` 經 jinze
|
||||
* 綁定為 `汉<jz-jinze>字<jz-char>。</jz-char></jz-jinze><code>`,pill 之左鄰字應
|
||||
@@ -30,34 +52,51 @@ var Finder = class {
|
||||
* jz-char 僅含標點(jiya 只 charify biaodian),標點非 pillNeighbor,故透明後 descend
|
||||
* 回傳標點 → 不補隙,與 collectRuns 之既有透明遞迴一致。jz-inner 亦須列入,否則
|
||||
* descend 進入 jz-char 後因 jz-inner 不透明而跳過其內標點。 */
|
||||
isTransparentInline(name) {
|
||||
transparentInline(el) {
|
||||
if (this.pillMatches(el) || this.isolateMatches(el) || this.avoidsSelf(el)) {
|
||||
return false;
|
||||
}
|
||||
const name = el.nodeName;
|
||||
return this.styleSet.has(name) || name === "JZ-HWS" || name === "JZ-JINZE" || name === "JZ-CHAR" || name === "JZ-INNER";
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / 使用者 avoid 選擇器 / skipAttribute / isSkipped)。 */
|
||||
/** 該元素**自身**是否為 avoid 邊界(內建 skip 名單 / SVG / contentEditable /
|
||||
* data-jz-skip / skipAttribute / avoid 選擇器 / isSkipped)。isAvoided 沿祖先
|
||||
* 鏈逐層呼叫本判定;遍歷原語另須對「正要進入」之元素直接判自身——舊版僅查
|
||||
* 祖先,元素自身命中 avoid 時被當透明穿越(下游問題六:scope.avoid 對 .katex
|
||||
* 形同虛設)。 */
|
||||
avoidsSelf(el) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(el)) {
|
||||
return true;
|
||||
}
|
||||
if (this.skipSet.has(el.nodeName)) {
|
||||
return true;
|
||||
}
|
||||
if (el.namespaceURI === SVG_NS) {
|
||||
return true;
|
||||
}
|
||||
if (el.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
if (el.hasAttribute("data-jz-skip")) {
|
||||
return true;
|
||||
}
|
||||
if (this.skipAttr && el.hasAttribute(this.skipAttr)) {
|
||||
return true;
|
||||
}
|
||||
if (this.avoidSelector && el.matches && el.matches(this.avoidSelector)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** 節點是否處於 avoid 子樹(祖先鏈上任一元素 avoidsSelf;自身不計——元素
|
||||
* 自身之邊界判定由遍歷原語以 avoidsSelf 執行)。 */
|
||||
isAvoided(node) {
|
||||
if (this.userIsSkipped && this.userIsSkipped(node)) {
|
||||
return true;
|
||||
}
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1) {
|
||||
const el = p;
|
||||
if (this.skipSet.has(el.nodeName)) {
|
||||
return true;
|
||||
}
|
||||
if (el.namespaceURI === SVG_NS) {
|
||||
return true;
|
||||
}
|
||||
if (el.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
if (el.hasAttribute("data-jz-skip")) {
|
||||
return true;
|
||||
}
|
||||
if (this.skipAttr && el.hasAttribute(this.skipAttr)) {
|
||||
return true;
|
||||
}
|
||||
if (this.avoidSelector && el.matches && el.matches(this.avoidSelector)) {
|
||||
if (this.avoidsSelf(p)) {
|
||||
return true;
|
||||
}
|
||||
p = p.parentNode;
|
||||
@@ -75,7 +114,7 @@ var Finder = class {
|
||||
}
|
||||
return !!(el.closest && el.closest(this.includeSelector));
|
||||
}
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / 非作用域)。 */
|
||||
/** 走訪 root 下所有「可處理」文本節點(已過濾 avoid / pill 內部 / 非作用域)。 */
|
||||
eachTextNode(root, cb) {
|
||||
const ownerDoc = root.ownerDocument || (typeof document !== "undefined" ? document : null);
|
||||
if (!ownerDoc) {
|
||||
@@ -86,7 +125,7 @@ var Finder = class {
|
||||
let n = walker.nextNode();
|
||||
while (n) {
|
||||
const t = n;
|
||||
if (!this.isAvoided(t) && this.inScope(t)) {
|
||||
if (!this.isAvoided(t) && !this.insidePill(t) && this.inScope(t)) {
|
||||
nodes.push(t);
|
||||
}
|
||||
n = walker.nextNode();
|
||||
@@ -148,11 +187,7 @@ var Finder = class {
|
||||
return;
|
||||
}
|
||||
const el = node;
|
||||
if (this.isAvoided(el)) {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
if (this.skipSet.has(el.nodeName) || this.blockSet.has(el.nodeName)) {
|
||||
if (this.avoidsSelf(el) || this.blockSet.has(el.nodeName)) {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
@@ -160,6 +195,16 @@ var Finder = class {
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
if (this.isolateMatches(el)) {
|
||||
flush();
|
||||
let ic = el.firstChild;
|
||||
while (ic) {
|
||||
visit(ic);
|
||||
ic = ic.nextSibling;
|
||||
}
|
||||
flush();
|
||||
return;
|
||||
}
|
||||
let c2 = el.firstChild;
|
||||
while (c2) {
|
||||
visit(c2);
|
||||
@@ -195,7 +240,10 @@ var Finder = class {
|
||||
if (n2.nodeType === 3 && n2.data.length > 0) {
|
||||
return pickEnd(n2);
|
||||
}
|
||||
if (n2.nodeType === 1 && this.isTransparentInline(n2.nodeName)) {
|
||||
if (n2.nodeType === 1) {
|
||||
if (!this.transparentInline(n2)) {
|
||||
return "blocked";
|
||||
}
|
||||
const r = descend(n2);
|
||||
if (r) {
|
||||
return r;
|
||||
@@ -218,22 +266,22 @@ var Finder = class {
|
||||
}
|
||||
if (n.nodeType === 1) {
|
||||
const e = n;
|
||||
if (this.isAvoided(e)) {
|
||||
if (!this.transparentInline(e)) {
|
||||
return null;
|
||||
}
|
||||
if (this.isTransparentInline(e.nodeName)) {
|
||||
const r = descend(e);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
n = direction === -1 ? e.previousSibling : e.nextSibling;
|
||||
continue;
|
||||
const r = descend(e);
|
||||
if (r === "blocked") {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
n = direction === -1 ? e.previousSibling : e.nextSibling;
|
||||
continue;
|
||||
}
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
}
|
||||
if (!p || p.nodeType !== 1 || !this.isTransparentInline(p.nodeName)) {
|
||||
if (!p || p.nodeType !== 1 || !this.transparentInline(p)) {
|
||||
return null;
|
||||
}
|
||||
const pe = p;
|
||||
@@ -812,7 +860,11 @@ var jiyaAdjacencyPass = {
|
||||
prev = el;
|
||||
} else if (finder.pillMatches(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isAvoided(el)) {
|
||||
} else if (finder.avoidsSelf(el)) {
|
||||
prev = null;
|
||||
} else if (finder.isolateMatches(el)) {
|
||||
prev = null;
|
||||
visit(el);
|
||||
prev = null;
|
||||
} else if (blockSet.has(nm)) {
|
||||
prev = null;
|
||||
@@ -840,26 +892,25 @@ var SPLITTABLE = /* @__PURE__ */ new Set([
|
||||
"U",
|
||||
"S",
|
||||
"SPAN",
|
||||
"MARK",
|
||||
"SMALL",
|
||||
"INS",
|
||||
"DEL",
|
||||
"SUB",
|
||||
"SUP"
|
||||
]);
|
||||
function canSplit(el) {
|
||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id");
|
||||
function canSplit(el, finder) {
|
||||
return SPLITTABLE.has(el.nodeName) && !el.hasAttribute("id") && !finder.pillMatches(el) && !finder.isolateMatches(el) && !finder.avoidsSelf(el);
|
||||
}
|
||||
function firstText(el) {
|
||||
function firstText(el, finder) {
|
||||
for (let c = el.firstChild; c; c = c.nextSibling) {
|
||||
if (c.nodeType === 3 && c.data.length > 0) {
|
||||
return c;
|
||||
}
|
||||
if (c.nodeType === 1) {
|
||||
if (!canSplit(c)) {
|
||||
if (!canSplit(c, finder)) {
|
||||
return null;
|
||||
}
|
||||
const r = firstText(c);
|
||||
const r = firstText(c, finder);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
@@ -867,16 +918,16 @@ function firstText(el) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function lastText(el) {
|
||||
function lastText(el, finder) {
|
||||
for (let c = el.lastChild; c; c = c.previousSibling) {
|
||||
if (c.nodeType === 3 && c.data.length > 0) {
|
||||
return c;
|
||||
}
|
||||
if (c.nodeType === 1) {
|
||||
if (!canSplit(c)) {
|
||||
if (!canSplit(c, finder)) {
|
||||
return null;
|
||||
}
|
||||
const r = lastText(c);
|
||||
const r = lastText(c, finder);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
@@ -908,8 +959,8 @@ function trailingTokenStart(d, anyCjk) {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function splitTreeAfter(top, textNode, offset, pass) {
|
||||
if (offset <= 0 && firstText(top) === textNode) {
|
||||
function splitTreeAfter(top, textNode, offset, pass, finder) {
|
||||
if (offset <= 0 && firstText(top, finder) === textNode) {
|
||||
return null;
|
||||
}
|
||||
let rightAtLevel;
|
||||
@@ -959,26 +1010,26 @@ function splitTreeAfter(top, textNode, offset, pass) {
|
||||
}
|
||||
return topClone;
|
||||
}
|
||||
function isolateBoundaryToken(el, side, anyCjk, pass) {
|
||||
if (!canSplit(el)) {
|
||||
function isolateBoundaryToken(el, side, anyCjk, pass, finder) {
|
||||
if (!canSplit(el, finder)) {
|
||||
return el;
|
||||
}
|
||||
const text = side === "tail" ? lastText(el) : firstText(el);
|
||||
const text = side === "tail" ? lastText(el, finder) : firstText(el, finder);
|
||||
if (!text) {
|
||||
return el;
|
||||
}
|
||||
const point = side === "tail" ? { textNode: text, offset: trailingTokenStart(text.data, anyCjk) } : { textNode: text, offset: leadingTokenEnd(text.data, anyCjk) };
|
||||
const clone = splitTreeAfter(el, point.textNode, point.offset, pass);
|
||||
const clone = splitTreeAfter(el, point.textNode, point.offset, pass, finder);
|
||||
if (!clone) {
|
||||
return el;
|
||||
}
|
||||
return side === "tail" ? clone : el;
|
||||
}
|
||||
function splitElementAt(el, textNode, offset, pass) {
|
||||
if (!canSplit(el)) {
|
||||
function splitElementAt(el, textNode, offset, pass, finder) {
|
||||
if (!canSplit(el, finder)) {
|
||||
return null;
|
||||
}
|
||||
return splitTreeAfter(el, textNode, offset, pass);
|
||||
return splitTreeAfter(el, textNode, offset, pass, finder);
|
||||
}
|
||||
|
||||
// src/typeset/jinze.ts
|
||||
@@ -1076,10 +1127,10 @@ function processParent(parent, finder, anyCjk) {
|
||||
} else if (u.nodeType === 1) {
|
||||
const el = u;
|
||||
if (needLast) {
|
||||
isolateBoundaryToken(el, "tail", anyCjk, PASS2);
|
||||
isolateBoundaryToken(el, "tail", anyCjk, PASS2, finder);
|
||||
}
|
||||
if (needFirst) {
|
||||
isolateBoundaryToken(el, "head", anyCjk, PASS2);
|
||||
isolateBoundaryToken(el, "head", anyCjk, PASS2, finder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1421,7 +1472,7 @@ function collectSubst(block, finder, blockSet, anyCjk, biaodian) {
|
||||
return;
|
||||
}
|
||||
const el = node;
|
||||
if (finder.isAvoided(el)) {
|
||||
if (finder.avoidsSelf(el)) {
|
||||
return;
|
||||
}
|
||||
if (blockSet.has(el.nodeName) || finder.pillMatches(el)) {
|
||||
@@ -1455,7 +1506,8 @@ function processBlock(block, finder, blockSet, anyCjk, biaodian, chars) {
|
||||
startChild,
|
||||
boundary.textNode,
|
||||
boundary.offset,
|
||||
PASS4
|
||||
PASS4,
|
||||
finder
|
||||
);
|
||||
if (clone) {
|
||||
startChild = clone;
|
||||
@@ -1489,6 +1541,9 @@ var orphanPass = {
|
||||
if (!finder.featureEnabledFor(block, PASS4)) {
|
||||
return;
|
||||
}
|
||||
if (finder.avoidsSelf(block) || finder.isAvoided(block)) {
|
||||
return;
|
||||
}
|
||||
const sel = Array.from(blockSet).map((t) => t.toLowerCase()).join(",");
|
||||
if (sel && block.querySelector(sel)) {
|
||||
return;
|
||||
@@ -1665,9 +1720,9 @@ function processBlock2(block, finder, R) {
|
||||
}
|
||||
}
|
||||
}
|
||||
function processPills(root, finder, pillSelector, pillNeighbor) {
|
||||
root.querySelectorAll(pillSelector).forEach((pill) => {
|
||||
if (finder.isAvoided(pill) || !finder.inScope(pill)) {
|
||||
function processPills(root, finder, boundarySelector, pillNeighbor) {
|
||||
root.querySelectorAll(boundarySelector).forEach((pill) => {
|
||||
if (finder.isAvoided(pill) || finder.insidePill(pill) || !finder.inScope(pill)) {
|
||||
return;
|
||||
}
|
||||
if (!finder.featureEnabledFor(pill, PASS5)) {
|
||||
@@ -1732,68 +1787,16 @@ var spacingPass = {
|
||||
if (!options.finder.blockTags.has(ctx.root.nodeName) && finder.featureEnabledFor(ctx.root, PASS5) && finder.inScope(ctx.root) && !finder.isAvoided(ctx.root)) {
|
||||
processBlock2(ctx.root, finder, R);
|
||||
}
|
||||
processPills(ctx.root, finder, options.finder.pillSelector, R.pillNeighbor);
|
||||
const boundarySelector = [options.finder.pillSelector, options.finder.isolateSelector].filter(Boolean).join(", ");
|
||||
if (boundarySelector) {
|
||||
processPills(ctx.root, finder, boundarySelector, R.pillNeighbor);
|
||||
}
|
||||
},
|
||||
revert(ctx) {
|
||||
revertPass(PASS5, ctx.root);
|
||||
}
|
||||
};
|
||||
|
||||
// src/compat/v1.ts
|
||||
function mapLongWord(v) {
|
||||
if (v === false) {
|
||||
return false;
|
||||
}
|
||||
if (v === void 0 || v === true || v === null) {
|
||||
return true;
|
||||
}
|
||||
const out = {};
|
||||
if (typeof v.minLength === "number") {
|
||||
out.minLength = v.minLength;
|
||||
}
|
||||
if (typeof v.lang === "string") {
|
||||
out.lang = v.lang;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function createCjkAutospace(options = {}) {
|
||||
const o = options || {};
|
||||
const mapped = {
|
||||
// finder 設定直通。
|
||||
...o.styleInlines ? { styleInlines: o.styleInlines } : {},
|
||||
...o.blockTags ? { blockTags: o.blockTags } : {},
|
||||
...o.skipTags ? { skipTags: o.skipTags } : {},
|
||||
pillSelector: o.pillSelector || "code, kbd",
|
||||
autospaceClass: o.autospaceClass || "cjk-autospace",
|
||||
skipAttribute: o.skipAttribute || "data-md-key",
|
||||
...o.isSkipped ? { isSkipped: o.isSkipped } : {},
|
||||
spacing: true,
|
||||
longWord: mapLongWord(o.longWordWrap),
|
||||
// v1 Pass D 不分 lang 地擠句內點號(開明式);compat 強制 kaiming
|
||||
// 政策以避免功能倒退(預設 zh-Hant→quanjiao 不擠壓)。
|
||||
jiya: o.punctuationSqueeze === false ? false : true,
|
||||
// v1 無連續標點綁定,保持 DOM 與 v1 一致:關閉 jinze。
|
||||
jinze: false,
|
||||
lang: {
|
||||
policy: {
|
||||
"zh-Hant": "kaiming",
|
||||
"zh-Hans": "kaiming",
|
||||
"ja": "kaiming",
|
||||
"other": "kaiming"
|
||||
}
|
||||
}
|
||||
};
|
||||
const jz = createJuzhen(mapped);
|
||||
return {
|
||||
apply(root) {
|
||||
jz.render(root);
|
||||
},
|
||||
revert(root) {
|
||||
jz.revert(root);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// src/index.ts
|
||||
var DEFAULT_STYLE_INLINES = [
|
||||
"STRONG",
|
||||
@@ -1932,7 +1935,6 @@ function normalizeOptions(opts = {}) {
|
||||
policy: langOpt.policy || {}
|
||||
},
|
||||
ruleset: resolveRuleset(opts.charClass),
|
||||
autospaceClass: opts.autospaceClass || "",
|
||||
justifyAtoms: opts.justifyAtoms === false ? false : true,
|
||||
levelText: opts.level && opts.level.text || null,
|
||||
scope: {
|
||||
@@ -1962,6 +1964,9 @@ function normalizeOptions(opts = {}) {
|
||||
blockTags: new Set(opts.blockTags || DEFAULT_BLOCK_TAGS),
|
||||
skipTags: new Set(opts.skipTags || DEFAULT_SKIP_TAGS),
|
||||
pillSelector: opts.pillSelector || "code, kbd",
|
||||
// 內外隔離(§4.1 三分類):預設 <mark>(瀏覽器預設帶背景,切分有可見
|
||||
// 接縫、邊界需留隙)。顯式判 undefined,使 "" 可停用。
|
||||
isolateSelector: opts.isolateSelector !== void 0 ? opts.isolateSelector : "mark",
|
||||
skipAttribute: opts.skipAttribute || null,
|
||||
isSkipped: typeof opts.isSkipped === "function" ? opts.isSkipped : null
|
||||
}
|
||||
@@ -2009,7 +2014,6 @@ function createJuzhen(opts = {}) {
|
||||
};
|
||||
}
|
||||
export {
|
||||
createCjkAutospace,
|
||||
createJuzhen,
|
||||
normalizeOptions
|
||||
};
|
||||
|
||||
Vendored
+7
-12
@@ -12,9 +12,6 @@ export interface OrphanOptions {
|
||||
/** 段末行至少保留之 CJK 實義字數(標點/符號不計;預設 2)。 */
|
||||
chars?: number;
|
||||
}
|
||||
/** 行端標點模式(§6.5.2):squeeze=行端半形(現有,預設);hang=標點懸掛右/左凸;
|
||||
* none=不處理。squeeze 與 hang 同屬渲染層、互斥(共用行端偵測碼)。 */
|
||||
export type LineEndMode = "squeeze" | "hang" | "none";
|
||||
/** 標點擠壓設定(架構 §6.3)。布林 true 等同 { halfWidth: "halt" }。 */
|
||||
export interface JiyaOptions {
|
||||
/** 半形渲染機制:
|
||||
@@ -53,8 +50,6 @@ export interface JuzhenOptions {
|
||||
level?: {
|
||||
text?: string;
|
||||
};
|
||||
/** 間隙標記沿用之 class(v1 相容;預設無額外 class)。 */
|
||||
autospaceClass?: string;
|
||||
/** justify 原子化:jz-char/jz-jinze 是否以 inline-block 作對齊單一原子
|
||||
* (預設 true)。設 false 改 display:inline——用於分頁器(如 Paged.js)之
|
||||
* 對齊引擎會把每個 inline-block 邊界當伸縮點、與 CJK inter-ideograph 疊加
|
||||
@@ -71,9 +66,9 @@ export interface JuzhenOptions {
|
||||
jiya?: boolean | JiyaOptions;
|
||||
jinze?: boolean;
|
||||
orphan?: boolean | OrphanOptions;
|
||||
/** @deprecated 標點懸掛**已全面停用**(§6.5.6):純負 margin 機制不可行(墨色不出版心/
|
||||
* 滿凸拉升後字重疊/resize 重排錯亂),原生 hanging-punctuation 為正解但 Chrome 不支援。
|
||||
* 此選項仍被接受以保 API 相容,但**無任何效果**(行端恒 squeeze);俟原生支援成熟再議。 */
|
||||
/** @deprecated 標點懸掛**已移除**(§6.5.6):純負 margin 機制經實測為機制級死路(寬度
|
||||
* 守恆矛盾,CSS 接觸不到斷行器),原生 hanging-punctuation 為唯一正解但僅 Safari 支援、
|
||||
* 且與本庫 inline-block 原子互斥。此選項仍被接受以保 API 相容,但**無任何效果**。 */
|
||||
hanging?: boolean;
|
||||
biaodian?: boolean;
|
||||
emphasis?: boolean;
|
||||
@@ -83,7 +78,10 @@ export interface JuzhenOptions {
|
||||
styleInlines?: string[];
|
||||
blockTags?: string[];
|
||||
skipTags?: string[];
|
||||
/** 整體(pill)元素選擇器(預設 "code, kbd")。 */
|
||||
pillSelector?: string;
|
||||
/** 內外隔離元素選擇器(預設 "mark")。傳 "" 停用(mark 回歸透明)。 */
|
||||
isolateSelector?: string;
|
||||
skipAttribute?: string;
|
||||
isSkipped?: (node: Node) => boolean;
|
||||
}
|
||||
@@ -92,7 +90,6 @@ export interface ResolvedOptions {
|
||||
locale: LocaleConfig;
|
||||
/** 最終字符分類規則集(內置默認 + charClass 覆寫)。 */
|
||||
ruleset: Ruleset;
|
||||
autospaceClass: string;
|
||||
justifyAtoms: boolean;
|
||||
/** 功能分級之 text-level 選擇器(§6.5.0a);null 表示未設。 */
|
||||
levelText: string | null;
|
||||
@@ -107,7 +104,6 @@ export interface ResolvedOptions {
|
||||
jiya: boolean;
|
||||
jinze: boolean;
|
||||
orphan: boolean;
|
||||
hanging: boolean;
|
||||
biaodian: boolean;
|
||||
emphasis: boolean;
|
||||
ruby: boolean;
|
||||
@@ -120,8 +116,6 @@ export interface ResolvedOptions {
|
||||
orphan: {
|
||||
chars: number;
|
||||
};
|
||||
/** 行端標點模式(解析後;§6.5.2)。 */
|
||||
lineEnd: LineEndMode;
|
||||
/** 擠壓半形渲染設定(halt 預設/margin 後備)。 */
|
||||
jiyaConfig: {
|
||||
halfWidth: "halt" | "margin";
|
||||
@@ -133,6 +127,7 @@ export interface ResolvedOptions {
|
||||
blockTags: Set<string>;
|
||||
skipTags: Set<string>;
|
||||
pillSelector: string;
|
||||
isolateSelector: string;
|
||||
skipAttribute: string | null;
|
||||
isSkipped: ((node: Node) => boolean) | null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user