| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172 |
- /**
- * utils/cjk-deep.js —— 数据边界的中英混排落地(v11c)。
- *
- * 为什么在数据边界而不是渲染期:
- * 1. 渲染期改 DOM(MutationObserver / updated() 里遍历)是「先渲染错、再事后擦」,
- * 带来常驻扫描、全局挂载、以及 Lit 下次渲染把改动覆盖回去的竞态;
- * 2. 在页面模板里逐个包 t() 要改几十处,漏一处就是 338 处残留(实测过);
- —— v11c 补齐:数据边界覆盖不到模板里**临时拼的**字符串(`S4 ${name}`、`${n} 小时前`),
- 所以渲染期另有一处落点 `t()`(pages/shared.js,= 本文件的 cjkString);两处并存、各管一半:
- * 3. 数据只有**一个入口**(board-service.load → buildModel),在这里做一次,
- * 所有视图、所有组件、所有未来新加的页面自动生效。
- *
- * 为什么安全:
- * - 只在「汉字 ↔ 拉丁/数字交界」处把空白换成 U+2009,其余字符一个不动;
- * - 幂等:已经带 U+2009 的不再处理;
- * - 只处理**展示用**的 model(buildModel 的产物),不碰 core 里的匹配/筛选逻辑源数据;
- * - 跳过 URL / 文件名 / 纯 ASCII / ObjectId / 日期 ISO 串。
- */
- import { spaceCJK, THIN } from './type.js';
- const HAS_CJK = /[\u4e00-\u9fa5]/;
- /** 汉字与拉丁/数字相邻(中间可以是普通空格、NBSP、全角空格,或已是一个 U+2009) */
- const NEAR = /[\u4e00-\u9fa5][ \t\u00a0\u3000\u2009]*[A-Za-z0-9]|[A-Za-z0-9][ \t\u00a0\u3000\u2009]*[\u4e00-\u9fa5]/;
- /** 不该动的串:URL、路径、文件名、ISO 时间、纯 ASCII 标识符、邮箱 */
- const SKIP = /^(?:[a-z]+:\/\/|mailto:|\/|\.\/|\.\.\/)|[\w.+-]+@[\w-]+\.[\w.]+|[\w-]+\.(?:js|mjs|json|md|css|png|jpe?g|svg|webp|html?|pdf|sh|txt)\b|^\d{4}-\d{2}-\d{2}T[\d:.]+Z?$|^[A-Za-z0-9_-]{12,}$/;
- /** 单串处理(幂等、无汉字或不相邻则原样返回) */
- export function cjkString(s) {
- if (typeof s !== 'string' || s.length === 0) return s;
- if (!HAS_CJK.test(s) || !NEAR.test(s)) return s;
- if (SKIP.test(s.trim())) return s;
- return spaceCJK(s);
- }
- /**
- * 深度遍历对象/数组,处理所有字符串值。
- * - 返回新对象(不改原 model,便于对比与回退)
- * - 保留 Date、null、数值、布尔、函数引用
- * - 循环引用保护
- */
- export function cjkDeep(value, seen = new WeakMap()) {
- if (typeof value === 'string') return cjkString(value);
- if (value === null || typeof value !== 'object') return value;
- if (value instanceof Date) return value;
- if (Array.isArray(value)) {
- if (seen.has(value)) return seen.get(value);
- const out = [];
- seen.set(value, out);
- for (const v of value) out.push(cjkDeep(v, seen));
- return out;
- }
- if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
- // Parse 的 {__type:'Date',iso:...} 这类包装、Map/Set 等一律原样返回
- if (value.__type === 'Date') return value;
- return value;
- }
- if (seen.has(value)) return seen.get(value);
- const out = {};
- seen.set(value, out);
- for (const k of Object.keys(value)) out[k] = cjkDeep(value[k], seen);
- return out;
- }
- /** 统计:给验收脚本用,检查一个 model 里 U+2009 的数量 */
- export function countThin(value, seen = new WeakSet()) {
- if (typeof value === 'string') return value.split(THIN).length - 1;
- if (value === null || typeof value !== 'object' || seen.has(value)) return 0;
- seen.add(value);
- if (Array.isArray(value)) return value.reduce((n, v) => n + countThin(v, seen), 0);
- return Object.keys(value).reduce((n, k) => n + countThin(value[k], seen), 0);
- }
|