@richscripts/richtexteditor 2.14.1 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +173 -10
  2. package/angular/build-manifest.json +23 -0
  3. package/angular/index.d.ts +71 -76
  4. package/angular/index.mjs +2 -0
  5. package/angular/package.json +4 -0
  6. package/headless/index.d.ts +3 -1
  7. package/headless/index.js +72 -16
  8. package/index.d.ts +33 -54
  9. package/integrations/initialization-scope.js +27 -0
  10. package/integrations/native-mount.js +30 -0
  11. package/integrations/shared.d.ts +23 -1
  12. package/integrations/shared.js +58 -22
  13. package/package.json +25 -3
  14. package/react/index.js +118 -37
  15. package/react/index.mjs +29 -0
  16. package/richtexteditor/all_plugins.js +7001 -1078
  17. package/richtexteditor/all_plugins.min.js +62 -59
  18. package/richtexteditor/lang/emoji/LICENSE-CLDR.txt +41 -0
  19. package/richtexteditor/lang/emoji/rte-emoji-de.js +3 -0
  20. package/richtexteditor/lang/emoji/rte-emoji-es.js +3 -0
  21. package/richtexteditor/lang/emoji/rte-emoji-fr.js +3 -0
  22. package/richtexteditor/lang/emoji/rte-emoji-it.js +3 -0
  23. package/richtexteditor/lang/emoji/rte-emoji-ja.js +3 -0
  24. package/richtexteditor/lang/emoji/rte-emoji-ko.js +3 -0
  25. package/richtexteditor/lang/emoji/rte-emoji-nl.js +3 -0
  26. package/richtexteditor/lang/emoji/rte-emoji-pt-pt.js +3 -0
  27. package/richtexteditor/lang/emoji/rte-emoji-pt.js +3 -0
  28. package/richtexteditor/lang/emoji/rte-emoji-ru.js +3 -0
  29. package/richtexteditor/lang/emoji/rte-emoji-zh-Hans.js +3 -0
  30. package/richtexteditor/lang/emoji/rte-emoji-zh-Hant.js +3 -0
  31. package/richtexteditor/lang/rte-lang-af.js +1 -0
  32. package/richtexteditor/lang/rte-lang-ar.js +1 -0
  33. package/richtexteditor/lang/rte-lang-bg.js +1 -0
  34. package/richtexteditor/lang/rte-lang-bn.js +1 -0
  35. package/richtexteditor/lang/rte-lang-bs.js +1 -0
  36. package/richtexteditor/lang/rte-lang-ca.js +1 -0
  37. package/richtexteditor/lang/rte-lang-cs.js +1 -0
  38. package/richtexteditor/lang/rte-lang-cy.js +1 -0
  39. package/richtexteditor/lang/rte-lang-da.js +1 -0
  40. package/richtexteditor/lang/rte-lang-de.js +1017 -2
  41. package/richtexteditor/lang/rte-lang-el.js +1 -0
  42. package/richtexteditor/lang/rte-lang-en.js +1 -0
  43. package/richtexteditor/lang/rte-lang-es.js +1023 -2
  44. package/richtexteditor/lang/rte-lang-et.js +1 -0
  45. package/richtexteditor/lang/rte-lang-fa.js +1 -0
  46. package/richtexteditor/lang/rte-lang-fi.js +1 -0
  47. package/richtexteditor/lang/rte-lang-fil.js +1 -0
  48. package/richtexteditor/lang/rte-lang-fj.js +1 -0
  49. package/richtexteditor/lang/rte-lang-fr.js +1023 -2
  50. package/richtexteditor/lang/rte-lang-ga.js +1 -0
  51. package/richtexteditor/lang/rte-lang-gu.js +1 -0
  52. package/richtexteditor/lang/rte-lang-he.js +1 -0
  53. package/richtexteditor/lang/rte-lang-hi.js +1 -0
  54. package/richtexteditor/lang/rte-lang-hr.js +1 -0
  55. package/richtexteditor/lang/rte-lang-ht.js +1 -0
  56. package/richtexteditor/lang/rte-lang-hu.js +1 -0
  57. package/richtexteditor/lang/rte-lang-id.js +1 -0
  58. package/richtexteditor/lang/rte-lang-is.js +1 -0
  59. package/richtexteditor/lang/rte-lang-it.js +1023 -2
  60. package/richtexteditor/lang/rte-lang-ja.js +1023 -2
  61. package/richtexteditor/lang/rte-lang-kn.js +1 -0
  62. package/richtexteditor/lang/rte-lang-ko.js +1023 -2
  63. package/richtexteditor/lang/rte-lang-lt.js +1 -0
  64. package/richtexteditor/lang/rte-lang-lv.js +1 -0
  65. package/richtexteditor/lang/rte-lang-mg.js +1 -0
  66. package/richtexteditor/lang/rte-lang-mi.js +1 -0
  67. package/richtexteditor/lang/rte-lang-ml.js +1 -0
  68. package/richtexteditor/lang/rte-lang-mr.js +1 -0
  69. package/richtexteditor/lang/rte-lang-ms.js +1 -0
  70. package/richtexteditor/lang/rte-lang-mt.js +1 -0
  71. package/richtexteditor/lang/rte-lang-mww.js +1 -0
  72. package/richtexteditor/lang/rte-lang-nb.js +1 -0
  73. package/richtexteditor/lang/rte-lang-nl.js +1023 -2
  74. package/richtexteditor/lang/rte-lang-otq.js +1 -0
  75. package/richtexteditor/lang/rte-lang-pa.js +1 -0
  76. package/richtexteditor/lang/rte-lang-pl.js +1 -0
  77. package/richtexteditor/lang/rte-lang-pt-pt.js +1023 -2
  78. package/richtexteditor/lang/rte-lang-pt.js +1023 -2
  79. package/richtexteditor/lang/rte-lang-ro.js +1 -0
  80. package/richtexteditor/lang/rte-lang-ru.js +1023 -2
  81. package/richtexteditor/lang/rte-lang-sk.js +1 -0
  82. package/richtexteditor/lang/rte-lang-sl.js +1 -0
  83. package/richtexteditor/lang/rte-lang-sm.js +1 -0
  84. package/richtexteditor/lang/rte-lang-sr-Cyrl.js +1 -0
  85. package/richtexteditor/lang/rte-lang-sr-Latn.js +1 -0
  86. package/richtexteditor/lang/rte-lang-sv.js +1 -0
  87. package/richtexteditor/lang/rte-lang-sw.js +1 -0
  88. package/richtexteditor/lang/rte-lang-ta.js +1 -0
  89. package/richtexteditor/lang/rte-lang-te.js +1 -0
  90. package/richtexteditor/lang/rte-lang-th.js +1 -0
  91. package/richtexteditor/lang/rte-lang-tlh-Latn.js +1 -0
  92. package/richtexteditor/lang/rte-lang-tlh-Piqd.js +1 -0
  93. package/richtexteditor/lang/rte-lang-to.js +1 -0
  94. package/richtexteditor/lang/rte-lang-tr.js +1 -0
  95. package/richtexteditor/lang/rte-lang-ty.js +1 -0
  96. package/richtexteditor/lang/rte-lang-uk.js +1 -0
  97. package/richtexteditor/lang/rte-lang-ur.js +1 -0
  98. package/richtexteditor/lang/rte-lang-vi.js +1 -0
  99. package/richtexteditor/lang/rte-lang-yua.js +1 -0
  100. package/richtexteditor/lang/rte-lang-zh-Hans.js +1017 -2
  101. package/richtexteditor/lang/rte-lang-zh-Hant.js +1017 -2
  102. package/richtexteditor/plugins/a11yenhance.js +2 -1
  103. package/richtexteditor/plugins/a11yenhance.min.js +1 -1
  104. package/richtexteditor/plugins/accessibilitychecker.js +252 -60
  105. package/richtexteditor/plugins/accessibilitychecker.min.js +1 -1
  106. package/richtexteditor/plugins/aitoolkit.js +966 -303
  107. package/richtexteditor/plugins/aitoolkit.min.js +7 -7
  108. package/richtexteditor/plugins/all_plugins.js +7001 -1078
  109. package/richtexteditor/plugins/all_plugins.min.js +62 -59
  110. package/richtexteditor/plugins/autocorrect.js +66 -31
  111. package/richtexteditor/plugins/autocorrect.min.js +1 -1
  112. package/richtexteditor/plugins/autoembed.js +17 -0
  113. package/richtexteditor/plugins/autoembed.min.js +1 -1
  114. package/richtexteditor/plugins/blocktypes.js +18 -9
  115. package/richtexteditor/plugins/blocktypes.min.js +1 -1
  116. package/richtexteditor/plugins/bookmarkcard.js +12 -5
  117. package/richtexteditor/plugins/bookmarkcard.min.js +2 -2
  118. package/richtexteditor/plugins/changecase.js +72 -19
  119. package/richtexteditor/plugins/changecase.min.js +1 -1
  120. package/richtexteditor/plugins/charlimit.js +62 -2
  121. package/richtexteditor/plugins/charlimit.min.js +2 -2
  122. package/richtexteditor/plugins/comments.js +458 -59
  123. package/richtexteditor/plugins/comments.min.js +4 -3
  124. package/richtexteditor/plugins/contentminimap.js +25 -6
  125. package/richtexteditor/plugins/contentminimap.min.js +1 -1
  126. package/richtexteditor/plugins/crossreference.js +1 -1
  127. package/richtexteditor/plugins/crossreference.min.js +1 -1
  128. package/richtexteditor/plugins/dialoga11y.js +26 -4
  129. package/richtexteditor/plugins/dialoga11y.min.js +1 -1
  130. package/richtexteditor/plugins/dictation.js +1 -1
  131. package/richtexteditor/plugins/documentimport.js +220 -51
  132. package/richtexteditor/plugins/documentimport.min.js +4 -4
  133. package/richtexteditor/plugins/documentoutline.js +47 -8
  134. package/richtexteditor/plugins/documentoutline.min.js +1 -1
  135. package/richtexteditor/plugins/docxexport.js +204 -32
  136. package/richtexteditor/plugins/docxexport.min.js +2 -2
  137. package/richtexteditor/plugins/draghandle.js +267 -3
  138. package/richtexteditor/plugins/draghandle.min.js +1 -1
  139. package/richtexteditor/plugins/emailtoolkit.js +28 -14
  140. package/richtexteditor/plugins/emailtoolkit.min.js +1 -1
  141. package/richtexteditor/plugins/emojiautocomplete.js +110 -56
  142. package/richtexteditor/plugins/emojiautocomplete.min.js +1 -1
  143. package/richtexteditor/plugins/footnotes.js +65 -11
  144. package/richtexteditor/plugins/footnotes.min.js +1 -1
  145. package/richtexteditor/plugins/formatpainter.js +10 -0
  146. package/richtexteditor/plugins/formatpainter.min.js +1 -1
  147. package/richtexteditor/plugins/formattingmarks.js +6 -6
  148. package/richtexteditor/plugins/formattingmarks.min.js +1 -1
  149. package/richtexteditor/plugins/gapcursor.js +60 -0
  150. package/richtexteditor/plugins/gapcursor.min.js +1 -1
  151. package/richtexteditor/plugins/ghostcomplete.js +1 -1
  152. package/richtexteditor/plugins/html2pdf.js +1 -1
  153. package/richtexteditor/plugins/html2pdf.min.js +1 -1
  154. package/richtexteditor/plugins/imageeditor.js +12 -6
  155. package/richtexteditor/plugins/imageeditor.min.js +1 -1
  156. package/richtexteditor/plugins/importstyles.js +1 -1
  157. package/richtexteditor/plugins/inlinecode.js +25 -0
  158. package/richtexteditor/plugins/inlinecode.min.js +1 -1
  159. package/richtexteditor/plugins/insertcode.js +21 -3
  160. package/richtexteditor/plugins/insertcode.min.js +4 -4
  161. package/richtexteditor/plugins/insertemoji.js +112 -15
  162. package/richtexteditor/plugins/insertemoji.min.js +1 -1
  163. package/richtexteditor/plugins/insertgallery.js +42 -36
  164. package/richtexteditor/plugins/insertgallery.min.js +1 -1
  165. package/richtexteditor/plugins/inserttemplate.js +33 -17
  166. package/richtexteditor/plugins/inserttemplate.min.js +1 -1
  167. package/richtexteditor/plugins/keyboarda11y.js +66 -3
  168. package/richtexteditor/plugins/keyboarda11y.min.js +1 -1
  169. package/richtexteditor/plugins/layouttable.js +20 -0
  170. package/richtexteditor/plugins/layouttable.min.js +1 -1
  171. package/richtexteditor/plugins/linkchecker.js +29 -9
  172. package/richtexteditor/plugins/linkchecker.min.js +1 -1
  173. package/richtexteditor/plugins/localdraft.js +51 -7
  174. package/richtexteditor/plugins/localdraft.min.js +1 -1
  175. package/richtexteditor/plugins/markdownexport.js +159 -0
  176. package/richtexteditor/plugins/markdownexport.min.js +2 -2
  177. package/richtexteditor/plugins/mathdialog.js +11 -5
  178. package/richtexteditor/plugins/mathdialog.min.js +1 -1
  179. package/richtexteditor/plugins/mentionplugin.js +88 -25
  180. package/richtexteditor/plugins/mentionplugin.min.js +1 -1
  181. package/richtexteditor/plugins/mermaiddiagram.js +9 -3
  182. package/richtexteditor/plugins/mermaiddiagram.min.js +2 -2
  183. package/richtexteditor/plugins/multilevellist.js +4 -0
  184. package/richtexteditor/plugins/multilevellist.min.js +1 -1
  185. package/richtexteditor/plugins/pdfexport.js +235 -35
  186. package/richtexteditor/plugins/pdfexport.min.js +12 -12
  187. package/richtexteditor/plugins/permanentpen.js +3 -3
  188. package/richtexteditor/plugins/permanentpen.min.js +1 -1
  189. package/richtexteditor/plugins/readabilitystats.js +25 -11
  190. package/richtexteditor/plugins/readabilitystats.min.js +1 -1
  191. package/richtexteditor/plugins/readaloud.js +29 -15
  192. package/richtexteditor/plugins/readaloud.min.js +1 -1
  193. package/richtexteditor/plugins/restrictedediting.js +166 -0
  194. package/richtexteditor/plugins/restrictedediting.min.js +2 -2
  195. package/richtexteditor/plugins/revisionhistory.js +169 -37
  196. package/richtexteditor/plugins/revisionhistory.min.js +3 -3
  197. package/richtexteditor/plugins/rubyannotation.js +19 -9
  198. package/richtexteditor/plugins/rubyannotation.min.js +1 -1
  199. package/richtexteditor/plugins/sanitizer.js +62 -3
  200. package/richtexteditor/plugins/sanitizer.min.js +1 -1
  201. package/richtexteditor/plugins/slashcommand.js +30 -16
  202. package/richtexteditor/plugins/slashcommand.min.js +2 -2
  203. package/richtexteditor/plugins/smartchips.js +36 -22
  204. package/richtexteditor/plugins/smartchips.min.js +2 -2
  205. package/richtexteditor/plugins/spellcheck.js +17 -8
  206. package/richtexteditor/plugins/spellcheck.min.js +2 -2
  207. package/richtexteditor/plugins/stateinspector.js +19 -9
  208. package/richtexteditor/plugins/stateinspector.min.js +2 -2
  209. package/richtexteditor/plugins/structured-content-bridge.js +56 -24
  210. package/richtexteditor/plugins/structured-content-bridge.min.js +7 -7
  211. package/richtexteditor/plugins/tableofcontents.js +5 -2
  212. package/richtexteditor/plugins/tableofcontents.min.js +1 -1
  213. package/richtexteditor/plugins/tabletools.js +388 -3
  214. package/richtexteditor/plugins/tabletools.min.js +1 -1
  215. package/richtexteditor/plugins/textdirection.js +18 -5
  216. package/richtexteditor/plugins/textdirection.min.js +1 -1
  217. package/richtexteditor/plugins/textpartlanguage.js +161 -32
  218. package/richtexteditor/plugins/textpartlanguage.min.js +1 -1
  219. package/richtexteditor/plugins/todolist.js +90 -6
  220. package/richtexteditor/plugins/todolist.min.js +1 -1
  221. package/richtexteditor/plugins/trackedchanges.js +1812 -25
  222. package/richtexteditor/plugins/trackedchanges.min.js +5 -3
  223. package/richtexteditor/plugins/wordexport.js +2 -2
  224. package/richtexteditor/plugins/yjscollab.js +9 -4
  225. package/richtexteditor/plugins/yjscollab.min.js +1 -1
  226. package/richtexteditor/plugins/zz_richtextbox_dialog_style.js +22 -13
  227. package/richtexteditor/plugins/zz_richtextbox_dialog_style.min.js +1 -1
  228. package/richtexteditor/rte-config.js +130 -50
  229. package/richtexteditor/rte-headless.js +19 -4
  230. package/richtexteditor/rte-webcomponent.js +89 -16
  231. package/richtexteditor/rte.js +132 -52
  232. package/scripts/build-angular.mjs +97 -0
  233. package/svelte/index.d.ts +2 -1
  234. package/svelte/index.js +80 -9
  235. package/vue/index.js +130 -34
  236. package/angular/component.ts +0 -297
  237. package/angular/index.ts +0 -24
@@ -140,7 +140,9 @@ function RTE_Plugin_DocumentImport() {
140
140
  if (mode === "insert") {
141
141
  if (typeof editor.insertHTML === "function") { editor.insertHTML(html); return true; }
142
142
  }
143
- if (typeof editor.setHTMLCode === "function") { editor.setHTMLCode(html); return true; }
143
+ // 2026-10-03 undoable: Ctrl+Z after an import gives back exactly the document it replaced
144
+ // (tools/import/import-undo.mjs)
145
+ if (typeof editor.setHTMLCode === "function") { editor.setHTMLCode(html, { undoable: true }); return true; }
144
146
  return false;
145
147
  }
146
148
 
@@ -168,14 +170,14 @@ function RTE_Plugin_DocumentImport() {
168
170
  // Library-free .docx: unzip word/document.xml (native
169
171
  // DecompressionStream) + transform WordprocessingML -> HTML.
170
172
  if (!docxSupported()) {
171
- notify("This browser can't unpack .docx (no DecompressionStream). Save as .doc/.html/.md, or wire config.documentImportResolver.");
173
+ notify((config && config.text_imp_nounpack) || "This browser can't unpack .docx (no DecompressionStream). Save as .doc/.html/.md, or wire config.documentImportResolver.");
172
174
  resolve(false);
173
175
  return;
174
176
  }
175
177
  readDocx(file).then(function (html) {
176
178
  if (html != null) resolve(applyHtml(html, mode));
177
- else { notify("Could not read this .docx file."); resolve(false); }
178
- }, function () { notify("Could not read this .docx file."); resolve(false); });
179
+ else { notify((config && config.text_imp_badfile) || "Could not read this .docx file."); resolve(false); }
180
+ }, function () { notify((config && config.text_imp_badfile) || "Could not read this .docx file."); resolve(false); });
179
181
  return;
180
182
  }
181
183
  if (typeof FileReader === "undefined") { resolve(false); return; }
@@ -345,6 +347,13 @@ function RTE_Plugin_DocumentImport() {
345
347
  ctx.rels = parseRels(utf8(b));
346
348
  }));
347
349
  }
350
+ // 2026-10-15 the document's default language, so it is not repeated as lang= on every run. tools/docx/run-props.mjs
351
+ if (byName["word/styles.xml"]) {
352
+ jobs.push(entryBytes(byName["word/styles.xml"]).then(function (b) {
353
+ var mm = /<w:docDefaults>[\s\S]*?<w:lang[^>]*w:val="([^"]+)"/.exec(utf8(b));
354
+ if (mm) ctx.defaultLang = mm[1].toLowerCase();
355
+ }));
356
+ }
348
357
  if (byName["word/numbering.xml"]) {
349
358
  jobs.push(entryBytes(byName["word/numbering.xml"]).then(function (b) {
350
359
  ctx.numbering = parseNumbering(utf8(b));
@@ -517,6 +526,9 @@ function RTE_Plugin_DocumentImport() {
517
526
  var ilvl = lvls[j].getAttributeNS(W, "ilvl") || lvls[j].getAttribute("w:ilvl") || "0";
518
527
  var fmt = val(lvls[j].getElementsByTagNameNS(W, "numFmt")[0]);
519
528
  levels[ilvl] = fmt || "decimal";
529
+ // 2026-10-04 the level's start number ("s" + ilvl), read by the list builder
530
+ var stEl = lvls[j].getElementsByTagNameNS(W, "start")[0];
531
+ if (stEl) levels["s" + ilvl] = parseInt(val(stEl), 10) || 1;
520
532
  }
521
533
  if (aid != null) abstracts[aid] = levels;
522
534
  }
@@ -526,7 +538,18 @@ function RTE_Plugin_DocumentImport() {
526
538
  var nid = nums[k].getAttributeNS(W, "numId") || nums[k].getAttribute("w:numId");
527
539
  var ref = nums[k].getElementsByTagNameNS(W, "abstractNumId")[0];
528
540
  var aref = val(ref);
529
- if (nid != null && abstracts[aref]) out[nid] = abstracts[aref];
541
+ if (nid != null && abstracts[aref]) {
542
+ // a copy per num: its lvlOverride/startOverride (how Word restarts a list) is its own
543
+ var own = {};
544
+ for (var key in abstracts[aref]) own[key] = abstracts[aref][key];
545
+ var ovs = nums[k].getElementsByTagNameNS(W, "lvlOverride");
546
+ for (var oi = 0; oi < ovs.length; oi++) {
547
+ var oIl = ovs[oi].getAttributeNS(W, "ilvl") || ovs[oi].getAttribute("w:ilvl") || "0";
548
+ var so = ovs[oi].getElementsByTagNameNS(W, "startOverride")[0];
549
+ if (so) own["s" + oIl] = parseInt(val(so), 10) || 1;
550
+ }
551
+ out[nid] = own;
552
+ }
530
553
  }
531
554
  } catch (e) {}
532
555
  return out;
@@ -555,34 +578,34 @@ function RTE_Plugin_DocumentImport() {
555
578
  // Passing "∑" through untouched produces a glyph that KaTeX cannot size or
556
579
  // place limits on.
557
580
  var MATH_SYMBOLS = {
558
- "∑": "\\sum", "∏": "\\prod", "∐": "\\coprod",
559
- "∫": "\\int", "∬": "\\iint", "∭": "\\iiint", "∮": "\\oint",
560
- "⋃": "\\bigcup", "⋂": "\\bigcap",
561
- "±": "\\pm", "∓": "\\mp", "×": "\\times", "÷": "\\div",
562
- "−": "-", "≠": "\\neq", "≤": "\\leq", "≥": "\\geq",
563
- "≈": "\\approx", "≡": "\\equiv", "∝": "\\propto",
564
- "∞": "\\infty", "∂": "\\partial", "∇": "\\nabla",
565
- "√": "\\sqrt", "∠": "\\angle", "⋅": "\\cdot", "…": "\\dots",
566
- "→": "\\to", "←": "\\leftarrow", "⇒": "\\Rightarrow", "⇔": "\\Leftrightarrow",
567
- "∈": "\\in", "∉": "\\notin", "⊂": "\\subset", "⊆": "\\subseteq",
568
- "∪": "\\cup", "∩": "\\cap", "∅": "\\emptyset",
569
- "∀": "\\forall", "∃": "\\exists", "¬": "\\neg",
570
- "∧": "\\land", "∨": "\\lor", "∴": "\\therefore",
571
- "α": "\\alpha", "β": "\\beta", "γ": "\\gamma", "δ": "\\delta",
572
- "ε": "\\epsilon", "ζ": "\\zeta", "η": "\\eta", "θ": "\\theta",
573
- "ι": "\\iota", "κ": "\\kappa", "λ": "\\lambda", "μ": "\\mu",
574
- "ν": "\\nu", "ξ": "\\xi", "π": "\\pi", "ρ": "\\rho",
575
- "σ": "\\sigma", "τ": "\\tau", "υ": "\\upsilon", "φ": "\\varphi",
576
- "χ": "\\chi", "ψ": "\\psi", "ω": "\\omega",
577
- "Γ": "\\Gamma", "Δ": "\\Delta", "Θ": "\\Theta", "Λ": "\\Lambda",
578
- "Ξ": "\\Xi", "Π": "\\Pi", "Σ": "\\Sigma", "Φ": "\\Phi",
579
- "Ψ": "\\Psi", "Ω": "\\Omega"
581
+ "\u2211": "\\sum", "\u220f": "\\prod", "\u2210": "\\coprod",
582
+ "\u222b": "\\int", "\u222c": "\\iint", "\u222d": "\\iiint", "\u222e": "\\oint",
583
+ "\u22c3": "\\bigcup", "\u22c2": "\\bigcap",
584
+ "\xb1": "\\pm", "\u2213": "\\mp", "\xd7": "\\times", "\xf7": "\\div",
585
+ "\u2212": "-", "\u2260": "\\neq", "\u2264": "\\leq", "\u2265": "\\geq",
586
+ "\u2248": "\\approx", "\u2261": "\\equiv", "\u221d": "\\propto",
587
+ "\u221e": "\\infty", "\u2202": "\\partial", "\u2207": "\\nabla",
588
+ "\u221a": "\\sqrt", "\u2220": "\\angle", "\u22c5": "\\cdot", "\u2026": "\\dots",
589
+ "\u2192": "\\to", "\u2190": "\\leftarrow", "\u21d2": "\\Rightarrow", "\u21d4": "\\Leftrightarrow",
590
+ "\u2208": "\\in", "\u2209": "\\notin", "\u2282": "\\subset", "\u2286": "\\subseteq",
591
+ "\u222a": "\\cup", "\u2229": "\\cap", "\u2205": "\\emptyset",
592
+ "\u2200": "\\forall", "\u2203": "\\exists", "\xac": "\\neg",
593
+ "\u2227": "\\land", "\u2228": "\\lor", "\u2234": "\\therefore",
594
+ "\u03b1": "\\alpha", "\u03b2": "\\beta", "\u03b3": "\\gamma", "\u03b4": "\\delta",
595
+ "\u03b5": "\\epsilon", "\u03b6": "\\zeta", "\u03b7": "\\eta", "\u03b8": "\\theta",
596
+ "\u03b9": "\\iota", "\u03ba": "\\kappa", "\u03bb": "\\lambda", "\u03bc": "\\mu",
597
+ "\u03bd": "\\nu", "\u03be": "\\xi", "\u03c0": "\\pi", "\u03c1": "\\rho",
598
+ "\u03c3": "\\sigma", "\u03c4": "\\tau", "\u03c5": "\\upsilon", "\u03c6": "\\varphi",
599
+ "\u03c7": "\\chi", "\u03c8": "\\psi", "\u03c9": "\\omega",
600
+ "\u0393": "\\Gamma", "\u0394": "\\Delta", "\u0398": "\\Theta", "\u039b": "\\Lambda",
601
+ "\u039e": "\\Xi", "\u03a0": "\\Pi", "\u03a3": "\\Sigma", "\u03a6": "\\Phi",
602
+ "\u03a8": "\\Psi", "\u03a9": "\\Omega"
580
603
  };
581
604
  // Accent character -> TeX accent command (m:acc carries the mark as a chr).
582
605
  var MATH_ACCENTS = {
583
- "̂": "\\hat", "̃": "\\tilde", "̄": "\\bar", "̅": "\\overline",
584
- "̆": "\\breve", "̇": "\\dot", "̈": "\\ddot", "̊": "\\mathring",
585
- "̌": "\\check", "→": "\\vec", "⃗": "\\vec", "́": "\\acute", "̀": "\\grave"
606
+ "\u0302": "\\hat", "\u0303": "\\tilde", "\u0304": "\\bar", "\u0305": "\\overline",
607
+ "\u0306": "\\breve", "\u0307": "\\dot", "\u0308": "\\ddot", "\u030a": "\\mathring",
608
+ "\u030c": "\\check", "\u2192": "\\vec", "\u20d7": "\\vec", "\u0301": "\\acute", "\u0300": "\\grave"
586
609
  };
587
610
 
588
611
  function ommlToTex(node) {
@@ -702,7 +725,7 @@ function RTE_Plugin_DocumentImport() {
702
725
  case "nary": {
703
726
  // The operator itself is an ATTRIBUTE (m:chr), not text. Miss
704
727
  // it and every summation imports as a bare limit pair.
705
- var chr = propOf(el, "naryPr", "chr") || "∫";
728
+ var chr = propOf(el, "naryPr", "chr") || "\u222b";
706
729
  var op = MATH_SYMBOLS[chr] || mapText(chr);
707
730
  var subHide = propOf(el, "naryPr", "subHide");
708
731
  var supHide = propOf(el, "naryPr", "supHide");
@@ -725,7 +748,7 @@ function RTE_Plugin_DocumentImport() {
725
748
  if (!ch) return side === "l" ? "\\left." : "\\right.";
726
749
  if (ch === "{" || ch === "}") return "\\" + ch;
727
750
  if (ch === "|") return "|";
728
- if (ch === "‖") return "\\|";
751
+ if (ch === "\u2016") return "\\|";
729
752
  // THREE different codepoints render as an angle
730
753
  // bracket and are indistinguishable on screen: the
731
754
  // mathematical pair (U+27E8/9), the CJK pair
@@ -736,10 +759,10 @@ function RTE_Plugin_DocumentImport() {
736
759
  var cp = ch.charCodeAt(0);
737
760
  if (cp === 0x27E8 || cp === 0x3008 || cp === 0x2329) return "\\langle";
738
761
  if (cp === 0x27E9 || cp === 0x3009 || cp === 0x232A) return "\\rangle";
739
- if (ch === "⌊") return "\\lfloor";
740
- if (ch === "⌋") return "\\rfloor";
741
- if (ch === "⌈") return "\\lceil";
742
- if (ch === "⌉") return "\\rceil";
762
+ if (ch === "\u230a") return "\\lfloor";
763
+ if (ch === "\u230b") return "\\rfloor";
764
+ if (ch === "\u2308") return "\\lceil";
765
+ if (ch === "\u2309") return "\\rceil";
743
766
  return ch;
744
767
  }
745
768
  var parts = children(el, "e");
@@ -763,7 +786,7 @@ function RTE_Plugin_DocumentImport() {
763
786
  case "limUpp":
764
787
  return "\\overset" + arg(child(el, "lim")) + arg(child(el, "e"));
765
788
  case "acc": {
766
- var mark = propOf(el, "accPr", "chr") || "̂";
789
+ var mark = propOf(el, "accPr", "chr") || "\u0302";
767
790
  var cmd = MATH_ACCENTS[mark] || "\\hat";
768
791
  return cmd + arg(child(el, "e"));
769
792
  }
@@ -771,7 +794,7 @@ function RTE_Plugin_DocumentImport() {
771
794
  return (propOf(el, "barPr", "pos") === "bot" ? "\\underline" : "\\overline") + arg(child(el, "e"));
772
795
  case "groupChr": {
773
796
  var g = propOf(el, "groupChrPr", "chr");
774
- if (g === "⏟") return "\\underbrace" + arg(child(el, "e"));
797
+ if (g === "\u23df") return "\\underbrace" + arg(child(el, "e"));
775
798
  return "\\overbrace" + arg(child(el, "e"));
776
799
  }
777
800
  case "borderBox":
@@ -820,6 +843,23 @@ function RTE_Plugin_DocumentImport() {
820
843
  }
821
844
 
822
845
  // <w:drawing>/<w:pict> -> <img> from word/media, inlined as a data URI.
846
+ // 2026-10-15 with its alt text (<wp:docPr descr>, else title) and size (<wp:extent>, EMU; or a VML shape's
847
+ // width/height in pt): every picture came back alt="" at natural size. tools/docx/image-alt-size.mjs
848
+ function imageAttrs(node) {
849
+ var all = node.getElementsByTagName("*"), alt = "", w = 0, h = 0;
850
+ for (var i = 0; i < all.length; i++) {
851
+ var e = all[i], ln = e.localName;
852
+ if (ln === "docPr") alt = e.getAttribute("descr") || e.getAttribute("title") || "";
853
+ else if (ln === "extent" && !w) { w = Math.round((parseInt(e.getAttribute("cx"), 10) || 0) * 96 / 914400); h = Math.round((parseInt(e.getAttribute("cy"), 10) || 0) * 96 / 914400); }
854
+ else if (ln === "shape" && !w) {
855
+ var st = e.getAttribute("style") || "", mw = /width:\s*([\d.]+)pt/.exec(st), mh = /height:\s*([\d.]+)pt/.exec(st);
856
+ if (mw && mh) { w = Math.round(parseFloat(mw[1]) * 96 / 72); h = Math.round(parseFloat(mh[1]) * 96 / 72); }
857
+ if (!alt) alt = e.getAttribute("alt") || "";
858
+ }
859
+ }
860
+ // esc() leaves quotes alone; this is an attribute value
861
+ return ' alt="' + esc(alt).replace(/"/g, "&quot;") + '"' + (w > 0 && h > 0 ? ' width="' + w + '" height="' + h + '"' : "");
862
+ }
823
863
  function imageHtml(node) {
824
864
  var blips = node.getElementsByTagName("*");
825
865
  for (var i = 0; i < blips.length; i++) {
@@ -830,8 +870,8 @@ function RTE_Plugin_DocumentImport() {
830
870
  var path = rel.target.replace(/^\.\//, "");
831
871
  var key = path.indexOf("word/") === 0 ? path : "word/" + path;
832
872
  var uri = ctx.media[key];
833
- if (uri) return '<img src="' + uri + '" alt="">';
834
- if (rel.external) return '<img src="' + esc(rel.target) + '" alt="">';
873
+ if (uri) return '<img src="' + uri + '"' + imageAttrs(node) + '>';
874
+ if (rel.external) return '<img src="' + esc(rel.target) + '"' + imageAttrs(node) + '>';
835
875
  // A vector metafile (WMF/EMF) has no browser-renderable form, so
836
876
  // there is no data URI for it. Dropping it silently leaves the
837
877
  // reader with no idea a figure was ever there -- and no filename
@@ -840,7 +880,7 @@ function RTE_Plugin_DocumentImport() {
840
880
  var fileName = path.split("/").pop();
841
881
  queueBlock('<p class="rte-imported-unsupported" data-source="' + esc(fileName) +
842
882
  '"><em>[Unsupported image: ' + esc(fileName) +
843
- " — Windows metafiles have no browser-renderable form]</em></p>");
883
+ " \u2014 Windows metafiles have no browser-renderable form]</em></p>");
844
884
  return "";
845
885
  }
846
886
  }
@@ -885,6 +925,9 @@ function RTE_Plugin_DocumentImport() {
885
925
  // and every tracked deletion imports as an empty run.
886
926
  if (c.localName === "t" || c.localName === "delText") t += c.textContent || "";
887
927
  else if (c.localName === "tab") t += "\t";
928
+ // 2026-10-15 a PAGE break is not a line break: mark it so the paragraph can be split around the
929
+ // editor's page-break marker. tools/docx/page-breaks.mjs
930
+ else if (c.localName === "br" && (c.getAttributeNS(W, "type") || c.getAttribute("w:type")) === "page") t += "\f";
888
931
  else if (c.localName === "br" || c.localName === "cr") t += "\n";
889
932
  }
890
933
  return t;
@@ -975,7 +1018,20 @@ function RTE_Plugin_DocumentImport() {
975
1018
  var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
976
1019
  if (sv > 0) css += "font-size:" + (sv / 2) + "pt;";
977
1020
  }
978
- if (css) txt = '<span style="' + css + '">' + txt + "</span>";
1021
+ // 2026-10-15 small caps, caps, letter spacing and the phrase's language. tools/docx/run-props.mjs
1022
+ var sc = firstChildEl(rpr, "smallCaps"), cp = firstChildEl(rpr, "caps");
1023
+ function on(el) { var v = el.getAttributeNS(W, "val") || el.getAttribute("w:val") || ""; return !/^(0|false|off)$/.test(v); }
1024
+ if (sc && on(sc)) css += "font-variant:small-caps;";
1025
+ if (cp && on(cp)) css += "text-transform:uppercase;";
1026
+ var spc = firstChildEl(rpr, "spacing");
1027
+ if (spc) { var spv = parseInt(spc.getAttributeNS(W, "val") || spc.getAttribute("w:val") || "", 10); if (spv) css += "letter-spacing:" + (Math.round(spv / 15 * 100) / 100) + "px;"; }
1028
+ var lg = firstChildEl(rpr, "lang"), lang = "";
1029
+ if (lg) {
1030
+ lang = (lg.getAttributeNS(W, "val") || lg.getAttribute("w:val") || "").replace(/[^A-Za-z0-9-]/g, "");
1031
+ // a run in the document's own language says nothing: Word stamps it on nearly every run
1032
+ if (ctx.defaultLang ? lang.toLowerCase() === ctx.defaultLang : /^en(-|$)/i.test(lang)) lang = "";
1033
+ }
1034
+ if (css || lang) txt = "<span" + (lang ? ' lang="' + lang + '"' : "") + (css ? ' style="' + css + '"' : "") + ">" + txt + "</span>";
979
1035
  }
980
1036
  return pics + txt;
981
1037
  }
@@ -1051,7 +1107,7 @@ function RTE_Plugin_DocumentImport() {
1051
1107
  items += '<li class="rte-fn-note" data-fn-id="' + f.id + '" data-fn-number="' + (i + 1) +
1052
1108
  '" id="fn-' + f.id + '">' + esc(f.text) +
1053
1109
  '<a class="rte-fn-back" data-fn-id="' + f.id + '" href="#fnref-' + f.id +
1054
- '" contenteditable="false" title="Back to reference">↩</a></li>';
1110
+ '" contenteditable="false" title="Back to reference">\u21a9</a></li>';
1055
1111
  }
1056
1112
  return '<section class="rte-footnotes"><h2 class="rte-fn-title">Footnotes</h2>' +
1057
1113
  '<ol class="rte-fn-list" data-numbering="decimal">' + items + "</ol></section>";
@@ -1165,6 +1221,10 @@ function RTE_Plugin_DocumentImport() {
1165
1221
  // ordered list. Defaulting to bullet when numbering.xml is absent
1166
1222
  // matches the old behaviour rather than inventing <ol>s.
1167
1223
  info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
1224
+ info.numId = nid;
1225
+ info.start = (levels && levels["s" + info.level]) || 1;
1226
+ // 2026-10-04 Word's lettered / Roman formats come back as list-style-type (they became 1. 2. 3.)
1227
+ info.listStyle = { lowerLetter: "lower-alpha", upperLetter: "upper-alpha", lowerRoman: "lower-roman", upperRoman: "upper-roman" }[fmt] || "";
1168
1228
  }
1169
1229
  // Word paragraph indentation. Nothing read this before 2026-09-04,
1170
1230
  // so every indented paragraph in every imported .docx arrived
@@ -1178,7 +1238,23 @@ function RTE_Plugin_DocumentImport() {
1178
1238
  // indent, already carried by the blockquote element itself.
1179
1239
  if (tw > 0 && !(info.quote && tw === 720))
1180
1240
  info.indentPx = Math.round(tw / 15);
1241
+ // 2026-10-15 first-line / hanging indent, line spacing and direction were not read.
1242
+ // tools/docx/paragraph-props.mjs
1243
+ var fl = parseInt(ind.getAttributeNS(W, "firstLine") || ind.getAttribute("w:firstLine") || "", 10);
1244
+ var hg = parseInt(ind.getAttributeNS(W, "hanging") || ind.getAttribute("w:hanging") || "", 10);
1245
+ if (hg > 0) info.textIndentPx = -Math.round(hg / 15);
1246
+ else if (fl > 0) info.textIndentPx = Math.round(fl / 15);
1247
+ }
1248
+ var sp = firstChildEl(ppr, "spacing");
1249
+ if (sp) {
1250
+ var ln = parseInt(sp.getAttributeNS(W, "line") || sp.getAttribute("w:line") || "", 10);
1251
+ var rule = sp.getAttributeNS(W, "lineRule") || sp.getAttribute("w:lineRule") || "auto";
1252
+ // Word's default single spacing (240 auto) is not worth a style
1253
+ if (ln > 0 && !(rule === "auto" && ln === 240)) info.lineHeight = rule === "auto" ? String(Math.round(ln / 240 * 100) / 100) : Math.round(ln / 15) + "px";
1181
1254
  }
1255
+ var bidi = firstChildEl(ppr, "bidi");
1256
+ if (bidi && !/^(0|false|off)$/.test(bidi.getAttributeNS(W, "val") || bidi.getAttribute("w:val") || "")) info.rtl = true;
1257
+ if (firstChildEl(ppr, "pageBreakBefore")) info.pageBreakBefore = true;
1182
1258
  var jc = firstChildEl(ppr, "jc");
1183
1259
  if (jc) {
1184
1260
  var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
@@ -1198,6 +1274,10 @@ function RTE_Plugin_DocumentImport() {
1198
1274
  // markup out of an importer undermines the whole clean-output promise.
1199
1275
  var listStack = [];
1200
1276
  var liOpen = [];
1277
+ // 2026-10-04 items already numbered per numId and level: Word continues a list across
1278
+ // other paragraphs (same numId), so a list reopened here numbers on, and a list that does not
1279
+ // start at 1 keeps its start. Both restarted at 1. tools/docx/import-list-start.mjs
1280
+ var numCount = {};
1201
1281
  function closeListsTo(depth) {
1202
1282
  while (listStack.length > depth) {
1203
1283
  var d = listStack.length - 1;
@@ -1223,6 +1303,32 @@ function RTE_Plugin_DocumentImport() {
1223
1303
  if (node.localName === "p") {
1224
1304
  var st = paraStyle(node);
1225
1305
  var inner = paraInner(node);
1306
+ // 2026-10-15 page breaks come back as the editor's page-break marker (they were dropped):
1307
+ // <w:pageBreakBefore/> before the paragraph, <w:br w:type="page"/> splitting it. tools/docx/page-breaks.mjs
1308
+ if (st.pageBreakBefore) { closeListsTo(0); out.push(pageBreakHtml()); }
1309
+ if (inner && inner.indexOf("\f") !== -1) {
1310
+ if (!st.list && !st.heading && !st.pre && !st.quote) {
1311
+ var pbParts = inner.split("\f");
1312
+ inner = pbParts.pop();
1313
+ closeListsTo(0);
1314
+ for (var pbi = 0; pbi < pbParts.length; pbi++) {
1315
+ if (pbParts[pbi].replace(/<[^>]*>/g, "").trim()) out.push("<p>" + pbParts[pbi] + "</p>");
1316
+ out.push(pageBreakHtml());
1317
+ }
1318
+ } else inner = inner.split("\f").join("");
1319
+ }
1320
+ // 2026-10-15 a paragraph that starts with a ballot box (open U+2610, ticked U+2611 / U+2612) is a task
1321
+ // item: that is how our export and Google Docs write a checklist, and how one is typed in Word. Each
1322
+ // becomes a one-item task list; adjacent ones are joined at the end. tools/docx/task-list.mjs
1323
+ var taskM = !st.list && !st.heading && inner && /^((?:<[^>]+>)*)\s*([\u2610\u2611\u2612])[\s\u00a0]?/.exec(inner);
1324
+ if (taskM) {
1325
+ closeListsTo(0);
1326
+ var taskBody = taskM[1] + inner.slice(taskM[0].length);
1327
+ out.push('<ul class="rte-task-list"><li class="rte-task-item" data-checked="' + (taskM[2] === "\u2610" ? "false" : "true") + '"><span class="rte-task-checkbox" contenteditable="false"></span>' + (taskBody || "<br>") + "</li></ul>");
1328
+ var tboxes = flushBlocks();
1329
+ if (tboxes) out.push(tboxes);
1330
+ continue;
1331
+ }
1226
1332
  if (st.list) {
1227
1333
  var want = (st.level || 0) + 1;
1228
1334
  var tag = st.ordered ? "ol" : "ul";
@@ -1235,11 +1341,24 @@ function RTE_Plugin_DocumentImport() {
1235
1341
  // Going deeper leaves the parent <li> open so the sublist
1236
1342
  // nests inside it.
1237
1343
  while (listStack.length < want) {
1238
- out.push("<" + tag + ">");
1344
+ var olStart = (tag === "ol" && listStack.length === want - 1) ? (st.start || 1) + (numCount[st.numId + ":" + st.level] || 0) : 1;
1345
+ var olAttrs = (olStart !== 1 ? " start=\"" + olStart + "\"" : "") +
1346
+ (tag === "ol" && listStack.length === want - 1 && st.listStyle ? " style=\"list-style-type: " + st.listStyle + "\"" : "");
1347
+ out.push("<" + tag + olAttrs + ">");
1239
1348
  listStack.push(tag);
1240
1349
  liOpen.push(false);
1241
1350
  }
1242
1351
  if (liOpen[want - 1]) { out.push("</li>"); liOpen[want - 1] = false; }
1352
+ if (st.ordered) {
1353
+ var ck = st.numId + ":" + st.level;
1354
+ numCount[ck] = (numCount[ck] || 0) + 1;
1355
+ }
1356
+ // any higher-level item - a BULLET too (2026-10-05 review) - restarts the numbered
1357
+ // levels below it (Word's default lvlRestart)
1358
+ for (var cKey in numCount) {
1359
+ var cp = cKey.split(":");
1360
+ if (cp[0] === String(st.numId) && +cp[1] > st.level) numCount[cKey] = 0;
1361
+ }
1243
1362
  out.push("<li>" + (inner || ""));
1244
1363
  liOpen[want - 1] = true;
1245
1364
  continue;
@@ -1264,7 +1383,9 @@ function RTE_Plugin_DocumentImport() {
1264
1383
  // indentUseMargin defaults to margin, and margin is what
1265
1384
  // w:ind means, so import writes margin-left to match.
1266
1385
  if (st.indentPx) decl.push("margin-left:" + st.indentPx + "px");
1267
- var style = decl.length ? ' style="' + decl.join(";") + '"' : "";
1386
+ if (st.textIndentPx) decl.push("text-indent:" + st.textIndentPx + "px");
1387
+ if (st.lineHeight) decl.push("line-height:" + st.lineHeight);
1388
+ var style = (st.rtl ? ' dir="rtl"' : "") + (decl.length ? ' style="' + decl.join(";") + '"' : "");
1268
1389
  // A paragraph that contained ONLY a text box has no text of
1269
1390
  // its own; emitting an empty <p> before the box adds a blank
1270
1391
  // line that was never in the document.
@@ -1285,10 +1406,23 @@ function RTE_Plugin_DocumentImport() {
1285
1406
  // The notes section goes last, after every marker has been collected.
1286
1407
  var notes = footnotesSection();
1287
1408
  if (notes) out.push(notes);
1288
- return out.join("\n");
1409
+ // one task list per run of task items
1410
+ return out.join("\n").replace(/<\/ul>\n<ul class="rte-task-list">/g, "");
1289
1411
 
1412
+ // the editor's own page-break marker (rte.js __UI_InsertPageBreak), so it looks and behaves the same
1413
+ function pageBreakHtml() {
1414
+ var lbl = editor && editor.getLangText ? editor.getLangText("insertpagebreak") : "";
1415
+ if (!lbl || lbl === "insertpagebreak") lbl = "Page break";
1416
+ return '<div data-rte-page-break="true" contenteditable="false" style="position:relative;text-align:center;border-top:2px dashed #94a3b8;margin:18px 0 14px;padding:0;height:0;line-height:0;user-select:none;">' +
1417
+ '<span style="position:relative;top:-0.75em;display:inline-block;padding:0 10px;background:#fff;color:#64748b;font-size:12px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;line-height:1.6;">' + esc(lbl) + "</span></div>";
1418
+ }
1290
1419
  function tableHtml(tbl) {
1291
1420
  var rows = [], kids = tbl.childNodes;
1421
+ // 2026-10-15 merged cells: <w:gridSpan> is colspan; <w:vMerge w:val="restart"/> starts a rowspan and each
1422
+ // bare <w:vMerge/> below it, at the same grid column, extends it instead of becoming a cell. Both were
1423
+ // ignored, so merged cells came back split. tools/docx/merged-cells.mjs
1424
+ var openMerge = {};
1425
+ function wv(el) { return el ? (el.getAttributeNS(W, "val") || el.getAttribute("w:val") || "") : ""; }
1292
1426
  for (var i = 0; i < kids.length; i++) {
1293
1427
  var tr = kids[i];
1294
1428
  if (tr.nodeType !== 1 || tr.localName !== "tr") continue;
@@ -1299,26 +1433,61 @@ function RTE_Plugin_DocumentImport() {
1299
1433
  var trPr = firstChildEl(tr, "trPr");
1300
1434
  var isHeaderRow = !!(trPr && firstChildEl(trPr, "tblHeader"));
1301
1435
  var cellTag = isHeaderRow ? "th" : "td";
1302
- var cells = [];
1436
+ var cells = [], gcol = 0;
1303
1437
  for (var j = 0; j < tr.childNodes.length; j++) {
1304
1438
  var tc = tr.childNodes[j];
1305
1439
  if (tc.nodeType !== 1 || tc.localName !== "tc") continue;
1440
+ var tcPr = firstChildEl(tc, "tcPr");
1441
+ var span = parseInt(wv(tcPr && firstChildEl(tcPr, "gridSpan")), 10) || 1;
1442
+ var vm = tcPr && firstChildEl(tcPr, "vMerge");
1443
+ if (vm && wv(vm) !== "restart" && openMerge[gcol]) { openMerge[gcol].rowspan++; gcol += span; continue; }
1306
1444
  var cellHtml = "";
1307
1445
  for (var k = 0; k < tc.childNodes.length; k++) {
1308
1446
  if (tc.childNodes[k].localName === "p") cellHtml += "<p>" + (paraInner(tc.childNodes[k]) || "<br>") + "</p>";
1309
1447
  }
1310
- cells.push("<" + cellTag + ">" + cellHtml + "</" + cellTag + ">");
1448
+ var cellObj = { tag: cellTag, html: cellHtml, colspan: span, rowspan: 1 };
1449
+ cells.push(cellObj);
1450
+ for (var sc = gcol; sc < gcol + span; sc++) delete openMerge[sc];
1451
+ if (vm && wv(vm) === "restart") openMerge[gcol] = cellObj;
1452
+ gcol += span;
1453
+ }
1454
+ rows.push(cells);
1455
+ }
1456
+ // 2026-10-15 the grid's column widths (as percentages, so the ratio holds at any page width) and a
1457
+ // percentage table width. tools/docx/table-widths.mjs
1458
+ var head = "<table", colgroup = "";
1459
+ var tPr = firstChildEl(tbl, "tblPr"), tW = tPr && firstChildEl(tPr, "tblW");
1460
+ if (tW && (tW.getAttributeNS(W, "type") || tW.getAttribute("w:type")) === "pct") {
1461
+ var pv = String(tW.getAttributeNS(W, "w") || tW.getAttribute("w:w") || "");
1462
+ var pn = /%$/.test(pv) ? parseFloat(pv) : parseFloat(pv) / 50;
1463
+ if (pn > 0) head += ' style="width:' + (Math.round(pn * 10) / 10) + '%"';
1464
+ }
1465
+ var grid = firstChildEl(tbl, "tblGrid");
1466
+ if (grid) {
1467
+ var gws = [], gsum = 0;
1468
+ for (var gi = 0; gi < grid.childNodes.length; gi++) {
1469
+ var gc = grid.childNodes[gi];
1470
+ if (gc.nodeType !== 1 || gc.localName !== "gridCol") continue;
1471
+ var gw = parseInt(gc.getAttributeNS(W, "w") || gc.getAttribute("w:w") || "", 10) || 0;
1472
+ gws.push(gw); gsum += gw;
1311
1473
  }
1312
- rows.push("<tr>" + cells.join("") + "</tr>");
1474
+ if (gws.length > 1 && gsum > 0 && gws.every(function (g) { return g > 0; }))
1475
+ colgroup = "<colgroup>" + gws.map(function (g) { return '<col style="width:' + (Math.round(g / gsum * 1000) / 10) + '%">'; }).join("") + "</colgroup>";
1313
1476
  }
1314
- return "<table>" + rows.join("") + "</table>";
1477
+ return head + ">" + colgroup + rows.map(function (cs) {
1478
+ return "<tr>" + cs.map(function (c) {
1479
+ return "<" + c.tag + (c.colspan > 1 ? ' colspan="' + c.colspan + '"' : "") + (c.rowspan > 1 ? ' rowspan="' + c.rowspan + '"' : "") + ">" + c.html + "</" + c.tag + ">";
1480
+ }).join("") + "</tr>";
1481
+ }).join("") + "</table>";
1315
1482
  }
1316
1483
  }
1317
1484
 
1318
1485
  function notify(msg) {
1319
1486
  try {
1320
1487
  if (editor && typeof editor.createDialog === "function") {
1321
- var d = editor.createDialog((editor.getLangText && editor.getLangText("importtitle")) || "Import document", "rte-dialog-import");
1488
+ // 2026-10-18 getLangText returns the KEY when a pack lacks it, so this dialog was titled "importtitle" on every
1489
+ // page; the title and messages read config.text_ keys. tools/i18n/raw-key-titles.mjs
1490
+ var d = editor.createDialog((config && config.text_importtitle) || "Import document", "rte-dialog-import");
1322
1491
  var w = d.ownerDocument.createElement("div");
1323
1492
  w.style.cssText = "padding:16px;max-width:420px;font:13px -apple-system,Segoe UI,sans-serif;line-height:1.5";
1324
1493
  w.textContent = msg;