@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
@@ -36,6 +36,21 @@ RTE_DefaultConfig.plugin_pdfexport = RTE_Plugin_PdfExport;
36
36
  if (typeof RTE_DefaultConfig.pdfExport === "undefined") RTE_DefaultConfig.pdfExport = true;
37
37
 
38
38
  function RTE_Plugin_PdfExport() {
39
+ // An ordered list item's number in its list style: 1, a..z then aa (CSS lower-alpha), i/I roman.
40
+ function listNumber(n, kind) {
41
+ if (kind === "a" || kind === "A") {
42
+ var t = "";
43
+ for (var k = n; k > 0; k = Math.floor((k - 1) / 26)) t = String.fromCharCode(97 + (k - 1) % 26) + t;
44
+ return t ? (kind === "A" ? t.toUpperCase() : t) : String(n); // 0 and below: a number, never ""
45
+ }
46
+ if ((kind === "i" || kind === "I") && n > 0 && n < 4000) {
47
+ var r = "", vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], sy = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
48
+ for (var q = 0; q < vals.length; q++) while (n >= vals[q]) { r += sy[q]; n -= vals[q]; }
49
+ return kind === "I" ? r.toUpperCase() : r;
50
+ }
51
+ return String(n);
52
+ }
53
+
39
54
  var obj = this;
40
55
  var config, editor;
41
56
 
@@ -264,8 +279,11 @@ function RTE_Plugin_PdfExport() {
264
279
  var s = {
265
280
  bold: inherited.bold, italic: inherited.italic, underline: inherited.underline,
266
281
  strike: inherited.strike, size: inherited.size, color: inherited.color,
267
- mono: inherited.mono, link: inherited.link
282
+ mono: inherited.mono, link: inherited.link, lang: inherited.lang
268
283
  };
284
+ // 2026-10-17 a run's language: the nearest lang attribute. tools/pdf/inline-lang.mjs
285
+ var nodeLang = node.getAttribute ? node.getAttribute("lang") : "";
286
+ if (nodeLang) s.lang = nodeLang;
269
287
  var tag = node.tagName ? node.tagName.toLowerCase() : "";
270
288
  if (tag === "b" || tag === "strong") s.bold = true;
271
289
  if (tag === "i" || tag === "em") s.italic = true;
@@ -415,6 +433,19 @@ function RTE_Plugin_PdfExport() {
415
433
  return px * 0.75;
416
434
  }
417
435
 
436
+ // The language a block is written in: its own lang, the nearest ancestor's inside the content, or - when its
437
+ // whole text is one <span lang> - that span's. "" when none is set.
438
+ function blockLang(node) {
439
+ for (var n = node; n && n.nodeType === 1 && n.tagName.toLowerCase() !== "body"; n = n.parentNode) {
440
+ if (n.getAttribute("lang")) return n.getAttribute("lang");
441
+ }
442
+ var el = null, k = node.childNodes;
443
+ for (var i = 0; i < k.length; i++) {
444
+ if (k[i].nodeType === 3 && k[i].nodeValue.replace(/[\s\u200b\u00a0]/g, "")) return "";
445
+ if (k[i].nodeType === 1) { if (el) return ""; el = k[i]; }
446
+ }
447
+ return el && el.getAttribute("lang") ? el.getAttribute("lang") : "";
448
+ }
418
449
  function block(node, style, indent, listMarker, listId, listOrdered, roleOverride) {
419
450
  var tag = node.tagName.toLowerCase();
420
451
  var s = styleOf(node, style);
@@ -430,6 +461,8 @@ function RTE_Plugin_PdfExport() {
430
461
  var lines = self.wrap(runs, width, tag === "pre" ? 1.25 : 1.4);
431
462
  self.blocks.push({
432
463
  type: "text", lines: lines, indent: indent, align: align,
464
+ // 2026-10-15 the block's own language, for /Lang on its structure element. tools/pdf/paragraph-lang.mjs
465
+ lang: blockLang(node),
433
466
  spaceBefore: HEADING_SCALE[tag] ? s.size * 0.6 : self.baseSize * 0.35,
434
467
  spaceAfter: HEADING_SCALE[tag] ? s.size * 0.3 : self.baseSize * 0.35,
435
468
  marker: listMarker || null, markerStyle: s,
@@ -470,7 +503,15 @@ function RTE_Plugin_PdfExport() {
470
503
 
471
504
  if (tag === "hr") { self.blocks.push({ type: "rule", indent: indent }); continue; }
472
505
  if (tag === "ul" || tag === "ol") {
473
- var counter = parseInt(c.getAttribute("start") || "1", 10) || 1;
506
+ var counter = parseInt(c.getAttribute("start"), 10);
507
+ if (isNaN(counter)) counter = 1;
508
+ // 2026-10-04 a lettered / Roman list is numbered the way the editor shows it (it was
509
+ // always 1. 2. 3.). tools/pdf/list-markers.mjs
510
+ var lst = String((c.style && c.style.listStyleType) || "").toLowerCase(), typ = c.getAttribute("type") || "";
511
+ var kind = /^(lower-alpha|lower-latin)$/.test(lst) || typ === "a" ? "a"
512
+ : /^(upper-alpha|upper-latin)$/.test(lst) || typ === "A" ? "A"
513
+ : lst === "lower-roman" || typ === "i" ? "i"
514
+ : lst === "upper-roman" || typ === "I" ? "I" : "1";
474
515
  // Every item of one list shares an id so the tag tree can
475
516
  // rebuild a single <L> around them; without it each item
476
517
  // becomes an isolated paragraph and a screen reader never
@@ -479,7 +520,11 @@ function RTE_Plugin_PdfExport() {
479
520
  for (var li = 0; li < c.childNodes.length; li++) {
480
521
  var item = c.childNodes[li];
481
522
  if (item.nodeType !== 1 || item.tagName.toLowerCase() !== "li") continue;
482
- var marker = tag === "ol" ? (counter++) + "." : "•";
523
+ var marker = tag === "ol" ? listNumber(counter++, kind) + "." : "\u2022";
524
+ // 2026-10-15 a task item shows whether it is done: every item got the plain bullet, so a printed
525
+ // checklist lost every tick. The standard fonts have no ballot box, so "[x]" / "[ ]".
526
+ // tools/pdf/task-list-marks.mjs
527
+ if (c.classList && c.classList.contains("rte-task-list")) marker = item.getAttribute("data-checked") === "true" ? "[x]" : "[ ]";
483
528
  // The item's own text, then any nested list one level in.
484
529
  block(item, styleOf(item, style), indent + self.baseSize * 1.6, marker, listId, tag === "ol");
485
530
  walkNestedLists(item, styleOf(item, style), indent + self.baseSize * 1.6);
@@ -495,6 +540,19 @@ function RTE_Plugin_PdfExport() {
495
540
  if (isBlockTag(tag)) {
496
541
  var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
497
542
  var childRole = (tag === "blockquote") ? "BlockQuote" : roleOverride;
543
+ // 2026-10-15 a block holding just an image - the shape the editor gives every inserted picture,
544
+ // <p><img></p> - is a Figure with its alt text. Only an <img> sitting directly at block level was;
545
+ // inside a <p> it was an inline run, so the PDF had no Figure and no /Alt (WCAG 1.1.1, PDF/UA).
546
+ // tools/pdf/figure-alt.mjs
547
+ var soleImgs = c.getElementsByTagName("img");
548
+ if (soleImgs.length === 1 && !(c.textContent || "").replace(/[\s\u200b\u00a0]/g, "") && !hasBlockChildren(c)) {
549
+ var fdims = imageDims(soleImgs[0], self.contentWidth - indent - own);
550
+ if (fdims.w) {
551
+ var falign = String((c.style && c.style.textAlign) || "").toLowerCase();
552
+ self.blocks.push({ type: "text", role: "Figure", alt: soleImgs[0].getAttribute("alt") || "", lines: [{ parts: [{ img: soleImgs[0], w: fdims.w, h: fdims.h, style: style }], width: fdims.w, size: fdims.h, height: fdims.h, image: true }], indent: indent + own, align: falign === "center" || falign === "right" ? falign : "left", spaceBefore: 4, spaceAfter: 4 });
553
+ continue;
554
+ }
555
+ }
498
556
  if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + own, childRole); continue; }
499
557
  block(c, style, indent + own, null, null, false, childRole);
500
558
  continue;
@@ -533,6 +591,10 @@ function RTE_Plugin_PdfExport() {
533
591
  var rows = [];
534
592
  var trs = node.getElementsByTagName("tr");
535
593
  var maxCells = 0;
594
+ // 2026-10-15 rowspan: covered[col] = rows a cell above still covers there. Cells in the rows below skip those
595
+ // columns (they were placed from column 0, shifting a row left under the wrong header), and each covered
596
+ // position becomes an empty placeholder cell so the grid stays aligned. tools/pdf/table-rowspan.mjs
597
+ var covered = {};
536
598
  for (var r = 0; r < trs.length; r++) {
537
599
  var cells = [];
538
600
  for (var c = 0; c < trs[r].childNodes.length; c++) {
@@ -540,38 +602,77 @@ function RTE_Plugin_PdfExport() {
540
602
  if (cell.nodeType !== 1) continue;
541
603
  var tag = cell.tagName.toLowerCase();
542
604
  if (tag !== "td" && tag !== "th") continue;
543
- cells.push({ node: cell, header: tag === "th" });
605
+ // 2026-10-15 colspan: a cell starts at its grid column and spans its columns. tools/pdf/table-colspan.mjs
606
+ var span = Math.max(1, Math.min(50, parseInt(cell.getAttribute("colspan"), 10) || 1));
607
+ var rspan = Math.max(1, Math.min(500, parseInt(cell.getAttribute("rowspan"), 10) || 1));
608
+ cells.push({ node: cell, header: tag === "th", span: span, rowspan: rspan });
544
609
  }
545
- if (cells.length) { rows.push(cells); if (cells.length > maxCells) maxCells = cells.length; }
610
+ var placed = [], gridCols = 0;
611
+ for (var gc = 0; gc < cells.length; gc++) {
612
+ while (covered[gridCols] > 0) { placed.push({ placeholder: true, start: gridCols, span: 1, header: false }); gridCols++; }
613
+ cells[gc].start = gridCols;
614
+ placed.push(cells[gc]);
615
+ if (cells[gc].rowspan > 1) for (var rc = gridCols; rc < gridCols + cells[gc].span; rc++) covered[rc] = cells[gc].rowspan;
616
+ gridCols += cells[gc].span;
617
+ }
618
+ // covered columns after the row's last cell
619
+ for (var tail in covered) if (+tail >= gridCols && covered[tail] > 0) { placed.push({ placeholder: true, start: +tail, span: 1, header: false }); if (+tail + 1 > gridCols) gridCols = +tail + 1; }
620
+ // this row is done: each covering cell has one row fewer left (a cell covering from THIS row still needs
621
+ // its own count, so it was set to rowspan and drops to rowspan - 1 here)
622
+ for (var dc in covered) { covered[dc]--; if (covered[dc] <= 0) delete covered[dc]; }
623
+ placed.sort(function (p1, p2) { return p1.start - p2.start; });
624
+ cells = cells.length ? placed : cells;
625
+ if (cells.length) { rows.push(cells); if (gridCols > maxCells) maxCells = gridCols; }
546
626
  }
547
627
  if (!rows.length) return;
548
628
 
549
629
  var width = this.contentWidth - indent;
550
630
  var colWidth = width / maxCells;
631
+ // 2026-10-15 column widths follow the <colgroup> (% or px), scaled to the table's width on the page: every
632
+ // table was split into equal columns. A table without one keeps equal columns. tools/pdf/table-col-widths.mjs
633
+ var colWidths = [], cg = [], cw = 0;
634
+ for (var ck = 0; ck < node.children.length; ck++) if (node.children[ck].tagName && node.children[ck].tagName.toLowerCase() === "colgroup") {
635
+ for (var cj = 0; cj < node.children[ck].children.length; cj++) if (node.children[ck].children[cj].tagName.toLowerCase() === "col") cg.push(node.children[ck].children[cj]);
636
+ }
637
+ if (cg.length === maxCells) {
638
+ for (var cq = 0; cq < cg.length; cq++) {
639
+ var cv = (cg[cq].style && cg[cq].style.width) || cg[cq].getAttribute("width") || "", cm = /^\s*([\d.]+)(%|px)?\s*$/.exec(cv);
640
+ if (!cm || !(parseFloat(cm[1]) > 0)) { colWidths = []; break; }
641
+ colWidths.push(parseFloat(cm[1]));
642
+ }
643
+ for (var cs2 = 0; cs2 < colWidths.length; cs2++) cw += colWidths[cs2];
644
+ if (cw > 0) colWidths = colWidths.map(function (v) { return v / cw * width; });
645
+ }
646
+ if (colWidths.length !== maxCells) { colWidths = []; for (var ce = 0; ce < maxCells; ce++) colWidths.push(colWidth); }
647
+ function spanWidth(start, span) { var s = 0; for (var sw = start; sw < start + span && sw < colWidths.length; sw++) s += colWidths[sw]; return s; }
551
648
  var pad = 4;
552
649
  var laid = [];
553
650
  for (var i = 0; i < rows.length; i++) {
554
651
  var out = [], tallest = 0;
555
652
  for (var j = 0; j < rows[i].length; j++) {
653
+ if (rows[i][j].placeholder) { out.push({ lines: [], header: false, start: rows[i][j].start, span: 1, placeholder: true }); continue; }
556
654
  var cs = styleOf(rows[i][j].node, style);
557
655
  if (rows[i][j].header) cs.bold = true;
558
- var lines = self.wrap(self.runsOf(rows[i][j].node, cs), colWidth - pad * 2, 1.35);
656
+ var lines = self.wrap(self.runsOf(rows[i][j].node, cs), spanWidth(rows[i][j].start, rows[i][j].span) - pad * 2, 1.35);
559
657
  var h = 0;
560
658
  for (var k = 0; k < lines.length; k++) h += lines[k].height;
561
659
  if (h > tallest) tallest = h;
562
- out.push({ lines: lines, header: rows[i][j].header });
660
+ out.push({ lines: lines, header: rows[i][j].header, start: rows[i][j].start, span: rows[i][j].span, rowspan: rows[i][j].rowspan || 1 });
563
661
  }
564
- laid.push({ cells: out, height: tallest + pad * 2, header: rows[i][0] && rows[i][0].header });
662
+ laid.push({ cells: out, height: tallest + pad * 2, header: rows[i][0] && !rows[i][0].placeholder && rows[i][0].header });
565
663
  }
566
664
  this.blocks.push({
567
- type: "table", rows: laid, colWidth: colWidth, cols: maxCells,
665
+ type: "table", rows: laid, colWidth: colWidth, colWidths: colWidths, cols: maxCells,
568
666
  indent: indent, pad: pad,
569
667
  spaceBefore: this.baseSize * 0.5, spaceAfter: this.baseSize * 0.5
570
668
  });
571
669
  };
572
670
 
573
671
  // ------------------------------------------------------------- rendering
672
+ // 2026-10-15 the document language, read by the tagger to mark blocks in other languages
673
+ var pdfDocLang = "en";
574
674
  function buildPdf(options) {
675
+ pdfDocLang = String((options && options.lang) || document.documentElement.lang || "en").toLowerCase();
575
676
  var familyKey = String(options.font || config.pdfExportFont || "helvetica").toLowerCase();
576
677
  if (!FONT_FAMILIES[familyKey]) familyKey = "helvetica";
577
678
  var baseSize = Number(options.fontSize || config.pdfExportBaseFontSize || 11);
@@ -583,6 +684,11 @@ function RTE_Plugin_PdfExport() {
583
684
  // overlays, watermarks and formatting marks into the PDF.
584
685
  var host = document.createElement("div");
585
686
  host.innerHTML = editor.getHTMLCode() || "";
687
+ var liveLists = editor.getEditable().querySelectorAll("ol,ul"), exportLists = host.querySelectorAll("ol,ul");
688
+ if (liveLists.length === exportLists.length) for (var li = 0; li < liveLists.length; li++) {
689
+ var listStyle = liveLists[li].ownerDocument.defaultView.getComputedStyle(liveLists[li]).listStyleType;
690
+ if (listStyle) exportLists[li].style.listStyleType = listStyle;
691
+ }
586
692
 
587
693
  var layout = new Layout(geom, baseSize, familyKey);
588
694
  var blocks = layout.run(host);
@@ -716,33 +822,67 @@ function RTE_Plugin_PdfExport() {
716
822
  }
717
823
  var pagesId = pdf.alloc();
718
824
 
719
- var allMarks = [];
825
+ var allMarks = [], heads = [], linkAnnots = [];
720
826
  for (var pi = 0; pi < pages.length; pi++) {
721
827
  var tagger = new Tagger(pi);
722
828
  var draw = renderPage(pages[pi], geom, familyKey, baseSize, imageRef, pi + 1, pages.length, options, tagger);
829
+ for (var hd = 0; hd < (draw.headings || []).length; hd++) { draw.headings[hd].page = pi; heads.push(draw.headings[hd]); }
723
830
  pdf.set(contentIds[pi], { dict: "<< /Length 0 >>", stream: draw.content });
724
831
  allMarks.push(draw.marks);
725
832
  for (var a = 0; a < draw.links.length; a++) {
726
833
  var lk = draw.links[a];
727
- annotIds[pi].push(pdf.add("<< /Type /Annot /Subtype /Link /Rect [" +
834
+ // 2026-10-15 each link annotation is in the tag tree: /StructParent (keys after the pages'), /Contents
835
+ // naming it, and a Link element built in buildStructTree. They were click targets a screen reader
836
+ // could not find (PDF/UA 7.18, Matterhorn 28-011 / 28-004). tools/pdf/link-tags.mjs
837
+ var lsp = pages.length + linkAnnots.length;
838
+ var lid = pdf.add("<< /Type /Annot /Subtype /Link /Rect [" +
728
839
  fixed(lk.x0) + " " + fixed(lk.y0) + " " + fixed(lk.x1) + " " + fixed(lk.y1) +
729
- "] /Border [0 0 0] /A << /S /URI /URI (" + escapeString(toWinAnsi(lk.href)) + ") >> >>"));
840
+ "] /Border [0 0 0] /StructParent " + lsp + " /Contents (" + escapeString(toWinAnsi(String(lk.text || lk.href).replace(/\s+/g, " ").trim() || lk.href)) + ")" +
841
+ " /A << /S /URI /URI (" + escapeString(toWinAnsi(lk.href)) + ") >> >>");
842
+ annotIds[pi].push(lid);
843
+ linkAnnots.push({ id: lid, page: pi, key: lsp, owner: lk.owner });
730
844
  }
731
845
  }
732
846
 
733
- var struct = buildStructTree(pdf, allMarks, pageIds);
847
+ var struct = buildStructTree(pdf, allMarks, pageIds, linkAnnots);
734
848
 
735
849
  for (var pj = 0; pj < pages.length; pj++) {
736
850
  pdf.set(pageIds[pj], "<< /Type /Page /Parent " + pagesId + " 0 R /MediaBox [0 0 " +
737
851
  fixed(geom.width) + " " + fixed(geom.height) + "] /Resources << /Font << " + fontRes.join(" ") + " >>" +
738
852
  (imageRes.length ? " /XObject << " + imageRes.join(" ") + " >>" : "") +
739
853
  " >> /Contents " + contentIds[pj] + " 0 R /StructParents " + pj +
740
- (annotIds[pj].length ? " /Annots [" + annotIds[pj].map(function (id) { return id + " 0 R"; }).join(" ") + "]" : "") +
854
+ (annotIds[pj].length ? " /Annots [" + annotIds[pj].map(function (id) { return id + " 0 R"; }).join(" ") + "] /Tabs /S" : "") +
741
855
  " >>");
742
856
  }
743
857
  pdf.set(pagesId, "<< /Type /Pages /Count " + pages.length + " /Kids [" +
744
858
  pageIds.map(function (id) { return id + " 0 R"; }).join(" ") + "] >>");
745
859
 
860
+ // 2026-10-15 bookmarks from the headings, nested by level, each opening its heading's page at the heading
861
+ // (Word and Chrome export them; a long report had nothing but scrolling - WCAG 2.4.5, PDF2).
862
+ // tools/pdf/outline-bookmarks.mjs
863
+ var outlineId = 0;
864
+ if (heads.length) {
865
+ outlineId = pdf.alloc();
866
+ var oroot = { id: outlineId, kids: [] }, ostack = [oroot], onodes = [];
867
+ for (var oh = 0; oh < heads.length; oh++) {
868
+ var on = { h: heads[oh], id: pdf.alloc(), kids: [], parent: null };
869
+ while (ostack.length > 1 && ostack[ostack.length - 1].h.level >= on.h.level) ostack.pop();
870
+ on.parent = ostack[ostack.length - 1];
871
+ on.parent.kids.push(on);
872
+ ostack.push(on);
873
+ onodes.push(on);
874
+ }
875
+ var ocount = function (n) { var c = n.kids.length; for (var k = 0; k < n.kids.length; k++) c += ocount(n.kids[k]); return c; };
876
+ for (var oi = 0; oi < onodes.length; oi++) {
877
+ var n = onodes[oi], sib = n.parent.kids, at = sib.indexOf(n);
878
+ pdf.set(n.id, "<< /Title (" + escapeString(toWinAnsi(n.h.title)) + ") /Parent " + n.parent.id + " 0 R" +
879
+ (at > 0 ? " /Prev " + sib[at - 1].id + " 0 R" : "") + (at < sib.length - 1 ? " /Next " + sib[at + 1].id + " 0 R" : "") +
880
+ (n.kids.length ? " /First " + n.kids[0].id + " 0 R /Last " + n.kids[n.kids.length - 1].id + " 0 R /Count " + ocount(n) : "") +
881
+ " /Dest [" + pageIds[n.h.page] + " 0 R /XYZ null " + fixed(n.h.y) + " null] >>");
882
+ }
883
+ pdf.set(outlineId, "<< /Type /Outlines /First " + oroot.kids[0].id + " 0 R /Last " + oroot.kids[oroot.kids.length - 1].id + " 0 R /Count " + ocount(oroot) + " >>");
884
+ }
885
+
746
886
  var title = options.title || config.pdfExportTitle || document.title || "Document";
747
887
  var author = options.author || config.pdfExportAuthor || "";
748
888
  // /MarkInfo and /StructTreeRoot declare the file tagged. DisplayDocTitle
@@ -752,6 +892,7 @@ function RTE_Plugin_PdfExport() {
752
892
  var catalogId = pdf.add("<< /Type /Catalog /Pages " + pagesId + " 0 R" +
753
893
  " /Lang (" + escapeString(String(options.lang || document.documentElement.lang || "en")) + ")" +
754
894
  " /MarkInfo << /Marked true >>" +
895
+ (outlineId ? " /Outlines " + outlineId + " 0 R" : "") +
755
896
  " /StructTreeRoot " + struct.rootId + " 0 R" +
756
897
  " /ViewerPreferences << /DisplayDocTitle true >> >>");
757
898
  var infoId = pdf.add("<< /Title (" + escapeString(toWinAnsi(String(title))) + ")" +
@@ -766,13 +907,13 @@ function RTE_Plugin_PdfExport() {
766
907
  // The tree is built from the marks rather than from the block list because a
767
908
  // single block can be split across pages: its structure element then holds
768
909
  // several marked-content references, each naming the page it lives on.
769
- function buildStructTree(pdf, allMarks, pageIds) {
910
+ function buildStructTree(pdf, allMarks, pageIds, linkAnnots) {
770
911
  var rootId = pdf.alloc();
771
912
  var nodes = []; // { id, role, kids, extra, children, parent }
772
913
  var parentTree = []; // per page: array of struct ids by MCID
773
914
 
774
915
  function node(role, extra) {
775
- var n = { id: pdf.alloc(), role: role, kids: [], extra: extra || "", children: [], parent: null };
916
+ var n = { id: pdf.alloc(), role: role, kids: [], extra: extra || "", children: [], parent: null, seq: [] };
776
917
  nodes.push(n);
777
918
  return n;
778
919
  }
@@ -785,6 +926,7 @@ function RTE_Plugin_PdfExport() {
785
926
  var tableNodes = new Map(); // block -> Table node
786
927
  var rowNodes = new Map(); // row -> TR node
787
928
  var cellNodes = new Map(); // row -> { col -> TD/TH node }
929
+ var linkNodes = new Map(); // link owner -> Link node (2026-10-17, tools/pdf/link-order.mjs)
788
930
 
789
931
  for (var p = 0; p < allMarks.length; p++) {
790
932
  var marks = allMarks[p];
@@ -792,7 +934,8 @@ function RTE_Plugin_PdfExport() {
792
934
  for (var m = 0; m < marks.length; m++) {
793
935
  var owner = marks[m].owner, mcid = marks[m].mcid;
794
936
  var target = resolve(owner);
795
- target.kids.push({ page: p, mcid: mcid });
937
+ target.kids.push({ page: p, mcid: mcid });
938
+ target.seq.push({ mcr: { page: p, mcid: mcid } });
796
939
  parentTree[p][mcid] = target.id;
797
940
  }
798
941
  }
@@ -803,9 +946,25 @@ function RTE_Plugin_PdfExport() {
803
946
  if (child.parent) return;
804
947
  child.parent = parent;
805
948
  parent.children.push(child);
949
+ parent.seq.push({ elem: child });
950
+ }
951
+ // a Link element per link annotation, under the paragraph / cell holding it
952
+ var linkKeys = [];
953
+ for (var la = 0; la < (linkAnnots || []).length; la++) {
954
+ var lnk;
955
+ if (linkAnnots[la].owner && linkAnnots[la].owner.kind === "link") lnk = resolve(linkAnnots[la].owner);
956
+ else { lnk = node("Link"); attach(resolve(linkAnnots[la].owner), lnk); }
957
+ lnk.objr = linkAnnots[la];
958
+ linkKeys.push(linkAnnots[la].key + " " + lnk.id + " 0 R");
806
959
  }
807
960
 
808
961
  function resolve(owner) {
962
+ // 2026-10-17 a link's words: their own Link element under the paragraph or cell, created where they occur
963
+ if (owner.kind === "link") {
964
+ var ln = linkNodes.get(owner);
965
+ if (!ln) { ln = node("Link"); linkNodes.set(owner, ln); attach(resolve(owner.parent), ln); }
966
+ return ln;
967
+ }
809
968
  if (owner.kind === "cell") {
810
969
  var table = tableNodes.get(owner.block);
811
970
  if (!table) { table = node("Table"); tableNodes.set(owner.block, table); attach(docNode, table); }
@@ -816,7 +975,10 @@ function RTE_Plugin_PdfExport() {
816
975
  if (!perRow[owner.col]) {
817
976
  // A header cell must be TH, not TD: it is what lets a screen
818
977
  // reader announce "Revenue, 1,240" instead of just "1,240".
819
- perRow[owner.col] = node(owner.header ? "TH" : "TD", owner.header ? " /Scope /Column" : "");
978
+ // 2026-10-15 table attributes belong in /A << /O /Table >> (where PDF/UA checkers read them; /Scope
979
+ // sat on the element), with /ColSpan for a merged cell. tools/pdf/table-colspan.mjs
980
+ var tattr = (owner.header ? " /Scope /Column" : "") + (owner.span > 1 ? " /ColSpan " + owner.span : "") + (owner.rowspan > 1 ? " /RowSpan " + owner.rowspan : "");
981
+ perRow[owner.col] = node(owner.header ? "TH" : "TD", tattr ? " /A << /O /Table" + tattr + " >>" : "");
820
982
  attach(row, perRow[owner.col]);
821
983
  }
822
984
  return perRow[owner.col];
@@ -849,6 +1011,11 @@ function RTE_Plugin_PdfExport() {
849
1011
  var extra = block.role === "Figure"
850
1012
  ? " /Alt (" + escapeString(toWinAnsi(block.alt || "Image")) + ")"
851
1013
  : "";
1014
+ // 2026-10-15 a block in another language says so, or it is read with the document's
1015
+ // pronunciation (WCAG 3.1.2, PDF/UA 7.2). tools/pdf/paragraph-lang.mjs
1016
+ var docLang = pdfDocLang;
1017
+ var bl = String(block.lang || "").replace(/[^A-Za-z0-9-]/g, "");
1018
+ if (bl && bl.toLowerCase() !== docLang) extra += " /Lang (" + bl + ")";
852
1019
  body = node(block.role || "P", extra);
853
1020
  }
854
1021
  bodyNodes.set(block, body);
@@ -857,14 +1024,17 @@ function RTE_Plugin_PdfExport() {
857
1024
  return body;
858
1025
  }
859
1026
 
860
- // Emit. /K holds child elements first, then marked-content references.
1027
+ // Emit. /K lists child elements and marked-content references in reading order (2026-10-17: child elements
1028
+ // used to come first, so a Link sat at the start of its paragraph). tools/pdf/link-order.mjs
861
1029
  for (var n = 0; n < nodes.length; n++) {
862
1030
  var nd = nodes[n];
863
1031
  var kids = [];
864
- for (var c = 0; c < nd.children.length; c++) kids.push(nd.children[c].id + " 0 R");
865
- for (var k = 0; k < nd.kids.length; k++) {
866
- kids.push("<< /Type /MCR /Pg " + pageIds[nd.kids[k].page] + " 0 R /MCID " + nd.kids[k].mcid + " >>");
1032
+ for (var k = 0; k < nd.seq.length; k++) {
1033
+ var sq = nd.seq[k];
1034
+ if (sq.elem) kids.push(sq.elem.id + " 0 R");
1035
+ else kids.push("<< /Type /MCR /Pg " + pageIds[sq.mcr.page] + " 0 R /MCID " + sq.mcr.mcid + " >>");
867
1036
  }
1037
+ if (nd.objr) kids.push("<< /Type /OBJR /Obj " + nd.objr.id + " 0 R /Pg " + pageIds[nd.objr.page] + " 0 R >>");
868
1038
  var parentRef = nd.parent ? nd.parent.id : rootId;
869
1039
  pdf.set(nd.id, "<< /Type /StructElem /S /" + nd.role + " /P " + parentRef + " 0 R" + nd.extra +
870
1040
  (kids.length ? " /K [" + kids.join(" ") + "]" : "") + " >>");
@@ -879,10 +1049,10 @@ function RTE_Plugin_PdfExport() {
879
1049
  for (var q = 0; q < arr.length; q++) refs.push((arr[q] || docNode.id) + " 0 R");
880
1050
  numsParts.push(pt + " [" + refs.join(" ") + "]");
881
1051
  }
882
- var parentTreeId = pdf.add("<< /Nums [" + numsParts.join(" ") + "] >>");
1052
+ var parentTreeId = pdf.add("<< /Nums [" + numsParts.concat(linkKeys).join(" ") + "] >>");
883
1053
 
884
1054
  pdf.set(rootId, "<< /Type /StructTreeRoot /K [" + docNode.id + " 0 R]" +
885
- " /ParentTree " + parentTreeId + " 0 R /ParentTreeNextKey " + parentTree.length + " >>");
1055
+ " /ParentTree " + parentTreeId + " 0 R /ParentTreeNextKey " + (parentTree.length + linkKeys.length) + " >>");
886
1056
 
887
1057
  return { rootId: rootId };
888
1058
  }
@@ -921,11 +1091,16 @@ function RTE_Plugin_PdfExport() {
921
1091
  };
922
1092
 
923
1093
  function renderPage(ops, geom, familyKey, baseSize, imageRef, pageNumber, pageCount, options, tagger) {
924
- var out = [], links = [];
1094
+ var out = [], links = [], headings = [];
925
1095
  var left = geom.margin.left;
926
1096
 
927
1097
  for (var i = 0; i < ops.length; i++) {
928
1098
  var op = ops[i];
1099
+ // 2026-10-15 where each heading starts, for the bookmarks. tools/pdf/outline-bookmarks.mjs
1100
+ if (op.op === "line" && op.first && op.block && /^H[1-6]$/.test(op.block.role || "")) {
1101
+ var htext = (op.block.lines || []).map(function (ln) { return (ln.parts || []).map(function (pt) { return pt.text || ""; }).join(""); }).join(" ").replace(/\s+/g, " ").trim();
1102
+ if (htext) headings.push({ level: +op.block.role.charAt(1), title: htext, y: op.y });
1103
+ }
929
1104
  if (op.op === "rule") {
930
1105
  // A horizontal rule is decoration, not content.
931
1106
  tagger.artifact(out, function () {
@@ -949,7 +1124,7 @@ function RTE_Plugin_PdfExport() {
949
1124
  });
950
1125
  }
951
1126
 
952
- return { content: out.join("\n"), links: links, marks: tagger.marks };
1127
+ return { content: out.join("\n"), links: links, marks: tagger.marks, headings: headings };
953
1128
  }
954
1129
 
955
1130
  function headerFooter(options) {
@@ -1011,7 +1186,9 @@ function RTE_Plugin_PdfExport() {
1011
1186
  // (just indent/align), so the owner has to be the cell descriptor the
1012
1187
  // caller supplied. Passing the synthetic block instead makes every cell
1013
1188
  // a top-level paragraph and the table loses its structure entirely.
1014
- if (tagger) tagger.open(out, line.image ? "Figure" : "Span", op.cell || { kind: "body", block: block });
1189
+ var lineOwner = op.cell || { kind: "body", block: block };
1190
+ var lineOpen = false;
1191
+ if (tagger) { tagger.open(out, line.image ? "Figure" : "Span", lineOwner); lineOpen = true; }
1015
1192
 
1016
1193
  // Wrapping split the text into words, but emitting one BT/Tj block per
1017
1194
  // WORD is wrong twice over: the content stream balloons, and text
@@ -1029,7 +1206,7 @@ function RTE_Plugin_PdfExport() {
1029
1206
  var last = segments.length ? segments[segments.length - 1] : null;
1030
1207
  var mergeable = last && !last.img && last.alias === alias && last.style.size === st.size &&
1031
1208
  sameColor(last.style.color, st.color) && last.style.underline === st.underline &&
1032
- last.style.strike === st.strike && last.style.link === st.link;
1209
+ last.style.strike === st.strike && last.style.link === st.link && (last.style.lang || "") === (st.lang || "");
1033
1210
  if (mergeable) { last.text += part.text; last.w += part.w; }
1034
1211
  else { segments.push({ text: part.text, style: st, alias: alias, x: cursor, w: part.w }); }
1035
1212
  cursor += part.w;
@@ -1050,8 +1227,25 @@ function RTE_Plugin_PdfExport() {
1050
1227
  // would only add an empty operation.
1051
1228
  if (!seg.text || /^\s+$/.test(seg.text)) continue;
1052
1229
  var sst = seg.style;
1230
+ // 2026-10-17 a link's words are marked content of their own, owned by the Link element, so the Link holds
1231
+ // its text at its place in the paragraph (PDF/UA 7.18.5). tools/pdf/link-order.mjs
1232
+ var linkOwner = null;
1233
+ if (tagger && !line.image && sst.link) {
1234
+ if (lineOpen) { tagger.close(out); lineOpen = false; }
1235
+ linkOwner = { kind: "link", parent: lineOwner };
1236
+ tagger.open(out, "Span", linkOwner);
1237
+ } else if (tagger && !lineOpen) {
1238
+ tagger.open(out, "Span", lineOwner); lineOpen = true;
1239
+ }
1240
+ // 2026-10-17 a run in another language than its block (a French phrase in an English sentence) is drawn
1241
+ // inside marked content with a /Lang property, so it is read with the right pronunciation (WCAG 3.1.2,
1242
+ // PDF/UA 7.2). The paragraph's own /Lang is on its structure element. tools/pdf/inline-lang.mjs
1243
+ var runLang = String(sst.lang || "").replace(/[^A-Za-z0-9-]/g, "");
1244
+ var langMark = runLang && runLang.toLowerCase() !== String(block.lang || pdfDocLang || "").toLowerCase();
1245
+ if (langMark) out.push("/Span << /Lang (" + runLang + ") >> BDC");
1053
1246
  out.push("BT /" + seg.alias + " " + fixed(sst.size) + " Tf " + colorOp(sst.color) + " " +
1054
1247
  fixed(seg.x) + " " + fixed(baseline) + " Td (" + escapeString(toWinAnsi(seg.text)) + ") Tj ET");
1248
+ if (langMark) out.push("EMC");
1055
1249
  if (sst.underline) {
1056
1250
  out.push(colorStroke(sst.color) + " 0.5 w " + fixed(seg.x) + " " + fixed(baseline - sst.size * 0.12) +
1057
1251
  " m " + fixed(seg.x + seg.w) + " " + fixed(baseline - sst.size * 0.12) + " l S");
@@ -1061,10 +1255,11 @@ function RTE_Plugin_PdfExport() {
1061
1255
  " m " + fixed(seg.x + seg.w) + " " + fixed(baseline + sst.size * 0.28) + " l S");
1062
1256
  }
1063
1257
  if (sst.link) {
1064
- links.push({ href: sst.link, x0: seg.x, y0: baseline - 2, x1: seg.x + seg.w, y1: baseline + sst.size });
1258
+ links.push({ href: sst.link, x0: seg.x, y0: baseline - 2, x1: seg.x + seg.w, y1: baseline + sst.size, text: seg.text || "", owner: linkOwner || lineOwner });
1065
1259
  }
1260
+ if (linkOwner) tagger.close(out);
1066
1261
  }
1067
- if (tagger) tagger.close(out);
1262
+ if (tagger && lineOpen) tagger.close(out);
1068
1263
  }
1069
1264
 
1070
1265
  function sameColor(a, b) {
@@ -1078,28 +1273,33 @@ function RTE_Plugin_PdfExport() {
1078
1273
  var top = op.y, bottom = op.y - row.height;
1079
1274
 
1080
1275
  for (var c = 0; c < row.cells.length; c++) {
1081
- var cellX = x + c * block.colWidth;
1276
+ var cspan = row.cells[c].span || 1, cstart = row.cells[c].start != null ? row.cells[c].start : c;
1277
+ var cellX = x, cellW = 0;
1278
+ for (var wi = 0; wi < cstart + cspan; wi++) {
1279
+ var colw = block.colWidths && block.colWidths[wi] != null ? block.colWidths[wi] : block.colWidth;
1280
+ if (wi < cstart) cellX += colw; else cellW += colw;
1281
+ }
1082
1282
  // Shading and borders are decoration; only the cell TEXT belongs to
1083
1283
  // the reading order.
1084
1284
  var paint = function () {
1085
1285
  if (row.header) {
1086
1286
  out.push("0.94 0.95 0.97 rg " + fixed(cellX) + " " + fixed(bottom) + " " +
1087
- fixed(block.colWidth) + " " + fixed(row.height) + " re f");
1287
+ fixed(cellW) + " " + fixed(row.height) + " re f");
1088
1288
  }
1089
1289
  out.push("0.75 0.78 0.82 RG 0.5 w " + fixed(cellX) + " " + fixed(bottom) + " " +
1090
- fixed(block.colWidth) + " " + fixed(row.height) + " re S");
1290
+ fixed(cellW) + " " + fixed(row.height) + " re S");
1091
1291
  };
1092
1292
  if (tagger) tagger.artifact(out, paint); else paint();
1093
1293
 
1094
1294
  // One structure element per CELL, identified by the row object and
1095
1295
  // column index so a table split across pages still resolves to the
1096
1296
  // right TD (and a repeated header row to the right TH).
1097
- var cellOwner = { kind: "cell", block: block, row: row, col: c, header: !!row.cells[c].header };
1297
+ var cellOwner = { kind: "cell", block: block, row: row, col: cstart, span: cspan, rowspan: row.cells[c].rowspan || 1, header: !!row.cells[c].header };
1098
1298
  var cy = top - block.pad;
1099
1299
  var lines = row.cells[c].lines;
1100
1300
  for (var l = 0; l < lines.length; l++) {
1101
1301
  renderLine({ line: lines[l], y: cy, block: { indent: 0, align: "left" }, first: l === 0, cell: cellOwner },
1102
- { width: cellX + block.colWidth + geom.margin.right, margin: { right: geom.margin.right } },
1302
+ { width: cellX + cellW + geom.margin.right, margin: { right: geom.margin.right } },
1103
1303
  out, cellX + block.pad, "helvetica", imageRef, links, tagger);
1104
1304
  cy -= lines[l].height;
1105
1305
  }