cjk-deep.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. /**
  2. * utils/cjk-deep.js —— 数据边界的中英混排落地(v11c)。
  3. *
  4. * 为什么在数据边界而不是渲染期:
  5. * 1. 渲染期改 DOM(MutationObserver / updated() 里遍历)是「先渲染错、再事后擦」,
  6. * 带来常驻扫描、全局挂载、以及 Lit 下次渲染把改动覆盖回去的竞态;
  7. * 2. 在页面模板里逐个包 t() 要改几十处,漏一处就是 338 处残留(实测过);
  8. —— v11c 补齐:数据边界覆盖不到模板里**临时拼的**字符串(`S4 ${name}`、`${n} 小时前`),
  9. 所以渲染期另有一处落点 `t()`(pages/shared.js,= 本文件的 cjkString);两处并存、各管一半:
  10. * 3. 数据只有**一个入口**(board-service.load → buildModel),在这里做一次,
  11. * 所有视图、所有组件、所有未来新加的页面自动生效。
  12. *
  13. * 为什么安全:
  14. * - 只在「汉字 ↔ 拉丁/数字交界」处把空白换成 U+2009,其余字符一个不动;
  15. * - 幂等:已经带 U+2009 的不再处理;
  16. * - 只处理**展示用**的 model(buildModel 的产物),不碰 core 里的匹配/筛选逻辑源数据;
  17. * - 跳过 URL / 文件名 / 纯 ASCII / ObjectId / 日期 ISO 串。
  18. */
  19. import { spaceCJK, THIN } from './type.js';
  20. const HAS_CJK = /[\u4e00-\u9fa5]/;
  21. /** 汉字与拉丁/数字相邻(中间可以是普通空格、NBSP、全角空格,或已是一个 U+2009) */
  22. const NEAR = /[\u4e00-\u9fa5][ \t\u00a0\u3000\u2009]*[A-Za-z0-9]|[A-Za-z0-9][ \t\u00a0\u3000\u2009]*[\u4e00-\u9fa5]/;
  23. /** 不该动的串:URL、路径、文件名、ISO 时间、纯 ASCII 标识符、邮箱 */
  24. 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,}$/;
  25. /** 单串处理(幂等、无汉字或不相邻则原样返回) */
  26. export function cjkString(s) {
  27. if (typeof s !== 'string' || s.length === 0) return s;
  28. if (!HAS_CJK.test(s) || !NEAR.test(s)) return s;
  29. if (SKIP.test(s.trim())) return s;
  30. return spaceCJK(s);
  31. }
  32. /**
  33. * 深度遍历对象/数组,处理所有字符串值。
  34. * - 返回新对象(不改原 model,便于对比与回退)
  35. * - 保留 Date、null、数值、布尔、函数引用
  36. * - 循环引用保护
  37. */
  38. export function cjkDeep(value, seen = new WeakMap()) {
  39. if (typeof value === 'string') return cjkString(value);
  40. if (value === null || typeof value !== 'object') return value;
  41. if (value instanceof Date) return value;
  42. if (Array.isArray(value)) {
  43. if (seen.has(value)) return seen.get(value);
  44. const out = [];
  45. seen.set(value, out);
  46. for (const v of value) out.push(cjkDeep(v, seen));
  47. return out;
  48. }
  49. if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
  50. // Parse 的 {__type:'Date',iso:...} 这类包装、Map/Set 等一律原样返回
  51. if (value.__type === 'Date') return value;
  52. return value;
  53. }
  54. if (seen.has(value)) return seen.get(value);
  55. const out = {};
  56. seen.set(value, out);
  57. for (const k of Object.keys(value)) out[k] = cjkDeep(value[k], seen);
  58. return out;
  59. }
  60. /** 统计:给验收脚本用,检查一个 model 里 U+2009 的数量 */
  61. export function countThin(value, seen = new WeakSet()) {
  62. if (typeof value === 'string') return value.split(THIN).length - 1;
  63. if (value === null || typeof value !== 'object' || seen.has(value)) return 0;
  64. seen.add(value);
  65. if (Array.isArray(value)) return value.reduce((n, v) => n + countThin(v, seen), 0);
  66. return Object.keys(value).reduce((n, k) => n + countThin(value[k], seen), 0);
  67. }