feat: Pass C —— 長英文段 lang 包裝(預設啟用)
把 [A-Za-z]{N,} 之長英文段包進 <span lang="en">,配合消費端 CSS
[lang|="en"] { hyphens: auto; ... } 用瀏覽器原生 en 字典斷詞 + 渲
染連字符。連字符純由 CSS 渲染,DOM 內不存在 → 選取 / 複製返回原文
無污染。冪等:parent 為 <span lang> 直接跳過。
skip 鏈共用 Pass A / B 之 isSkipped(CODE / KBD / PRE / SAMP / TT
/ VAR / SCRIPT / STYLE / TEXTAREA / INPUT / SVG / contentEditable
/ data-md-key)。
預設啟用以解 CJK 兩端對齊正文中行末長英文詞之斷行問題;消費端可傳
longWordWrap: false 顯式關閉,或 { minLength, lang } 自訂粒度與標
語。
README + .d.ts 同步說明,含字型陷阱(CJK serif 把 U+2010 設為全形)
與消費端 CSS 範本(含 hyphenate-character: "-")。
This commit is contained in:
+89
-1
@@ -14,7 +14,7 @@
|
||||
* 處理。預設 'code, kbd',可由消費端擴充。
|
||||
* - BLOCK — 段落級標籤,Pass A 把它們當作 run 邊界。
|
||||
*
|
||||
* 兩 pass:
|
||||
* 三 pass:
|
||||
* Pass A — 對每個 BLOCK 走 DFS 收集邏輯文本(STYLE 透明、PILL 與子
|
||||
* BLOCK 中斷 run),整段套 panguSpace,把 MARK 反映射回 textNode/
|
||||
* offset。MARK 對應原文若是單 ASCII space 則 strip 後插 marker,
|
||||
@@ -22,6 +22,10 @@
|
||||
* 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 / 容許
|
||||
@@ -61,6 +65,21 @@ function createCjkAutospace(options)
|
||||
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 = "⺀-⼀-"
|
||||
+ "-ゟ゠-ヺー-ヿ"
|
||||
@@ -442,6 +461,72 @@ function createCjkAutospace(options)
|
||||
});
|
||||
}
|
||||
|
||||
/* ----- Pass C: long English word wrap ----- */
|
||||
/* 冪等性:parent 為 <span lang="…"> 直接跳過。已被本 pass 或作者標
|
||||
語的英文段都會被 catch;e.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)
|
||||
{
|
||||
@@ -462,6 +547,9 @@ function createCjkAutospace(options)
|
||||
|
||||
/* Pass B */
|
||||
processPills(root);
|
||||
|
||||
/* Pass C(opt-in) */
|
||||
if (longWordCfg) { processLongWords(root, longWordCfg); }
|
||||
}
|
||||
|
||||
return { apply: apply };
|
||||
|
||||
Reference in New Issue
Block a user