@richscripts/richtexteditor 2.4.0 → 2.4.1

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 (58) hide show
  1. package/migration/README.md +100 -0
  2. package/migration/cli.js +294 -0
  3. package/migration/index.d.ts +18 -0
  4. package/migration/index.js +242 -0
  5. package/migration/report-schema-v2.json +127 -0
  6. package/package.json +11 -1
  7. package/richtexteditor/CHANGELOG.md +326 -1
  8. package/richtexteditor/all_plugins.js +14959 -4832
  9. package/richtexteditor/all_plugins.min.js +62 -42
  10. package/richtexteditor/plugins/accessibilitychecker.js +8 -7
  11. package/richtexteditor/plugins/aitoolkit.css +17 -8
  12. package/richtexteditor/plugins/aitoolkit.js +8 -8
  13. package/richtexteditor/plugins/all_plugins.js +14959 -4832
  14. package/richtexteditor/plugins/all_plugins.min.js +62 -42
  15. package/richtexteditor/plugins/autocorrect.js +353 -0
  16. package/richtexteditor/plugins/bookmarkcard.js +12 -1
  17. package/richtexteditor/plugins/changecase.js +238 -0
  18. package/richtexteditor/plugins/comments.js +15 -28
  19. package/richtexteditor/plugins/contentminimap.js +6 -5
  20. package/richtexteditor/plugins/crossreference.js +408 -0
  21. package/richtexteditor/plugins/documentimport.js +915 -21
  22. package/richtexteditor/plugins/documentoutline.js +5 -4
  23. package/richtexteditor/plugins/docxexport.js +960 -0
  24. package/richtexteditor/plugins/draghandle.js +309 -0
  25. package/richtexteditor/plugins/footnotes.js +398 -0
  26. package/richtexteditor/plugins/formatpainter.js +298 -0
  27. package/richtexteditor/plugins/formattingmarks.js +246 -0
  28. package/richtexteditor/plugins/html2pdf.js +108 -4
  29. package/richtexteditor/plugins/imageeditor.js +37 -7
  30. package/richtexteditor/plugins/insertgallery.js +553 -170
  31. package/richtexteditor/plugins/keyboarda11y.js +316 -0
  32. package/richtexteditor/plugins/linenumbers.js +352 -0
  33. package/richtexteditor/plugins/linkchecker.js +396 -0
  34. package/richtexteditor/plugins/mentionplugin.js +4 -3
  35. package/richtexteditor/plugins/mergefields.js +328 -0
  36. package/richtexteditor/plugins/multilevellist.js +137 -0
  37. package/richtexteditor/plugins/pagination.js +505 -0
  38. package/richtexteditor/plugins/pdfexport.js +1137 -0
  39. package/richtexteditor/plugins/permanentpen.js +211 -0
  40. package/richtexteditor/plugins/revisionhistory.js +47 -34
  41. package/richtexteditor/plugins/rtlui.js +150 -0
  42. package/richtexteditor/plugins/sanitizer.js +486 -0
  43. package/richtexteditor/plugins/slashcommand.js +142 -17
  44. package/richtexteditor/plugins/spellcheck.js +6 -3
  45. package/richtexteditor/plugins/tableofcontents.js +353 -0
  46. package/richtexteditor/plugins/tabletools.js +342 -0
  47. package/richtexteditor/plugins/textdirection.js +313 -0
  48. package/richtexteditor/plugins/trackedchanges.js +37 -6
  49. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
  50. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
  51. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
  52. package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
  53. package/richtexteditor/plugins/watermark.js +214 -0
  54. package/richtexteditor/plugins/wordexport.js +0 -0
  55. package/richtexteditor/plugins/yjscollab.js +49 -11
  56. package/richtexteditor/rte-config.js +44 -44
  57. package/richtexteditor/rte.js +91 -63
  58. package/richtexteditor/rte_theme_default.css +462 -384
@@ -233,10 +233,129 @@ function RTE_Plugin_SlashCommand() {
233
233
  push("Tools", "readaloud", "Read aloud", "Speak the selection or document (text-to-speech)", ["tts", "speak", "speech", "voice", "accessibility", "narrate"], iconReadAloud(),
234
234
  function () { editor.execCommand("readaloud"); });
235
235
  }
236
+ // 2026-07-27 Footnotes / format painter / change case. Each is gated
237
+ // behind a premium plan by CKEditor and/or TinyMCE; all three ship here.
238
+ // One entry per defined merge field, so the slash menu's own search does
239
+ // the filtering — the same shape as TinyMCE's searchable merge-tag menu.
240
+ if (typeof editor.sortTableAtCaret === "function") {
241
+ push("Tools", "sorttableasc", "Sort table by this column", "Sort the table rows by the column the caret is in", ["sort", "table", "column", "order", "ascending"], iconSort(),
242
+ function () { editor.sortTableAtCaret("asc"); });
243
+ push("Tools", "sorttabledesc", "Sort table (descending)", "Sort the table rows by this column, largest first", ["sort", "table", "column", "descending", "reverse"], iconSort(),
244
+ function () { editor.sortTableAtCaret("desc"); });
245
+ push("Tools", "tablerownumbers", "Number table rows", "Add or remove an automatic row-numbering column", ["number", "rows", "table", "count", "index"], iconSort(),
246
+ function () { editor.execCommand("tablerownumbers"); });
247
+ }
248
+ if (typeof editor.moveBlockUp === "function") {
249
+ push("Tools", "moveblockup", "Move block up", "Move this paragraph or block above the previous one (Alt+Shift+Up)", ["move", "block", "up", "reorder", "drag"], iconMoveBlock(),
250
+ function () { editor.moveBlockUp(); });
251
+ push("Tools", "moveblockdown", "Move block down", "Move this paragraph or block below the next one (Alt+Shift+Down)", ["move", "block", "down", "reorder", "drag"], iconMoveBlock(),
252
+ function () { editor.moveBlockDown(); });
253
+ }
254
+ if (typeof editor.toggleRtlUserInterface === "function") {
255
+ push("Tools", "rtlui", "Mirror editor for RTL", "Flip the toolbar, menus and dialogs for right-to-left languages", ["rtl", "mirror", "interface", "toolbar", "arabic", "hebrew"], iconRtl(),
256
+ function () { editor.toggleRtlUserInterface(); });
257
+ }
258
+ if (typeof editor.setTextDirection === "function") {
259
+ push("Tools", "rtl", "Right-to-left", "Set this paragraph to right-to-left (Arabic, Hebrew, Persian)", ["rtl", "arabic", "hebrew", "persian", "urdu", "direction", "bidi"], iconRtl(),
260
+ function () { editor.setTextDirection("rtl"); });
261
+ push("Tools", "ltr", "Left-to-right", "Set this paragraph to left-to-right", ["ltr", "direction", "bidi"], iconLtr(),
262
+ function () { editor.setTextDirection("ltr"); });
263
+ push("Tools", "autodir", "Detect text direction", "Set each paragraph's direction from its own content", ["direction", "detect", "auto", "rtl", "bidi"], iconRtl(),
264
+ function () { editor.autoDetectTextDirection(); });
265
+ }
266
+ if (typeof editor.toggleFormattingMarks === "function") {
267
+ push("Tools", "formattingmarks", "Formatting marks", "Show paragraph marks, block outlines and invisible characters", ["marks", "pilcrow", "paragraph", "invisible", "whitespace", "nbsp"], iconPilcrow(),
268
+ function () { editor.toggleFormattingMarks(); });
269
+ }
270
+ if (typeof editor.toggleWatermark === "function") {
271
+ push("Tools", "watermark", "Watermark", "Draw DRAFT or CONFIDENTIAL behind the content", ["watermark", "draft", "confidential", "background", "stamp"], iconWatermark(),
272
+ function () { editor.toggleWatermark(); });
273
+ }
274
+ if (typeof editor.toggleLineNumbers === "function") {
275
+ push("Tools", "linenumbers", "Line numbers", "Number every line in the margin, as pleadings and transcripts require", ["line", "numbers", "margin", "pleading", "legal", "transcript"], iconLineNumbers(),
276
+ function () { editor.toggleLineNumbers(); });
277
+ }
278
+ if (typeof editor.togglePermanentPen === "function") {
279
+ push("Tools", "permanentpen", "Permanent pen", "Keep typing in a fixed annotation format until switched off", ["pen", "marker", "annotate", "highlight", "review"], iconPen(),
280
+ function () { editor.togglePermanentPen(); });
281
+ }
282
+ if (typeof editor.checkLinks === "function") {
283
+ push("Tools", "checklinks", "Check links", "Find broken anchors, unsafe schemes and unlabelled links", ["link", "check", "broken", "audit", "url", "accessibility"], iconLinkCheck(),
284
+ function () {
285
+ editor.highlightLinkIssues(true);
286
+ editor.checkLinks();
287
+ });
288
+ }
289
+ if (typeof editor.getMergeFieldDefinitions === "function") {
290
+ var mfDefs = editor.getMergeFieldDefinitions();
291
+ for (var mfi = 0; mfi < mfDefs.length; mfi++) {
292
+ (function (def) {
293
+ if (!def || !def.id) return;
294
+ push("Insert", "mergefield-" + def.id, def.label || def.id,
295
+ "Insert the " + (def.label || def.id) + " merge field",
296
+ ["merge", "field", "placeholder", "variable", "mail merge", def.id],
297
+ iconMergeField(),
298
+ function () { editor.insertMergeField(def.id); });
299
+ })(mfDefs[mfi]);
300
+ }
301
+ push("Tools", "mergepreview", "Preview merge fields", "Swap merge-field placeholders for sample data", ["merge", "preview", "sample", "data", "placeholder"], iconMergeField(),
302
+ function () { editor.toggleMergeFieldPreview(); });
303
+ }
304
+ // One entry per referable target, resolved when the menu is built, so the
305
+ // slash menu's own search filters headings/clauses/tables/figures.
306
+ if (typeof editor.listCrossReferenceTargets === "function") {
307
+ var xrefTargets = [];
308
+ try { xrefTargets = editor.listCrossReferenceTargets(); } catch (e) { xrefTargets = []; }
309
+ for (var xi = 0; xi < xrefTargets.length && xi < 40; xi++) {
310
+ (function (tg) {
311
+ if (!tg || !tg.id) return;
312
+ var shown = tg.number ? (tg.number + " " + tg.label) : tg.label;
313
+ push("Insert", "xref-" + tg.id, "Reference: " + shown,
314
+ "Insert a cross-reference to this " + tg.type + " that updates itself",
315
+ ["cross", "reference", "xref", "see", "refer", tg.type, tg.number || ""],
316
+ iconXref(),
317
+ function () { editor.insertCrossReference(tg.id, tg.number ? "label" : "text"); });
318
+ })(xrefTargets[xi]);
319
+ }
320
+ }
321
+ if (typeof editor.insertTableOfContents === "function") {
322
+ push("Insert", "toc", "Table of contents", "Insert a contents block that updates itself from your headings", ["toc", "contents", "outline", "index", "headings"], iconToc(),
323
+ function () { editor.insertTableOfContents(); });
324
+ }
325
+ if (typeof editor.insertFootnote === "function") {
326
+ push("Insert", "footnote", "Footnote", "Insert a numbered footnote reference and note", ["footnote", "note", "citation", "reference", "endnote", "cite"], iconFootnote(),
327
+ function () { editor.insertFootnote(); });
328
+ }
329
+ if (typeof editor.toggleFormatPainter === "function") {
330
+ push("Tools", "formatpainter", "Format painter", "Copy formatting here, then select the text to paint it onto", ["format", "painter", "brush", "copy", "style", "clone"], iconBrush(),
331
+ function () { editor.toggleFormatPainter(); });
332
+ }
333
+ if (typeof editor.changeCase === "function") {
334
+ push("Tools", "caseupper", "UPPERCASE", "Convert the selection to upper case", ["case", "upper", "caps", "capitals"], iconCase(),
335
+ function () { editor.changeCase("upper"); });
336
+ push("Tools", "caselower", "lowercase", "Convert the selection to lower case", ["case", "lower", "small"], iconCase(),
337
+ function () { editor.changeCase("lower"); });
338
+ push("Tools", "casetitle", "Title Case", "Capitalise each significant word in the selection", ["case", "title", "headline", "capitalise", "capitalize"], iconCase(),
339
+ function () { editor.changeCase("title"); });
340
+ push("Tools", "casesentence", "Sentence case", "Capitalise the first letter of each sentence", ["case", "sentence", "capitalise", "capitalize"], iconCase(),
341
+ function () { editor.changeCase("sentence"); });
342
+ }
236
343
  if (typeof editor.requestGhostText === "function") {
237
344
  push("Tools", "aicomplete", "AI complete", "Suggest an inline AI completion of the current sentence", ["ai", "complete", "autocomplete", "ghost", "suggest", "continue", "copilot"], iconGhost(),
238
345
  function () { editor.requestGhostText(); });
239
346
  }
347
+ // Gated on the API rather than registered by the plugin itself: plugins
348
+ // initialise in bundle order, so anything sorting before "slashcommand"
349
+ // would call slashCommands.register() before it exists and vanish from
350
+ // this menu without any error. This check runs when the menu opens.
351
+ if (typeof editor.exportToPdf === "function") {
352
+ push("Tools", "exportpdf", "Export to PDF", "Real text, not a screenshot — selectable, searchable and screen-reader friendly", ["pdf", "export", "download", "save", "print", "text", "accessible", "searchable"], iconPdfText(),
353
+ function () { editor.exportToPdf(); });
354
+ }
355
+ if (typeof editor.exportToDocx === "function") {
356
+ push("Tools", "exportdocx", "Export to Word (.docx)", "A real OOXML document, built in the browser with no upload", ["word", "docx", "ooxml", "export", "download", "save", "office"], iconWordFile(),
357
+ function () { editor.exportToDocx(); });
358
+ }
240
359
  if (typeof editor.copyAsMarkdown === "function") {
241
360
  push("Tools", "copymarkdown", "Copy as Markdown", "Copy the whole document to the clipboard as Markdown", ["markdown", "md", "copy", "clipboard", "export"], iconMarkdown(),
242
361
  function () { editor.copyAsMarkdown(); });
@@ -436,9 +555,8 @@ function RTE_Plugin_SlashCommand() {
436
555
  popupEl = popupHost.createElement("div");
437
556
  popupEl.className = "rte-slash-popup";
438
557
  // The popup is body-appended (outside the editor container), so the
439
- // container's rte-dark class can't reach it via CSS. Mirror forced
440
- // dark mode here; automatic (prefers-color-scheme) dark is handled by
441
- // the media query in the injected stylesheet.
558
+ // container's rte-dark class can't reach it via CSS. Mirror only an
559
+ // explicit forced-dark editor; system color preference stays light.
442
560
  try {
443
561
  var host = editor.container || (editor.getEditable && editor.getEditable().closest && editor.getEditable().closest(".richtexteditor"));
444
562
  if (host && host.classList && host.classList.contains("rte-dark")) popupEl.className += " rte-slash-popup-dark";
@@ -641,20 +759,8 @@ function RTE_Plugin_SlashCommand() {
641
759
  ".rte-slash-item-desc{font-size:11px;color:#64748b;line-height:1.24;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
642
760
  ".rte-slash-item-active .rte-slash-item-desc{color:#315277}",
643
761
  ".rte-slash-empty{padding:16px;color:#52657e;font-size:12px;font-weight:700;text-align:center}",
644
- // Dark mode — applied automatically via prefers-color-scheme, and via
645
- // an explicit .rte-slash-popup-dark class set when the editor is in
646
- // forced dark mode (the popup is body-appended, outside the editor
647
- // container, so the container's .rte-dark class can't reach it).
648
- "@media (prefers-color-scheme:dark){",
649
- " .rte-slash-popup{background:linear-gradient(180deg,#1e293b,#0f172a);border-color:#334155;color:#e2e8f0;box-shadow:0 20px 46px rgba(0,0,0,.5),0 2px 8px rgba(0,0,0,.4)}",
650
- " .rte-slash-section{background:linear-gradient(180deg,rgba(30,41,59,.98),rgba(30,41,59,.86));color:#94a3b8}",
651
- " .rte-slash-item:hover,.rte-slash-item-active{background:#1d4ed8;color:#fff}",
652
- " .rte-slash-item-icon{color:#cbd5e1;background:#0f172a;box-shadow:inset 0 0 0 1px rgba(148,163,184,.25)}",
653
- " .rte-slash-item-active .rte-slash-item-icon{color:#fff;background:#1e3a8a}",
654
- " .rte-slash-item-desc{color:#94a3b8}",
655
- " .rte-slash-item-active .rte-slash-item-desc{color:#dbeafe}",
656
- " .rte-slash-empty{color:#94a3b8}",
657
- "}",
762
+ // The command menu remains light by default. Its explicit
763
+ // rte-slash-popup-dark rules below follow a forced-dark editor.
658
764
  ".rte-slash-popup-dark{background:linear-gradient(180deg,#1e293b,#0f172a);border-color:#334155;color:#e2e8f0;box-shadow:0 20px 46px rgba(0,0,0,.5),0 2px 8px rgba(0,0,0,.4)}",
659
765
  ".rte-slash-popup-dark .rte-slash-section{background:linear-gradient(180deg,rgba(30,41,59,.98),rgba(30,41,59,.86));color:#94a3b8}",
660
766
  ".rte-slash-popup-dark .rte-slash-item:hover,.rte-slash-popup-dark .rte-slash-item-active{background:#1d4ed8;color:#fff}",
@@ -694,6 +800,21 @@ function RTE_Plugin_SlashCommand() {
694
800
  function iconEmail() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M4 7l8 6 8-6"/></svg>'; }
695
801
  function iconDiagram() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="6" height="5" rx="1"/><rect x="15" y="3" width="6" height="5" rx="1"/><rect x="9" y="16" width="6" height="5" rx="1"/><path d="M6 8v3h12V8M12 11v5"/></svg>'; }
696
802
  function iconReadAloud() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9v6h4l5 4V5L8 9H4z"/><path d="M16 8.5a4 4 0 010 7"/><path d="M19 6a7 7 0 010 12"/></svg>'; }
803
+ function iconMoveBlock() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"/><path d="M8 7l4-4 4 4"/><path d="M8 17l4 4 4-4"/></svg>'; }
804
+ function iconRtl() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6H10a3 3 0 000 6h3"/><path d="M13 6v12"/><path d="M17 6v12"/><path d="M7 18l-3-3 3-3"/></svg>'; }
805
+ function iconLtr() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 6H7a3 3 0 000 6h3"/><path d="M11 6v12"/><path d="M7 6v12"/><path d="M17 12l3 3-3 3"/></svg>'; }
806
+ function iconPilcrow() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 4v16"/><path d="M17 4v16"/><path d="M13 4H9a4 4 0 000 8h4"/></svg>'; }
807
+ function iconWatermark() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 15l4-5 3 3 3-4"/></svg>'; }
808
+ function iconLineNumbers() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h1v4"/><path d="M4 14h2l-2 3h2"/><path d="M10 7h10"/><path d="M10 12h10"/><path d="M10 17h10"/></svg>'; }
809
+ function iconSort() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4v16"/><path d="M4 8l3-4 3 4"/><path d="M14 6h6"/><path d="M14 12h5"/><path d="M14 18h3"/></svg>'; }
810
+ function iconPen() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3l6 6L9 21H3v-6z"/><path d="M13 5l6 6"/></svg>'; }
811
+ function iconLinkCheck() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 13a4 4 0 005.7 0l2-2a4 4 0 10-5.7-5.7l-1 1"/><path d="M13 11a4 4 0 00-5.7 0l-2 2a4 4 0 105.7 5.7l1-1"/><path d="M16 18l2 2 4-4"/></svg>'; }
812
+ function iconMergeField() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4H7a2 2 0 00-2 2v3a2 2 0 01-2 2 2 2 0 012 2v3a2 2 0 002 2h2"/><path d="M15 4h2a2 2 0 012 2v3a2 2 0 002 2 2 2 0 00-2 2v3a2 2 0 01-2 2h-2"/></svg>'; }
813
+ function iconXref() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h8"/><path d="M4 12h5"/><path d="M4 17h8"/><path d="M14 12h6"/><path d="M17 9l3 3-3 3"/></svg>'; }
814
+ function iconToc() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h7"/><path d="M4 11h9"/><path d="M4 16h5"/><path d="M17 6h3"/><path d="M17 11h3"/><path d="M17 16h3"/></svg>'; }
815
+ function iconFootnote() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h9"/><path d="M4 11h9"/><path d="M4 18h16"/><path d="M17 4v6"/><path d="M20 7h-6"/></svg>'; }
816
+ function iconBrush() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h11a2 2 0 012 2v4a2 2 0 01-2 2H6z"/><path d="M12 11v3"/><path d="M10 14h4v5a2 2 0 01-4 0z"/></svg>'; }
817
+ function iconCase() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18L8 6l5 12"/><path d="M4.6 14.5h6.8"/><path d="M21 11.5a3 3 0 00-6 0"/><path d="M21 11.5V18"/><path d="M21 15.5a3 3 0 11-6 0 3 3 0 016 0z"/></svg>'; }
697
818
  function iconTodo() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="7" rx="1.5"/><path d="M4.5 7.5 6 9l2.5-3"/><line x1="13" y1="6" x2="21" y2="6"/><line x1="13" y1="13" x2="21" y2="13"/><line x1="3" y1="17" x2="21" y2="17"/><line x1="3" y1="20.5" x2="14" y2="20.5"/></svg>'; }
698
819
  function iconFold() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l3 3 3-3"/><path d="M9 18l3-3 3 3"/><line x1="4" y1="12" x2="20" y2="12"/></svg>'; }
699
820
  function iconKeyboard() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M6 14h.01M18 14h.01M9 14h6"/></svg>'; }
@@ -702,4 +823,8 @@ function RTE_Plugin_SlashCommand() {
702
823
  function iconFocus() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M3 12h3M18 12h3M12 3v3M12 18v3"/></svg>'; }
703
824
  function iconMarkdown() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M6 15V9l3 3 3-3v6"/><path d="M17 9v6M14.5 12.5L17 15l2.5-2.5"/></svg>'; }
704
825
  function iconGhost() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V9a7 7 0 0114 0v12l-3-2-2 2-2-2-2 2-3-2z"/><circle cx="9.5" cy="10" r="1" fill="currentColor"/><circle cx="14.5" cy="10" r="1" fill="currentColor"/></svg>'; }
826
+ // A page with text lines on it, rather than the usual PDF badge: the point
827
+ // of this entry is that the output contains text.
828
+ function iconPdfText() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h4"/></svg>'; }
829
+ function iconWordFile() { return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8z"/><path d="M14 3v5h5"/><path d="M8.5 12l1.3 5 2.2-3.6 2.2 3.6 1.3-5"/></svg>'; }
705
830
  }
@@ -62,6 +62,8 @@ function RTE_Plugin_SpellCheck() {
62
62
  var host = editable.ownerDocument;
63
63
  var t = host.createElement("div");
64
64
  t.className = "rte-spell-toast";
65
+ t.setAttribute("role", "status");
66
+ t.setAttribute("aria-live", "polite");
65
67
  t.textContent = msg;
66
68
  host.body.appendChild(t);
67
69
  setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 2600);
@@ -111,7 +113,7 @@ function RTE_Plugin_SpellCheck() {
111
113
  "rte-dialog-spell"
112
114
  );
113
115
  var close = typeof dlg.close === "function" ? function () { dlg.close(); } : function () { editor.closeCurrentPopup(); };
114
- var wrap = append(dlg, "div", "padding:14px;min-width:420px;max-height:60vh;overflow:auto;font:13px -apple-system,Segoe UI,sans-serif");
116
+ var wrap = append(dlg, "div", "box-sizing:border-box;width:min(420px,calc(100vw - 32px));max-height:60vh;overflow:auto;padding:14px;font:13px -apple-system,Segoe UI,sans-serif");
115
117
  var status = append(wrap, "div", "color:#64748b;margin-bottom:8px");
116
118
  status.innerText = "Checking…";
117
119
  var list = append(wrap, "div", "");
@@ -134,7 +136,7 @@ function RTE_Plugin_SpellCheck() {
134
136
  append(sug, "span", "font-size:12px;color:#94a3b8").textContent = "(no suggestions)";
135
137
  }
136
138
  suggestions.forEach(function (s) {
137
- var chip = append(sug, "button", "font-size:12px;padding:3px 10px;border:1px solid #cbd5e1;border-radius:14px;background:#f8fafc;cursor:pointer");
139
+ var chip = append(sug, "button", "font-size:12px;padding:4px 9px;border:1px solid #cbd5e1;border-radius:6px;background:#f8fbff;color:#315277;cursor:pointer");
138
140
  chip.type = "button";
139
141
  chip.textContent = s;
140
142
  chip.onclick = function () {
@@ -165,7 +167,8 @@ function RTE_Plugin_SpellCheck() {
165
167
 
166
168
  function injectStyles() {
167
169
  var css = [
168
- ".rte-spell-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);background:#0f172a;color:#fff;padding:8px 16px;border-radius:8px;font:13px -apple-system,Segoe UI,sans-serif;z-index:2147483600;box-shadow:0 8px 24px rgba(15,23,42,.28)}",
170
+ "/* 2026-07-12 spell-check feedback precision */",
171
+ ".rte-spell-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);max-width:calc(100vw - 32px);padding:9px 12px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;color:#172033;font:600 13px/1.35 -apple-system,Segoe UI,sans-serif;z-index:2147483600;box-shadow:0 10px 24px rgba(15,23,42,.14)}",
169
172
  ".rte-spell-row:last-child{border-bottom:0}"
170
173
  ].join("\n");
171
174
  try {
@@ -0,0 +1,353 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-27 Table of contents. An insertable, self-updating contents block
4
+ // built from the document's headings, with links that jump to them.
5
+ //
6
+ // Premium in both majors:
7
+ // - CKEditor 5 table of contents: "Unlock this feature with selected CKEditor
8
+ // Plans". Their pitch is that "the list stays up-to-date automatically as
9
+ // the user works on the document", which is the bar this has to clear.
10
+ // - TinyMCE tableofcontents: "This plugin is only available for paid TinyMCE
11
+ // subscriptions".
12
+ //
13
+ // This editor already ships documentoutline.js, but that is a NAVIGATION PANEL
14
+ // beside the editor — chrome, not content. It never lands in the saved HTML.
15
+ // A table of contents is the opposite: it is part of the document you publish
16
+ // or print, so it has to survive getHTMLCode() and read correctly with no
17
+ // JavaScript running.
18
+ //
19
+ // Design notes:
20
+ // - The block is contenteditable=false and rebuilt from the headings on every
21
+ // mutation, so it cannot drift from the document. Hand-editing generated
22
+ // content is a trap, so the block simply is not editable; delete it as a
23
+ // unit and re-insert to move it.
24
+ // - Entry text and numbers are written as REAL TEXT into the block (the same
25
+ // reasoning as footnotes.js): the saved HTML has to stand alone once it
26
+ // leaves the editor, where no script will regenerate anything.
27
+ // - Headings inside generated regions (this block, the footnotes section) are
28
+ // skipped, otherwise the TOC lists its own title.
29
+ // - Page numbers are shown when pagination.js is loaded AND page view is on,
30
+ // via editor.getPageOfElement(). Outside page view a page number would be a
31
+ // fiction, so none is written.
32
+ RTE_DefaultConfig.plugin_tableofcontents = RTE_Plugin_TableOfContents;
33
+
34
+ // Heading above the list. Set to "" to omit it.
35
+ if (typeof RTE_DefaultConfig.tocTitle === "undefined") RTE_DefaultConfig.tocTitle = "Contents";
36
+ // Heading levels included, inclusive.
37
+ if (typeof RTE_DefaultConfig.tocMinLevel === "undefined") RTE_DefaultConfig.tocMinLevel = 1;
38
+ if (typeof RTE_DefaultConfig.tocMaxLevel === "undefined") RTE_DefaultConfig.tocMaxLevel = 3;
39
+ // "auto" = page numbers when page view is on | true = always try | false = never.
40
+ if (typeof RTE_DefaultConfig.tocPageNumbers === "undefined") RTE_DefaultConfig.tocPageNumbers = "auto";
41
+
42
+ function RTE_Plugin_TableOfContents() {
43
+ var obj = this;
44
+ var config, editor;
45
+ var boundDoc = null;
46
+ var queued = false;
47
+ var idSeq = 0;
48
+
49
+ obj.PluginName = "TableOfContents";
50
+
51
+ obj.InitConfig = function (argconfig) { config = argconfig; };
52
+
53
+ obj.InitEditor = function (argeditor) {
54
+ editor = argeditor;
55
+
56
+ editor.attachEvent("exec_command_inserttoc", function (state) {
57
+ state.returnValue = true;
58
+ obj.Insert();
59
+ });
60
+
61
+ setup();
62
+ try { editor.attachEvent("ready", setup); } catch (e) {}
63
+ try { editor.attachEvent("aftersethtml", function () { setup(); queue(); }); } catch (e) {}
64
+ setTimeout(function () { setup(); queue(); }, 0);
65
+
66
+ // Public API.
67
+ editor.insertTableOfContents = function () { return obj.Insert(); };
68
+ editor.updateTableOfContents = function () { return rebuild(); };
69
+ editor.removeTableOfContents = function () { return obj.Remove(); };
70
+ editor.hasTableOfContents = function () { return !!block(); };
71
+ editor.getTableOfContents = function () { return obj.List(); };
72
+ editor.getTocCss = function () { return css(); };
73
+ };
74
+
75
+ function setup() {
76
+ var doc = getDoc();
77
+ if (!doc) return;
78
+ injectStyles(doc);
79
+ if (doc === boundDoc) return;
80
+ boundDoc = doc;
81
+ var editable = getEditable();
82
+ if (!editable) return;
83
+ editable.addEventListener("keyup", queue);
84
+ editable.addEventListener("cut", queue);
85
+ editable.addEventListener("paste", queue);
86
+ editable.addEventListener("drop", queue);
87
+ // Clicking an entry scrolls to its heading. The block is not editable, so
88
+ // the anchors would otherwise do nothing inside the iframe.
89
+ editable.addEventListener("click", function (e) {
90
+ var a = closestClass(e.target, "rte-toc-link", editable);
91
+ if (!a) return;
92
+ e.preventDefault();
93
+ var target = editable.querySelector("#" + cssEscapeId(a.getAttribute("data-toc-target")));
94
+ if (!target) return;
95
+ try { target.scrollIntoView({ block: "center" }); } catch (e2) { target.scrollIntoView(); }
96
+ });
97
+ }
98
+
99
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
100
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
101
+
102
+ // setTimeout not rAF: a hidden iframe or background tab never fires rAF, and
103
+ // the contents would silently stop updating.
104
+ function queue() {
105
+ if (queued) return;
106
+ queued = true;
107
+ setTimeout(function () { queued = false; rebuild(); }, 0);
108
+ }
109
+
110
+ function closestClass(node, cls, root) {
111
+ while (node && node !== root) {
112
+ if (node.nodeType === 1 && node.classList && node.classList.contains(cls)) return node;
113
+ node = node.parentNode;
114
+ }
115
+ return null;
116
+ }
117
+
118
+ function cssEscapeId(s) { return String(s || "").replace(/["\\\]\[]/g, "\\$&"); }
119
+
120
+ function block() {
121
+ var editable = getEditable();
122
+ return editable ? editable.querySelector("nav.rte-toc") : null;
123
+ }
124
+
125
+ // ---- heading collection ---------------------------------------------
126
+
127
+ function levelRange() {
128
+ var min = Math.max(1, Math.min(6, parseInt(config.tocMinLevel, 10) || 1));
129
+ var max = Math.max(min, Math.min(6, parseInt(config.tocMaxLevel, 10) || 3));
130
+ return { min: min, max: max };
131
+ }
132
+
133
+ function headings() {
134
+ var editable = getEditable();
135
+ if (!editable) return [];
136
+ var r = levelRange();
137
+ var sel = [];
138
+ for (var l = r.min; l <= r.max; l++) sel.push("h" + l);
139
+ var all = editable.querySelectorAll(sel.join(","));
140
+ var out = [];
141
+ for (var i = 0; i < all.length; i++) {
142
+ var h = all[i];
143
+ // Skip generated regions, or the TOC lists its own "Contents" title
144
+ // and the footnotes section heading.
145
+ if (h.closest && (h.closest("nav.rte-toc") || h.closest("section.rte-footnotes"))) continue;
146
+ if (!(h.textContent || "").trim()) continue; // empty heading = noise
147
+ out.push(h);
148
+ }
149
+ return out;
150
+ }
151
+
152
+ function ensureHeadingId(h) {
153
+ var id = h.getAttribute("id");
154
+ if (id && id.indexOf("rte-h-") === 0) return id;
155
+ if (id) return id; // respect an id the author already chose
156
+ id = "rte-h-" + Math.floor(Math.random() * 1e9).toString(36) + (idSeq++).toString(36);
157
+ h.setAttribute("id", id);
158
+ return id;
159
+ }
160
+
161
+ function wantPageNumbers() {
162
+ var mode = config.tocPageNumbers;
163
+ if (mode === false) return false;
164
+ if (typeof editor.getPageOfElement !== "function") return false;
165
+ if (mode === true) return true;
166
+ // "auto": only in page view, where the numbers mean something.
167
+ try { return typeof editor.isPageView === "function" && editor.isPageView(); } catch (e) { return false; }
168
+ }
169
+
170
+ // ---- build -----------------------------------------------------------
171
+
172
+ obj.Insert = function () {
173
+ var doc = getDoc();
174
+ var editable = getEditable();
175
+ if (!doc || !editable) return false;
176
+ var existing = block();
177
+ if (existing) { rebuild(); return true; } // one per document
178
+
179
+ var nav = doc.createElement("nav");
180
+ nav.className = "rte-toc";
181
+ nav.setAttribute("contenteditable", "false");
182
+ nav.setAttribute("data-rte-toc", "1");
183
+
184
+ var placed = false;
185
+ try {
186
+ if (typeof editor.insertElement === "function") { editor.insertElement(nav); placed = true; }
187
+ } catch (e) {}
188
+ if (!placed) editable.insertBefore(nav, editable.firstChild);
189
+
190
+ rebuild();
191
+ fireChange();
192
+ return true;
193
+ };
194
+
195
+ obj.Remove = function () {
196
+ var b = block();
197
+ if (!b || !b.parentNode) return false;
198
+ b.parentNode.removeChild(b);
199
+ fireChange();
200
+ return true;
201
+ };
202
+
203
+ function rebuild() {
204
+ var nav = block();
205
+ if (!nav) return [];
206
+ var doc = getDoc();
207
+ if (!doc) return [];
208
+
209
+ var hs = headings();
210
+ var r = levelRange();
211
+ var pages = wantPageNumbers();
212
+
213
+ // Carry forward the page numbers already in the block, keyed by heading.
214
+ // Without this, opening a document that was saved in page view and then
215
+ // rebuilding with page view OFF silently deletes numbers that were
216
+ // computed for real — destroying part of the saved document just by
217
+ // loading it. Stale-until-you-re-enter-page-view matches how a word
218
+ // processor treats a contents field; silently losing them does not.
219
+ var priorPages = {};
220
+ var priorItems = nav.querySelectorAll("li.rte-toc-item");
221
+ for (var p = 0; p < priorItems.length; p++) {
222
+ var pa = priorItems[p].querySelector("a.rte-toc-link");
223
+ var pp = priorItems[p].querySelector(".rte-toc-page");
224
+ if (pa && pp) {
225
+ var n = parseInt(pp.textContent, 10);
226
+ if (n) priorPages[pa.getAttribute("data-toc-target")] = n;
227
+ }
228
+ }
229
+
230
+ // Rebuild the inner list wholesale. The block is not editable, so there
231
+ // is no caret inside it to preserve.
232
+ while (nav.firstChild) nav.removeChild(nav.firstChild);
233
+
234
+ var title = String(config.tocTitle == null ? "" : config.tocTitle);
235
+ if (title) {
236
+ var h = doc.createElement("div");
237
+ h.className = "rte-toc-title";
238
+ h.textContent = title;
239
+ nav.appendChild(h);
240
+ }
241
+
242
+ if (!hs.length) {
243
+ var empty = doc.createElement("div");
244
+ empty.className = "rte-toc-empty";
245
+ empty.textContent = "No headings yet.";
246
+ nav.appendChild(empty);
247
+ return [];
248
+ }
249
+
250
+ var ol = doc.createElement("ol");
251
+ ol.className = "rte-toc-list";
252
+ var out = [];
253
+ for (var i = 0; i < hs.length; i++) {
254
+ var head = hs[i];
255
+ var level = parseInt(head.nodeName.substring(1), 10) || 1;
256
+ var id = ensureHeadingId(head);
257
+ var text = (head.textContent || "").trim();
258
+
259
+ var li = doc.createElement("li");
260
+ li.className = "rte-toc-item rte-toc-l" + (level - r.min + 1);
261
+
262
+ var a = doc.createElement("a");
263
+ a.className = "rte-toc-link";
264
+ a.setAttribute("href", "#" + id);
265
+ a.setAttribute("data-toc-target", id);
266
+ a.textContent = text;
267
+ li.appendChild(a);
268
+
269
+ var page = null;
270
+ if (pages) {
271
+ try { page = editor.getPageOfElement(head); } catch (e) { page = null; }
272
+ }
273
+ // Could not compute one (page view off) but this entry already had a
274
+ // number: keep it rather than throw it away.
275
+ if (!page && priorPages[id]) page = priorPages[id];
276
+ if (page) {
277
+ var span = doc.createElement("span");
278
+ span.className = "rte-toc-page";
279
+ span.textContent = String(page);
280
+ li.appendChild(span);
281
+ }
282
+ ol.appendChild(li);
283
+ out.push({ id: id, level: level, text: text, page: page });
284
+ }
285
+ nav.appendChild(ol);
286
+ return out;
287
+ }
288
+
289
+ obj.List = function () {
290
+ var nav = block();
291
+ if (!nav) return [];
292
+ var items = nav.querySelectorAll("li.rte-toc-item");
293
+ var out = [];
294
+ for (var i = 0; i < items.length; i++) {
295
+ var a = items[i].querySelector("a.rte-toc-link");
296
+ var p = items[i].querySelector(".rte-toc-page");
297
+ var cls = items[i].className.match(/rte-toc-l(\d+)/);
298
+ out.push({
299
+ id: a ? a.getAttribute("data-toc-target") : null,
300
+ level: cls ? parseInt(cls[1], 10) : 1,
301
+ text: a ? a.textContent : "",
302
+ page: p ? parseInt(p.textContent, 10) : null
303
+ });
304
+ }
305
+ return out;
306
+ };
307
+
308
+ function fireChange() {
309
+ try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
310
+ try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
311
+ }
312
+
313
+ // ---- styles ----------------------------------------------------------
314
+
315
+ function css() {
316
+ return (
317
+ "nav.rte-toc{margin:0 0 1.4em;padding:.9em 1.1em;border:1px solid #ddd;border-radius:6px;" +
318
+ "background:#fafbfc;}" +
319
+ "nav.rte-toc .rte-toc-title{font-weight:700;margin-bottom:.45em;}" +
320
+ "nav.rte-toc .rte-toc-empty{color:#888;font-style:italic;}" +
321
+ "ol.rte-toc-list{list-style:none;margin:0;padding:0;counter-reset:rte-toc;}" +
322
+ "ol.rte-toc-list>li{counter-increment:rte-toc;margin:.16em 0;display:flex;align-items:baseline;gap:.5em;}" +
323
+ // A leader line between the entry and its page number, the way a
324
+ // printed contents page sets it.
325
+ "ol.rte-toc-list>li>a{color:#1474ea;text-decoration:none;cursor:pointer;}" +
326
+ "ol.rte-toc-list>li>a:hover{text-decoration:underline;}" +
327
+ "ol.rte-toc-list>li:has(>.rte-toc-page)>a::after{content:'';flex:1 1 auto;margin:0 .4em;" +
328
+ "border-bottom:1px dotted #bbb;transform:translateY(-.25em);display:inline-block;min-width:1.5em;}" +
329
+ "ol.rte-toc-list>li>a{flex:1 1 auto;display:flex;align-items:baseline;}" +
330
+ ".rte-toc-page{flex:0 0 auto;color:#666;font-variant-numeric:tabular-nums;}" +
331
+ ".rte-toc-l2{padding-left:1.3em;}" +
332
+ ".rte-toc-l3{padding-left:2.6em;}" +
333
+ ".rte-toc-l4{padding-left:3.9em;}" +
334
+ ".rte-toc-l5{padding-left:5.2em;}" +
335
+ ".rte-toc-l6{padding-left:6.5em;}"
336
+ );
337
+ }
338
+
339
+ function injectStyles(doc) {
340
+ if (!doc) return;
341
+ var existing = doc.getElementById("rte-toc-styles");
342
+ var text = css();
343
+ if (existing) {
344
+ if (existing.getAttribute("data-css") === text) return;
345
+ existing.parentNode && existing.parentNode.removeChild(existing);
346
+ }
347
+ var st = doc.createElement("style");
348
+ st.id = "rte-toc-styles";
349
+ st.setAttribute("data-css", text);
350
+ st.appendChild(doc.createTextNode(text));
351
+ (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
352
+ }
353
+ }