Files
cjk-autospace/cjk-autospace.js
T
admin a5faec5abf feat: 補 ESM export { createCjkAutospace }
下游 Vite / Rollup / TypeScript 靜態 ESM 解析需要真正之 `export`
關鍵字;原 UMD-ish(module.exports + globalThis)對 ESM 消費端
仍須走副作用導入 + globalThis 取值之 workaround。

於檔尾既有 CJS / globalThis 兩塊後再加 `export { createCjkAutospace };
順序刻意如此,讓串接端(IIFE 函式體內不能有 module-level export)
只需以 `sed '/^export {/d'` 剝最後一行即安全。

four-form export 同時支援:ESM named import / CommonJS require /
globalThis / IIFE 串接(後者需剝 export 行)。README 同步補上。
2026-05-26 17:25:34 +08:00

583 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/* cjk-autospace — Pangu.js v7.2.1 風格 CJK ↔ Latin 邊界自動間距 shim。
*
* 工廠模式:`createCjkAutospace(options).apply(root)`。配置由消費端注入;
* 預設值對應 HTML 模板 runtime 之原狀(pill = code, kbd;標籤三分類;
* marker class 為 .cjk-autospaceskip 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;其餘跳過)。
* Pass C — 可選(options.longWordWrap)。把 [A-Za-z]{N,} 之長英文段
* 包進 <span lang="en">;配合消費端 CSS `[lang|="en"]{hyphens:auto}`
* 讓瀏覽器原生連字斷詞。連字符純由 CSS 渲染(DOM 內不存在),複製
* 不污染原文。預設關閉以保持向後相容。
*
* 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;
/* Pass C 設定。預設啟用(minLength=6 / lang="en");顯式傳 false 才
關閉。可給 { minLength, lang } 局部自訂。
注意:斷點前後最少字符數由消費端 CSS `hyphenate-limit-chars` 控
制(建議 `6 3 3`),lib 不涉入。 */
const longWordCfg = (function ()
{
const v = options.longWordWrap;
if (v === false) { return null; }
if (v === undefined || v === true || v === null) { return { minLength: 6, lang: "en" }; }
return {
minLength: typeof v.minLength === "number" && v.minLength >= 2 ? v.minLength : 6,
lang: typeof v.lang === "string" && v.lang ? v.lang : "en",
};
}());
/* ----- 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-passstrip 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 三類都中斷 runpill 與 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);
}
}
}
});
}
/* ----- Pass C: long English word wrap ----- */
/* 冪等性:parent 為 <span lang="…"> 直接跳過。已被本 pass 或作者標
語的英文段都會被 catche.g. <span lang="en-US">…</span> 雖然 lang
不等於 "en",但 caller 已意圖標語,重複包裝無實益。 */
function isInLangSpan(node)
{
const p = node.parentNode;
if (!p || p.nodeType !== 1) { return false; }
if (p.nodeName !== "SPAN") { return false; }
return p.hasAttribute && p.hasAttribute("lang");
}
function wrapLongWordsIn(textNode, cfg)
{
const re = new RegExp("[A-Za-z]{" + cfg.minLength + ",}", "g");
const data = textNode.data;
const matches = [];
let m;
while ((m = re.exec(data)) !== null)
{
matches.push({ start: m.index, end: m.index + m[0].length });
}
if (matches.length === 0) { return; }
const frag = document.createDocumentFragment();
let cursor = 0;
for (let i = 0; i < matches.length; i += 1)
{
const mm = matches[i];
if (mm.start > cursor)
{
frag.appendChild(document.createTextNode(data.slice(cursor, mm.start)));
}
const span = document.createElement("span");
span.setAttribute("lang", cfg.lang);
span.textContent = data.slice(mm.start, mm.end);
frag.appendChild(span);
cursor = mm.end;
}
if (cursor < data.length)
{
frag.appendChild(document.createTextNode(data.slice(cursor)));
}
textNode.parentNode.replaceChild(frag, textNode);
}
function processLongWords(root, cfg)
{
const reTest = new RegExp("[A-Za-z]{" + cfg.minLength + ",}");
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
const targets = [];
let n = walker.nextNode();
while (n)
{
if (!isSkipped(n) && !isInLangSpan(n) && reTest.test(n.data))
{
targets.push(n);
}
n = walker.nextNode();
}
for (let i = 0; i < targets.length; i += 1)
{
wrapLongWordsIn(targets[i], cfg);
}
}
/* ----- 對外 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);
/* Pass Copt-in */
if (longWordCfg) { processLongWords(root, longWordCfg); }
}
return { apply: apply };
}
/* ----- 多形 export -----
*
* 四種消費場景並存:
* ESMVite / Rollup / TypeScript):使用 `export { createCjkAutospace }`
* 可 `import { createCjkAutospace } from "./cjk-autospace.js"`
* CommonJSNode `require`):使用 `module.exports`
* Browser global / `<script>` 標籤:使用 `globalThis` 賦值;
* IIFE 串接(build.sh / Nvim peek hook 把本檔內容 cat 進單個閉包):
* `export` 語句屬 ESM 之 module-level 語法,串入函式體內會語法錯誤,
* 故串接端須剝掉 `export {` 起始之那行(單行 `sed '/^export {/d'`
* 即可)。其餘 module.exports / globalThis 兩塊在閉包內安全(typeof
* 檢查不會 throw、globalThis 賦值無副作用)。
*
* 順序:先 CJS / globalThis(不影響語法解析),最後 ESM `export`
* 讓串接端只需剝最後一塊。
*/
if (typeof module !== "undefined" && module.exports)
{
module.exports = { createCjkAutospace: createCjkAutospace };
}
if (typeof globalThis !== "undefined" && !globalThis.createCjkAutospace)
{
globalThis.createCjkAutospace = createCjkAutospace;
}
export { createCjkAutospace };