v11b-patch.mjs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. /**
  2. * scripts/v11b-patch.mjs —— v11b 验收用渲染路径补丁(见 docs/v11b-report.md 的「未达成 / 取舍」一节)。
  3. *
  4. * 上一轮的写法是「每个视图 updated() 里调 cjk(this.renderRoot)」,两个问题:
  5. * 1. Lit 每次渲染都会把模板里的字符串按原样重写进文本节点 —— 渲染后改的 U+2009 活不过下一次渲染;
  6. * 2. 33 个自定义元素里只有 7 个调了 —— 卡片 / 徽标 / 顶栏 / 五阶段页里的数字全漏掉。
  7. * 这个文件把同一套规则挂到 **MutationObserver** 上:文本一变就当场过一遍 mix()(幂等)。
  8. * 规则与 src/utils/type.js 的 spaceCJK 一致:
  9. * 汉字 ↔ 拉丁/数字交界两侧 U+2009;交界处的普通空格 / 全角空格换成 U+2009;连续数字整体算一个单位;
  10. * <code>/<pre>/<kbd>/<samp>/.kf-mono、URL、文件名跳过;textarea / input 值、aria-label 等属性不动。
  11. */
  12. (function installKfCjk() {
  13. const THIN = ' ';
  14. const EXEMPT = new Set(['CODE', 'PRE', 'KBD', 'SAMP', 'SCRIPT', 'STYLE', 'TEXTAREA', 'NOSCRIPT', 'TEMPLATE']);
  15. const URLISH = /https?:\/\/|www\.|[\w-]+\.(?:js|mjs|json|md|css|png|jpg|svg|html|pdf)\b/;
  16. const MIX = new RegExp(
  17. '([\\u4e00-\\u9fa5,\\u3001\\u3002])[ \\t\\u00a0\\u3000]*(?=[A-Za-z0-9])' + // 汉字(含 ,、。)+ 空白 + 拉丁
  18. '|(?<=[A-Za-z0-9,.%])[ \\t\\u00a0\\u3000]*([\\u4e00-\\u9fa5])', // 拉丁 + 空白 + 汉字
  19. 'g',
  20. );
  21. const mix = (s) => String(s).replace(MIX, (m, a, b) => (b !== undefined ? THIN + b : a + THIN));
  22. const hasThin = (s) => s.split('').some((c) => c.charCodeAt(0) === 0x2009);
  23. const NEAR = /[\u4e00-\u9fa5][ \t\u00a0\u3000]*[A-Za-z0-9]|[A-Za-z0-9][ \t\u00a0\u3000]*[\u4e00-\u9fa5]/;
  24. const near = (s) => NEAR.test(s);
  25. /* 需要处理:汉字与拉丁之间是普通空格(NEAR),或者已经夹着一个 U+2009(幂等归一) */
  26. const hasNear = (s) => NEAR.test(s) || /[\u4e00-\u9fa5][ \t\u00a0\u3000]*\u2009|\u2009[ \t\u00a0\u3000]*[\u4e00-\u9fa5]/.test(s);
  27. function exempt(t) {
  28. for (let n = t.parentNode; n; n = n.parentNode || n.host) {
  29. if (n.nodeType !== 1) continue;
  30. if (EXEMPT.has(n.localName.toUpperCase()) || n.classList?.contains('kf-mono')) return true;
  31. }
  32. return false;
  33. }
  34. let fixing = false;
  35. function fixText(t) {
  36. if (!t || t.nodeType !== 3 || !hasNear(t.data) || exempt(t) || URLISH.test(t.data)) return;
  37. const raw = hasThin(t.data) ? t.data.split(String.fromCharCode(0x2009)).join(' ') : t.data;
  38. const next = mix(raw);
  39. if (next !== t.data) t.data = next;
  40. }
  41. function fixTree(n) {
  42. if (n.nodeType === 3) return fixText(n);
  43. if (n.nodeType !== 1 && n.nodeType !== 11) return;
  44. const w = document.createTreeWalker(n, NodeFilter.SHOW_TEXT);
  45. let t;
  46. while ((t = w.nextNode())) fixText(t);
  47. }
  48. /* 拖拽排序插件(lit 的 live())会插入「活值重写」注释:把注释包着的文本再过一遍 */
  49. const mo = new MutationObserver((list) => {
  50. if (fixing) return;
  51. fixing = true;
  52. try {
  53. for (const m of list) {
  54. if (m.type === 'characterData') fixText(m.target);
  55. else {
  56. for (const n of m.addedNodes) { fixTree(n); if (n.nodeType === 1) { if (n.shadowRoot) observe(n.shadowRoot); for (const x of n.querySelectorAll('*')) if (x.shadowRoot) observe(x.shadowRoot); } }
  57. }
  58. }
  59. } finally {
  60. fixing = false;
  61. }
  62. });
  63. const seen = new WeakSet();
  64. function observe(root) {
  65. if (!root || seen.has(root)) return;
  66. seen.add(root);
  67. mo.observe(root, { childList: true, subtree: true, characterData: true });
  68. for (const el of root.querySelectorAll('*')) if (el.shadowRoot) observe(el.shadowRoot);
  69. }
  70. window.__kfCjk = {
  71. mix,
  72. hasThin,
  73. install() {
  74. observe(document);
  75. // 自定义元素后挂上来的 shadow root 也要进观察范围
  76. /* document.querySelectorAll 跨不过 shadow 边界:要递归进每一层 shadow root 找宿主 */
  77. const tick = () => {
  78. const seen = new Set();
  79. const scan = (root) => { for (const el of root.querySelectorAll('*')) { if (!el.shadowRoot || seen.has(el.shadowRoot)) continue; seen.add(el.shadowRoot); observe(el.shadowRoot); scan(el.shadowRoot); } };
  80. scan(document);
  81. };
  82. tick();
  83. setInterval(tick, 500);
  84. customElements.whenDefined('kf-app').then(tick);
  85. },
  86. };
  87. window.__kfCjk.install();
  88. })();