@richscripts/richtexteditor 2.7.2 → 2.8.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.
@@ -35368,6 +35368,446 @@ function RTE_Plugin_MultiRoot() {
35368
35368
 
35369
35369
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
35370
35370
 
35371
+ RTE_DefaultConfig.plugin_nodetypes = RTE_Plugin_NodeTypes;
35372
+
35373
+ // 2026-09-04 Custom node type registry.
35374
+ //
35375
+ // WHAT THE GAP ACTUALLY IS. Measured before this was designed, rather than
35376
+ // assumed. A custom element with data-* attributes already survives more of this
35377
+ // editor than expected:
35378
+ //
35379
+ // setHTML -> getHTML round trip ......... survives, attributes intact
35380
+ // contenteditable="false" atomicity ..... preserved
35381
+ // undo of an editor-driven delete ....... restored, attributes intact
35382
+ // sanitizer ............................. already extensible via
35383
+ // config.sanitizerAllowTags / -Attributes
35384
+ // markdown export ....................... DEGRADES to the element's text
35385
+ // .docx export .......................... DEGRADES to the element's text
35386
+ // an API to declare any of this ......... DID NOT EXIST
35387
+ //
35388
+ // So the gap is not "you cannot put your own element in the document" - you can.
35389
+ // The gap is that nothing lets you DECLARE that the element is a node type, and
35390
+ // therefore nothing downstream can treat it deliberately. Degrading to text in
35391
+ // Markdown and .docx is a defensible default; having no way to say what should
35392
+ // happen instead is not.
35393
+ //
35394
+ // This registry is that declaration. Register a type once and it gets:
35395
+ // - its tags and attributes allowed through the sanitizer automatically
35396
+ // - atomicity enforced and repaired (chips stop being atomic the moment
35397
+ // contenteditable is lost, and then the caret edits their internals)
35398
+ // - JSON serialization of every instance, for saving state outside the HTML
35399
+ // - a Markdown representation you control instead of raw text content
35400
+ //
35401
+ // ORDERING NOTE: plugins initialise in alphabetical bundle order. "nodetypes"
35402
+ // sorts BEFORE "sanitizer" and BEFORE "slashcommand", so this file must NOT call
35403
+ // editor.slashCommands.register() from init - it would silently no-op. Feeding
35404
+ // the sanitizer works precisely because we run first: we write config before
35405
+ // sanitizer.js reads it.
35406
+ function RTE_Plugin_NodeTypes() {
35407
+ var obj = this;
35408
+ var config;
35409
+ var editor;
35410
+ var types = []; // registration order
35411
+ var byName = {};
35412
+ var enforceTimer = 0;
35413
+ var originalToMarkdown = null;
35414
+
35415
+ obj.PluginName = "NodeTypes";
35416
+
35417
+ obj.InitConfig = function (argconfig) {
35418
+ config = argconfig;
35419
+ if (config.nodeTypesEnabled === false) return;
35420
+
35421
+ // Types can be declared in config so a host does not have to wait for an
35422
+ // editor instance to exist.
35423
+ if (Array.isArray(config.nodeTypes)) {
35424
+ for (var i = 0; i < config.nodeTypes.length; i++) registerType(config.nodeTypes[i]);
35425
+ }
35426
+ };
35427
+
35428
+ obj.InitEditor = function (argeditor) {
35429
+ editor = argeditor;
35430
+ if (config.nodeTypesEnabled === false) return;
35431
+
35432
+ editor.nodeTypes = {
35433
+ find: function (name) { return findInstances(name); },
35434
+ get: function (name) { return byName[name] || null; },
35435
+ insert: function (name, data) { return insertNode(name, data); },
35436
+ list: function () { return types.map(describe); },
35437
+ register: function (def) { return registerType(def); },
35438
+ serialize: function () { return serializeAll(); },
35439
+ typeOf: function (el) { var t = typeFor(el); return t ? t.name : null; },
35440
+ unregister: function (name) { return unregisterType(name); }
35441
+ };
35442
+
35443
+ // Atomicity is a live invariant, not a one-off: a paste, an undo or a
35444
+ // competing plugin can strip contenteditable and the node silently stops
35445
+ // behaving like one unit.
35446
+ //
35447
+ // Observed rather than event-driven, deliberately. setHTML() does not
35448
+ // fire "change", so an invariant hung off editor events is not enforced
35449
+ // on loaded content at all - which is the most common way a node enters
35450
+ // the document. Measured: with only the "change" handler, a node loaded
35451
+ // via setHTML never gained contenteditable="false".
35452
+ editor.attachEvent("change", function () { scheduleEnforce(); });
35453
+ observeEditable();
35454
+
35455
+ wrapMarkdown();
35456
+
35457
+ // Deferred deliberately. Plugins initialise in alphabetical bundle order:
35458
+ // docxexport (19) runs BEFORE nodetypes (49), but pdfexport (51) and
35459
+ // wordexport (72) run AFTER, so editor.getPdfBytes does not exist yet at
35460
+ // this point. Wrapping now would silently miss PDF entirely - the same
35461
+ // ordering trap that makes slashCommands.register() a no-op from here.
35462
+ // A zero-delay timeout lands after every plugin has initialised.
35463
+ setTimeout(installExportHooks, 0);
35464
+
35465
+ enforceAll();
35466
+ };
35467
+
35468
+ // ------------------------------------------------------------ registration
35469
+
35470
+ function registerType(def) {
35471
+ if (!def || !def.name) return false;
35472
+ if (typeof def.match !== "string" && typeof def.match !== "function") return false;
35473
+ if (byName[def.name]) unregisterType(def.name);
35474
+
35475
+ var type = {
35476
+ name: def.name,
35477
+ match: def.match,
35478
+ atomic: def.atomic !== false, // atomic by default
35479
+ inline: !!def.inline,
35480
+ toJSON: typeof def.toJSON === "function" ? def.toJSON : null,
35481
+ fromJSON: typeof def.fromJSON === "function" ? def.fromJSON : null,
35482
+ toMarkdown: typeof def.toMarkdown === "function" ? def.toMarkdown : null,
35483
+ // Export representations. toExportHTML covers .docx and PDF at once;
35484
+ // toDocx / toPdf override it per format when they differ.
35485
+ toExportHTML: typeof def.toExportHTML === "function" ? def.toExportHTML : null,
35486
+ toDocx: typeof def.toDocx === "function" ? def.toDocx : null,
35487
+ toPdf: typeof def.toPdf === "function" ? def.toPdf : null,
35488
+ toWord: typeof def.toWord === "function" ? def.toWord : null,
35489
+ allowTags: def.allowTags || [],
35490
+ allowAttributes: def.allowAttributes || []
35491
+ };
35492
+ types.push(type);
35493
+ byName[type.name] = type;
35494
+ feedSanitizer(type);
35495
+ if (editor) { enforceAll(); }
35496
+ return true;
35497
+ }
35498
+
35499
+ function unregisterType(name) {
35500
+ if (!byName[name]) return false;
35501
+ types = types.filter(function (t) { return t.name !== name; });
35502
+ delete byName[name];
35503
+ return true;
35504
+ }
35505
+
35506
+ function describe(t) {
35507
+ return {
35508
+ name: t.name, atomic: t.atomic, inline: t.inline,
35509
+ hasToJSON: !!t.toJSON, hasFromJSON: !!t.fromJSON, hasToMarkdown: !!t.toMarkdown,
35510
+ hasExportHTML: !!(t.toExportHTML || t.toDocx || t.toPdf),
35511
+ instances: findInstances(t.name).length
35512
+ };
35513
+ }
35514
+
35515
+ // The sanitizer is an allowlist: an unknown tag is dropped on input, on
35516
+ // output and from the live DOM. A registered type that did not feed the
35517
+ // allowlist would be deleted by the editor's own security layer, so this is
35518
+ // wired automatically rather than left to the host to remember.
35519
+ function feedSanitizer(type) {
35520
+ if (!config) return;
35521
+ mergeInto("sanitizerAllowTags", type.allowTags);
35522
+ mergeInto("sanitizerAllowAttributes", type.allowAttributes);
35523
+ }
35524
+
35525
+ function mergeInto(key, values) {
35526
+ if (!values || !values.length) return;
35527
+ var list = config[key];
35528
+ if (!Array.isArray(list)) list = list ? [list] : [];
35529
+ for (var i = 0; i < values.length; i++) {
35530
+ if (list.indexOf(values[i]) === -1) list.push(values[i]);
35531
+ }
35532
+ config[key] = list;
35533
+ }
35534
+
35535
+ // ------------------------------------------------------------- instances
35536
+
35537
+ function matches(type, el) {
35538
+ if (!el || el.nodeType !== 1) return false;
35539
+ if (typeof type.match === "string") {
35540
+ try { return el.matches(type.match); } catch (e) { return false; }
35541
+ }
35542
+ try { return !!type.match(el); } catch (e) { return false; }
35543
+ }
35544
+
35545
+ function typeFor(el) {
35546
+ for (var i = 0; i < types.length; i++) {
35547
+ if (matches(types[i], el)) return types[i];
35548
+ }
35549
+ return null;
35550
+ }
35551
+
35552
+ function editable() {
35553
+ return editor && editor.getEditable ? editor.getEditable() : null;
35554
+ }
35555
+
35556
+ function findInstances(name) {
35557
+ var root = editable();
35558
+ var type = byName[name];
35559
+ var out = [];
35560
+ if (!root || !type) return out;
35561
+ // Include the root's own children: querySelectorAll finds descendants
35562
+ // only, and a node pasted at the top level is a child of the root. That
35563
+ // exact blind spot is why the accessibility checker used to miss
35564
+ // top-level images.
35565
+ var all = root.querySelectorAll("*");
35566
+ for (var i = 0; i < all.length; i++) {
35567
+ if (matches(type, all[i])) out.push(all[i]);
35568
+ }
35569
+ return out;
35570
+ }
35571
+
35572
+ function allInstances() {
35573
+ var root = editable();
35574
+ var out = [];
35575
+ if (!root) return out;
35576
+ var all = root.querySelectorAll("*");
35577
+ for (var i = 0; i < all.length; i++) {
35578
+ var t = typeFor(all[i]);
35579
+ if (t) out.push({ type: t, el: all[i] });
35580
+ }
35581
+ return out;
35582
+ }
35583
+
35584
+ function insertNode(name, data) {
35585
+ var type = byName[name];
35586
+ if (!type || !type.fromJSON || !editor) return null;
35587
+ var produced = type.fromJSON(data || {});
35588
+ var html = typeof produced === "string" ? produced : (produced && produced.outerHTML);
35589
+ if (!html) return null;
35590
+ editor.insertHTML(html);
35591
+ scheduleEnforce();
35592
+ return html;
35593
+ }
35594
+
35595
+ function serializeAll() {
35596
+ var out = [];
35597
+ var found = allInstances();
35598
+ for (var i = 0; i < found.length; i++) {
35599
+ var t = found[i].type, el = found[i].el;
35600
+ out.push({
35601
+ name: t.name,
35602
+ data: t.toJSON ? safe(t.toJSON, el, {}) : readDataAttributes(el)
35603
+ });
35604
+ }
35605
+ return out;
35606
+ }
35607
+
35608
+ function readDataAttributes(el) {
35609
+ var data = {};
35610
+ var attrs = el.attributes;
35611
+ for (var i = 0; attrs && i < attrs.length; i++) {
35612
+ if (attrs[i].name.indexOf("data-") === 0) data[attrs[i].name] = attrs[i].value;
35613
+ }
35614
+ return data;
35615
+ }
35616
+
35617
+ function safe(fn, arg, fallback) {
35618
+ try { return fn(arg); } catch (e) { return fallback; }
35619
+ }
35620
+
35621
+ // -------------------------------------------------------------- atomicity
35622
+
35623
+ function observeEditable() {
35624
+ var root = editable();
35625
+ if (!root || typeof MutationObserver === "undefined") return;
35626
+ var observer = new MutationObserver(function () { scheduleEnforce(); });
35627
+ observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["contenteditable"] });
35628
+ }
35629
+
35630
+ function scheduleEnforce() {
35631
+ clearTimeout(enforceTimer);
35632
+ enforceTimer = setTimeout(enforceAll, 60);
35633
+ }
35634
+
35635
+ function enforceAll() {
35636
+ var found = allInstances();
35637
+ for (var i = 0; i < found.length; i++) {
35638
+ if (found[i].type.atomic && found[i].el.getAttribute("contenteditable") !== "false") {
35639
+ found[i].el.setAttribute("contenteditable", "false");
35640
+ }
35641
+ }
35642
+ }
35643
+
35644
+ // ----------------------------------------------------------------- export
35645
+
35646
+ // .docx and PDF both build from the live editable, so the same swap the
35647
+ // Markdown path uses works here: replace each registered node with the HTML
35648
+ // the type says represents it, export, then put the originals back.
35649
+ //
35650
+ // Unlike Markdown these are partly ASYNCHRONOUS - getDocxBlob returns a
35651
+ // Promise - so the restore has to wait for the result to settle. Restoring
35652
+ // synchronously would put the originals back while the exporter was still
35653
+ // reading the DOM.
35654
+ function installExportHooks() {
35655
+ wrapExport("getDocxBlob", "toDocx");
35656
+ wrapExport("exportToDocx", "toDocx");
35657
+ wrapExport("getPdfBytes", "toPdf");
35658
+ wrapExport("exportToPdf", "toPdf");
35659
+ wrapExport("downloadPdf", "toPdf");
35660
+ // wordexport.js sorts at 72 - after this file at 49 - which is exactly
35661
+ // why the install is deferred. It builds from editor.getHTMLCode(), and
35662
+ // getHTMLCode() serializes the live DOM, so the same swap reaches it.
35663
+ wrapExport("getWordDocument", "toWord");
35664
+ wrapExport("exportToWord", "toWord");
35665
+ wrapExport("downloadWord", "toWord");
35666
+ }
35667
+
35668
+ function wrapExport(apiName, hookName) {
35669
+ if (!editor || typeof editor[apiName] !== "function") return;
35670
+ var original = editor[apiName];
35671
+ editor[apiName] = function () {
35672
+ var args = arguments;
35673
+ var found = allInstances().filter(function (f) {
35674
+ return f.type[hookName] || f.type.toExportHTML;
35675
+ });
35676
+ if (!found.length) return original.apply(editor, args);
35677
+
35678
+ var restore = swapNodes(found, hookName);
35679
+ var result;
35680
+ try {
35681
+ result = original.apply(editor, args);
35682
+ } catch (e) {
35683
+ restore();
35684
+ throw e;
35685
+ }
35686
+ if (result && typeof result.then === "function") {
35687
+ return result.then(function (v) { restore(); return v; },
35688
+ function (e) { restore(); throw e; });
35689
+ }
35690
+ restore();
35691
+ return result;
35692
+ };
35693
+ }
35694
+
35695
+ // Replaces each instance with its declared export HTML and returns an
35696
+ // idempotent restore function.
35697
+ function swapNodes(found, hookName) {
35698
+ var doc = editor.getDocument();
35699
+ var swaps = [];
35700
+ var savedRange = null;
35701
+ try {
35702
+ var sel = doc.getSelection();
35703
+ if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
35704
+ } catch (e) { savedRange = null; }
35705
+
35706
+ for (var i = 0; i < found.length; i++) {
35707
+ var el = found[i].el;
35708
+ var fn = found[i].type[hookName] || found[i].type.toExportHTML;
35709
+ var html = fn ? safe(fn, el, null) : null;
35710
+ if (html === null || html === undefined) continue;
35711
+ var holder = doc.createElement(found[i].type.inline ? "span" : "div");
35712
+ holder.innerHTML = String(html);
35713
+ el.parentNode.insertBefore(holder, el);
35714
+ el.parentNode.removeChild(el);
35715
+ swaps.push({ holder: holder, original: el });
35716
+ }
35717
+
35718
+ var done = false;
35719
+ return function () {
35720
+ if (done) return;
35721
+ done = true;
35722
+ for (var j = swaps.length - 1; j >= 0; j--) {
35723
+ var s = swaps[j];
35724
+ if (s.holder.parentNode) {
35725
+ s.holder.parentNode.insertBefore(s.original, s.holder);
35726
+ s.holder.parentNode.removeChild(s.holder);
35727
+ }
35728
+ }
35729
+ if (savedRange) {
35730
+ try {
35731
+ var sel2 = doc.getSelection();
35732
+ sel2.removeAllRanges();
35733
+ sel2.addRange(savedRange);
35734
+ } catch (e) { /* selection may no longer be placeable */ }
35735
+ }
35736
+ };
35737
+ }
35738
+
35739
+ // --------------------------------------------------------------- markdown
35740
+
35741
+ // The core's markdown engine takes a root element, but the public
35742
+ // editor.toMarkdown() closes over the live editable and accepts no argument,
35743
+ // so there is no way to hand it a modified clone. The only seam is to swap
35744
+ // registered nodes for their markdown text in the live DOM, convert, and put
35745
+ // the originals back.
35746
+ //
35747
+ // That mutates the document the user is editing, so it is wrapped in
35748
+ // try/finally: if a host's toMarkdown throws, the originals are restored
35749
+ // anyway. Selection is saved and restored for the same reason.
35750
+ function wrapMarkdown() {
35751
+ if (!editor || typeof editor.toMarkdown !== "function") return;
35752
+ originalToMarkdown = editor.toMarkdown;
35753
+ editor.toMarkdown = function () {
35754
+ var found = allInstances().filter(function (f) { return f.type.toMarkdown; });
35755
+ if (!found.length) return originalToMarkdown.call(editor);
35756
+
35757
+ var doc = editor.getDocument();
35758
+ var swaps = [];
35759
+ var tokens = [];
35760
+ var savedRange = null;
35761
+ try {
35762
+ var sel = doc.getSelection();
35763
+ if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
35764
+ } catch (e) { savedRange = null; }
35765
+
35766
+ try {
35767
+ for (var i = 0; i < found.length; i++) {
35768
+ var el = found[i].el;
35769
+ var md = safe(found[i].type.toMarkdown, el, null);
35770
+ if (md === null || md === undefined) continue;
35771
+ // An ALPHANUMERIC sentinel, not the markdown itself. The core
35772
+ // engine escapes markdown punctuation in text content, so a
35773
+ // hook returning "**Approval**" would emit "\*\*Approval\*\*".
35774
+ // The sentinel has nothing to escape; the real markdown is
35775
+ // substituted into the finished string instead.
35776
+ var token = "RTENODETYPETOKEN" + i + "ENDRTENODETYPETOKEN";
35777
+ var holder = doc.createElement(found[i].type.inline ? "span" : "p");
35778
+ holder.textContent = token;
35779
+ el.parentNode.insertBefore(holder, el);
35780
+ el.parentNode.removeChild(el);
35781
+ swaps.push({ holder: holder, original: el });
35782
+ tokens.push({ token: token, markdown: String(md) });
35783
+ }
35784
+ var out = originalToMarkdown.call(editor);
35785
+ for (var k = 0; k < tokens.length; k++) {
35786
+ out = out.split(tokens[k].token).join(tokens[k].markdown);
35787
+ }
35788
+ return out;
35789
+ } finally {
35790
+ for (var j = swaps.length - 1; j >= 0; j--) {
35791
+ var s = swaps[j];
35792
+ if (s.holder.parentNode) {
35793
+ s.holder.parentNode.insertBefore(s.original, s.holder);
35794
+ s.holder.parentNode.removeChild(s.holder);
35795
+ }
35796
+ }
35797
+ if (savedRange) {
35798
+ try {
35799
+ var sel2 = doc.getSelection();
35800
+ sel2.removeAllRanges();
35801
+ sel2.addRange(savedRange);
35802
+ } catch (e) { /* selection may no longer be placeable */ }
35803
+ }
35804
+ }
35805
+ };
35806
+ }
35807
+ }
35808
+
35809
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
35810
+
35371
35811
  // 2026-07-17 Pagination / page view. Renders the editable as a stack of paper
35372
35812
  // pages — the "print layout" view Word and Google Docs have, and the feature
35373
35813
  // CKEditor and Tiptap both sell as a paid add-on.
@@ -38691,6 +39131,262 @@ function RTE_Plugin_RtlUi() {
38691
39131
 
38692
39132
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
38693
39133
 
39134
+ RTE_DefaultConfig.plugin_rubyannotation = RTE_Plugin_RubyAnnotation;
39135
+
39136
+ // 2026-09-04 Ruby annotation (phonetic guides above base text).
39137
+ //
39138
+ // Furigana in Japanese, pinyin/bopomofo in Chinese, ruby glosses in academic
39139
+ // typesetting. Word has it, Lexical ships a RubyExtension, and it was the one
39140
+ // item in the Lexical feature gap register worth scheduling: small, self
39141
+ // contained, and an i18n/accessibility feature rather than a novelty.
39142
+ //
39143
+ // The markup is standard HTML, not an invention:
39144
+ //
39145
+ // <ruby>漢字<rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>
39146
+ //
39147
+ // The <rp> parentheses are the accessibility half and are why they are emitted
39148
+ // by default: a browser or assistive technology with no ruby support falls back
39149
+ // to rendering "漢字(かんじ)" inline instead of running the reading straight
39150
+ // into the base text as "漢字かんじ". Copy-paste to a plain-text target gets the
39151
+ // same readable fallback.
39152
+ //
39153
+ // ruby/rt/rp are ALREADY in the sanitizer allowlist (checked, not assumed), so
39154
+ // the markup survives sanitisation with no allowlist change.
39155
+ function RTE_Plugin_RubyAnnotation() {
39156
+ var obj = this;
39157
+ var config;
39158
+ var editor;
39159
+
39160
+ obj.PluginName = "RubyAnnotation";
39161
+
39162
+ obj.InitConfig = function (argconfig) {
39163
+ config = argconfig;
39164
+ if (config.rubyAnnotationEnabled === false) return;
39165
+
39166
+ if (typeof config.rubyAnnotationParentheses !== "boolean") config.rubyAnnotationParentheses = true;
39167
+
39168
+ // BOTH toolbars. config.toolbar defaults to "default", which resolves to
39169
+ // toolbar_default, so appending only to toolbar_full would leave the
39170
+ // button unreachable for every host that never configured a toolbar -
39171
+ // the mistake that shipped inlinecode invisible for a whole release.
39172
+ // The default toolbar gets it in the overflow set, where a niche
39173
+ // typographic feature belongs; toolbar_full gets it inline.
39174
+ appendToolbarCommand("toolbar_full", "#{rubyannotation}");
39175
+ appendSubToolbarCommand("subtoolbar_more", "rubyannotation");
39176
+ };
39177
+
39178
+ obj.InitEditor = function (argeditor) {
39179
+ editor = argeditor;
39180
+ if (config.rubyAnnotationEnabled === false) return;
39181
+
39182
+ editor.ruby = {
39183
+ add: function (reading) { return applyRuby(reading); },
39184
+ get: function () { var r = findRubyAtSelection(); return r ? readingOf(r) : null; },
39185
+ isRuby: function () { return !!findRubyAtSelection(); },
39186
+ open: function () { obj.OpenRubyDialog(); },
39187
+ remove: function () { return removeRuby(); }
39188
+ };
39189
+
39190
+ injectStyles();
39191
+
39192
+ editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
39193
+ editor.toolbarFactoryMap["rubyannotation"] = function (cmd) {
39194
+ return editor.createToolbarButton(cmd);
39195
+ };
39196
+
39197
+ editor.attachEvent("exec_command_rubyannotation", function (state) {
39198
+ state.returnValue = true;
39199
+ state.stopBubble = true;
39200
+ obj.OpenRubyDialog();
39201
+ });
39202
+ };
39203
+
39204
+ function appendToolbarCommand(toolbar, item) {
39205
+ if (!config[toolbar]) return;
39206
+ if (config[toolbar].indexOf(item) !== -1) return;
39207
+ config[toolbar] = config[toolbar] + item;
39208
+ }
39209
+
39210
+ // subtoolbar_* entries are a braced, comma-separated list rather than the
39211
+ // "#{name}" form the main toolbars use.
39212
+ function appendSubToolbarCommand(toolbar, name) {
39213
+ var val = config[toolbar];
39214
+ if (!val || val.indexOf(name) !== -1) return;
39215
+ var i = val.indexOf("}");
39216
+ if (i === -1) return;
39217
+ config[toolbar] = val.substring(0, i) + "," + name + val.substring(i);
39218
+ }
39219
+
39220
+ // Ruby has no default styling worth relying on: browsers vary in whether the
39221
+ // reading is sized down at all, and <rp> must be hidden wherever ruby DOES
39222
+ // render or the parentheses show up twice.
39223
+ function injectStyles() {
39224
+ var hostDoc = config.container.ownerDocument;
39225
+ if (hostDoc.getElementById("rte-ruby-style")) return;
39226
+ var style = hostDoc.createElement("style");
39227
+ style.id = "rte-ruby-style";
39228
+ style.innerHTML = [
39229
+ ".rte-ruby-annotated{ruby-position:over;}",
39230
+ ".rte-ruby-annotated>rt{font-size:.55em;line-height:1.15;opacity:.85;user-select:none;}",
39231
+ "@supports (display:ruby){.rte-ruby-annotated>rp{display:none;}}"
39232
+ ].join("");
39233
+ hostDoc.head.appendChild(style);
39234
+ }
39235
+
39236
+ function editableRoot() {
39237
+ return editor && editor.getEditable ? editor.getEditable() : null;
39238
+ }
39239
+
39240
+ function currentRange() {
39241
+ var doc = editor.getDocument();
39242
+ try {
39243
+ var sel = doc.getSelection();
39244
+ if (!sel || !sel.rangeCount) return null;
39245
+ return sel.getRangeAt(0);
39246
+ } catch (e) { return null; }
39247
+ }
39248
+
39249
+ function findRubyAtSelection() {
39250
+ var range = currentRange();
39251
+ var root = editableRoot();
39252
+ if (!range || !root) return null;
39253
+ var node = range.startContainer;
39254
+ while (node && node !== root) {
39255
+ if (node.nodeType === 1 && node.nodeName === "RUBY") return node;
39256
+ node = node.parentNode;
39257
+ }
39258
+ return null;
39259
+ }
39260
+
39261
+ function readingOf(rubyEl) {
39262
+ var rt = rubyEl.querySelector("rt");
39263
+ return rt ? (rt.textContent || "") : "";
39264
+ }
39265
+
39266
+ function baseOf(rubyEl) {
39267
+ var out = "";
39268
+ for (var i = 0; i < rubyEl.childNodes.length; i++) {
39269
+ var n = rubyEl.childNodes[i];
39270
+ if (n.nodeType === 3) out += n.nodeValue;
39271
+ else if (n.nodeType === 1 && n.nodeName !== "RT" && n.nodeName !== "RP") out += n.textContent || "";
39272
+ }
39273
+ return out;
39274
+ }
39275
+
39276
+ function esc(s) {
39277
+ return String(s == null ? "" : s)
39278
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;")
39279
+ .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
39280
+ }
39281
+
39282
+ function buildRuby(base, reading) {
39283
+ var parens = config.rubyAnnotationParentheses;
39284
+ return '<ruby class="rte-ruby-annotated">' + esc(base) +
39285
+ (parens ? "<rp>(</rp>" : "") +
39286
+ "<rt>" + esc(reading) + "</rt>" +
39287
+ (parens ? "<rp>)</rp>" : "") +
39288
+ "</ruby>";
39289
+ }
39290
+
39291
+ function applyRuby(reading) {
39292
+ var existing = findRubyAtSelection();
39293
+ if (existing) {
39294
+ var rt = existing.querySelector("rt");
39295
+ if (!rt) {
39296
+ rt = existing.ownerDocument.createElement("rt");
39297
+ existing.appendChild(rt);
39298
+ }
39299
+ rt.textContent = String(reading == null ? "" : reading);
39300
+ editor.focus();
39301
+ return existing;
39302
+ }
39303
+ var range = currentRange();
39304
+ if (!range || range.collapsed) return null;
39305
+ var base = String(range.toString() || "");
39306
+ if (!base) return null;
39307
+ editor.insertHTML(buildRuby(base, reading));
39308
+ editor.focus();
39309
+ return true;
39310
+ }
39311
+
39312
+ // Unwrap back to the base text, discarding the reading and the <rp> fallback
39313
+ // parentheses. Without dropping <rp> the text would read "漢字()".
39314
+ function removeRuby() {
39315
+ var rubyEl = findRubyAtSelection();
39316
+ if (!rubyEl) return false;
39317
+ var doc = rubyEl.ownerDocument;
39318
+ var text = doc.createTextNode(baseOf(rubyEl));
39319
+ rubyEl.parentNode.insertBefore(text, rubyEl);
39320
+ rubyEl.parentNode.removeChild(rubyEl);
39321
+ editor.focus();
39322
+ return true;
39323
+ }
39324
+
39325
+ function append(parent, tag, cssText, className) {
39326
+ var el = parent.ownerDocument.createElement(tag);
39327
+ if (cssText) el.style.cssText = cssText;
39328
+ if (className) el.className = className;
39329
+ parent.appendChild(el);
39330
+ return el;
39331
+ }
39332
+
39333
+ obj.OpenRubyDialog = function () {
39334
+ var existing = findRubyAtSelection();
39335
+ var range = currentRange();
39336
+ var baseText = existing ? baseOf(existing) : (range ? String(range.toString() || "") : "");
39337
+
39338
+ var dlg = editor.createDialog(
39339
+ (editor.getLangText && editor.getLangText("rubyannotationtitle")) || "Ruby annotation",
39340
+ "rte-dialog-ruby"
39341
+ );
39342
+ var close = typeof dlg.close === "function" ? function () { dlg.close(); } : function () { editor.closeCurrentPopup(); };
39343
+
39344
+ var wrap = append(dlg, "div", "padding:14px;min-width:340px;font:13px -apple-system,Segoe UI,sans-serif");
39345
+
39346
+ append(wrap, "div", "font-weight:600;margin-bottom:4px").innerText = "Base text";
39347
+ var baseView = append(wrap, "div",
39348
+ "padding:8px 10px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;min-height:20px;word-break:break-word");
39349
+ baseView.textContent = baseText;
39350
+
39351
+ append(wrap, "div", "font-weight:600;margin:10px 0 4px").innerText = "Reading";
39352
+ var input = append(wrap, "input",
39353
+ "width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #cbd5e1;border-radius:8px;font:13px inherit");
39354
+ input.type = "text";
39355
+ input.value = existing ? readingOf(existing) : "";
39356
+ input.placeholder = "e.g. かんじ";
39357
+
39358
+ var note = append(wrap, "div", "font-size:11px;color:#94a3b8;margin-top:6px");
39359
+ note.innerText = baseText
39360
+ ? "Shown above the base text. Parentheses are emitted for readers without ruby support."
39361
+ : "Select the text to annotate first, then reopen this dialog.";
39362
+
39363
+ var footer = append(wrap, "div", "display:flex;justify-content:space-between;gap:8px;margin-top:14px");
39364
+ var left = append(footer, "div", "display:flex;gap:8px");
39365
+ if (existing) {
39366
+ var rm = append(left, "button", "padding:6px 14px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;color:#b91c1c;cursor:pointer");
39367
+ rm.type = "button"; rm.textContent = "Remove";
39368
+ rm.onclick = function () { removeRuby(); close(); };
39369
+ }
39370
+ var right = append(footer, "div", "display:flex;gap:8px");
39371
+ var cancel = append(right, "button", "padding:6px 14px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;cursor:pointer");
39372
+ cancel.type = "button"; cancel.textContent = "Cancel"; cancel.onclick = close;
39373
+ var ok = append(right, "button", "padding:6px 14px;border:1px solid #1d67ba;border-radius:8px;background:#1d67ba;color:#fff;cursor:pointer");
39374
+ ok.type = "button"; ok.textContent = existing ? "Update" : "Add";
39375
+ ok.disabled = !baseText;
39376
+
39377
+ ok.onclick = function () {
39378
+ var reading = input.value.trim();
39379
+ if (!reading || !baseText) { close(); return; }
39380
+ applyRuby(reading);
39381
+ close();
39382
+ };
39383
+
39384
+ setTimeout(function () { input.focus(); }, 0);
39385
+ };
39386
+ }
39387
+
39388
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
39389
+
38694
39390
  // 2026-08-02 Content sanitizer.
38695
39391
  //
38696
39392
  // The editor core already does most of the hard part: it strips inline event
@@ -40937,6 +41633,873 @@ function RTE_Plugin_SpellCheck() {
40937
41633
 
40938
41634
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
40939
41635
 
41636
+ RTE_DefaultConfig.plugin_stateinspector = RTE_Plugin_StateInspector;
41637
+
41638
+ // 2026-09-04 Document state inspector (developer tool).
41639
+ //
41640
+ // The editor's source of truth is the contenteditable DOM, not a separate
41641
+ // document model, so there is no state object to print. This plugin derives
41642
+ // one: it walks the editable subtree and produces a normalized, serializable
41643
+ // snapshot (tree + selection + plugin init order), then runs a rule set over
41644
+ // that snapshot looking for the structural faults that have actually caused
41645
+ // defects in this product - zero-length text nodes, empty inline wrappers,
41646
+ // duplicate nested formatting, control characters, sanitizer bypasses, broken
41647
+ // atomic chips, invalid list and heading structure.
41648
+ //
41649
+ // The snapshot is the point. `editor.stateInspector.snapshot()` returns plain
41650
+ // JSON, so a support case can carry the document's real structure instead of a
41651
+ // screenshot, and a test can assert on structure instead of on an HTML string.
41652
+ //
41653
+ // It is deliberately NOT in any default toolbar: set stateInspectorToolbarButton
41654
+ // to true to add it, or drive it from the API.
41655
+ function RTE_Plugin_StateInspector() {
41656
+ var obj = this;
41657
+ var config;
41658
+ var editor;
41659
+ var shell = null;
41660
+ var panel = null;
41661
+ var body = null;
41662
+ var tabsBar = null;
41663
+ var summaryEl = null;
41664
+ var refreshTimer = 0;
41665
+ var activeTab = "issues";
41666
+ var pluginOrder = [];
41667
+ var eventLog = [];
41668
+ var logLimit = 60;
41669
+
41670
+ var TABS = [
41671
+ { id: "issues", label: "Issues" },
41672
+ { id: "tree", label: "Tree" },
41673
+ { id: "selection", label: "Selection" },
41674
+ { id: "plugins", label: "Plugins" },
41675
+ { id: "log", label: "Log" }
41676
+ ];
41677
+
41678
+ var INLINE_TAGS = {
41679
+ A: 1, ABBR: 1, B: 1, BDI: 1, BDO: 1, CITE: 1, CODE: 1, DEL: 1, DFN: 1,
41680
+ EM: 1, I: 1, INS: 1, KBD: 1, MARK: 1, Q: 1, S: 1, SAMP: 1, SMALL: 1,
41681
+ SPAN: 1, STRONG: 1, SUB: 1, SUP: 1, U: 1, VAR: 1, FONT: 1
41682
+ };
41683
+ var VOID_TAGS = {
41684
+ AREA: 1, BASE: 1, BR: 1, COL: 1, EMBED: 1, HR: 1, IMG: 1, INPUT: 1,
41685
+ LINK: 1, META: 1, PARAM: 1, SOURCE: 1, TRACK: 1, WBR: 1
41686
+ };
41687
+ var BLOCK_TAGS = {
41688
+ ADDRESS: 1, ARTICLE: 1, ASIDE: 1, BLOCKQUOTE: 1, DIV: 1, DL: 1, DT: 1,
41689
+ DD: 1, FIELDSET: 1, FIGCAPTION: 1, FIGURE: 1, FOOTER: 1, FORM: 1,
41690
+ H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, HEADER: 1, HR: 1, LI: 1,
41691
+ MAIN: 1, NAV: 1, OL: 1, P: 1, PRE: 1, SECTION: 1, TABLE: 1, UL: 1
41692
+ };
41693
+ // Control characters that make Word reject an entire .docx package and that
41694
+ // silently truncate grep over a bundle. NUL through backspace, vertical tab,
41695
+ // form feed, and shift-out through unit separator.
41696
+ // Written as escapes, never as literal bytes: a literal control character
41697
+ // in source makes grep treat the whole bundle as binary and silently
41698
+ // truncate every search over it.
41699
+ var CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/;
41700
+
41701
+ obj.PluginName = "StateInspector";
41702
+
41703
+ obj.InitConfig = function (argconfig) {
41704
+ config = argconfig;
41705
+ if (config.stateInspectorEnabled === false) return;
41706
+
41707
+ config.stateInspectorTitle = config.stateInspectorTitle || "State inspector";
41708
+ config.stateInspectorHint = config.stateInspectorHint || "Derived document model, structural checks, and a copyable snapshot for bug reports.";
41709
+ config.stateInspectorEmptyText = config.stateInspectorEmptyText || "No content to inspect yet.";
41710
+ if (typeof config.stateInspectorAutoOpen !== "boolean") config.stateInspectorAutoOpen = false;
41711
+ if (typeof config.stateInspectorToolbarButton !== "boolean") config.stateInspectorToolbarButton = false;
41712
+ if (typeof config.stateInspectorMaxDepth !== "number") config.stateInspectorMaxDepth = 24;
41713
+ if (typeof config.stateInspectorLogLimit === "number") logLimit = Math.max(10, Math.min(500, config.stateInspectorLogLimit));
41714
+
41715
+ // Captured here rather than in InitEditor because `for (var p in config)`
41716
+ // reproduces exactly the order the core uses to instantiate plugins -
41717
+ // which is bundle concatenation order, i.e. alphabetical by filename.
41718
+ // A plugin that registers into another plugin's API only works if it
41719
+ // sorts after it; that has silently broken registration before, so the
41720
+ // order is worth being able to see.
41721
+ pluginOrder = collectPluginOrder(config);
41722
+
41723
+ if (config.stateInspectorToolbarButton) {
41724
+ // BOTH presets, deliberately. config.toolbar defaults to "default",
41725
+ // which resolves to toolbar_default - so appending only to
41726
+ // toolbar_full means a host that sets stateInspectorToolbarButton on
41727
+ // a default toolbar gets no button at all. "It is in the config" and
41728
+ // "a user can see it" are different claims. inlinecode shipped a
41729
+ // whole release cycle invisible for exactly this reason.
41730
+ //
41731
+ // If you write a test for this, scope it to [role=toolbar]. Searching
41732
+ // the whole document for the button's label matches the inspector
41733
+ // PANEL's aria-label - the panel is built at InitEditor and merely
41734
+ // hidden - so a page-wide text search reports the button present in
41735
+ // every case, including the two where no button exists. A check whose
41736
+ // population is wrong reports confidently and does not look broken.
41737
+ appendToolbarCommand("toolbar_default", "#{stateinspector}");
41738
+ appendToolbarCommand("toolbar_full", "#{stateinspector}");
41739
+ }
41740
+ };
41741
+
41742
+ obj.InitEditor = function (argeditor) {
41743
+ editor = argeditor;
41744
+ if (config.stateInspectorEnabled === false) return;
41745
+
41746
+ editor.stateInspector = {
41747
+ close: function () { closePanel(); },
41748
+ isOpen: function () { return !!(shell && shell.classList.contains("is-open")); },
41749
+ issues: function () { return analyze(snapshotTree()).issues; },
41750
+ log: function () { return eventLog.slice(0); },
41751
+ open: function () { openPanel(); },
41752
+ plugins: function () { return pluginOrder.slice(0); },
41753
+ refresh: function () { render(); },
41754
+ snapshot: function () { return buildSnapshot(); },
41755
+ toggle: function () { togglePanel(); }
41756
+ };
41757
+
41758
+ injectStyles();
41759
+
41760
+ editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
41761
+ editor.toolbarFactoryMap["stateinspector"] = function (cmd) {
41762
+ return editor.createToolbarButton(cmd);
41763
+ };
41764
+
41765
+ editor.attachEvent("exec_command_stateinspector", function (state) {
41766
+ state.returnValue = true;
41767
+ state.stopBubble = true;
41768
+ togglePanel();
41769
+ });
41770
+ editor.attachEvent("change", function () {
41771
+ recordEvent("change");
41772
+ scheduleRefresh();
41773
+ });
41774
+ editor.attachEvent("selectionchange", function () {
41775
+ recordEvent("selectionchange");
41776
+ scheduleRefresh();
41777
+ });
41778
+
41779
+ // Safe to register here, unlike documentimport/pdfexport/readabilitystats:
41780
+ // plugins initialise in alphabetical bundle order and "stateinspector"
41781
+ // sorts AFTER "slashcommand", so editor.slashCommands already exists.
41782
+ // Guarded anyway, because that ordering is a filename away from changing.
41783
+ if (editor.slashCommands && editor.slashCommands.register) {
41784
+ editor.slashCommands.register({
41785
+ id: "stateinspector",
41786
+ section: "Tools",
41787
+ title: config.stateInspectorTitle,
41788
+ description: "Inspect document structure, selection, and structural issues",
41789
+ keywords: ["inspect", "debug", "devtools", "state", "structure"],
41790
+ run: function () { openPanel(); }
41791
+ });
41792
+ }
41793
+
41794
+ // Build the wrapper NOW, not lazily on first open. ensureShell() reparents
41795
+ // the editor container, and reparenting a populated editing surface makes
41796
+ // Chrome reseat it - which blanks the visible content while the document
41797
+ // is still there (the status bar keeps counting words nobody can see).
41798
+ // documentoutline.js and contentminimap.js do the same thing for the same
41799
+ // reason: at InitEditor the surface is still empty, so the reseat is free.
41800
+ ensureShell();
41801
+
41802
+ if (config.stateInspectorAutoOpen) openPanel();
41803
+ };
41804
+
41805
+ function appendToolbarCommand(toolbar, item) {
41806
+ if (!config[toolbar]) return;
41807
+ if (config[toolbar].indexOf(item) !== -1) return;
41808
+ config[toolbar] = config[toolbar] + item;
41809
+ }
41810
+
41811
+ function collectPluginOrder(cfg) {
41812
+ var order = [];
41813
+ for (var p in cfg) {
41814
+ if (p[0] !== "p" || p.substr(0, 7) !== "plugin_") continue;
41815
+ if (!cfg[p] || !(cfg[p] instanceof Function)) continue;
41816
+ order.push({ index: order.length, key: p, name: p.substring(7) });
41817
+ }
41818
+ return order;
41819
+ }
41820
+
41821
+ function recordEvent(name) {
41822
+ eventLog.push({ at: Date.now(), event: name });
41823
+ if (eventLog.length > logLimit) eventLog.splice(0, eventLog.length - logLimit);
41824
+ }
41825
+
41826
+ // ---------------------------------------------------------------- snapshot
41827
+
41828
+ function describeAttributes(el) {
41829
+ var attrs = {};
41830
+ var list = el.attributes;
41831
+ if (!list) return attrs;
41832
+ for (var i = 0; i < list.length; i++) {
41833
+ var value = list[i].value;
41834
+ attrs[list[i].name] = value.length > 220 ? value.substring(0, 220) + "…" : value;
41835
+ }
41836
+ return attrs;
41837
+ }
41838
+
41839
+ function walk(node, depth, maxDepth, counters) {
41840
+ if (node.nodeType === 3) {
41841
+ var text = node.nodeValue || "";
41842
+ counters.textNodes++;
41843
+ counters.characters += text.length;
41844
+ return {
41845
+ type: "text",
41846
+ length: text.length,
41847
+ text: text.length > 80 ? text.substring(0, 80) + "…" : text
41848
+ };
41849
+ }
41850
+ if (node.nodeType === 8) {
41851
+ counters.comments++;
41852
+ return { type: "comment", length: (node.nodeValue || "").length };
41853
+ }
41854
+ if (node.nodeType !== 1) return null;
41855
+
41856
+ counters.elements++;
41857
+ var name = node.nodeName;
41858
+ counters.tags[name] = (counters.tags[name] || 0) + 1;
41859
+
41860
+ var entry = {
41861
+ type: "element",
41862
+ name: name.toLowerCase(),
41863
+ attrs: describeAttributes(node),
41864
+ children: []
41865
+ };
41866
+ if (depth >= maxDepth) {
41867
+ entry.truncated = true;
41868
+ entry.childCount = node.childNodes.length;
41869
+ return entry;
41870
+ }
41871
+ for (var i = 0; i < node.childNodes.length; i++) {
41872
+ var child = walk(node.childNodes[i], depth + 1, maxDepth, counters);
41873
+ if (child) entry.children.push(child);
41874
+ }
41875
+ return entry;
41876
+ }
41877
+
41878
+ function snapshotTree() {
41879
+ var editable = editor && editor.getEditable ? editor.getEditable() : null;
41880
+ var counters = { elements: 0, textNodes: 0, comments: 0, characters: 0, tags: {} };
41881
+ if (!editable) {
41882
+ return { root: null, counters: counters, editable: null };
41883
+ }
41884
+ var maxDepth = Math.max(2, config.stateInspectorMaxDepth || 24);
41885
+ var root = {
41886
+ type: "element",
41887
+ name: "root",
41888
+ attrs: {},
41889
+ children: []
41890
+ };
41891
+ for (var i = 0; i < editable.childNodes.length; i++) {
41892
+ var child = walk(editable.childNodes[i], 1, maxDepth, counters);
41893
+ if (child) root.children.push(child);
41894
+ }
41895
+ return { root: root, counters: counters, editable: editable };
41896
+ }
41897
+
41898
+ function describeSelection() {
41899
+ var doc = editor && editor.getDocument ? editor.getDocument() : null;
41900
+ var editable = editor && editor.getEditable ? editor.getEditable() : null;
41901
+ var info = { available: false };
41902
+ if (!doc || !doc.getSelection) return info;
41903
+
41904
+ var sel;
41905
+ try { sel = doc.getSelection(); } catch (e) { return info; }
41906
+ if (!sel || sel.rangeCount === 0) return info;
41907
+
41908
+ var range;
41909
+ try { range = sel.getRangeAt(0); } catch (e) { return info; }
41910
+ if (!range) return info;
41911
+
41912
+ info.available = true;
41913
+ info.collapsed = !!range.collapsed;
41914
+ info.anchor = describeBoundary(range.startContainer, range.startOffset);
41915
+ info.focus = describeBoundary(range.endContainer, range.endOffset);
41916
+ info.insideEditable = !!(editable && editable.contains(range.startContainer) && editable.contains(range.endContainer));
41917
+ try {
41918
+ var selected = String(range.toString() || "");
41919
+ info.textLength = selected.length;
41920
+ info.text = selected.length > 120 ? selected.substring(0, 120) + "…" : selected;
41921
+ } catch (e) {
41922
+ info.textLength = 0;
41923
+ }
41924
+ info.block = describeBlockPath(range.startContainer, editable);
41925
+ return info;
41926
+ }
41927
+
41928
+ function describeBoundary(container, offset) {
41929
+ if (!container) return { node: "(none)", offset: offset };
41930
+ if (container.nodeType === 3) {
41931
+ var text = container.nodeValue || "";
41932
+ return {
41933
+ node: "#text",
41934
+ offset: offset,
41935
+ length: text.length,
41936
+ // A caret sitting in a zero-length text node is the shape of a
41937
+ // caret trap; surfacing it here is the whole point.
41938
+ zeroLength: text.length === 0
41939
+ };
41940
+ }
41941
+ return {
41942
+ node: container.nodeName ? container.nodeName.toLowerCase() : "(unknown)",
41943
+ offset: offset,
41944
+ childCount: container.childNodes ? container.childNodes.length : 0
41945
+ };
41946
+ }
41947
+
41948
+ function describeBlockPath(node, editable) {
41949
+ var path = [];
41950
+ var current = node;
41951
+ while (current && current !== editable) {
41952
+ if (current.nodeType === 1) path.unshift(current.nodeName.toLowerCase());
41953
+ current = current.parentNode;
41954
+ }
41955
+ return path.join(" > ");
41956
+ }
41957
+
41958
+ function buildSnapshot() {
41959
+ var tree = snapshotTree();
41960
+ var analysis = analyze(tree);
41961
+ return {
41962
+ snapshotVersion: 1,
41963
+ generatedAt: new Date().toISOString(),
41964
+ counters: tree.counters,
41965
+ issues: analysis.issues,
41966
+ plugins: pluginOrder.slice(0),
41967
+ recentEvents: eventLog.slice(-20),
41968
+ selection: describeSelection(),
41969
+ tree: tree.root
41970
+ };
41971
+ }
41972
+
41973
+ // ----------------------------------------------------------------- analysis
41974
+
41975
+ function issue(list, id, severity, message, detail) {
41976
+ list.push({ id: id, severity: severity, message: message, detail: detail || "" });
41977
+ }
41978
+
41979
+ function elementPath(el, editable) {
41980
+ var path = [];
41981
+ var current = el;
41982
+ while (current && current !== editable && current.nodeType === 1) {
41983
+ var step = current.nodeName.toLowerCase();
41984
+ if (current.id) step += "#" + current.id;
41985
+ path.unshift(step);
41986
+ current = current.parentNode;
41987
+ }
41988
+ return path.join(" > ");
41989
+ }
41990
+
41991
+ function hasMeaningfulContent(el) {
41992
+ if ((el.textContent || "").replace(/[\s\u200B\uFEFF]/g, "") !== "") return true;
41993
+ for (var i = 0; i < el.childNodes.length; i++) {
41994
+ var child = el.childNodes[i];
41995
+ if (child.nodeType === 1 && VOID_TAGS[child.nodeName]) return true;
41996
+ }
41997
+ return false;
41998
+ }
41999
+
42000
+ function analyze(tree) {
42001
+ var issues = [];
42002
+ var editable = tree.editable;
42003
+ if (!editable) return { issues: issues };
42004
+
42005
+ var walker = editable.ownerDocument.createTreeWalker(
42006
+ editable,
42007
+ // NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
42008
+ 1 | 4,
42009
+ null,
42010
+ false
42011
+ );
42012
+ var lastHeadingLevel = 0;
42013
+ var node;
42014
+
42015
+ while ((node = walker.nextNode())) {
42016
+ if (node.nodeType === 3) {
42017
+ analyzeText(node, editable, issues);
42018
+ continue;
42019
+ }
42020
+ analyzeElement(node, editable, issues);
42021
+
42022
+ var level = headingLevel(node);
42023
+ if (level) {
42024
+ if (lastHeadingLevel && level > lastHeadingLevel + 1) {
42025
+ issue(issues, "heading-skip", "warn",
42026
+ "Heading level jumps from h" + lastHeadingLevel + " to h" + level,
42027
+ elementPath(node, editable));
42028
+ }
42029
+ lastHeadingLevel = level;
42030
+ }
42031
+ }
42032
+
42033
+ // Top-level images are children of the editable root, so a walker that
42034
+ // starts below the root would miss them - which is exactly how the
42035
+ // accessibility checker used to skip pasted images.
42036
+ for (var i = 0; i < editable.childNodes.length; i++) {
42037
+ var child = editable.childNodes[i];
42038
+ if (child.nodeType === 1 && child.nodeName === "IMG") {
42039
+ checkImageAlt(child, editable, issues);
42040
+ }
42041
+ }
42042
+
42043
+ return { issues: issues };
42044
+ }
42045
+
42046
+ function headingLevel(el) {
42047
+ var name = el.nodeName;
42048
+ if (name.length === 2 && name[0] === "H" && name[1] >= "1" && name[1] <= "6") {
42049
+ return parseInt(name[1], 10);
42050
+ }
42051
+ return 0;
42052
+ }
42053
+
42054
+ function analyzeText(node, editable, issues) {
42055
+ var text = node.nodeValue || "";
42056
+ if (text.length === 0) {
42057
+ issue(issues, "zero-length-text", "warn",
42058
+ "Zero-length text node",
42059
+ elementPath(node.parentNode, editable));
42060
+ return;
42061
+ }
42062
+ if (CONTROL_CHARS.test(text)) {
42063
+ issue(issues, "control-characters", "error",
42064
+ "Text contains control characters - Word will reject an exported .docx",
42065
+ elementPath(node.parentNode, editable));
42066
+ }
42067
+ }
42068
+
42069
+ function analyzeElement(el, editable, issues) {
42070
+ var name = el.nodeName;
42071
+ var path = elementPath(el, editable);
42072
+
42073
+ // Sanitizer invariants. These are the shapes that got through before.
42074
+ var attrs = el.attributes;
42075
+ for (var i = 0; attrs && i < attrs.length; i++) {
42076
+ var attrName = attrs[i].name.toLowerCase();
42077
+ var attrValue = attrs[i].value || "";
42078
+ if (attrName.indexOf("on") === 0 && attrName.length > 2) {
42079
+ issue(issues, "event-handler-attribute", "error",
42080
+ "Inline event handler " + attrName + " present in document content", path);
42081
+ }
42082
+ if (attrName === "href" || attrName === "src" || attrName === "xlink:href") {
42083
+ // Strip control characters and whitespace BEFORE testing the scheme;
42084
+ // "java script:" is a live bypass otherwise.
42085
+ var scheme = attrValue.replace(/[\u0000-\u0020]/g, "").toLowerCase();
42086
+ if (scheme.indexOf("javascript:") === 0) {
42087
+ issue(issues, "unsafe-url-scheme", "error",
42088
+ "javascript: URL in " + attrName, path);
42089
+ } else if (scheme.indexOf("data:text/html") === 0 || scheme.indexOf("data:image/svg+xml") === 0) {
42090
+ issue(issues, "unsafe-url-scheme", "error",
42091
+ "Scriptable data: URL in " + attrName, path);
42092
+ }
42093
+ }
42094
+ }
42095
+ if (name === "IFRAME" && el.hasAttribute("srcdoc")) {
42096
+ issue(issues, "iframe-srcdoc", "error",
42097
+ "iframe[srcdoc] executes when the saved content is rendered downstream", path);
42098
+ }
42099
+
42100
+ // Structural invariants.
42101
+ if (INLINE_TAGS[name] && !hasMeaningfulContent(el)) {
42102
+ issue(issues, "empty-inline", "warn",
42103
+ "Empty inline <" + name.toLowerCase() + "> - a caret entering it cannot leave by typing", path);
42104
+ }
42105
+ if (INLINE_TAGS[name] && el.parentNode && el.parentNode.nodeName === name) {
42106
+ issue(issues, "nested-duplicate-inline", "warn",
42107
+ "<" + name.toLowerCase() + "> nested directly inside another <" + name.toLowerCase() + ">", path);
42108
+ }
42109
+ if (INLINE_TAGS[name]) {
42110
+ for (var c = 0; c < el.childNodes.length; c++) {
42111
+ var child = el.childNodes[c];
42112
+ if (child.nodeType === 1 && BLOCK_TAGS[child.nodeName]) {
42113
+ issue(issues, "block-inside-inline", "warn",
42114
+ "Block <" + child.nodeName.toLowerCase() + "> inside inline <" + name.toLowerCase() + ">", path);
42115
+ break;
42116
+ }
42117
+ }
42118
+ }
42119
+ if (name === "LI") {
42120
+ var parentName = el.parentNode ? el.parentNode.nodeName : "";
42121
+ if (parentName !== "UL" && parentName !== "OL") {
42122
+ issue(issues, "orphan-list-item", "warn",
42123
+ "<li> outside a <ul> or <ol>", path);
42124
+ }
42125
+ }
42126
+ if (name === "IMG") {
42127
+ checkImageAlt(el, editable, issues);
42128
+ }
42129
+ // Atomic content - chips, footnote markers, merge fields - stops being
42130
+ // atomic the moment it loses contenteditable=false, and the caret then
42131
+ // edits its internals.
42132
+ if (el.hasAttribute && el.hasAttribute("data-rte-atomic") && el.getAttribute("contenteditable") !== "false") {
42133
+ issue(issues, "atomic-not-protected", "error",
42134
+ "Atomic element without contenteditable=\"false\"", path);
42135
+ }
42136
+ }
42137
+
42138
+ function checkImageAlt(el, editable, issues) {
42139
+ if (el.getAttribute("role") === "presentation" || el.getAttribute("aria-hidden") === "true") return;
42140
+ var alt = el.getAttribute("alt");
42141
+ if (alt === null) {
42142
+ issue(issues, "image-missing-alt", "warn",
42143
+ "Image has no alt attribute", elementPath(el, editable));
42144
+ }
42145
+ }
42146
+
42147
+ // -------------------------------------------------------------------- view
42148
+
42149
+ function injectStyles() {
42150
+ var hostDoc = config.container.ownerDocument;
42151
+ if (hostDoc.getElementById("rte-state-inspector-style")) return;
42152
+ var style = hostDoc.createElement("style");
42153
+ style.id = "rte-state-inspector-style";
42154
+ style.innerHTML = [
42155
+ ".rte-state-inspector-shell{display:flex;align-items:stretch;gap:10px;}",
42156
+ ".rte-state-inspector-shell>.rte-state-inspector-host{flex:1 1 auto;min-width:0;}",
42157
+ ".rte-state-inspector-panel{display:none;flex:0 0 340px;min-width:290px;max-width:min(400px,36vw);border:1px solid rgba(148,163,184,.22);border-radius:8px;background:#fff;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;}",
42158
+ ".rte-state-inspector-shell.is-open>.rte-state-inspector-panel{display:flex;flex-direction:column;}",
42159
+ ".rte-state-inspector-header{padding:12px 12px 9px 14px;border-bottom:1px solid rgba(148,163,184,.16);background:rgba(248,251,255,.72);}",
42160
+ ".rte-state-inspector-kicker{font-size:10px;line-height:1.3;letter-spacing:.08em;text-transform:uppercase;color:#52657e;font-weight:850;}",
42161
+ ".rte-state-inspector-title{margin-top:3px;font-size:15px;line-height:1.2;font-weight:850;color:#172033;}",
42162
+ ".rte-state-inspector-copy{margin-top:5px;font-size:12px;line-height:1.42;color:#52657e;}",
42163
+ ".rte-state-inspector-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px 8px 14px;border-bottom:1px solid rgba(148,163,184,.16);}",
42164
+ ".rte-state-inspector-summary{min-width:0;font-size:12px;font-weight:750;color:#52657e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
42165
+ ".rte-state-inspector-actions{display:flex;gap:6px;flex:0 0 auto;}",
42166
+ ".rte-state-inspector-btn{appearance:none;border:1px solid rgba(100,116,139,.18);background:#fff;color:#315277;cursor:pointer;font-size:12px;font-weight:750;padding:6px 9px;border-radius:7px;line-height:1;}",
42167
+ ".rte-state-inspector-btn:hover,.rte-state-inspector-btn:focus-visible{background:#eef4ff;color:#0f3f9f;border-color:rgba(37,99,235,.28);}",
42168
+ ".rte-state-inspector-tabs{display:flex;gap:2px;padding:8px 10px 0 10px;border-bottom:1px solid rgba(148,163,184,.16);}",
42169
+ ".rte-state-inspector-tab{appearance:none;border:1px solid transparent;border-bottom:none;background:transparent;color:#52657e;cursor:pointer;font-size:12px;font-weight:800;padding:7px 10px;border-radius:7px 7px 0 0;line-height:1;}",
42170
+ ".rte-state-inspector-tab:hover,.rte-state-inspector-tab:focus-visible{background:#f8fbff;color:#0f3f9f;}",
42171
+ ".rte-state-inspector-tab[aria-selected='true']{background:#eef4ff;border-color:rgba(37,99,235,.26);color:#0f3f9f;}",
42172
+ ".rte-state-inspector-body{padding:10px;overflow:auto;min-height:160px;max-height:540px;scrollbar-width:thin;font-size:12px;line-height:1.5;}",
42173
+ ".rte-state-inspector-issue{display:block;padding:8px 10px;margin-bottom:6px;border:1px solid rgba(148,163,184,.24);border-left-width:3px;border-radius:7px;background:#fff;}",
42174
+ ".rte-state-inspector-issue.is-error{border-left-color:#dc2626;background:#fef4f4;}",
42175
+ ".rte-state-inspector-issue.is-warn{border-left-color:#d97706;background:#fffaf1;}",
42176
+ ".rte-state-inspector-issue-msg{font-weight:750;color:#172033;}",
42177
+ ".rte-state-inspector-issue-meta{margin-top:3px;font-size:11px;color:#52657e;font-family:Consolas,'Cascadia Mono',monospace;word-break:break-all;}",
42178
+ ".rte-state-inspector-ok{padding:14px;color:#166534;font-weight:750;background:#f0fdf4;border:1px dashed rgba(22,101,52,.3);border-radius:8px;}",
42179
+ ".rte-state-inspector-empty{padding:14px;color:#52657e;font-weight:700;background:#fff;border:1px dashed rgba(148,163,184,.34);border-radius:8px;}",
42180
+ ".rte-state-inspector-pre{margin:0;font-family:Consolas,'Cascadia Mono',monospace;font-size:11px;line-height:1.55;color:#172033;white-space:pre;}",
42181
+ ".rte-state-inspector-row{display:flex;gap:8px;padding:4px 0;border-bottom:1px solid rgba(148,163,184,.14);}",
42182
+ ".rte-state-inspector-row-key{flex:0 0 116px;color:#52657e;font-weight:750;}",
42183
+ ".rte-state-inspector-row-val{flex:1 1 auto;min-width:0;font-family:Consolas,'Cascadia Mono',monospace;word-break:break-all;}",
42184
+ ".rte-state-inspector-row.is-flagged .rte-state-inspector-row-val{color:#b91c1c;font-weight:750;}",
42185
+ "@media (max-width: 1100px){.rte-state-inspector-shell{display:block;}.rte-state-inspector-panel{margin-top:12px;max-width:none;width:100%;}.rte-state-inspector-body{max-height:340px;}}"
42186
+ ].join("");
42187
+ hostDoc.head.appendChild(style);
42188
+ }
42189
+
42190
+ function ensureShell() {
42191
+ if (shell) return shell;
42192
+ var container = config.container;
42193
+ var hostDoc = container.ownerDocument;
42194
+ shell = hostDoc.createElement("div");
42195
+ shell.className = "rte-state-inspector-shell";
42196
+
42197
+ var host = hostDoc.createElement("div");
42198
+ host.className = "rte-state-inspector-host";
42199
+
42200
+ container.parentNode.insertBefore(shell, container);
42201
+ shell.appendChild(host);
42202
+ host.appendChild(container);
42203
+
42204
+ panel = hostDoc.createElement("aside");
42205
+ panel.className = "rte-state-inspector-panel";
42206
+ panel.setAttribute("role", "complementary");
42207
+ panel.setAttribute("aria-label", config.stateInspectorTitle);
42208
+
42209
+ var header = hostDoc.createElement("div");
42210
+ header.className = "rte-state-inspector-header";
42211
+ var kicker = hostDoc.createElement("div");
42212
+ kicker.className = "rte-state-inspector-kicker";
42213
+ kicker.innerText = "Developer";
42214
+ var title = hostDoc.createElement("div");
42215
+ title.className = "rte-state-inspector-title";
42216
+ title.innerText = config.stateInspectorTitle;
42217
+ var copy = hostDoc.createElement("div");
42218
+ copy.className = "rte-state-inspector-copy";
42219
+ copy.innerText = config.stateInspectorHint;
42220
+ header.appendChild(kicker);
42221
+ header.appendChild(title);
42222
+ header.appendChild(copy);
42223
+
42224
+ var toolbar = hostDoc.createElement("div");
42225
+ toolbar.className = "rte-state-inspector-toolbar";
42226
+ summaryEl = hostDoc.createElement("div");
42227
+ summaryEl.className = "rte-state-inspector-summary";
42228
+ summaryEl.setAttribute("data-rte-inspector-summary", "1");
42229
+ var actions = hostDoc.createElement("div");
42230
+ actions.className = "rte-state-inspector-actions";
42231
+ actions.appendChild(makeButton(hostDoc, "Copy", "Copy the state snapshot as JSON", copySnapshot));
42232
+ actions.appendChild(makeButton(hostDoc, "Hide", "Hide the state inspector", closePanel));
42233
+ toolbar.appendChild(summaryEl);
42234
+ toolbar.appendChild(actions);
42235
+
42236
+ tabsBar = hostDoc.createElement("div");
42237
+ tabsBar.className = "rte-state-inspector-tabs";
42238
+ tabsBar.setAttribute("role", "tablist");
42239
+ for (var i = 0; i < TABS.length; i++) {
42240
+ tabsBar.appendChild(makeTab(hostDoc, TABS[i]));
42241
+ }
42242
+
42243
+ body = hostDoc.createElement("div");
42244
+ body.className = "rte-state-inspector-body";
42245
+
42246
+ panel.appendChild(header);
42247
+ panel.appendChild(toolbar);
42248
+ panel.appendChild(tabsBar);
42249
+ panel.appendChild(body);
42250
+ shell.appendChild(panel);
42251
+ return shell;
42252
+ }
42253
+
42254
+ function makeButton(hostDoc, label, aria, handler) {
42255
+ var btn = hostDoc.createElement("button");
42256
+ btn.type = "button";
42257
+ btn.className = "rte-state-inspector-btn";
42258
+ btn.innerText = label;
42259
+ btn.setAttribute("aria-label", aria);
42260
+ btn.onclick = handler;
42261
+ return btn;
42262
+ }
42263
+
42264
+ function makeTab(hostDoc, tab) {
42265
+ var btn = hostDoc.createElement("button");
42266
+ btn.type = "button";
42267
+ btn.className = "rte-state-inspector-tab";
42268
+ btn.innerText = tab.label;
42269
+ btn.setAttribute("role", "tab");
42270
+ btn.setAttribute("data-rte-inspector-tab", tab.id);
42271
+ btn.setAttribute("aria-selected", tab.id === activeTab ? "true" : "false");
42272
+ btn.onclick = function () {
42273
+ activeTab = tab.id;
42274
+ syncTabs();
42275
+ render();
42276
+ };
42277
+ return btn;
42278
+ }
42279
+
42280
+ function syncTabs() {
42281
+ if (!tabsBar) return;
42282
+ var buttons = tabsBar.querySelectorAll("[data-rte-inspector-tab]");
42283
+ for (var i = 0; i < buttons.length; i++) {
42284
+ var id = buttons[i].getAttribute("data-rte-inspector-tab");
42285
+ buttons[i].setAttribute("aria-selected", id === activeTab ? "true" : "false");
42286
+ }
42287
+ }
42288
+
42289
+ function togglePanel() {
42290
+ if (shell && shell.classList.contains("is-open")) closePanel();
42291
+ else openPanel();
42292
+ }
42293
+
42294
+ function openPanel() {
42295
+ ensureShell();
42296
+ shell.classList.add("is-open");
42297
+ render();
42298
+ }
42299
+
42300
+ function closePanel() {
42301
+ if (!shell) return;
42302
+ shell.classList.remove("is-open");
42303
+ }
42304
+
42305
+ function scheduleRefresh() {
42306
+ if (!shell || !shell.classList.contains("is-open")) return;
42307
+ clearTimeout(refreshTimer);
42308
+ refreshTimer = setTimeout(function () { render(); }, 120);
42309
+ }
42310
+
42311
+ function copySnapshot() {
42312
+ var json = JSON.stringify(buildSnapshot(), null, 2);
42313
+ if (navigator.clipboard && navigator.clipboard.writeText) {
42314
+ navigator.clipboard.writeText(json);
42315
+ return;
42316
+ }
42317
+ var hostDoc = config.container.ownerDocument;
42318
+ var area = hostDoc.createElement("textarea");
42319
+ area.value = json;
42320
+ area.style.position = "fixed";
42321
+ area.style.opacity = "0";
42322
+ hostDoc.body.appendChild(area);
42323
+ area.select();
42324
+ try { hostDoc.execCommand("copy"); } catch (e) { /* clipboard unavailable */ }
42325
+ hostDoc.body.removeChild(area);
42326
+ }
42327
+
42328
+ function clear(el) {
42329
+ while (el.firstChild) el.removeChild(el.firstChild);
42330
+ }
42331
+
42332
+ function render() {
42333
+ if (!shell || !body) return;
42334
+ var tree = snapshotTree();
42335
+ var analysis = analyze(tree);
42336
+ var hostDoc = config.container.ownerDocument;
42337
+
42338
+ var errors = 0;
42339
+ for (var i = 0; i < analysis.issues.length; i++) {
42340
+ if (analysis.issues[i].severity === "error") errors++;
42341
+ }
42342
+ if (summaryEl) {
42343
+ // Kept short on purpose: the toolbar row ellipsises, and "3 errors" is
42344
+ // the part that must survive the truncation. Full counts are in the
42345
+ // snapshot and the tree tab.
42346
+ summaryEl.innerText = tree.counters.elements + " el · " +
42347
+ analysis.issues.length + " issue" + (analysis.issues.length === 1 ? "" : "s") +
42348
+ (errors ? " · " + errors + " error" + (errors === 1 ? "" : "s") : "");
42349
+ }
42350
+
42351
+ clear(body);
42352
+ if (activeTab === "issues") renderIssues(hostDoc, analysis.issues);
42353
+ else if (activeTab === "tree") renderTree(hostDoc, tree);
42354
+ else if (activeTab === "selection") renderSelection(hostDoc);
42355
+ else if (activeTab === "plugins") renderPlugins(hostDoc);
42356
+ else if (activeTab === "log") renderLog(hostDoc);
42357
+ }
42358
+
42359
+ function renderIssues(hostDoc, issues) {
42360
+ if (!issues.length) {
42361
+ var ok = hostDoc.createElement("div");
42362
+ ok.className = "rte-state-inspector-ok";
42363
+ ok.innerText = "No structural issues found.";
42364
+ body.appendChild(ok);
42365
+ return;
42366
+ }
42367
+ for (var i = 0; i < issues.length; i++) {
42368
+ var item = issues[i];
42369
+ var wrap = hostDoc.createElement("div");
42370
+ wrap.className = "rte-state-inspector-issue is-" + (item.severity === "error" ? "error" : "warn");
42371
+ wrap.setAttribute("data-rte-issue-id", item.id);
42372
+ var msg = hostDoc.createElement("div");
42373
+ msg.className = "rte-state-inspector-issue-msg";
42374
+ msg.innerText = item.message;
42375
+ var meta = hostDoc.createElement("div");
42376
+ meta.className = "rte-state-inspector-issue-meta";
42377
+ meta.innerText = (item.detail || "(document root)") + " · " + item.id;
42378
+ wrap.appendChild(msg);
42379
+ wrap.appendChild(meta);
42380
+ body.appendChild(wrap);
42381
+ }
42382
+ }
42383
+
42384
+ function renderTree(hostDoc, tree) {
42385
+ if (!tree.root || !tree.root.children.length) {
42386
+ var empty = hostDoc.createElement("div");
42387
+ empty.className = "rte-state-inspector-empty";
42388
+ empty.innerText = config.stateInspectorEmptyText;
42389
+ body.appendChild(empty);
42390
+ return;
42391
+ }
42392
+ var pre = hostDoc.createElement("pre");
42393
+ pre.className = "rte-state-inspector-pre";
42394
+ pre.innerText = formatTree(tree.root, 0).join("\n");
42395
+ body.appendChild(pre);
42396
+ }
42397
+
42398
+ function formatTree(node, depth) {
42399
+ var lines = [];
42400
+ var pad = new Array(depth + 1).join(" ");
42401
+ if (node.type === "text") {
42402
+ lines.push(pad + "#text(" + node.length + ") " + JSON.stringify(node.text));
42403
+ return lines;
42404
+ }
42405
+ if (node.type === "comment") {
42406
+ lines.push(pad + "#comment(" + node.length + ")");
42407
+ return lines;
42408
+ }
42409
+ var label = pad + "<" + node.name;
42410
+ var keys = [];
42411
+ for (var k in node.attrs) keys.push(k);
42412
+ if (keys.length) {
42413
+ keys.sort();
42414
+ for (var i = 0; i < keys.length && i < 4; i++) {
42415
+ label += " " + keys[i] + "=" + JSON.stringify(node.attrs[keys[i]]);
42416
+ }
42417
+ if (keys.length > 4) label += " …+" + (keys.length - 4);
42418
+ }
42419
+ label += ">";
42420
+ if (node.truncated) label += " … " + node.childCount + " children (depth limit)";
42421
+ lines.push(label);
42422
+ for (var c = 0; c < (node.children || []).length; c++) {
42423
+ lines = lines.concat(formatTree(node.children[c], depth + 1));
42424
+ }
42425
+ return lines;
42426
+ }
42427
+
42428
+ function renderSelection(hostDoc) {
42429
+ var info = describeSelection();
42430
+ if (!info.available) {
42431
+ var empty = hostDoc.createElement("div");
42432
+ empty.className = "rte-state-inspector-empty";
42433
+ empty.innerText = "No selection in the document.";
42434
+ body.appendChild(empty);
42435
+ return;
42436
+ }
42437
+ addRow(hostDoc, "Collapsed", String(info.collapsed));
42438
+ addRow(hostDoc, "In editable", String(info.insideEditable), !info.insideEditable);
42439
+ addRow(hostDoc, "Block path", info.block || "(root)");
42440
+ addRow(hostDoc, "Anchor", formatBoundary(info.anchor), !!(info.anchor && info.anchor.zeroLength));
42441
+ addRow(hostDoc, "Focus", formatBoundary(info.focus), !!(info.focus && info.focus.zeroLength));
42442
+ addRow(hostDoc, "Selected", info.textLength + " chars");
42443
+ if (info.text) addRow(hostDoc, "Text", info.text);
42444
+ }
42445
+
42446
+ function formatBoundary(boundary) {
42447
+ if (!boundary) return "(none)";
42448
+ var out = boundary.node + " @ " + boundary.offset;
42449
+ if (typeof boundary.length === "number") out += " (len " + boundary.length + ")";
42450
+ if (typeof boundary.childCount === "number") out += " (" + boundary.childCount + " children)";
42451
+ if (boundary.zeroLength) out += " ← zero-length text node";
42452
+ return out;
42453
+ }
42454
+
42455
+ function addRow(hostDoc, key, value, flagged) {
42456
+ var row = hostDoc.createElement("div");
42457
+ row.className = "rte-state-inspector-row" + (flagged ? " is-flagged" : "");
42458
+ var k = hostDoc.createElement("div");
42459
+ k.className = "rte-state-inspector-row-key";
42460
+ k.innerText = key;
42461
+ var v = hostDoc.createElement("div");
42462
+ v.className = "rte-state-inspector-row-val";
42463
+ v.innerText = value;
42464
+ row.appendChild(k);
42465
+ row.appendChild(v);
42466
+ body.appendChild(row);
42467
+ }
42468
+
42469
+ function renderPlugins(hostDoc) {
42470
+ if (!pluginOrder.length) {
42471
+ var empty = hostDoc.createElement("div");
42472
+ empty.className = "rte-state-inspector-empty";
42473
+ empty.innerText = "No plugins registered.";
42474
+ body.appendChild(empty);
42475
+ return;
42476
+ }
42477
+ var note = hostDoc.createElement("div");
42478
+ note.className = "rte-state-inspector-copy";
42479
+ note.innerText = "Initialization order. A plugin can only register into another plugin's API if it appears below it.";
42480
+ body.appendChild(note);
42481
+ for (var i = 0; i < pluginOrder.length; i++) {
42482
+ addRow(hostDoc, String(i + 1).padStart ? String(i + 1).padStart(3, "0") : String(i + 1), pluginOrder[i].name);
42483
+ }
42484
+ }
42485
+
42486
+ function renderLog(hostDoc) {
42487
+ if (!eventLog.length) {
42488
+ var empty = hostDoc.createElement("div");
42489
+ empty.className = "rte-state-inspector-empty";
42490
+ empty.innerText = "No editor events recorded yet.";
42491
+ body.appendChild(empty);
42492
+ return;
42493
+ }
42494
+ var start = eventLog[0].at;
42495
+ for (var i = eventLog.length - 1; i >= 0; i--) {
42496
+ addRow(hostDoc, "+" + (eventLog[i].at - start) + "ms", eventLog[i].event);
42497
+ }
42498
+ }
42499
+ }
42500
+
42501
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
42502
+
40940
42503
  // 2026-07-27 Table of contents. An insertable, self-updating contents block
40941
42504
  // built from the document's headings, with links that jump to them.
40942
42505
  //