@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
@@ -14,20 +14,41 @@ function RTE_Plugin_AccessibilityChecker() {
14
14
  var refreshTimer = 0;
15
15
  var lastResult = null;
16
16
  var selectedIssueIndex = -1;
17
+ var destroyed = false;
17
18
 
18
19
  obj.PluginName = "AccessibilityChecker";
19
20
 
21
+ // 2026-10-18 the panel's words - its chrome, every rule's message and the repairs - read config.text_ keys with
22
+ // English as the fallback ({0} / {1} are filled in). The rule codes stay as identifiers.
23
+ // tools/i18n/a11y-checker-language.mjs
24
+ function aT(key, en, a, b) {
25
+ var s = (config && config["text_" + key]) || en;
26
+ if (a !== undefined) s = s.split("{0}").join(String(a));
27
+ if (b !== undefined) s = s.split("{1}").join(String(b));
28
+ return s;
29
+ }
30
+
20
31
  obj.InitConfig = function (argconfig) {
21
32
  config = argconfig;
22
33
  if (config.accessibilityCheckerEnabled === false) return;
23
34
 
24
35
  if (typeof config.accessibilityCheckerAutoOpen !== "boolean") config.accessibilityCheckerAutoOpen = false;
25
- config.accessibilityCheckerTitle = config.accessibilityCheckerTitle || "Accessibility";
26
- config.accessibilityCheckerHint = config.accessibilityCheckerHint || "Review heading structure, image alt text, and table headers without leaving the editor.";
27
- config.accessibilityCheckerEmptyText = config.accessibilityCheckerEmptyText || "No accessibility issues found in the current document.";
36
+ config.accessibilityCheckerTitle = config.accessibilityCheckerTitle || config.text_a11yc_title || "Accessibility";
37
+ config.accessibilityCheckerHint = config.accessibilityCheckerHint || config.text_a11yc_hint || "Review heading structure, image alt text, and table headers without leaving the editor.";
38
+ config.accessibilityCheckerEmptyText = config.accessibilityCheckerEmptyText || config.text_a11yc_empty || "No accessibility issues found in the current document.";
28
39
 
29
40
  appendToolbarCommand("toolbar_default", "#{accessibilitychecker}");
30
41
  appendToolbarCommand("toolbar_full", "#{accessibilitychecker}");
42
+ // 2026-10-03 Build the shell NOW, before the editor creates its iframe. ensureShell() moves the
43
+ // editor container; done in InitEditor it moved the live iframe, which then gets a fresh
44
+ // document - and every plugin that had already bound listeners to the old one went deaf (the
45
+ // slash menu, @mentions and emoji autocomplete, until they moved to editor.bindDocumentEvent;
46
+ // customer plugins binding the document directly still were). Nothing is loaded yet here.
47
+ // InitEditor's own ensureShell() call finds the shell and returns it.
48
+ // tools/keyboard/plugin-listeners.mjs
49
+ if (config.accessibilityCheckerEnabled !== false && config.container && config.container.parentNode) {
50
+ try { ensureShell(); } catch (e) { /* InitEditor builds it */ }
51
+ }
31
52
  };
32
53
 
33
54
  obj.InitEditor = function (argeditor) {
@@ -59,6 +80,14 @@ function RTE_Plugin_AccessibilityChecker() {
59
80
  editor.attachEvent("change", function () {
60
81
  scheduleRefresh();
61
82
  });
83
+ // 2026-10-18 setHTMLCode fires no "change": a document loaded or replaced that way kept the previous audit
84
+ // ("Ready to publish" over a document with six issues). tools/a11y/checker-opens-fresh.mjs
85
+ try { editor.attachEvent("aftersethtml", function () { scheduleRefresh(); }); } catch (e) { /* older cores */ }
86
+ editor.attachEvent("destroy", function () {
87
+ destroyed = true;
88
+ clearTimeout(refreshTimer);
89
+ refreshTimer = 0;
90
+ });
62
91
 
63
92
  ensureShell();
64
93
  runAudit();
@@ -80,7 +109,7 @@ function RTE_Plugin_AccessibilityChecker() {
80
109
  ".rte-a11y-shell{display:flex;align-items:stretch;gap:10px;}",
81
110
  ".rte-a11y-shell>.rte-a11y-host{flex:1 1 auto;min-width:0;}",
82
111
  "/* 2026-07-12 accessibility panel precision */",
83
- ".rte-a11y-panel{display:none;flex:0 0 310px;min-width:270px;max-width:min(360px,32vw);border:1px solid rgba(148,163,184,.22);border-radius:8px;background:linear-gradient(180deg,rgba(255,255,255,.98),rgba(248,251,255,.96));box-shadow:0 14px 32px rgba(29,78,216,.1),0 2px 8px rgba(15,23,42,.06);overflow:hidden;color:#172033;font-family:Aptos,'Segoe UI',sans-serif;}",
112
+ ".rte-a11y-panel{display:none;flex:0 0 310px;min-width:270px;max-width:min(360px,32vw);border:1px solid rgba(148,163,184,.22);border-radius:8px;background:linear-gradient(180deg,rgba(255,255,255,.98),rgba(248,251,255,.96));box-shadow:0 14px 32px rgba(29,78,216,.1),0 2px 8px rgba(15,23,42,.06);overflow:hidden;color:#172033;font-family:Aptos,'Segoe UI',sans-serif;}.rte-a11y-locked-note{margin:6px 10px;padding:6px 8px;border:1px solid #e3b341;border-radius:6px;background:#fff8e1;color:#5c3d00;font-size:12px;}",
84
113
  ".rte-a11y-shell.is-open>.rte-a11y-panel{display:flex;flex-direction:column;}",
85
114
  ".rte-a11y-header{padding:12px 12px 9px 14px;border-bottom:1px solid rgba(148,163,184,.16);background:rgba(255,255,255,.72);}",
86
115
  ".rte-a11y-kicker{font-size:10px;line-height:1.3;letter-spacing:.08em;text-transform:uppercase;color:#52657e;font-weight:850;}",
@@ -113,6 +142,17 @@ function RTE_Plugin_AccessibilityChecker() {
113
142
  ".rte-a11y-button{appearance:none;border:1px solid #1d67ba;border-radius:7px;background:#1d67ba;color:#fff;padding:8px 12px;font-size:12px;font-weight:750;cursor:pointer;box-shadow:0 6px 14px rgba(29,103,186,.16);}",
114
143
  ".rte-a11y-button-secondary{background:#fff;color:#315277;border-color:rgba(100,116,139,.18);box-shadow:none;}",
115
144
  ".rte-a11y-button:hover,.rte-a11y-button:focus-visible{transform:translateY(-1px);}",
145
+ ".rte-a11y-panel.rte-a11y-dark{background:linear-gradient(180deg,#1e293b,#0f172a);border-color:#334155;color:#e2e8f0;box-shadow:0 18px 40px rgba(0,0,0,.45);}",
146
+ ".rte-a11y-dark .rte-a11y-header,.rte-a11y-dark .rte-a11y-toolbar{background:rgba(15,23,42,.6);border-color:#334155;}",
147
+ ".rte-a11y-dark .rte-a11y-kicker,.rte-a11y-dark .rte-a11y-copy,.rte-a11y-dark .rte-a11y-count,.rte-a11y-dark .rte-a11y-detail-copy,.rte-a11y-dark .rte-a11y-label{color:#94a3b8;}",
148
+ ".rte-a11y-dark .rte-a11y-code{color:#a5b4c8;}",
149
+ ".rte-a11y-dark .rte-a11y-title,.rte-a11y-dark .rte-a11y-message,.rte-a11y-dark .rte-a11y-detail-title{color:#f1f5f9;}",
150
+ ".rte-a11y-dark .rte-a11y-item,.rte-a11y-dark .rte-a11y-detail,.rte-a11y-dark .rte-a11y-empty{background:rgba(15,23,42,.72);border-color:#334155;color:#e2e8f0;}",
151
+ ".rte-a11y-dark .rte-a11y-link,.rte-a11y-dark .rte-a11y-button-secondary{background:#0f172a;color:#bfdbfe;border-color:#475569;}",
152
+ ".rte-a11y-dark .rte-a11y-input{background:#0f172a;color:#e2e8f0;border-color:#475569;}",
153
+ ".rte-a11y-dark .rte-a11y-badge-warning{background:#422006;color:#fde68a;border-color:#92400e;}",
154
+ ".rte-a11y-dark .rte-a11y-badge-error{background:#4c0519;color:#fecdd3;border-color:#9f1239;}",
155
+ ".rte-a11y-dark .rte-a11y-locked-note{background:#422006;color:#fde68a;border-color:#92400e;}",
116
156
  ".rte-a11y-empty{padding:14px;border-radius:8px;background:#fff;color:#52657e;font-size:13px;font-weight:700;line-height:1.55;border:1px dashed rgba(148,163,184,.34);}",
117
157
  "@media (max-width: 1420px){.rte-a11y-shell{display:block;}.rte-a11y-panel{margin-top:12px;max-width:none;width:100%;}.rte-a11y-body{max-height:360px;}}"
118
158
  ].join("");
@@ -145,16 +185,22 @@ function RTE_Plugin_AccessibilityChecker() {
145
185
  panel.className = "rte-a11y-panel";
146
186
  panel.setAttribute("aria-label", config.accessibilityCheckerTitle);
147
187
  panel.setAttribute("role", "complementary");
188
+ // 2026-10-17 drag / arrow-key resizable (tools/ui/panel-resize.mjs)
189
+ setTimeout(function () { if (editor && typeof editor.makePanelResizable === "function") editor.makePanelResizable(panel, { label: aT("a11yc_resize", "Resize accessibility panel") }); }, 0);
148
190
 
149
191
  var header = hostDoc.createElement("div");
150
192
  header.className = "rte-a11y-header";
151
- header.innerHTML = '<div class="rte-a11y-kicker">Accessibility</div><div class="rte-a11y-title"></div><div class="rte-a11y-copy"></div>';
193
+ header.innerHTML = '<div class="rte-a11y-kicker"></div><div class="rte-a11y-title"></div><div class="rte-a11y-copy"></div>';
194
+ header.querySelector(".rte-a11y-kicker").innerText = aT("a11yc_title", "Accessibility");
152
195
  header.querySelector(".rte-a11y-title").innerText = config.accessibilityCheckerTitle;
153
196
  header.querySelector(".rte-a11y-copy").innerText = config.accessibilityCheckerHint;
154
197
 
155
198
  var toolbar = hostDoc.createElement("div");
156
199
  toolbar.className = "rte-a11y-toolbar";
157
- toolbar.innerHTML = '<div class="rte-a11y-count" data-rte-a11y-count="1"></div><div class="rte-a11y-actions"><button type="button" class="rte-a11y-link" data-rte-a11y-refresh="1">Refresh</button><button type="button" class="rte-a11y-link" data-rte-a11y-close="1" aria-label="Hide accessibility checker">Hide</button></div>';
200
+ toolbar.innerHTML = '<div class="rte-a11y-count" data-rte-a11y-count="1"></div><div class="rte-a11y-actions"><button type="button" class="rte-a11y-link" data-rte-a11y-refresh="1"></button><button type="button" class="rte-a11y-link" data-rte-a11y-close="1"></button></div>';
201
+ toolbar.querySelector("[data-rte-a11y-refresh]").innerText = aT("a11yc_refresh", "Refresh");
202
+ toolbar.querySelector("[data-rte-a11y-close]").innerText = aT("a11yc_hide", "Hide");
203
+ toolbar.querySelector("[data-rte-a11y-close]").setAttribute("aria-label", aT("a11yc_hidearia", "Hide accessibility checker"));
158
204
  toolbar.querySelector("[data-rte-a11y-refresh]").onclick = function () { runAudit(); };
159
205
  toolbar.querySelector("[data-rte-a11y-close]").onclick = function () { closePanel(); };
160
206
 
@@ -183,10 +229,20 @@ function RTE_Plugin_AccessibilityChecker() {
183
229
  else openPanel();
184
230
  }
185
231
 
232
+ // 2026-10-17 the panel sits beside the editor's element, so rte-dark could not reach it: mirrored (and
233
+ // rte-auto-dark under a dark system setting). tools/a11y/checker-panel-dark.mjs
234
+ function editorIsDark() {
235
+ try {
236
+ var k = editor.container && editor.container.classList;
237
+ return !!k && (k.contains("rte-dark") || (k.contains("rte-auto-dark") && !k.contains("rte-light") && !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches)));
238
+ } catch (x) { return false; }
239
+ }
186
240
  function openPanel() {
187
241
  ensureShell();
242
+ if (panel && panel.classList) { if (editorIsDark()) panel.classList.add("rte-a11y-dark"); else panel.classList.remove("rte-a11y-dark"); }
188
243
  shell.classList.add("is-open");
189
- renderPanel();
244
+ // audit what is shown now, not what was there at the last change event
245
+ runAudit();
190
246
  notifyResize();
191
247
  }
192
248
 
@@ -217,6 +273,7 @@ function RTE_Plugin_AccessibilityChecker() {
217
273
  }
218
274
 
219
275
  function scheduleRefresh() {
276
+ if (destroyed) return;
220
277
  clearTimeout(refreshTimer);
221
278
  refreshTimer = setTimeout(function () {
222
279
  runAudit();
@@ -224,6 +281,7 @@ function RTE_Plugin_AccessibilityChecker() {
224
281
  }
225
282
 
226
283
  function runAudit() {
284
+ if (destroyed) return lastResult || { document: null, issues: [], valid: true, source: "dom" };
227
285
  ensureShell();
228
286
  var ctor = window.RichTextEditor;
229
287
  var documentModel = editor.getJSON ? editor.getJSON() : null;
@@ -236,7 +294,30 @@ function RTE_Plugin_AccessibilityChecker() {
236
294
  return lastResult;
237
295
  }
238
296
 
297
+ // 2026-10-17 a locked document stays locked: the fixes edit the DOM directly (neither insertHTML's nor
298
+ // setHTMLCode's lock reaches them) and the checker's button stays enabled in comments-only mode. A repair is
299
+ // refused while the editor is read-only or comments-only, and the panel says so. tools/a11y/checker-repair-locked.mjs
300
+ function checkerLocked() {
301
+ try { return !!((editor.getReadOnly && editor.getReadOnly()) || (editor.getCommentsOnly && editor.getCommentsOnly())); } catch (e) { return false; }
302
+ }
303
+
304
+ function showLockedNote() {
305
+ if (!panel) return;
306
+ var note = panel.querySelector(".rte-a11y-locked-note");
307
+ if (!note) {
308
+ note = panel.ownerDocument.createElement("div");
309
+ note.className = "rte-a11y-locked-note";
310
+ note.setAttribute("role", "status");
311
+ panel.insertBefore(note, panel.children[1] || null);
312
+ }
313
+ note.textContent = "";
314
+ var text = (config && config.text_a11ycheckerlocked) || "This document is read-only, so the fix was not applied.";
315
+ setTimeout(function () { note.textContent = text; }, 30);
316
+ note.textContent = text;
317
+ }
318
+
239
319
  function repairIssue(issueIndex, options) {
320
+ if (checkerLocked()) { showLockedNote(); return lastResult || runAudit(); }
240
321
  if (!lastResult || !lastResult.issues || !lastResult.issues[issueIndex]) return runAudit();
241
322
  var ctor = window.RichTextEditor;
242
323
  // An issue carrying `_target` came from the DOM audit and is addressed
@@ -289,16 +370,16 @@ function RTE_Plugin_AccessibilityChecker() {
289
370
  // document, and the reverse). We flag only what we can be certain of —
290
371
  // a checker that cries wolf gets switched off.
291
372
  var SCRIPTS = [
292
- { name: "Hebrew", lang: "he", re: /[֐-׿]/g },
293
- { name: "Arabic", lang: "ar", re: /[؀-ۿݐ-ݿﭐ-﷿ﹰ-]/g },
294
- { name: "Greek", lang: "el", re: /[Ͱ-Ͽἀ-῿]/g },
295
- { name: "Cyrillic", lang: "ru", re: /[Ѐ-ӿ]/g },
296
- { name: "Han", lang: "zh", re: /[㐀-䶿一-鿿]/g },
297
- { name: "Kana", lang: "ja", re: /[぀-ゟ゠-ヿ]/g },
298
- { name: "Hangul", lang: "ko", re: /[가-힯ᄀ-ᇿ]/g },
299
- { name: "Devanagari", lang: "hi", re: /[ऀ-ॿ]/g },
300
- { name: "Thai", lang: "th", re: /[฀-๿]/g },
301
- { name: "Latin", lang: "en", re: /[A-Za-zÀ-ɏ]/g }
373
+ { name: "Hebrew", lang: "he", re: /[\u0590-\u05ff]/g },
374
+ { name: "Arabic", lang: "ar", re: /[\u0600-\u06ff\u0750-\u077f\ufb50-\ufdff\ufe70-\ufeff]/g },
375
+ { name: "Greek", lang: "el", re: /[\u0370-\u03ff\u1f00-\u1fff]/g },
376
+ { name: "Cyrillic", lang: "ru", re: /[\u0400-\u04ff]/g },
377
+ { name: "Han", lang: "zh", re: /[\u3400-\u4dbf\u4e00-\u9fff]/g },
378
+ { name: "Kana", lang: "ja", re: /[\u3040-\u309f\u30a0-\u30ff]/g },
379
+ { name: "Hangul", lang: "ko", re: /[\uac00-\ud7af\u1100-\u11ff]/g },
380
+ { name: "Devanagari", lang: "hi", re: /[\u0900-\u097f]/g },
381
+ { name: "Thai", lang: "th", re: /[\u0e00-\u0e7f]/g },
382
+ { name: "Latin", lang: "en", re: /[A-Za-z\xc0-\u024f]/g }
302
383
  ];
303
384
 
304
385
  // Below this, a "run" is more likely a stray glyph, a maths symbol or a
@@ -362,8 +443,7 @@ function RTE_Plugin_AccessibilityChecker() {
362
443
  issues.push({
363
444
  code: "language-of-parts",
364
445
  severity: "warning",
365
- message: script.name + " text is not marked with a language. Select the phrase and set its " +
366
- "language so assistive technology pronounces it correctly (WCAG 3.1.2).",
446
+ message: aT("a11yc_m_lang", "{0} text is not marked with a language. Select the phrase and set its language so assistive technology pronounces it correctly (WCAG 3.1.2).", aT("a11yc_script_" + script.lang, script.name)),
367
447
  path: path + ".lang",
368
448
  _target: block,
369
449
  _lang: script.lang,
@@ -398,7 +478,7 @@ function RTE_Plugin_AccessibilityChecker() {
398
478
  issues.push({
399
479
  code: "heading-empty",
400
480
  severity: "warning",
401
- message: "Heading is empty. Add a concise label or remove the heading.",
481
+ message: aT("a11yc_m_hempty", "Heading is empty. Add a concise label or remove the heading."),
402
482
  path: path,
403
483
  _target: node
404
484
  });
@@ -407,7 +487,7 @@ function RTE_Plugin_AccessibilityChecker() {
407
487
  issues.push({
408
488
  code: "heading-level-skip",
409
489
  severity: "warning",
410
- message: "Heading jumps from H" + previousHeadingLevel + " to H" + level + ". Use the next heading level for a clearer outline.",
490
+ message: aT("a11yc_m_hskip", "Heading jumps from H{0} to H{1}. Use the next heading level for a clearer outline.", previousHeadingLevel, level),
411
491
  path: path + ".attrs.level",
412
492
  _target: node
413
493
  });
@@ -428,7 +508,7 @@ function RTE_Plugin_AccessibilityChecker() {
428
508
  issues.push({
429
509
  code: "image-missing-alt",
430
510
  severity: "error",
431
- message: "Image is missing alt text. Describe the image or mark it decorative in your content workflow.",
511
+ message: aT("a11yc_m_alt", "Image is missing alt text. Describe the image or mark it decorative in your content workflow."),
432
512
  path: path + ".image[" + imageIndex + "]",
433
513
  _target: image
434
514
  });
@@ -452,7 +532,7 @@ function RTE_Plugin_AccessibilityChecker() {
452
532
  issues.push({
453
533
  code: "link-empty",
454
534
  severity: "error",
455
- message: "Link has no text, so it is announced only as \"link\". Add link text, an aria-label, or alt text on the image inside it.",
535
+ message: aT("a11yc_m_linkempty", "Link has no text, so it is announced only as \"link\". Add link text, an aria-label, or alt text on the image inside it."),
456
536
  path: path + ".link[" + linkIndex + "]",
457
537
  _target: link
458
538
  });
@@ -477,7 +557,7 @@ function RTE_Plugin_AccessibilityChecker() {
477
557
  issues.push({
478
558
  code: "layout-table-semantics",
479
559
  severity: "error",
480
- message: "Layout table still carries data-table markup (header cells, scope or a caption). A screen reader will announce relationships this table does not have.",
560
+ message: aT("a11yc_m_layouttable", "Layout table still carries data-table markup (header cells, scope or a caption). A screen reader will announce relationships this table does not have."),
481
561
  path: path + ".table[" + tableIndex + "]",
482
562
  _target: table
483
563
  });
@@ -487,7 +567,7 @@ function RTE_Plugin_AccessibilityChecker() {
487
567
  issues.push({
488
568
  code: "table-missing-header",
489
569
  severity: "warning",
490
- message: "Table has no header cells. Promote the first row to headers, or mark it as a layout table when it only positions content.",
570
+ message: aT("a11yc_m_noheader", "Table has no header cells. Promote the first row to headers, or mark it as a layout table when it only positions content."),
491
571
  path: path + ".table[" + tableIndex + "]",
492
572
  _target: table
493
573
  });
@@ -521,7 +601,7 @@ function RTE_Plugin_AccessibilityChecker() {
521
601
  issues.push({
522
602
  code: "table-header-empty",
523
603
  severity: "error",
524
- message: "Header cell is empty, so the column or row it labels is announced with no name. Give it text, or make it a normal cell if it labels nothing.",
604
+ message: aT("a11yc_m_headerempty", "Header cell is empty, so the column or row it labels is announced with no name. Give it text, or make it a normal cell if it labels nothing."),
525
605
  path: path + ".table[" + tableIndex + "].th[" + headerIndex + "]",
526
606
  _target: headerCell
527
607
  });
@@ -562,10 +642,9 @@ function RTE_Plugin_AccessibilityChecker() {
562
642
  issues.push({
563
643
  code: "contrast-insufficient",
564
644
  severity: "error",
565
- message: "Text contrast is " + ratio.toFixed(2) + ":1 against " +
566
- (bg.assumed ? "an assumed white background" : "its background") +
567
- ", below the " + need + ":1 needed for " +
568
- (large ? "large text" : "body text") + " (WCAG 1.4.3).",
645
+ message: aT("a11yc_m_contrast_" + (bg.assumed ? "w" : "b") + (large ? "l" : "b"),
646
+ "Text contrast is {0}:1 against " + (bg.assumed ? "an assumed white background" : "its background") +
647
+ ", below the {1}:1 needed for " + (large ? "large text" : "body text") + " (WCAG 1.4.3).", ratio.toFixed(2), need),
569
648
  path: path + ".contrast[" + cIndex + "]",
570
649
  _target: cEl
571
650
  });
@@ -596,7 +675,7 @@ function RTE_Plugin_AccessibilityChecker() {
596
675
  issues.push({
597
676
  code: "blockquote-as-indent",
598
677
  severity: "warning",
599
- message: "This looks like indentation, not a quotation: a <blockquote> with a left margin and no quote styling. Screen readers announce it as a quotation. Use the indent button instead.",
678
+ message: aT("a11yc_m_bqindent", "This looks like indentation, not a quotation: a <blockquote> with a left margin and no quote styling. Screen readers announce it as a quotation. Use the indent button instead."),
600
679
  path: path + ".blockquote[" + qIndex + "]",
601
680
  _target: q
602
681
  });
@@ -616,7 +695,7 @@ function RTE_Plugin_AccessibilityChecker() {
616
695
  issues.push({
617
696
  code: "link-ambiguous-text",
618
697
  severity: "warning",
619
- message: "Link text \"" + aName + "\" does not say where it goes. Out of context a screen reader announces only this text.",
698
+ message: aT("a11yc_m_ambig", "Link text \"{0}\" does not say where it goes. Out of context a screen reader announces only this text.", aName),
620
699
  path: path + ".link[" + aIndex + "]",
621
700
  _target: aEl
622
701
  });
@@ -645,7 +724,7 @@ function RTE_Plugin_AccessibilityChecker() {
645
724
  issues.push({
646
725
  code: "duplicate-id",
647
726
  severity: "error",
648
- message: "id \"" + dupId + "\" is used more than once. Any aria-labelledby, aria-describedby or label pointing at it resolves to only the first element.",
727
+ message: aT("a11yc_m_dupid", "id \"{0}\" is used more than once. Any aria-labelledby, aria-describedby or label pointing at it resolves to only the first element.", dupId),
649
728
  path: "document.id[" + dupIndex + "]",
650
729
  _target: allWithIds[dupIndex]
651
730
  });
@@ -677,7 +756,7 @@ function RTE_Plugin_AccessibilityChecker() {
677
756
  issues.push({
678
757
  code: "paragraph-as-heading",
679
758
  severity: "warning",
680
- message: "\"" + pText + "\" looks like a heading (bold, large, on its own line) but is a paragraph. Screen-reader users cannot jump to it. Use a heading level instead.",
759
+ message: aT("a11yc_m_pheading", "\"{0}\" looks like a heading (bold, large, on its own line) but is a paragraph. Screen-reader users cannot jump to it. Use a heading level instead.", pText),
681
760
  path: "document.p[" + pi + "]",
682
761
  _target: pEl
683
762
  });
@@ -689,7 +768,7 @@ function RTE_Plugin_AccessibilityChecker() {
689
768
  if (!el || String(el.nodeName).toLowerCase() !== "p") return null;
690
769
  var t = getText(el);
691
770
  if (/^\d{1,3}[.)]\s+\S/.test(t)) return "ol";
692
- if (/^[-*•▪●]\s+\S/.test(t)) return "ul";
771
+ if (/^[-*\u2022\u25aa\u25cf]\s+\S/.test(t)) return "ul";
693
772
  return null;
694
773
  };
695
774
  var kids = editable.children;
@@ -702,7 +781,7 @@ function RTE_Plugin_AccessibilityChecker() {
702
781
  issues.push({
703
782
  code: "fake-list",
704
783
  severity: "warning",
705
- message: run + " paragraphs are typed as a " + (kind === "ol" ? "numbered" : "bulleted") + " list. Screen readers do not announce them as a list or say how many items it has. Use the list button instead.",
784
+ message: aT("a11yc_m_fakelist_" + (kind === "ol" ? "ol" : "ul"), "{0} paragraphs are typed as a " + (kind === "ol" ? "numbered" : "bulleted") + " list. Screen readers do not announce them as a list or say how many items it has. Use the list button instead.", run),
706
785
  path: "content[" + ki + "]",
707
786
  _target: kids[ki]
708
787
  });
@@ -720,7 +799,7 @@ function RTE_Plugin_AccessibilityChecker() {
720
799
  issues.push({
721
800
  code: "image-alt-filename",
722
801
  severity: "error",
723
- message: "Alt text \"" + alt + "\" is a file name, which describes nothing. Say what the image shows.",
802
+ message: aT("a11yc_m_altfile", "Alt text \"{0}\" is a file name, which describes nothing. Say what the image shows.", alt),
724
803
  path: "document.img[" + ii + "]",
725
804
  _target: imgs[ii]
726
805
  });
@@ -728,7 +807,7 @@ function RTE_Plugin_AccessibilityChecker() {
728
807
  issues.push({
729
808
  code: "image-alt-too-long",
730
809
  severity: "warning",
731
- message: "Alt text is " + alt.length + " characters. Keep it to a short description; put longer detail in the text or a caption.",
810
+ message: aT("a11yc_m_altlong", "Alt text is {0} characters. Keep it to a short description; put longer detail in the text or a caption.", alt.length),
732
811
  path: "document.img[" + ii + "]",
733
812
  _target: imgs[ii]
734
813
  });
@@ -750,7 +829,7 @@ function RTE_Plugin_AccessibilityChecker() {
750
829
  issues.push({
751
830
  code: "link-adjacent-duplicate",
752
831
  severity: "warning",
753
- message: "Two links next to each other go to the same place, so it is announced twice. Combine them into one link.",
832
+ message: aT("a11yc_m_adjlinks", "Two links next to each other go to the same place, so it is announced twice. Combine them into one link."),
754
833
  path: "document.link[" + li + "]",
755
834
  _target: cur
756
835
  });
@@ -766,7 +845,7 @@ function RTE_Plugin_AccessibilityChecker() {
766
845
  issues.push({
767
846
  code: "table-missing-caption",
768
847
  severity: "warning",
769
- message: "Data table has no caption. A short caption lets screen-reader users know what the table is before they enter it.",
848
+ message: aT("a11yc_m_nocaption", "Data table has no caption. A short caption lets screen-reader users know what the table is before they enter it."),
770
849
  path: "document.table[" + ti + "]",
771
850
  _target: tb
772
851
  });
@@ -779,6 +858,13 @@ function RTE_Plugin_AccessibilityChecker() {
779
858
  function repairDomIssue(issue, options) {
780
859
  if (!issue || !issue._target) return runAudit();
781
860
  var target = issue._target;
861
+ // 2026-10-05 a target edited away since the audit: re-audit (the repair threw half-way, leaving the
862
+ // undo step open). tools/review/plugin-review-20261005.mjs
863
+ var liveRoot = editor.getEditable ? editor.getEditable() : null;
864
+ if (!target.parentNode || (liveRoot && !liveRoot.contains(target))) return runAudit();
865
+ // 2026-10-04 every repair is one undo step: the state before it is recorded first, the result after.
866
+ // Undo skipped a repair entirely. tools/a11y/checker-action-repairs.mjs
867
+ commitUndo();
782
868
 
783
869
  if (issue.code === "image-missing-alt") {
784
870
  target.setAttribute("alt", String(options.altText || "").replace(/^\s+|\s+$/g, ""));
@@ -815,11 +901,18 @@ function RTE_Plugin_AccessibilityChecker() {
815
901
  target.parentNode.replaceChild(cell, target);
816
902
  }
817
903
  }
818
- else if (issue.code === "link-empty") {
904
+ else if (issue.code === "link-empty" || issue.code === "link-ambiguous-text") {
819
905
  // Prefer real text: an aria-label is invisible, so a sighted editor
820
906
  // cannot see what the link says and it drifts out of date.
821
907
  var linkText = String((options && options.linkText) || "").replace(/^\s+|\s+$/g, "");
822
- if (linkText) target.textContent = linkText;
908
+ // 2026-10-05 only the link's TEXT is replaced: an image or formatting inside it stays (textContent
909
+ // wiped them). tools/review/plugin-review-20261005.mjs
910
+ if (linkText) {
911
+ var lw = target.ownerDocument.createTreeWalker(target, 4, null, false), ltn, ltexts = [];
912
+ while ((ltn = lw.nextNode())) if (/\S/.test(ltn.data)) ltexts.push(ltn);
913
+ if (!ltexts.length) { if (target.firstChild && target.firstChild.nodeType === 1) target.appendChild(target.ownerDocument.createTextNode(" ")); target.appendChild(target.ownerDocument.createTextNode(linkText)); }
914
+ else { ltexts[0].data = linkText; for (var lti = 1; lti < ltexts.length; lti++) ltexts[lti].data = ""; }
915
+ }
823
916
  else {
824
917
  var linkLabel = String((options && options.linkLabel) || "").replace(/^\s+|\s+$/g, "");
825
918
  if (linkLabel) target.setAttribute("aria-label", linkLabel);
@@ -829,6 +922,34 @@ function RTE_Plugin_AccessibilityChecker() {
829
922
  markLanguageRuns(target, options && options.lang ? options.lang : issue._lang, issue._script);
830
923
  }
831
924
 
925
+ else if (issue.code === "duplicate-id") {
926
+ // 2026-10-04 the first element keeps the id (links and labels already resolve to it); the repeat
927
+ // loses it. tools/a11y/checker-action-repairs.mjs
928
+ target.removeAttribute("id");
929
+ }
930
+ else if (issue.code === "blockquote-as-indent") {
931
+ // the quote goes; its left margin moves onto each block it held (bare text becomes a paragraph)
932
+ var ml = target.style.marginLeft, qd = target.ownerDocument, qp = target.parentNode, run = null;
933
+ while (target.firstChild) {
934
+ var qc = target.firstChild;
935
+ // 2026-10-05 every block tag (a rule or a section was wrapped in a <p>), margins ADD (a 20px
936
+ // paragraph in a 40px quote is 60px), and a space between inline words stays (it was dropped:
937
+ // "Helloworld"). tools/review/plugin-review-20261005.mjs
938
+ var isBlock = qc.nodeType === 1 && /^(P|DIV|H[1-6]|UL|OL|DL|PRE|TABLE|BLOCKQUOTE|FIGURE|ADDRESS|HR|SECTION|ARTICLE|ASIDE|NAV|HEADER|FOOTER|DETAILS|FIELDSET|FORM)$/.test(qc.nodeName);
939
+ if (isBlock) {
940
+ run = null;
941
+ if (ml && qc.nodeName !== "HR") {
942
+ var own = qc.style.marginLeft, ownPx = /^([\d.]+)px$/.exec(own || ""), qPx = /^([\d.]+)px$/.exec(ml);
943
+ qc.style.marginLeft = !own ? ml : (ownPx && qPx ? (parseFloat(ownPx[1]) + parseFloat(qPx[1])) + "px" : "calc(" + ml + " + " + own + ")");
944
+ }
945
+ qp.insertBefore(qc, target); continue;
946
+ }
947
+ if (qc.nodeType === 3 && !/\S/.test(qc.data) && !run) { target.removeChild(qc); continue; }
948
+ if (!run) { run = qd.createElement("p"); if (ml) run.style.marginLeft = ml; qp.insertBefore(run, target); }
949
+ run.appendChild(qc);
950
+ }
951
+ qp.removeChild(target);
952
+ }
832
953
  else if (issue.code === "paragraph-as-heading") {
833
954
  var lvl = Math.max(1, Math.min(6, parseInt(options && options.targetLevel, 10) || 2));
834
955
  var h = target.ownerDocument.createElement("h" + lvl);
@@ -846,7 +967,7 @@ function RTE_Plugin_AccessibilityChecker() {
846
967
  var node = target;
847
968
  while (node && node.nodeName === "P") {
848
969
  var t = (node.textContent || "").replace(/^\s+/, "");
849
- var m = ordered ? /^\d{1,3}[.)]\s+([\s\S]*)$/.exec(t) : /^[-*•▪●]\s+([\s\S]*)$/.exec(t);
970
+ var m = ordered ? /^\d{1,3}[.)]\s+([\s\S]*)$/.exec(t) : /^[-*\u2022\u25aa\u25cf]\s+([\s\S]*)$/.exec(t);
850
971
  if (!m) break;
851
972
  var li = d.createElement("li");
852
973
  li.textContent = m[1].replace(/\s+$/, "");
@@ -879,10 +1000,21 @@ function RTE_Plugin_AccessibilityChecker() {
879
1000
  target.setAttribute("alt", String((options && options.altText) || "").replace(/^\s+|\s+$/g, ""));
880
1001
  }
881
1002
  selectedIssueIndex = 0;
1003
+ recordUndo();
882
1004
  scheduleEditorChange();
883
1005
  return runAudit();
884
1006
  }
885
1007
 
1008
+ // record the repaired document as its own step (notifySelectionChange saves the undo snapshot), then
1009
+ // seal it so the next edit starts a new one
1010
+ function recordUndo() {
1011
+ try { if (editor.recordUndo) editor.recordUndo(); else if (editor.notifySelectionChange) editor.notifySelectionChange(); if (editor.commitBookmark) editor.commitBookmark(); } catch (er) { /* ignore */ }
1012
+ commitUndo();
1013
+ }
1014
+ function commitUndo() {
1015
+ if (typeof editor.commitBookmark === "function") { try { editor.commitBookmark(); } catch (er) { /* ignore */ } }
1016
+ }
1017
+
886
1018
  // Wrap the offending script's runs in <span lang> — the same markup
887
1019
  // textpartlanguage.js produces, so the two features agree on one shape and
888
1020
  // the author can retarget the result from the Language dropdown afterwards.
@@ -921,8 +1053,8 @@ function RTE_Plugin_AccessibilityChecker() {
921
1053
  var text = m[0];
922
1054
  // Trim the neutrals the greedy match pulled in at the edges;
923
1055
  // they belong to the surrounding sentence, not the phrase.
924
- var lead = text.length - text.replace(/^[\s.,;:!?'"()‐-‧«»“”]+/, "").length;
925
- var trail = text.length - text.replace(/[\s.,;:!?'"()‐-‧«»“”]+$/, "").length;
1056
+ var lead = text.length - text.replace(/^[\s.,;:!?'"()\u2010-\u2027\xab\xbb\u201c\u201d]+/, "").length;
1057
+ var trail = text.length - text.replace(/[\s.,;:!?'"()\u2010-\u2027\xab\xbb\u201c\u201d]+$/, "").length;
926
1058
  var start = m.index + lead;
927
1059
  var end = m.index + text.length - trail;
928
1060
  if (end - start < MIN_SCRIPT_RUN) continue;
@@ -1092,8 +1224,8 @@ function RTE_Plugin_AccessibilityChecker() {
1092
1224
  var count = panel.querySelector("[data-rte-a11y-count]");
1093
1225
  if (count) {
1094
1226
  count.innerText = issues.length
1095
- ? (issues.length + " issue" + (issues.length === 1 ? "" : "s") + " to review")
1096
- : "Ready to publish";
1227
+ ? (issues.length === 1 ? aT("a11yc_count1", "{0} issue to review", 1) : aT("a11yc_countn", "{0} issues to review", issues.length))
1228
+ : aT("a11yc_ready", "Ready to publish");
1097
1229
  }
1098
1230
 
1099
1231
  empty.style.display = issues.length ? "none" : "block";
@@ -1106,11 +1238,12 @@ function RTE_Plugin_AccessibilityChecker() {
1106
1238
  button.setAttribute("aria-selected", issueIndex === selectedIssueIndex ? "true" : "false");
1107
1239
  button.innerHTML =
1108
1240
  '<div class="rte-a11y-item-top">' +
1109
- '<span class="rte-a11y-badge rte-a11y-badge-' + issue.severity + '">' + issue.severity + '</span>' +
1241
+ '<span class="rte-a11y-badge rte-a11y-badge-' + issue.severity + '"></span>' +
1110
1242
  '<span class="rte-a11y-code">' + issue.code + '</span>' +
1111
1243
  "</div>" +
1112
1244
  '<div class="rte-a11y-message"></div>';
1113
1245
  button.querySelector(".rte-a11y-message").innerText = issue.message;
1246
+ button.querySelector(".rte-a11y-badge").innerText = aT("a11yc_sev_" + issue.severity, issue.severity);
1114
1247
  button.onclick = function () {
1115
1248
  selectedIssueIndex = issueIndex;
1116
1249
  focusIssue(issue);
@@ -1128,7 +1261,9 @@ function RTE_Plugin_AccessibilityChecker() {
1128
1261
 
1129
1262
  var issues = lastResult && lastResult.issues ? lastResult.issues : [];
1130
1263
  if (!issues.length || selectedIssueIndex < 0 || !issues[selectedIssueIndex]) {
1131
- detail.innerHTML = '<div class="rte-a11y-detail-title">No fixes pending</div><div class="rte-a11y-detail-copy">Run the checker again after major content changes to keep headings, image descriptions, and tables in shape.</div>';
1264
+ detail.innerHTML = '<div class="rte-a11y-detail-title"></div><div class="rte-a11y-detail-copy"></div>';
1265
+ detail.querySelector(".rte-a11y-detail-title").innerText = aT("a11yc_nofixes", "No fixes pending");
1266
+ detail.querySelector(".rte-a11y-detail-copy").innerText = aT("a11yc_nofixeshint", "Run the checker again after major content changes to keep headings, image descriptions, and tables in shape.");
1132
1267
  return;
1133
1268
  }
1134
1269
 
@@ -1143,14 +1278,68 @@ function RTE_Plugin_AccessibilityChecker() {
1143
1278
  detail.appendChild(copy);
1144
1279
 
1145
1280
  if (issue.code === "image-missing-alt") {
1146
- renderTextRepair("Alt text", "Describe the image for screen-reader users.", function (value) {
1281
+ renderTextRepair(aT("a11yc_f_alt", "Alt text"), aT("a11yc_f_althint", "Describe the image for screen-reader users."), function (value) {
1147
1282
  repairIssue(selectedIssueIndex, { altText: value });
1148
1283
  });
1149
1284
  return;
1150
1285
  }
1151
1286
 
1287
+ // 2026-10-04 Every repair that takes text gets a field here. These repairs existed (repairDomIssue)
1288
+ // but the panel showed only the message for them. tools/a11y/checker-text-repairs.mjs
1289
+ var TEXT_REPAIRS = {
1290
+ "table-missing-caption": [aT("a11yc_f_caption", "Caption"), aT("a11yc_f_captionhint", "Say what the table shows, e.g. \"Sales by quarter\"."), "captionText"],
1291
+ "table-header-empty": [aT("a11yc_f_header", "Header text"), aT("a11yc_f_headerhint", "Name the column or row this cell labels."), "headerText"],
1292
+ "link-empty": [aT("a11yc_f_link", "Link text"), aT("a11yc_f_linkhint", "Say where the link goes."), "linkText"],
1293
+ "link-ambiguous-text": [aT("a11yc_f_link", "Link text"), aT("a11yc_f_linkambighint", "Say where the link goes, not \"click here\"."), "linkText"],
1294
+ "image-alt-filename": [aT("a11yc_f_alt", "Alt text"), aT("a11yc_f_altfilehint", "Describe what the image shows."), "altText"],
1295
+ "image-alt-too-long": [aT("a11yc_f_alt", "Alt text"), aT("a11yc_f_altlonghint", "A short description; keep detail in the text."), "altText"]
1296
+ };
1297
+ if (TEXT_REPAIRS[issue.code]) {
1298
+ var tr = TEXT_REPAIRS[issue.code];
1299
+ renderTextRepair(tr[0], tr[1], function (value) {
1300
+ var o = {}; o[tr[2]] = value;
1301
+ repairIssue(selectedIssueIndex, o);
1302
+ });
1303
+ return;
1304
+ }
1305
+
1306
+ // ...and every repair that needs no typing gets a button (they too showed only the message).
1307
+ // tools/a11y/checker-action-repairs.mjs
1308
+ var ACTION_REPAIRS = {
1309
+ "fake-list": [aT("a11yc_a_list", "Make it a list"), function () { return {}; }],
1310
+ "paragraph-as-heading": [aT("a11yc_a_heading", "Make it a heading"), function () {
1311
+ // one level below the heading before it (H2 when there is none)
1312
+ var before = 1, t = issue._target, all = t && t.ownerDocument ? t.ownerDocument.querySelectorAll("h1,h2,h3,h4,h5,h6") : [];
1313
+ for (var hi = 0; hi < all.length; hi++) if (all[hi].compareDocumentPosition(t) & 4) before = parseInt(all[hi].nodeName.charAt(1), 10);
1314
+ return { targetLevel: Math.min(6, before + 1) };
1315
+ }],
1316
+ "layout-table-semantics": [aT("a11yc_a_layout", "Remove table semantics"), function () { return {}; }],
1317
+ "link-adjacent-duplicate": [aT("a11yc_a_merge", "Merge the links"), function () { return {}; }],
1318
+ "duplicate-id": [aT("a11yc_a_dupid", "Remove the repeated id"), function () { return {}; }],
1319
+ "blockquote-as-indent": [aT("a11yc_a_indent", "Make it indentation"), function () { return {}; }]
1320
+ };
1321
+ if (ACTION_REPAIRS[issue.code]) {
1322
+ var ar = ACTION_REPAIRS[issue.code];
1323
+ var arRow = detail.ownerDocument.createElement("div");
1324
+ arRow.className = "rte-a11y-row";
1325
+ var arDo = detail.ownerDocument.createElement("button");
1326
+ arDo.type = "button";
1327
+ arDo.className = "rte-a11y-button";
1328
+ arDo.innerText = ar[0];
1329
+ arDo.onclick = function () { repairIssue(selectedIssueIndex, ar[1]()); };
1330
+ var arFocus = detail.ownerDocument.createElement("button");
1331
+ arFocus.type = "button";
1332
+ arFocus.className = "rte-a11y-button rte-a11y-button-secondary";
1333
+ arFocus.innerText = aT("a11yc_focus", "Focus issue");
1334
+ arFocus.onclick = function () { focusIssue(issue); };
1335
+ arRow.appendChild(arDo);
1336
+ arRow.appendChild(arFocus);
1337
+ detail.appendChild(arRow);
1338
+ return;
1339
+ }
1340
+
1152
1341
  if (issue.code === "heading-empty") {
1153
- renderTextRepair("Heading text", "Add a concise heading label before publishing.", function (value) {
1342
+ renderTextRepair(aT("a11yc_f_heading", "Heading text"), aT("a11yc_f_headinghint", "Add a concise heading label before publishing."), function (value) {
1154
1343
  repairIssue(selectedIssueIndex, { headingText: value });
1155
1344
  });
1156
1345
  return;
@@ -1163,14 +1352,14 @@ function RTE_Plugin_AccessibilityChecker() {
1163
1352
  var normalize = detail.ownerDocument.createElement("button");
1164
1353
  normalize.type = "button";
1165
1354
  normalize.className = "rte-a11y-button";
1166
- normalize.innerText = "Normalize to H" + suggestedLevel;
1355
+ normalize.innerText = aT("a11yc_normalize", "Normalize to H{0}", suggestedLevel);
1167
1356
  normalize.onclick = function () {
1168
1357
  repairIssue(selectedIssueIndex, { targetLevel: suggestedLevel });
1169
1358
  };
1170
1359
  var focus = detail.ownerDocument.createElement("button");
1171
1360
  focus.type = "button";
1172
1361
  focus.className = "rte-a11y-button rte-a11y-button-secondary";
1173
- focus.innerText = "Focus issue";
1362
+ focus.innerText = aT("a11yc_focus", "Focus issue");
1174
1363
  focus.onclick = function () { focusIssue(issue); };
1175
1364
  row.appendChild(normalize);
1176
1365
  row.appendChild(focus);
@@ -1184,14 +1373,14 @@ function RTE_Plugin_AccessibilityChecker() {
1184
1373
  var promote = detail.ownerDocument.createElement("button");
1185
1374
  promote.type = "button";
1186
1375
  promote.className = "rte-a11y-button";
1187
- promote.innerText = "Promote first row to headers";
1376
+ promote.innerText = aT("a11yc_promote", "Promote first row to headers");
1188
1377
  promote.onclick = function () {
1189
1378
  repairIssue(selectedIssueIndex, {});
1190
1379
  };
1191
1380
  var review = detail.ownerDocument.createElement("button");
1192
1381
  review.type = "button";
1193
1382
  review.className = "rte-a11y-button rte-a11y-button-secondary";
1194
- review.innerText = "Focus issue";
1383
+ review.innerText = aT("a11yc_focus", "Focus issue");
1195
1384
  review.onclick = function () { focusIssue(issue); };
1196
1385
  actionRow.appendChild(promote);
1197
1386
  actionRow.appendChild(review);
@@ -1205,12 +1394,12 @@ function RTE_Plugin_AccessibilityChecker() {
1205
1394
  var mark = detail.ownerDocument.createElement("button");
1206
1395
  mark.type = "button";
1207
1396
  mark.className = "rte-a11y-button";
1208
- mark.innerText = "Mark as " + issue._script;
1397
+ mark.innerText = aT("a11yc_markas", "Mark as {0}", aT("a11yc_script_" + issue._lang, issue._script));
1209
1398
  mark.onclick = function () { repairIssue(selectedIssueIndex, { lang: issue._lang }); };
1210
1399
  var look = detail.ownerDocument.createElement("button");
1211
1400
  look.type = "button";
1212
1401
  look.className = "rte-a11y-button rte-a11y-button-secondary";
1213
- look.innerText = "Focus issue";
1402
+ look.innerText = aT("a11yc_focus", "Focus issue");
1214
1403
  look.onclick = function () { focusIssue(issue); };
1215
1404
  langRow.appendChild(mark);
1216
1405
  langRow.appendChild(look);
@@ -1218,7 +1407,10 @@ function RTE_Plugin_AccessibilityChecker() {
1218
1407
  return;
1219
1408
  }
1220
1409
 
1221
- detail.innerHTML += '<div class="rte-a11y-detail-copy">This issue is visible here, but does not have an automatic repair action yet.</div>';
1410
+ var noRepair = detail.ownerDocument.createElement("div");
1411
+ noRepair.className = "rte-a11y-detail-copy";
1412
+ noRepair.innerText = aT("a11yc_norepair", "This issue is visible here, but does not have an automatic repair action yet.");
1413
+ detail.appendChild(noRepair);
1222
1414
  }
1223
1415
 
1224
1416
  function renderTextRepair(labelText, hintText, apply) {
@@ -1236,7 +1428,7 @@ function RTE_Plugin_AccessibilityChecker() {
1236
1428
  var button = detail.ownerDocument.createElement("button");
1237
1429
  button.type = "button";
1238
1430
  button.className = "rte-a11y-button";
1239
- button.innerText = "Apply fix";
1431
+ button.innerText = aT("a11yc_applyfix", "Apply fix");
1240
1432
  button.onclick = function () {
1241
1433
  var value = String(input.value || "").replace(/^\s+|\s+$/g, "");
1242
1434
  if (!value) return;
@@ -1245,7 +1437,7 @@ function RTE_Plugin_AccessibilityChecker() {
1245
1437
  var focus = detail.ownerDocument.createElement("button");
1246
1438
  focus.type = "button";
1247
1439
  focus.className = "rte-a11y-button rte-a11y-button-secondary";
1248
- focus.innerText = "Focus issue";
1440
+ focus.innerText = aT("a11yc_focus", "Focus issue");
1249
1441
  focus.onclick = function () {
1250
1442
  var issues = lastResult && lastResult.issues ? lastResult.issues : [];
1251
1443
  if (issues[selectedIssueIndex]) focusIssue(issues[selectedIssueIndex]);