2d42c7ecf2
從零重寫之第一垂直切片(依 ARCHITECTURE.md 契約),涵蓋並超越 v1 全部功能。 核心基座(src/core):unicode 字元表+標點子類+字形側位;locale 語言與 全角/開明政策(全域 lang.style +逐 lang policy);dom(jz-* 工廠+可還原 標記);finder 遍歷引擎(avoid/scope.include/邏輯 run/pill 鄰字/分塊閘); pass 聚合管線。 功能 pass(src/typeset): - spacing:中西間隙採 margin 模型——包裹邊界左側字、以 margin-right 留隙, 不注入空白字元(textContent 完全不變、複製緊湊、justify 不暴增、不撐框、 行首乾淨)。含跨樣式邊界、pill、原檔空格移除。 - gapTrim(lineedge):layout pass,行末去隙,修正 justify 右緣內縮。 - longword:長英文詞 <span lang> + hyphens。 - jiya:標點 charify + 全角/開明/半角(全域 + 逐 lang);半形以字型 OpenType halt 渲染(按字形正確定位、不重疊,居中字形 !? 與繁體 locl 句逗點號皆正確); 行內連續擠壓。 - jinze:行首行末避頭尾(閉類/點號綁前字、開類綁後字,以詞元為邊界、長引文 中段可斷);亦為 justify 綁定載體。 - lineedge:行端擠壓 layout pass(括號引號;ResizeObserver 重算,瀏覽器專用)。 §8 justify:jz-char inline-block 原子,句末標點單份間距。 v1 相容層(compat/v1):createCjkAutospace().apply 映射,IIFE 全域別名。 構建:esbuild → ESM/IIFE/CSS;tsc → d.ts。測試:39 個 jsdom 案例。 demo:簡繁並列+justify+強制繁體開明+標點寬度量測,載 Noto webfont 作參考 (擠壓依賴字型 halt)。間隔號 · 不觸發中西間隙;200% 等詞元不被禁則切斷。
176 lines
10 KiB
HTML
176 lines
10 KiB
HTML
<!doctype html>
|
||
<html lang="zh-Hans">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>聚珍(Juzhen)切片 1 — 目视验证</title>
|
||
<!-- 明确载入 Noto 系列 webfont 作参考:擠壓依赖字型 OpenType halt,
|
||
系统 fallback 字型(如 PingFang)可能无 halt,Safari 下尤其表现为开明式无效。
|
||
Noto Serif/Sans SC/TC 含 halt,确保跨浏览器一致参考。 -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC&family=Noto+Sans+TC&family=Noto+Serif+SC&family=Noto+Serif+TC&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="../dist/juzhen.css">
|
||
<style>
|
||
/* demo 自身样式(非套件之一)。 */
|
||
body { margin: 0; padding: 2rem; font-size: 18px; line-height: 2; background: #fafafa; color: #1a1a1a; }
|
||
h1 { font-size: 1.4rem; }
|
||
.toolbar { position: sticky; top: 0; background: #fff; border: 1px solid #ddd; padding: .75rem 1rem; margin-bottom: 1.5rem; display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; z-index: 10; }
|
||
.toolbar button, .toolbar label { font-size: .85rem; }
|
||
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
||
.panel { border: 1px solid #ddd; padding: 1rem 1.25rem; background: #fff; }
|
||
.panel h2 { font-size: .95rem; color: #666; margin: 0 0 .5rem; font-weight: 600; }
|
||
.ruler { border-left: 1px dashed #c00; border-right: 1px dashed #c00; }
|
||
.narrow { max-width: 20em; }
|
||
.primary { border-color: #06c; border-width: 2px; }
|
||
code { background: #eee; padding: 0 .25em; border-radius: 3px; font-family: ui-monospace, monospace; }
|
||
.note { font-size: .8rem; color: #888; margin-top: 1.5rem; }
|
||
.selbox { background: #fffbe6; }
|
||
body.justify .juzhen p { text-align: justify; }
|
||
.cases p { margin: .4rem 0; }
|
||
.label { font-size: .72rem; color: #999; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>聚珍(Juzhen)切片 1 — 目视验证页</h1>
|
||
|
||
<div class="toolbar">
|
||
<button id="render">render</button>
|
||
<button id="revert">revert</button>
|
||
<label><input type="checkbox" id="justify"> text-align: justify</label>
|
||
<label><input type="checkbox" id="wide"> 放宽栏(看行内擠壓)</label>
|
||
<label><input type="checkbox" id="force-kaiming"> 强制繁体开明式(兼容性检查)</label>
|
||
<button id="measure">测量标点宽度</button>
|
||
<span id="status" style="font-size:.8rem;color:#888;"></span>
|
||
</div>
|
||
|
||
<div class="cols">
|
||
<!-- 主:简体开明式。窄栏强制换行,以观察行端擠壓(机制 B)。 -->
|
||
<div class="panel juzhen narrow ruler primary cases" lang="zh-Hans">
|
||
<h2>主 · 简体(开明式)— 句内点号+括号引号半形</h2>
|
||
<p><span class="label">中西混排:</span>我用 Neovim 写 TypeScript,配合 ripgrep 与 fzf 做模糊搜索,效率提升了 200%,达到约 3.5 倍。</p>
|
||
<p><span class="label">句末+引号:</span>他问:“真的吗?”我说:“当然!”这是 GPT-4 与 state-of-the-art 工具的时代。</p>
|
||
<p><span class="label">连续标点:</span>完了……怎么办?!真是的——他竟然走了。</p>
|
||
<p><span class="label">括号嵌套:</span>(含括号)与【方括号】以及《标题》,还有“她讲过‘小心’二字”。</p>
|
||
<p><span class="label">间隔号:</span>卡尔·马克思与弗里德里希·恩格斯合著。</p>
|
||
<p><span class="label">列举:</span>第一、第二、第三;以及其他(杂项)的内容。</p>
|
||
</div>
|
||
|
||
<!-- 对比:繁体全角式。同样内容,用以发现政策分支差异。 -->
|
||
<div class="panel juzhen narrow ruler cases" lang="zh-Hant">
|
||
<h2>对比 · 繁體(全角式)— 句內標點不擠,僅鄰接/行端收縮</h2>
|
||
<p><span class="label">中西混排:</span>我用 Neovim 寫 TypeScript,配合 ripgrep 與 fzf 做模糊搜尋,效率提升了 200%,達到約 3.5 倍。</p>
|
||
<p><span class="label">句末+引號:</span>他問:「真的嗎?」我說:「當然!」這是 GPT-4 與 state-of-the-art 工具的時代。</p>
|
||
<p><span class="label">連續標點:</span>完了……怎麼辦?!真是的——他竟然走了。</p>
|
||
<p><span class="label">括號嵌套:</span>(含括號)與【方括號】以及《標題》,還有「她講過『小心』二字」。</p>
|
||
<p><span class="label">間隔號:</span>卡爾·馬克思與弗里德里希·恩格斯合著。</p>
|
||
<p><span class="label">列舉:</span>第一、第二、第三;以及其他(雜項)的內容。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel juzhen selbox" lang="zh-Hans" style="margin-top:1.5rem;">
|
||
<h2>copy-clean 测试 — 框选整段并复制到纯文本编辑器,中西间隙的空格不应被带入</h2>
|
||
<p>请选取此句:我用 Neovim 编辑配置文件,搭配 fzf 与 ripgrep 做模糊搜索。复制后粘到纯文本,应为“我用Neovim编辑配置文件,搭配fzf与ripgrep做模糊搜索。”般紧凑(标点原样保留,中西间隙不入剪贴板)。</p>
|
||
</div>
|
||
|
||
<p class="note">
|
||
验证重点(jsdom 无法覆盖之版面层):①justify 下句末标点只占一份间距(不被拉成两份);
|
||
②开明式句内点号与括号引号视觉半形、繁体全角式不擠(仅相邻/行端收缩);
|
||
③句末标点后接闭引号(如 。 接 ”)行内应约 1.5em、行尾应约 1em;
|
||
④长英文词在窄栏可原生连字断行(需字体支持);⑤复制粘贴中西间隙不污染原文。
|
||
请于 Chrome / Safari / Firefox 三引擎各核对一次。
|
||
<br><b>字型注意</b>:擠壓(开明式半形)依赖字型的 OpenType <code>halt</code> 特性。
|
||
本页已用 webfont 载入 Noto Serif SC/TC(含 halt)作参考;若改用无 halt 的字型
|
||
(含部分系统字型如 PingFang),Safari 等浏览器下开明式擠壓会不生效——这是字型限制,非逻辑错误。
|
||
</p>
|
||
|
||
<details open style="margin-top:1rem;">
|
||
<summary style="font-size:.85rem;color:#666;cursor:pointer;">标点实测宽度(诊断用——点「测量标点宽度」后把下表复制给我)</summary>
|
||
<pre id="measure-out" style="font-size:.75rem;background:#f4f4f4;padding:1rem;overflow:auto;white-space:pre-wrap;">(点上方「测量标点宽度」)</pre>
|
||
</details>
|
||
|
||
<script src="../dist/juzhen.iife.js"></script>
|
||
<script>
|
||
var forceKaiming = false;
|
||
function makeJz()
|
||
{
|
||
// 強制繁體開明式:policy 將 zh-Hant(含 zh-HK/zh-TW,皆歸 zh-Hant)覆寫為
|
||
// 開明式,用以檢查開明政策套到繁體之相容性。
|
||
return Juzhen.createJuzhen({
|
||
scope: { include: ".juzhen" },
|
||
lang: forceKaiming ? { policy: { "zh-Hant": "kaiming" } } : {}
|
||
});
|
||
}
|
||
var jz = makeJz();
|
||
var statusEl = document.getElementById("status");
|
||
function render()
|
||
{
|
||
jz.revert(document.body); // 先還原(重套政策時清除上次產物;首次為 no-op)
|
||
jz = makeJz();
|
||
jz.render(document.body);
|
||
statusEl.textContent = "已 render(" + document.querySelectorAll("jz-hws").length + " 间隙、" + document.querySelectorAll("jz-char").length + " 标点原子;繁体政策:"
|
||
+ (forceKaiming ? "开明式" : "全角式") + ")";
|
||
}
|
||
function revert()
|
||
{
|
||
jz.revert(document.body);
|
||
statusEl.textContent = "已 revert(DOM 还原)";
|
||
}
|
||
document.getElementById("force-kaiming").addEventListener("change", function (e)
|
||
{
|
||
forceKaiming = e.target.checked;
|
||
render();
|
||
});
|
||
// 量測每個 jz-char 之實際渲染寬度(em)。輸出純 ASCII(碼位+class+數值),
|
||
// 並按 lang/codepoint/class 去重,便於回報診斷 halt 是否生效、收哪側。
|
||
function measure()
|
||
{
|
||
var out = [];
|
||
var byPanel = {};
|
||
document.querySelectorAll(".juzhen").forEach(function (panel)
|
||
{
|
||
var lang = panel.getAttribute("lang") || "?";
|
||
var key = lang + " | " + (panel.querySelector("h2") ? panel.querySelector("h2").textContent.slice(0, 10) : "");
|
||
panel.querySelectorAll("jz-char").forEach(function (c)
|
||
{
|
||
var fs = parseFloat(getComputedStyle(c).fontSize) || 16;
|
||
var w = c.getBoundingClientRect().width / fs;
|
||
var cp = "U+" + c.textContent.codePointAt(0).toString(16).toUpperCase();
|
||
while (cp.length < 6) { cp = cp.slice(0, 2) + "0" + cp.slice(2); }
|
||
var cls = Array.prototype.slice.call(c.classList).join(",") || "-";
|
||
var line = key + " " + cp + " " + w.toFixed(3) + "em [" + cls + "]";
|
||
if (!byPanel[line]) { byPanel[line] = true; out.push(line); }
|
||
});
|
||
});
|
||
// 參考:一個漢字寬度(應 ≈ 1.000em)。
|
||
var ref = document.querySelector(".juzhen p");
|
||
if (ref)
|
||
{
|
||
var span = document.createElement("span");
|
||
span.textContent = "字";
|
||
ref.appendChild(span);
|
||
var fs2 = parseFloat(getComputedStyle(span).fontSize) || 16;
|
||
out.unshift("参考 漢字「字」 " + (span.getBoundingClientRect().width / fs2).toFixed(3) + "em(应 ≈ 1.000)");
|
||
ref.removeChild(span);
|
||
}
|
||
document.getElementById("measure-out").textContent = out.join("\n");
|
||
}
|
||
document.getElementById("measure").addEventListener("click", measure);
|
||
document.getElementById("render").addEventListener("click", render);
|
||
document.getElementById("revert").addEventListener("click", revert);
|
||
document.getElementById("justify").addEventListener("change", function (e)
|
||
{
|
||
document.body.classList.toggle("justify", e.target.checked);
|
||
});
|
||
document.getElementById("wide").addEventListener("change", function (e)
|
||
{
|
||
document.querySelectorAll(".narrow").forEach(function (el)
|
||
{
|
||
el.style.maxWidth = e.target.checked ? "none" : "";
|
||
});
|
||
});
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|