初始:cjk-autospace 工廠函式 + TS 宣告 + README
雙 pass 架構: - Pass A — 對每個 BLOCK 走 DFS 收集邏輯文本(樣式標籤透明、PILL 與子 BLOCK 中斷 run),套 panguSpace,把 MARK 反映射回 textNode / offset 並插入 .cjk-autospace marker span - Pass B — 對每個 PILL(pillSelector)透過樣式鏈遞迴找邏輯鄰字, 按 0 / 1 / 多 + 特殊規則處理空白 工廠 createCjkAutospace(options) 接受 pillSelector / styleInlines / blockTags / skipTags / autospaceClass / skipAttribute / isSkipped 等 配置。UMD-ish export 同時支援 ESM、CommonJS、browser global 與 inline script 形式。 規則表 port 自 Pangu.js v7.2.1(MIT),加上跨樣式邊界透明與行內塊 邊界兩 pass 為原創設計。
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
"use strict";
|
||||
|
||||
/* cjk-autospace — Pangu.js v7.2.1 風格 CJK ↔ Latin 邊界自動間距 shim。
|
||||
*
|
||||
* 工廠模式:`createCjkAutospace(options).apply(root)`。配置由消費端注入;
|
||||
* 預設值對應 HTML 模板 runtime 之原狀(pill = code, kbd;標籤三分類;
|
||||
* marker class 為 .cjk-autospace;skip ancestor 為 mono / SVG /
|
||||
* contentEditable / [data-md-key])。
|
||||
*
|
||||
* 標籤三分類:
|
||||
* - STYLE_INLINES — 樣式標籤,shim 透明走過。跨樣式邊界(如
|
||||
* <strong>是</strong> Neovim)由 Pass A 在剝離樣式後識別。
|
||||
* - PILL — 行內塊(有獨立背景,如 <code>),邊界由 Pass B
|
||||
* 處理。預設 'code, kbd',可由消費端擴充。
|
||||
* - BLOCK — 段落級標籤,Pass A 把它們當作 run 邊界。
|
||||
*
|
||||
* 兩 pass:
|
||||
* Pass A — 對每個 BLOCK 走 DFS 收集邏輯文本(STYLE 透明、PILL 與子
|
||||
* BLOCK 中斷 run),整段套 panguSpace,把 MARK 反映射回 textNode/
|
||||
* offset。MARK 對應原文若是單 ASCII space 則 strip 後插 marker,
|
||||
* 否則純插。actions 倒序執行確保前面偏移不被後面動作影響。
|
||||
* Pass B — 對每個 PILL 透過 STYLE 鏈遞迴找邏輯鄰字(adjacentLogical-
|
||||
* Char),按 0/1/多+特殊規則處理(0 個或 1 個 ASCII space 插
|
||||
* marker;其餘跳過)。
|
||||
*
|
||||
* Marker = <span class="cjk-autospace" aria-hidden="true">(空白)</span>。
|
||||
* 內含真實 ASCII 空白:layout 視為 whitespace(行末 collapse / 容許
|
||||
* 換行 / justify 可拉伸);配 CSS user-select: none 讓選取與剪貼板跳
|
||||
* 過該 span。textContent 不變(無視 span 內容)→ Copy / Paste / 序列
|
||||
* 化都乾淨。
|
||||
*/
|
||||
|
||||
function createCjkAutospace(options)
|
||||
{
|
||||
options = options || {};
|
||||
|
||||
/* ----- 預設配置 ----- */
|
||||
const DEFAULT_STYLE_INLINES = [
|
||||
"STRONG", "EM", "A", "B", "I", "U", "S", "MARK", "INS", "DEL",
|
||||
"SUB", "SUP", "SMALL", "ABBR", "CITE", "DFN", "Q", "SPAN",
|
||||
"VAR", "SAMP", "TIME", "BDO", "BDI", "RUBY", "RB", "RT", "RP", "WBR",
|
||||
];
|
||||
const DEFAULT_BLOCK_TAGS = [
|
||||
"BODY", "P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6", "LI",
|
||||
"DT", "DD", "BLOCKQUOTE", "FIGURE", "FIGCAPTION", "TD", "TH",
|
||||
"CAPTION", "SUMMARY", "DETAILS", "HEADER", "FOOTER", "SECTION",
|
||||
"ARTICLE", "ASIDE", "NAV", "MAIN", "ADDRESS", "PRE", "UL", "OL",
|
||||
"DL", "TABLE", "TR", "THEAD", "TBODY", "TFOOT", "FORM",
|
||||
"FIELDSET", "LEGEND", "LABEL", "BR", "HR",
|
||||
];
|
||||
const DEFAULT_SKIP_TAGS = [
|
||||
"CODE", "KBD", "PRE", "SAMP", "TT", "VAR",
|
||||
"SCRIPT", "STYLE", "TEXTAREA", "INPUT",
|
||||
];
|
||||
|
||||
const STYLE_SET = new Set(options.styleInlines || DEFAULT_STYLE_INLINES);
|
||||
const BLOCK_SET = new Set(options.blockTags || DEFAULT_BLOCK_TAGS);
|
||||
const SKIP_SET = new Set(options.skipTags || DEFAULT_SKIP_TAGS);
|
||||
const pillSelector = options.pillSelector || "code, kbd";
|
||||
const autospaceCls = options.autospaceClass || "cjk-autospace";
|
||||
const skipAttr = options.skipAttribute || "data-md-key";
|
||||
const userIsSkipped = typeof options.isSkipped === "function" ? options.isSkipped : null;
|
||||
|
||||
/* ----- Pangu 字符類與規則表 ----- */
|
||||
const PANGU_CJK = "⺀-⼀-"
|
||||
+ "-ゟ゠-ヺー-ヿ"
|
||||
+ "-ㄯ㈀-㋿"
|
||||
+ "㐀-䶿一-鿿豈-";
|
||||
const AN_CHARS = "A-Za-z0-9";
|
||||
const A_CHARS = "A-Za-z";
|
||||
const OPS_HYPHEN = "\\+\\*=&\\-";
|
||||
const OPS_GRADE = "\\+\\-\\*";
|
||||
const QUOTES_CLS = "`\"״";
|
||||
const LBR_BASIC = "\\(\\[\\{";
|
||||
const RBR_BASIC = "\\)\\]\\}";
|
||||
const LBR_EXT = "\\(\\[\\{<>";
|
||||
const RBR_EXT = "\\)\\]\\}<>";
|
||||
const ANS_CJK_AFTER = A_CHARS + "Ͱ-Ͽ0-9@\\$%\\^&\\*\\-\\+\\\\=¡-ÿ⅐-✀-➿";
|
||||
const ANS_BEFORE_CJK = A_CHARS + "Ͱ-Ͽ0-9\\$%\\^&\\*\\-\\+\\\\=¡-ÿ⅐-✀-➿";
|
||||
|
||||
const ANY_CJK = new RegExp("[" + PANGU_CJK + "]");
|
||||
|
||||
/* PUA marker chars. 不會撞到正常 prose 中之 glyph。 */
|
||||
const MARK = "";
|
||||
const PH_OPEN = "";
|
||||
const PH_CLOSE = "";
|
||||
|
||||
const STRIP_CJK_SPACE_ANS = new RegExp("([" + PANGU_CJK + "]) ([" + ANS_CJK_AFTER + "])", "g");
|
||||
const STRIP_ANS_SPACE_CJK = new RegExp("([" + ANS_BEFORE_CJK + "]) ([" + PANGU_CJK + "])", "g");
|
||||
|
||||
const DOTS_CJK = new RegExp("([\\.]{2,}|…)([" + PANGU_CJK + "])", "g");
|
||||
const CJK_PUNCTUATION = new RegExp("([" + PANGU_CJK + "])([!;,\\?:]+)(?=[" + PANGU_CJK + AN_CHARS + "])", "g");
|
||||
const AN_PUNCTUATION_CJK = new RegExp("([" + AN_CHARS + "])([!;,\\?]+)([" + PANGU_CJK + "])", "g");
|
||||
const CJK_TILDE = new RegExp("([" + PANGU_CJK + "])(~+)(?!=)(?=[" + PANGU_CJK + AN_CHARS + "])", "g");
|
||||
const CJK_TILDE_EQUALS = new RegExp("([" + PANGU_CJK + "])(~=)", "g");
|
||||
const CJK_PERIOD = new RegExp("([" + PANGU_CJK + "])(\\.)(?![" + AN_CHARS + "\\./])(?=[" + PANGU_CJK + AN_CHARS + "])", "g");
|
||||
const AN_PERIOD_CJK = new RegExp("([" + AN_CHARS + "])(\\.)([" + PANGU_CJK + "])", "g");
|
||||
const AN_COLON_CJK = new RegExp("([" + AN_CHARS + "])(:)([" + PANGU_CJK + "])", "g");
|
||||
const CJK_QUOTE = new RegExp("([" + PANGU_CJK + "])([" + QUOTES_CLS + "])", "g");
|
||||
const QUOTE_CJK = new RegExp("([" + QUOTES_CLS + "])([" + PANGU_CJK + "])", "g");
|
||||
const QUOTE_AN = new RegExp("([”])([" + AN_CHARS + "])", "g");
|
||||
const CJK_QUOTE_AN = new RegExp("([" + PANGU_CJK + "])(\")([" + AN_CHARS + "])", "g");
|
||||
const CJK_HASH = new RegExp("([" + PANGU_CJK + "])(#([^ ]))", "g");
|
||||
const HASH_CJK = new RegExp("(([^ ])#)([" + PANGU_CJK + "])", "g");
|
||||
const SINGLE_LETTER_GRADE = new RegExp("\\b([" + A_CHARS + "])([" + OPS_GRADE + "])([" + PANGU_CJK + "])", "g");
|
||||
const CJK_OPERATOR_ANS = new RegExp("([" + PANGU_CJK + "])([" + OPS_HYPHEN + "])([" + AN_CHARS + "])", "g");
|
||||
const ANS_OPERATOR_CJK = new RegExp("([" + AN_CHARS + "])([" + OPS_HYPHEN + "])([" + PANGU_CJK + "])", "g");
|
||||
const CJK_LESS_THAN = new RegExp("([" + PANGU_CJK + "])(<)([" + AN_CHARS + "])", "g");
|
||||
const LESS_THAN_CJK = new RegExp("([" + AN_CHARS + "])(<)([" + PANGU_CJK + "])", "g");
|
||||
const CJK_GREATER_THAN = new RegExp("([" + PANGU_CJK + "])(>)([" + AN_CHARS + "])", "g");
|
||||
const GREATER_THAN_CJK = new RegExp("([" + AN_CHARS + "])(>)([" + PANGU_CJK + "])", "g");
|
||||
const CJK_LEFT_BRACKET = new RegExp("([" + PANGU_CJK + "])([" + LBR_EXT + "])", "g");
|
||||
const RIGHT_BRACKET_CJK = new RegExp("([" + RBR_EXT + "])([" + PANGU_CJK + "])", "g");
|
||||
const AN_LEFT_BRACKET = new RegExp("([" + AN_CHARS + "])(?<!\\.[" + AN_CHARS + "]*)([" + LBR_BASIC + "])", "g");
|
||||
const RIGHT_BRACKET_AN = new RegExp("([" + RBR_BASIC + "])([" + AN_CHARS + "])", "g");
|
||||
const CJK_ANS = new RegExp("([" + PANGU_CJK + "])([" + ANS_CJK_AFTER + "])", "g");
|
||||
const ANS_CJK = new RegExp("([" + ANS_BEFORE_CJK + "])([" + PANGU_CJK + "])", "g");
|
||||
const S_A = new RegExp("(%)([" + A_CHARS + "])", "g");
|
||||
|
||||
/* Compound words like GPT-4 / state-of-the-art mustn't get operator
|
||||
spaces wedged into them. Mask before operator passes, restore
|
||||
before bracket / CJK_ANS / ANS_CJK passes (so the boundary
|
||||
between 是 and GPT-4 still gets spaced). */
|
||||
const COMPOUND_WORD = /\b(?:[A-Za-z0-9]*[a-z][A-Za-z0-9]*-[A-Za-z0-9]+|[A-Za-z0-9]+-[A-Za-z0-9]*[a-z][A-Za-z0-9]*|[A-Za-z]+-[0-9]+|[A-Za-z]+[0-9]+-[A-Za-z0-9]+)(?:-[A-Za-z0-9]+)*\b/g;
|
||||
const PH_PATTERN = new RegExp(PH_OPEN + "(\\d+)" + PH_CLOSE, "g");
|
||||
|
||||
function panguSpace(text)
|
||||
{
|
||||
if (text.length <= 1 || !ANY_CJK.test(text)) { return text; }
|
||||
|
||||
let s = text;
|
||||
|
||||
/* Pre-pass:strip authored single spaces at CJK↔ANS boundaries so
|
||||
the rest of the pipeline can treat the text as if it had been
|
||||
written flush. */
|
||||
s = s.replace(STRIP_CJK_SPACE_ANS, "$1$2");
|
||||
s = s.replace(STRIP_ANS_SPACE_CJK, "$1$2");
|
||||
|
||||
s = s.replace(DOTS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_PUNCTUATION, "$1$2" + MARK);
|
||||
s = s.replace(AN_PUNCTUATION_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_TILDE, "$1$2" + MARK);
|
||||
s = s.replace(CJK_TILDE_EQUALS, "$1" + MARK + "$2" + MARK);
|
||||
s = s.replace(CJK_PERIOD, "$1$2" + MARK);
|
||||
s = s.replace(AN_PERIOD_CJK, "$1$2" + MARK + "$3");
|
||||
s = s.replace(AN_COLON_CJK, "$1$2" + MARK + "$3");
|
||||
|
||||
s = s.replace(CJK_QUOTE, "$1" + MARK + "$2");
|
||||
s = s.replace(QUOTE_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(QUOTE_AN, "$1" + MARK + "$2");
|
||||
s = s.replace(CJK_QUOTE_AN, "$1$2" + MARK + "$3");
|
||||
|
||||
s = s.replace(CJK_HASH, "$1" + MARK + "$2");
|
||||
s = s.replace(HASH_CJK, "$1" + MARK + "$3");
|
||||
|
||||
const compounds = [];
|
||||
s = s.replace(COMPOUND_WORD, function (m)
|
||||
{
|
||||
compounds.push(m);
|
||||
return PH_OPEN + (compounds.length - 1) + PH_CLOSE;
|
||||
});
|
||||
|
||||
s = s.replace(SINGLE_LETTER_GRADE, "$1$2" + MARK + "$3");
|
||||
s = s.replace(CJK_OPERATOR_ANS, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(ANS_OPERATOR_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
|
||||
s = s.replace(CJK_LESS_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(LESS_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(CJK_GREATER_THAN, "$1" + MARK + "$2" + MARK + "$3");
|
||||
s = s.replace(GREATER_THAN_CJK, "$1" + MARK + "$2" + MARK + "$3");
|
||||
|
||||
/* Restore compound words BEFORE bracket / CJK_ANS / ANS_CJK
|
||||
passes — otherwise their PUA placeholders look like neither
|
||||
CJK nor AN and the boundaries silently skip. */
|
||||
s = s.replace(PH_PATTERN, function (_m, idx) { return compounds[+idx] || ""; });
|
||||
|
||||
s = s.replace(CJK_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(RIGHT_BRACKET_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(AN_LEFT_BRACKET, "$1" + MARK + "$2");
|
||||
s = s.replace(RIGHT_BRACKET_AN, "$1" + MARK + "$2");
|
||||
|
||||
s = s.replace(CJK_ANS, "$1" + MARK + "$2");
|
||||
s = s.replace(ANS_CJK, "$1" + MARK + "$2");
|
||||
s = s.replace(S_A, "$1" + MARK + "$2");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ----- skip ancestor 判定 ----- */
|
||||
function isSkipped(node)
|
||||
{
|
||||
if (userIsSkipped && userIsSkipped(node)) { return true; }
|
||||
let p = node.parentNode;
|
||||
while (p && p.nodeType === 1)
|
||||
{
|
||||
if (SKIP_SET.has(p.nodeName)) { return true; }
|
||||
if (p.namespaceURI === "http://www.w3.org/2000/svg") { return true; }
|
||||
if (p.isContentEditable) { return true; }
|
||||
if (skipAttr && p.hasAttribute && p.hasAttribute(skipAttr)) { return true; }
|
||||
p = p.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ----- helpers ----- */
|
||||
function insertSpanAt(parent, refNode)
|
||||
{
|
||||
const span = document.createElement("span");
|
||||
span.className = autospaceCls;
|
||||
span.setAttribute("aria-hidden", "true");
|
||||
span.textContent = " ";
|
||||
parent.insertBefore(span, refNode);
|
||||
}
|
||||
function insertMarkerAfterPosition(pos)
|
||||
{
|
||||
const tn = pos.textNode;
|
||||
const afterIdx = pos.offset + 1;
|
||||
if (afterIdx >= tn.data.length)
|
||||
{
|
||||
insertSpanAt(tn.parentNode, tn.nextSibling);
|
||||
}
|
||||
else
|
||||
{
|
||||
const split = tn.splitText(afterIdx);
|
||||
insertSpanAt(split.parentNode, split);
|
||||
}
|
||||
}
|
||||
function isAutospaceSpan(node)
|
||||
{
|
||||
return node && node.nodeType === 1
|
||||
&& node.classList && node.classList.contains(autospaceCls);
|
||||
}
|
||||
|
||||
/* ----- Pass A: per-block Pangu on tag-stripped text ----- */
|
||||
function processBlockText(block)
|
||||
{
|
||||
const runs = [];
|
||||
let curText = "";
|
||||
let curMap = [];
|
||||
|
||||
function flushRun()
|
||||
{
|
||||
if (curText.length >= 2) { runs.push({ text: curText, map: curMap }); }
|
||||
curText = "";
|
||||
curMap = [];
|
||||
}
|
||||
|
||||
function visit(node)
|
||||
{
|
||||
if (!node) { return; }
|
||||
if (node.nodeType === 3)
|
||||
{
|
||||
if (isSkipped(node)) { return; }
|
||||
const data = node.data;
|
||||
for (let i = 0; i < data.length; i += 1)
|
||||
{
|
||||
curText += data.charAt(i);
|
||||
curMap.push({ textNode: node, offset: i });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== 1) { return; }
|
||||
if (isSkipped(node)) { return; }
|
||||
if (isAutospaceSpan(node)) { flushRun(); return; }
|
||||
const tag = node.nodeName;
|
||||
/* PILL / BLOCK / SKIP_SET 三類都中斷 run(pill 與 block 邊界
|
||||
另由 Pass B / 各自子 block 處理)。 */
|
||||
if (SKIP_SET.has(tag) || BLOCK_SET.has(tag))
|
||||
{
|
||||
flushRun();
|
||||
return;
|
||||
}
|
||||
if (node.matches && node.matches(pillSelector))
|
||||
{
|
||||
flushRun();
|
||||
return;
|
||||
}
|
||||
/* 樣式標籤或未知元素:透明遞迴。 */
|
||||
let c = node.firstChild;
|
||||
while (c) { visit(c); c = c.nextSibling; }
|
||||
}
|
||||
|
||||
let c = block.firstChild;
|
||||
while (c) { visit(c); c = c.nextSibling; }
|
||||
flushRun();
|
||||
|
||||
for (let r = 0; r < runs.length; r += 1)
|
||||
{
|
||||
const text = runs[r].text;
|
||||
const map = runs[r].map;
|
||||
const spaced = panguSpace(text);
|
||||
if (spaced === text) { continue; }
|
||||
|
||||
/* Walk spaced + text in parallel. 每個 MARK 在 spaced 中要麼
|
||||
(a) 取代了原文同位之 single space,要麼 (b) 是純插入。 */
|
||||
const actions = [];
|
||||
let origIdx = 0;
|
||||
let spacedIdx = 0;
|
||||
while (spacedIdx < spaced.length)
|
||||
{
|
||||
const sc = spaced.charAt(spacedIdx);
|
||||
if (sc === MARK)
|
||||
{
|
||||
const oc = origIdx < text.length ? text.charAt(origIdx) : null;
|
||||
if (oc === " ")
|
||||
{
|
||||
actions.push({ before: map[origIdx - 1], space: map[origIdx] });
|
||||
origIdx += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.push({ before: map[origIdx - 1], space: null });
|
||||
}
|
||||
spacedIdx += 1;
|
||||
continue;
|
||||
}
|
||||
origIdx += 1;
|
||||
spacedIdx += 1;
|
||||
}
|
||||
|
||||
/* 倒序執行,避免 text-node 修改影響前面動作之 offset。 */
|
||||
for (let i = actions.length - 1; i >= 0; i -= 1)
|
||||
{
|
||||
const act = actions[i];
|
||||
if (!act.before) { continue; }
|
||||
if (act.space)
|
||||
{
|
||||
const tn = act.space.textNode;
|
||||
tn.data = tn.data.slice(0, act.space.offset) + tn.data.slice(act.space.offset + 1);
|
||||
}
|
||||
insertMarkerAfterPosition(act.before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- Pass B: pill boundary, style-inline aware ----- */
|
||||
function adjacentLogicalChar(el, direction)
|
||||
{
|
||||
function pickEnd(tn)
|
||||
{
|
||||
if (direction === -1)
|
||||
{
|
||||
return { textNode: tn, offset: tn.data.length - 1, ch: tn.data.charAt(tn.data.length - 1) };
|
||||
}
|
||||
return { textNode: tn, offset: 0, ch: tn.data.charAt(0) };
|
||||
}
|
||||
function descend(into)
|
||||
{
|
||||
let n = direction === -1 ? into.lastChild : into.firstChild;
|
||||
while (n)
|
||||
{
|
||||
if (n.nodeType === 3 && n.data.length > 0) { return pickEnd(n); }
|
||||
if (n.nodeType === 1 && STYLE_SET.has(n.nodeName) && !isAutospaceSpan(n))
|
||||
{
|
||||
const r = descend(n);
|
||||
if (r) { return r; }
|
||||
}
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let n = direction === -1 ? el.previousSibling : el.nextSibling;
|
||||
let p = el.parentNode;
|
||||
while (true)
|
||||
{
|
||||
while (n)
|
||||
{
|
||||
if (n.nodeType === 3)
|
||||
{
|
||||
if (n.data.length > 0) { return pickEnd(n); }
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
continue;
|
||||
}
|
||||
if (n.nodeType === 1)
|
||||
{
|
||||
if (isSkipped(n)) { return null; }
|
||||
if (isAutospaceSpan(n)) { return null; }
|
||||
if (STYLE_SET.has(n.nodeName))
|
||||
{
|
||||
const r = descend(n);
|
||||
if (r) { return r; }
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
continue;
|
||||
}
|
||||
/* Pill 或 block — 停。 */
|
||||
return null;
|
||||
}
|
||||
n = direction === -1 ? n.previousSibling : n.nextSibling;
|
||||
}
|
||||
if (!p || !STYLE_SET.has(p.nodeName)) { return null; }
|
||||
n = direction === -1 ? p.previousSibling : p.nextSibling;
|
||||
p = p.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
const PILL_NEIGHBOR = new RegExp("[A-Za-z0-9" + PANGU_CJK + "]");
|
||||
|
||||
function processPills(root)
|
||||
{
|
||||
root.querySelectorAll(pillSelector).forEach(function (pill)
|
||||
{
|
||||
if (isSkipped(pill)) { return; }
|
||||
const parent = pill.parentNode;
|
||||
if (!parent) { return; }
|
||||
|
||||
const prev = adjacentLogicalChar(pill, -1);
|
||||
if (prev)
|
||||
{
|
||||
if (PILL_NEIGHBOR.test(prev.ch))
|
||||
{
|
||||
insertSpanAt(parent, pill);
|
||||
}
|
||||
else if (prev.ch === " " && prev.offset >= 1)
|
||||
{
|
||||
const tn = prev.textNode;
|
||||
const bc = tn.data.charAt(prev.offset - 1);
|
||||
if (bc !== " " && PILL_NEIGHBOR.test(bc))
|
||||
{
|
||||
tn.data = tn.data.slice(0, prev.offset) + tn.data.slice(prev.offset + 1);
|
||||
insertSpanAt(parent, pill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const next = adjacentLogicalChar(pill, +1);
|
||||
if (next)
|
||||
{
|
||||
if (PILL_NEIGHBOR.test(next.ch))
|
||||
{
|
||||
insertSpanAt(parent, pill.nextSibling);
|
||||
}
|
||||
else if (next.ch === " " && next.textNode.data.length >= 2)
|
||||
{
|
||||
const ac = next.textNode.data.charAt(1);
|
||||
if (ac !== " " && PILL_NEIGHBOR.test(ac))
|
||||
{
|
||||
next.textNode.data = next.textNode.data.slice(1);
|
||||
insertSpanAt(parent, pill.nextSibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ----- 對外 API ----- */
|
||||
function apply(root)
|
||||
{
|
||||
root = root || (typeof document !== "undefined" ? document.body : null);
|
||||
if (!root) { return; }
|
||||
|
||||
/* Pass A:對每個 block 逐個處理。把 BLOCK_TAGS 轉成 selector
|
||||
並對 root 之 descendants 查詢;若 root 本身是 block 也處理。 */
|
||||
const blockSel = Array.from(BLOCK_SET).map(function (t) { return t.toLowerCase(); }).join(",");
|
||||
if (blockSel)
|
||||
{
|
||||
root.querySelectorAll(blockSel).forEach(processBlockText);
|
||||
}
|
||||
if (root.nodeType === 1 && BLOCK_SET.has(root.nodeName))
|
||||
{
|
||||
processBlockText(root);
|
||||
}
|
||||
|
||||
/* Pass B */
|
||||
processPills(root);
|
||||
}
|
||||
|
||||
return { apply: apply };
|
||||
}
|
||||
|
||||
/* ----- UMD-ish export ----- */
|
||||
if (typeof module !== "undefined" && module.exports)
|
||||
{
|
||||
module.exports = { createCjkAutospace: createCjkAutospace };
|
||||
}
|
||||
if (typeof globalThis !== "undefined" && !globalThis.createCjkAutospace)
|
||||
{
|
||||
globalThis.createCjkAutospace = createCjkAutospace;
|
||||
}
|
||||
Reference in New Issue
Block a user