@richscripts/richtexteditor 2.7.2 → 2.9.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.
- package/package.json +1 -1
- package/richtexteditor/all_plugins.js +2583 -719
- package/richtexteditor/all_plugins.min.js +59 -58
- package/richtexteditor/plugins/accessibilitychecker.js +7 -0
- package/richtexteditor/plugins/all_plugins.js +2583 -719
- package/richtexteditor/plugins/all_plugins.min.js +59 -58
- package/richtexteditor/plugins/autoembed.js +7 -0
- package/richtexteditor/plugins/blocktypes.js +7 -0
- package/richtexteditor/plugins/bookmarkcard.js +7 -0
- package/richtexteditor/plugins/charlimit.js +7 -0
- package/richtexteditor/plugins/comments.js +14 -0
- package/richtexteditor/plugins/contentminimap.js +7 -0
- package/richtexteditor/plugins/crossreference.js +7 -0
- package/richtexteditor/plugins/dictation.js +7 -0
- package/richtexteditor/plugins/documentoutline.js +9 -3
- package/richtexteditor/plugins/draghandle.js +7 -0
- package/richtexteditor/plugins/foldheadings.js +7 -0
- package/richtexteditor/plugins/footnotes.js +7 -0
- package/richtexteditor/plugins/formatpainter.js +7 -0
- package/richtexteditor/plugins/formattingmarks.js +7 -0
- package/richtexteditor/plugins/gapcursor.js +7 -0
- package/richtexteditor/plugins/ghostcomplete.js +7 -0
- package/richtexteditor/plugins/layouttable.js +7 -0
- package/richtexteditor/plugins/linenumbers.js +7 -0
- package/richtexteditor/plugins/linkchecker.js +7 -0
- package/richtexteditor/plugins/mentionplugin.js +14 -0
- package/richtexteditor/plugins/mergefields.js +7 -0
- package/richtexteditor/plugins/mermaiddiagram.js +7 -0
- package/richtexteditor/plugins/multilevellist.js +7 -0
- package/richtexteditor/plugins/nodetypes.js +439 -0
- package/richtexteditor/plugins/pagination.js +7 -0
- package/richtexteditor/plugins/restrictedediting.js +7 -0
- package/richtexteditor/plugins/revisionhistory.js +7 -0
- package/richtexteditor/plugins/rtlui.js +7 -0
- package/richtexteditor/plugins/rubyannotation.js +255 -0
- package/richtexteditor/plugins/slashcommand.js +7 -0
- package/richtexteditor/plugins/smartchips.js +7 -0
- package/richtexteditor/plugins/spellcheck.js +7 -0
- package/richtexteditor/plugins/stateinspector.js +866 -0
- package/richtexteditor/plugins/tableofcontents.js +7 -0
- package/richtexteditor/plugins/textdirection.js +7 -0
- package/richtexteditor/plugins/textpartlanguage.js +7 -0
- package/richtexteditor/plugins/todolist.js +7 -0
- package/richtexteditor/plugins/trackedchanges.js +730 -716
- package/richtexteditor/plugins/typewriter.js +7 -0
- package/richtexteditor/plugins/watermark.js +15 -0
- package/richtexteditor/plugins/yjscollab.js +7 -0
- package/richtexteditor/rte.js +2 -2
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
RTE_DefaultConfig.plugin_stateinspector = RTE_Plugin_StateInspector;
|
|
4
|
+
|
|
5
|
+
// 2026-09-04 Document state inspector (developer tool).
|
|
6
|
+
//
|
|
7
|
+
// The editor's source of truth is the contenteditable DOM, not a separate
|
|
8
|
+
// document model, so there is no state object to print. This plugin derives
|
|
9
|
+
// one: it walks the editable subtree and produces a normalized, serializable
|
|
10
|
+
// snapshot (tree + selection + plugin init order), then runs a rule set over
|
|
11
|
+
// that snapshot looking for the structural faults that have actually caused
|
|
12
|
+
// defects in this product - zero-length text nodes, empty inline wrappers,
|
|
13
|
+
// duplicate nested formatting, control characters, sanitizer bypasses, broken
|
|
14
|
+
// atomic chips, invalid list and heading structure.
|
|
15
|
+
//
|
|
16
|
+
// The snapshot is the point. `editor.stateInspector.snapshot()` returns plain
|
|
17
|
+
// JSON, so a support case can carry the document's real structure instead of a
|
|
18
|
+
// screenshot, and a test can assert on structure instead of on an HTML string.
|
|
19
|
+
//
|
|
20
|
+
// It is deliberately NOT in any default toolbar: set stateInspectorToolbarButton
|
|
21
|
+
// to true to add it, or drive it from the API.
|
|
22
|
+
function RTE_Plugin_StateInspector() {
|
|
23
|
+
var obj = this;
|
|
24
|
+
var config;
|
|
25
|
+
var editor;
|
|
26
|
+
var shell = null;
|
|
27
|
+
var panel = null;
|
|
28
|
+
var body = null;
|
|
29
|
+
var tabsBar = null;
|
|
30
|
+
var summaryEl = null;
|
|
31
|
+
var refreshTimer = 0;
|
|
32
|
+
var activeTab = "issues";
|
|
33
|
+
var pluginOrder = [];
|
|
34
|
+
var eventLog = [];
|
|
35
|
+
var logLimit = 60;
|
|
36
|
+
|
|
37
|
+
var TABS = [
|
|
38
|
+
{ id: "issues", label: "Issues" },
|
|
39
|
+
{ id: "tree", label: "Tree" },
|
|
40
|
+
{ id: "selection", label: "Selection" },
|
|
41
|
+
{ id: "plugins", label: "Plugins" },
|
|
42
|
+
{ id: "log", label: "Log" }
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
var INLINE_TAGS = {
|
|
46
|
+
A: 1, ABBR: 1, B: 1, BDI: 1, BDO: 1, CITE: 1, CODE: 1, DEL: 1, DFN: 1,
|
|
47
|
+
EM: 1, I: 1, INS: 1, KBD: 1, MARK: 1, Q: 1, S: 1, SAMP: 1, SMALL: 1,
|
|
48
|
+
SPAN: 1, STRONG: 1, SUB: 1, SUP: 1, U: 1, VAR: 1, FONT: 1
|
|
49
|
+
};
|
|
50
|
+
var VOID_TAGS = {
|
|
51
|
+
AREA: 1, BASE: 1, BR: 1, COL: 1, EMBED: 1, HR: 1, IMG: 1, INPUT: 1,
|
|
52
|
+
LINK: 1, META: 1, PARAM: 1, SOURCE: 1, TRACK: 1, WBR: 1
|
|
53
|
+
};
|
|
54
|
+
var BLOCK_TAGS = {
|
|
55
|
+
ADDRESS: 1, ARTICLE: 1, ASIDE: 1, BLOCKQUOTE: 1, DIV: 1, DL: 1, DT: 1,
|
|
56
|
+
DD: 1, FIELDSET: 1, FIGCAPTION: 1, FIGURE: 1, FOOTER: 1, FORM: 1,
|
|
57
|
+
H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, HEADER: 1, HR: 1, LI: 1,
|
|
58
|
+
MAIN: 1, NAV: 1, OL: 1, P: 1, PRE: 1, SECTION: 1, TABLE: 1, UL: 1
|
|
59
|
+
};
|
|
60
|
+
// Control characters that make Word reject an entire .docx package and that
|
|
61
|
+
// silently truncate grep over a bundle. NUL through backspace, vertical tab,
|
|
62
|
+
// form feed, and shift-out through unit separator.
|
|
63
|
+
// Written as escapes, never as literal bytes: a literal control character
|
|
64
|
+
// in source makes grep treat the whole bundle as binary and silently
|
|
65
|
+
// truncate every search over it.
|
|
66
|
+
var CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/;
|
|
67
|
+
|
|
68
|
+
obj.PluginName = "StateInspector";
|
|
69
|
+
|
|
70
|
+
obj.InitConfig = function (argconfig) {
|
|
71
|
+
config = argconfig;
|
|
72
|
+
if (config.stateInspectorEnabled === false) return;
|
|
73
|
+
|
|
74
|
+
config.stateInspectorTitle = config.stateInspectorTitle || "State inspector";
|
|
75
|
+
config.stateInspectorHint = config.stateInspectorHint || "Derived document model, structural checks, and a copyable snapshot for bug reports.";
|
|
76
|
+
config.stateInspectorEmptyText = config.stateInspectorEmptyText || "No content to inspect yet.";
|
|
77
|
+
if (typeof config.stateInspectorAutoOpen !== "boolean") config.stateInspectorAutoOpen = false;
|
|
78
|
+
if (typeof config.stateInspectorToolbarButton !== "boolean") config.stateInspectorToolbarButton = false;
|
|
79
|
+
if (typeof config.stateInspectorMaxDepth !== "number") config.stateInspectorMaxDepth = 24;
|
|
80
|
+
if (typeof config.stateInspectorLogLimit === "number") logLimit = Math.max(10, Math.min(500, config.stateInspectorLogLimit));
|
|
81
|
+
|
|
82
|
+
// Captured here rather than in InitEditor because `for (var p in config)`
|
|
83
|
+
// reproduces exactly the order the core uses to instantiate plugins -
|
|
84
|
+
// which is bundle concatenation order, i.e. alphabetical by filename.
|
|
85
|
+
// A plugin that registers into another plugin's API only works if it
|
|
86
|
+
// sorts after it; that has silently broken registration before, so the
|
|
87
|
+
// order is worth being able to see.
|
|
88
|
+
pluginOrder = collectPluginOrder(config);
|
|
89
|
+
|
|
90
|
+
if (config.stateInspectorToolbarButton) {
|
|
91
|
+
// BOTH presets, deliberately. config.toolbar defaults to "default",
|
|
92
|
+
// which resolves to toolbar_default - so appending only to
|
|
93
|
+
// toolbar_full means a host that sets stateInspectorToolbarButton on
|
|
94
|
+
// a default toolbar gets no button at all. "It is in the config" and
|
|
95
|
+
// "a user can see it" are different claims. inlinecode shipped a
|
|
96
|
+
// whole release cycle invisible for exactly this reason.
|
|
97
|
+
//
|
|
98
|
+
// If you write a test for this, scope it to [role=toolbar]. Searching
|
|
99
|
+
// the whole document for the button's label matches the inspector
|
|
100
|
+
// PANEL's aria-label - the panel is built at InitEditor and merely
|
|
101
|
+
// hidden - so a page-wide text search reports the button present in
|
|
102
|
+
// every case, including the two where no button exists. A check whose
|
|
103
|
+
// population is wrong reports confidently and does not look broken.
|
|
104
|
+
appendToolbarCommand("toolbar_default", "#{stateinspector}");
|
|
105
|
+
appendToolbarCommand("toolbar_full", "#{stateinspector}");
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
obj.InitEditor = function (argeditor) {
|
|
110
|
+
editor = argeditor;
|
|
111
|
+
if (config.stateInspectorEnabled === false) return;
|
|
112
|
+
|
|
113
|
+
editor.stateInspector = {
|
|
114
|
+
close: function () { closePanel(); },
|
|
115
|
+
isOpen: function () { return !!(shell && shell.classList.contains("is-open")); },
|
|
116
|
+
issues: function () { return analyze(snapshotTree()).issues; },
|
|
117
|
+
log: function () { return eventLog.slice(0); },
|
|
118
|
+
open: function () { openPanel(); },
|
|
119
|
+
plugins: function () { return pluginOrder.slice(0); },
|
|
120
|
+
refresh: function () { render(); },
|
|
121
|
+
snapshot: function () { return buildSnapshot(); },
|
|
122
|
+
toggle: function () { togglePanel(); }
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
injectStyles();
|
|
126
|
+
|
|
127
|
+
editor.toolbarFactoryMap = editor.toolbarFactoryMap || {};
|
|
128
|
+
editor.toolbarFactoryMap["stateinspector"] = function (cmd) {
|
|
129
|
+
return editor.createToolbarButton(cmd);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
editor.attachEvent("exec_command_stateinspector", function (state) {
|
|
133
|
+
state.returnValue = true;
|
|
134
|
+
state.stopBubble = true;
|
|
135
|
+
togglePanel();
|
|
136
|
+
});
|
|
137
|
+
editor.attachEvent("change", function () {
|
|
138
|
+
recordEvent("change");
|
|
139
|
+
scheduleRefresh();
|
|
140
|
+
});
|
|
141
|
+
editor.attachEvent("selectionchange", function () {
|
|
142
|
+
recordEvent("selectionchange");
|
|
143
|
+
scheduleRefresh();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Safe to register here, unlike documentimport/pdfexport/readabilitystats:
|
|
147
|
+
// plugins initialise in alphabetical bundle order and "stateinspector"
|
|
148
|
+
// sorts AFTER "slashcommand", so editor.slashCommands already exists.
|
|
149
|
+
// Guarded anyway, because that ordering is a filename away from changing.
|
|
150
|
+
if (editor.slashCommands && editor.slashCommands.register) {
|
|
151
|
+
editor.slashCommands.register({
|
|
152
|
+
id: "stateinspector",
|
|
153
|
+
section: "Tools",
|
|
154
|
+
title: config.stateInspectorTitle,
|
|
155
|
+
description: "Inspect document structure, selection, and structural issues",
|
|
156
|
+
keywords: ["inspect", "debug", "devtools", "state", "structure"],
|
|
157
|
+
run: function () { openPanel(); }
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Build the wrapper NOW, not lazily on first open. ensureShell() reparents
|
|
162
|
+
// the editor container, and reparenting a populated editing surface makes
|
|
163
|
+
// Chrome reseat it - which blanks the visible content while the document
|
|
164
|
+
// is still there (the status bar keeps counting words nobody can see).
|
|
165
|
+
// documentoutline.js and contentminimap.js do the same thing for the same
|
|
166
|
+
// reason: at InitEditor the surface is still empty, so the reseat is free.
|
|
167
|
+
ensureShell();
|
|
168
|
+
|
|
169
|
+
if (config.stateInspectorAutoOpen) openPanel();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function appendToolbarCommand(toolbar, item) {
|
|
173
|
+
if (!config[toolbar]) return;
|
|
174
|
+
if (config[toolbar].indexOf(item) !== -1) return;
|
|
175
|
+
config[toolbar] = config[toolbar] + item;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function collectPluginOrder(cfg) {
|
|
179
|
+
var order = [];
|
|
180
|
+
for (var p in cfg) {
|
|
181
|
+
if (p[0] !== "p" || p.substr(0, 7) !== "plugin_") continue;
|
|
182
|
+
if (!cfg[p] || !(cfg[p] instanceof Function)) continue;
|
|
183
|
+
order.push({ index: order.length, key: p, name: p.substring(7) });
|
|
184
|
+
}
|
|
185
|
+
return order;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function recordEvent(name) {
|
|
189
|
+
eventLog.push({ at: Date.now(), event: name });
|
|
190
|
+
if (eventLog.length > logLimit) eventLog.splice(0, eventLog.length - logLimit);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------- snapshot
|
|
194
|
+
|
|
195
|
+
function describeAttributes(el) {
|
|
196
|
+
var attrs = {};
|
|
197
|
+
var list = el.attributes;
|
|
198
|
+
if (!list) return attrs;
|
|
199
|
+
for (var i = 0; i < list.length; i++) {
|
|
200
|
+
var value = list[i].value;
|
|
201
|
+
attrs[list[i].name] = value.length > 220 ? value.substring(0, 220) + "…" : value;
|
|
202
|
+
}
|
|
203
|
+
return attrs;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function walk(node, depth, maxDepth, counters) {
|
|
207
|
+
if (node.nodeType === 3) {
|
|
208
|
+
var text = node.nodeValue || "";
|
|
209
|
+
counters.textNodes++;
|
|
210
|
+
counters.characters += text.length;
|
|
211
|
+
return {
|
|
212
|
+
type: "text",
|
|
213
|
+
length: text.length,
|
|
214
|
+
text: text.length > 80 ? text.substring(0, 80) + "…" : text
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (node.nodeType === 8) {
|
|
218
|
+
counters.comments++;
|
|
219
|
+
return { type: "comment", length: (node.nodeValue || "").length };
|
|
220
|
+
}
|
|
221
|
+
if (node.nodeType !== 1) return null;
|
|
222
|
+
|
|
223
|
+
counters.elements++;
|
|
224
|
+
var name = node.nodeName;
|
|
225
|
+
counters.tags[name] = (counters.tags[name] || 0) + 1;
|
|
226
|
+
|
|
227
|
+
var entry = {
|
|
228
|
+
type: "element",
|
|
229
|
+
name: name.toLowerCase(),
|
|
230
|
+
attrs: describeAttributes(node),
|
|
231
|
+
children: []
|
|
232
|
+
};
|
|
233
|
+
if (depth >= maxDepth) {
|
|
234
|
+
entry.truncated = true;
|
|
235
|
+
entry.childCount = node.childNodes.length;
|
|
236
|
+
return entry;
|
|
237
|
+
}
|
|
238
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
239
|
+
var child = walk(node.childNodes[i], depth + 1, maxDepth, counters);
|
|
240
|
+
if (child) entry.children.push(child);
|
|
241
|
+
}
|
|
242
|
+
return entry;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function snapshotTree() {
|
|
246
|
+
var editable = editor && editor.getEditable ? editor.getEditable() : null;
|
|
247
|
+
var counters = { elements: 0, textNodes: 0, comments: 0, characters: 0, tags: {} };
|
|
248
|
+
if (!editable) {
|
|
249
|
+
return { root: null, counters: counters, editable: null };
|
|
250
|
+
}
|
|
251
|
+
var maxDepth = Math.max(2, config.stateInspectorMaxDepth || 24);
|
|
252
|
+
var root = {
|
|
253
|
+
type: "element",
|
|
254
|
+
name: "root",
|
|
255
|
+
attrs: {},
|
|
256
|
+
children: []
|
|
257
|
+
};
|
|
258
|
+
for (var i = 0; i < editable.childNodes.length; i++) {
|
|
259
|
+
var child = walk(editable.childNodes[i], 1, maxDepth, counters);
|
|
260
|
+
if (child) root.children.push(child);
|
|
261
|
+
}
|
|
262
|
+
return { root: root, counters: counters, editable: editable };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function describeSelection() {
|
|
266
|
+
var doc = editor && editor.getDocument ? editor.getDocument() : null;
|
|
267
|
+
var editable = editor && editor.getEditable ? editor.getEditable() : null;
|
|
268
|
+
var info = { available: false };
|
|
269
|
+
if (!doc || !doc.getSelection) return info;
|
|
270
|
+
|
|
271
|
+
var sel;
|
|
272
|
+
try { sel = doc.getSelection(); } catch (e) { return info; }
|
|
273
|
+
if (!sel || sel.rangeCount === 0) return info;
|
|
274
|
+
|
|
275
|
+
var range;
|
|
276
|
+
try { range = sel.getRangeAt(0); } catch (e) { return info; }
|
|
277
|
+
if (!range) return info;
|
|
278
|
+
|
|
279
|
+
info.available = true;
|
|
280
|
+
info.collapsed = !!range.collapsed;
|
|
281
|
+
info.anchor = describeBoundary(range.startContainer, range.startOffset);
|
|
282
|
+
info.focus = describeBoundary(range.endContainer, range.endOffset);
|
|
283
|
+
info.insideEditable = !!(editable && editable.contains(range.startContainer) && editable.contains(range.endContainer));
|
|
284
|
+
try {
|
|
285
|
+
var selected = String(range.toString() || "");
|
|
286
|
+
info.textLength = selected.length;
|
|
287
|
+
info.text = selected.length > 120 ? selected.substring(0, 120) + "…" : selected;
|
|
288
|
+
} catch (e) {
|
|
289
|
+
info.textLength = 0;
|
|
290
|
+
}
|
|
291
|
+
info.block = describeBlockPath(range.startContainer, editable);
|
|
292
|
+
return info;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function describeBoundary(container, offset) {
|
|
296
|
+
if (!container) return { node: "(none)", offset: offset };
|
|
297
|
+
if (container.nodeType === 3) {
|
|
298
|
+
var text = container.nodeValue || "";
|
|
299
|
+
return {
|
|
300
|
+
node: "#text",
|
|
301
|
+
offset: offset,
|
|
302
|
+
length: text.length,
|
|
303
|
+
// A caret sitting in a zero-length text node is the shape of a
|
|
304
|
+
// caret trap; surfacing it here is the whole point.
|
|
305
|
+
zeroLength: text.length === 0
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
node: container.nodeName ? container.nodeName.toLowerCase() : "(unknown)",
|
|
310
|
+
offset: offset,
|
|
311
|
+
childCount: container.childNodes ? container.childNodes.length : 0
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function describeBlockPath(node, editable) {
|
|
316
|
+
var path = [];
|
|
317
|
+
var current = node;
|
|
318
|
+
while (current && current !== editable) {
|
|
319
|
+
if (current.nodeType === 1) path.unshift(current.nodeName.toLowerCase());
|
|
320
|
+
current = current.parentNode;
|
|
321
|
+
}
|
|
322
|
+
return path.join(" > ");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function buildSnapshot() {
|
|
326
|
+
var tree = snapshotTree();
|
|
327
|
+
var analysis = analyze(tree);
|
|
328
|
+
return {
|
|
329
|
+
snapshotVersion: 1,
|
|
330
|
+
generatedAt: new Date().toISOString(),
|
|
331
|
+
counters: tree.counters,
|
|
332
|
+
issues: analysis.issues,
|
|
333
|
+
plugins: pluginOrder.slice(0),
|
|
334
|
+
recentEvents: eventLog.slice(-20),
|
|
335
|
+
selection: describeSelection(),
|
|
336
|
+
tree: tree.root
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ----------------------------------------------------------------- analysis
|
|
341
|
+
|
|
342
|
+
function issue(list, id, severity, message, detail) {
|
|
343
|
+
list.push({ id: id, severity: severity, message: message, detail: detail || "" });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function elementPath(el, editable) {
|
|
347
|
+
var path = [];
|
|
348
|
+
var current = el;
|
|
349
|
+
while (current && current !== editable && current.nodeType === 1) {
|
|
350
|
+
var step = current.nodeName.toLowerCase();
|
|
351
|
+
if (current.id) step += "#" + current.id;
|
|
352
|
+
path.unshift(step);
|
|
353
|
+
current = current.parentNode;
|
|
354
|
+
}
|
|
355
|
+
return path.join(" > ");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function hasMeaningfulContent(el) {
|
|
359
|
+
if ((el.textContent || "").replace(/[\s\u200B\uFEFF]/g, "") !== "") return true;
|
|
360
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
361
|
+
var child = el.childNodes[i];
|
|
362
|
+
if (child.nodeType === 1 && VOID_TAGS[child.nodeName]) return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function analyze(tree) {
|
|
368
|
+
var issues = [];
|
|
369
|
+
var editable = tree.editable;
|
|
370
|
+
if (!editable) return { issues: issues };
|
|
371
|
+
|
|
372
|
+
var walker = editable.ownerDocument.createTreeWalker(
|
|
373
|
+
editable,
|
|
374
|
+
// NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
|
|
375
|
+
1 | 4,
|
|
376
|
+
null,
|
|
377
|
+
false
|
|
378
|
+
);
|
|
379
|
+
var lastHeadingLevel = 0;
|
|
380
|
+
var node;
|
|
381
|
+
|
|
382
|
+
while ((node = walker.nextNode())) {
|
|
383
|
+
if (node.nodeType === 3) {
|
|
384
|
+
analyzeText(node, editable, issues);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
analyzeElement(node, editable, issues);
|
|
388
|
+
|
|
389
|
+
var level = headingLevel(node);
|
|
390
|
+
if (level) {
|
|
391
|
+
if (lastHeadingLevel && level > lastHeadingLevel + 1) {
|
|
392
|
+
issue(issues, "heading-skip", "warn",
|
|
393
|
+
"Heading level jumps from h" + lastHeadingLevel + " to h" + level,
|
|
394
|
+
elementPath(node, editable));
|
|
395
|
+
}
|
|
396
|
+
lastHeadingLevel = level;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Top-level images are children of the editable root, so a walker that
|
|
401
|
+
// starts below the root would miss them - which is exactly how the
|
|
402
|
+
// accessibility checker used to skip pasted images.
|
|
403
|
+
for (var i = 0; i < editable.childNodes.length; i++) {
|
|
404
|
+
var child = editable.childNodes[i];
|
|
405
|
+
if (child.nodeType === 1 && child.nodeName === "IMG") {
|
|
406
|
+
checkImageAlt(child, editable, issues);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { issues: issues };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function headingLevel(el) {
|
|
414
|
+
var name = el.nodeName;
|
|
415
|
+
if (name.length === 2 && name[0] === "H" && name[1] >= "1" && name[1] <= "6") {
|
|
416
|
+
return parseInt(name[1], 10);
|
|
417
|
+
}
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function analyzeText(node, editable, issues) {
|
|
422
|
+
var text = node.nodeValue || "";
|
|
423
|
+
if (text.length === 0) {
|
|
424
|
+
issue(issues, "zero-length-text", "warn",
|
|
425
|
+
"Zero-length text node",
|
|
426
|
+
elementPath(node.parentNode, editable));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (CONTROL_CHARS.test(text)) {
|
|
430
|
+
issue(issues, "control-characters", "error",
|
|
431
|
+
"Text contains control characters - Word will reject an exported .docx",
|
|
432
|
+
elementPath(node.parentNode, editable));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function analyzeElement(el, editable, issues) {
|
|
437
|
+
var name = el.nodeName;
|
|
438
|
+
var path = elementPath(el, editable);
|
|
439
|
+
|
|
440
|
+
// Sanitizer invariants. These are the shapes that got through before.
|
|
441
|
+
var attrs = el.attributes;
|
|
442
|
+
for (var i = 0; attrs && i < attrs.length; i++) {
|
|
443
|
+
var attrName = attrs[i].name.toLowerCase();
|
|
444
|
+
var attrValue = attrs[i].value || "";
|
|
445
|
+
if (attrName.indexOf("on") === 0 && attrName.length > 2) {
|
|
446
|
+
issue(issues, "event-handler-attribute", "error",
|
|
447
|
+
"Inline event handler " + attrName + " present in document content", path);
|
|
448
|
+
}
|
|
449
|
+
if (attrName === "href" || attrName === "src" || attrName === "xlink:href") {
|
|
450
|
+
// Strip control characters and whitespace BEFORE testing the scheme;
|
|
451
|
+
// "java script:" is a live bypass otherwise.
|
|
452
|
+
var scheme = attrValue.replace(/[\u0000-\u0020]/g, "").toLowerCase();
|
|
453
|
+
if (scheme.indexOf("javascript:") === 0) {
|
|
454
|
+
issue(issues, "unsafe-url-scheme", "error",
|
|
455
|
+
"javascript: URL in " + attrName, path);
|
|
456
|
+
} else if (scheme.indexOf("data:text/html") === 0 || scheme.indexOf("data:image/svg+xml") === 0) {
|
|
457
|
+
issue(issues, "unsafe-url-scheme", "error",
|
|
458
|
+
"Scriptable data: URL in " + attrName, path);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (name === "IFRAME" && el.hasAttribute("srcdoc")) {
|
|
463
|
+
issue(issues, "iframe-srcdoc", "error",
|
|
464
|
+
"iframe[srcdoc] executes when the saved content is rendered downstream", path);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Structural invariants.
|
|
468
|
+
if (INLINE_TAGS[name] && !hasMeaningfulContent(el)) {
|
|
469
|
+
issue(issues, "empty-inline", "warn",
|
|
470
|
+
"Empty inline <" + name.toLowerCase() + "> - a caret entering it cannot leave by typing", path);
|
|
471
|
+
}
|
|
472
|
+
if (INLINE_TAGS[name] && el.parentNode && el.parentNode.nodeName === name) {
|
|
473
|
+
issue(issues, "nested-duplicate-inline", "warn",
|
|
474
|
+
"<" + name.toLowerCase() + "> nested directly inside another <" + name.toLowerCase() + ">", path);
|
|
475
|
+
}
|
|
476
|
+
if (INLINE_TAGS[name]) {
|
|
477
|
+
for (var c = 0; c < el.childNodes.length; c++) {
|
|
478
|
+
var child = el.childNodes[c];
|
|
479
|
+
if (child.nodeType === 1 && BLOCK_TAGS[child.nodeName]) {
|
|
480
|
+
issue(issues, "block-inside-inline", "warn",
|
|
481
|
+
"Block <" + child.nodeName.toLowerCase() + "> inside inline <" + name.toLowerCase() + ">", path);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (name === "LI") {
|
|
487
|
+
var parentName = el.parentNode ? el.parentNode.nodeName : "";
|
|
488
|
+
if (parentName !== "UL" && parentName !== "OL") {
|
|
489
|
+
issue(issues, "orphan-list-item", "warn",
|
|
490
|
+
"<li> outside a <ul> or <ol>", path);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (name === "IMG") {
|
|
494
|
+
checkImageAlt(el, editable, issues);
|
|
495
|
+
}
|
|
496
|
+
// Atomic content - chips, footnote markers, merge fields - stops being
|
|
497
|
+
// atomic the moment it loses contenteditable=false, and the caret then
|
|
498
|
+
// edits its internals.
|
|
499
|
+
if (el.hasAttribute && el.hasAttribute("data-rte-atomic") && el.getAttribute("contenteditable") !== "false") {
|
|
500
|
+
issue(issues, "atomic-not-protected", "error",
|
|
501
|
+
"Atomic element without contenteditable=\"false\"", path);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function checkImageAlt(el, editable, issues) {
|
|
506
|
+
if (el.getAttribute("role") === "presentation" || el.getAttribute("aria-hidden") === "true") return;
|
|
507
|
+
var alt = el.getAttribute("alt");
|
|
508
|
+
if (alt === null) {
|
|
509
|
+
issue(issues, "image-missing-alt", "warn",
|
|
510
|
+
"Image has no alt attribute", elementPath(el, editable));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// -------------------------------------------------------------------- view
|
|
515
|
+
|
|
516
|
+
function injectStyles() {
|
|
517
|
+
var hostDoc = config.container.ownerDocument;
|
|
518
|
+
if (hostDoc.getElementById("rte-state-inspector-style")) return;
|
|
519
|
+
var style = hostDoc.createElement("style");
|
|
520
|
+
style.id = "rte-state-inspector-style";
|
|
521
|
+
style.innerHTML = [
|
|
522
|
+
".rte-state-inspector-shell{display:flex;align-items:stretch;gap:10px;}",
|
|
523
|
+
".rte-state-inspector-shell>.rte-state-inspector-host{flex:1 1 auto;min-width:0;}",
|
|
524
|
+
".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;}",
|
|
525
|
+
".rte-state-inspector-shell.is-open>.rte-state-inspector-panel{display:flex;flex-direction:column;}",
|
|
526
|
+
".rte-state-inspector-header{padding:12px 12px 9px 14px;border-bottom:1px solid rgba(148,163,184,.16);background:rgba(248,251,255,.72);}",
|
|
527
|
+
".rte-state-inspector-kicker{font-size:10px;line-height:1.3;letter-spacing:.08em;text-transform:uppercase;color:#52657e;font-weight:850;}",
|
|
528
|
+
".rte-state-inspector-title{margin-top:3px;font-size:15px;line-height:1.2;font-weight:850;color:#172033;}",
|
|
529
|
+
".rte-state-inspector-copy{margin-top:5px;font-size:12px;line-height:1.42;color:#52657e;}",
|
|
530
|
+
".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);}",
|
|
531
|
+
".rte-state-inspector-summary{min-width:0;font-size:12px;font-weight:750;color:#52657e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
|
|
532
|
+
".rte-state-inspector-actions{display:flex;gap:6px;flex:0 0 auto;}",
|
|
533
|
+
".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;}",
|
|
534
|
+
".rte-state-inspector-btn:hover,.rte-state-inspector-btn:focus-visible{background:#eef4ff;color:#0f3f9f;border-color:rgba(37,99,235,.28);}",
|
|
535
|
+
".rte-state-inspector-tabs{display:flex;gap:2px;padding:8px 10px 0 10px;border-bottom:1px solid rgba(148,163,184,.16);}",
|
|
536
|
+
".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;}",
|
|
537
|
+
".rte-state-inspector-tab:hover,.rte-state-inspector-tab:focus-visible{background:#f8fbff;color:#0f3f9f;}",
|
|
538
|
+
".rte-state-inspector-tab[aria-selected='true']{background:#eef4ff;border-color:rgba(37,99,235,.26);color:#0f3f9f;}",
|
|
539
|
+
".rte-state-inspector-body{padding:10px;overflow:auto;min-height:160px;max-height:540px;scrollbar-width:thin;font-size:12px;line-height:1.5;}",
|
|
540
|
+
".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;}",
|
|
541
|
+
".rte-state-inspector-issue.is-error{border-left-color:#dc2626;background:#fef4f4;}",
|
|
542
|
+
".rte-state-inspector-issue.is-warn{border-left-color:#d97706;background:#fffaf1;}",
|
|
543
|
+
".rte-state-inspector-issue-msg{font-weight:750;color:#172033;}",
|
|
544
|
+
".rte-state-inspector-issue-meta{margin-top:3px;font-size:11px;color:#52657e;font-family:Consolas,'Cascadia Mono',monospace;word-break:break-all;}",
|
|
545
|
+
".rte-state-inspector-ok{padding:14px;color:#166534;font-weight:750;background:#f0fdf4;border:1px dashed rgba(22,101,52,.3);border-radius:8px;}",
|
|
546
|
+
".rte-state-inspector-empty{padding:14px;color:#52657e;font-weight:700;background:#fff;border:1px dashed rgba(148,163,184,.34);border-radius:8px;}",
|
|
547
|
+
".rte-state-inspector-pre{margin:0;font-family:Consolas,'Cascadia Mono',monospace;font-size:11px;line-height:1.55;color:#172033;white-space:pre;}",
|
|
548
|
+
".rte-state-inspector-row{display:flex;gap:8px;padding:4px 0;border-bottom:1px solid rgba(148,163,184,.14);}",
|
|
549
|
+
".rte-state-inspector-row-key{flex:0 0 116px;color:#52657e;font-weight:750;}",
|
|
550
|
+
".rte-state-inspector-row-val{flex:1 1 auto;min-width:0;font-family:Consolas,'Cascadia Mono',monospace;word-break:break-all;}",
|
|
551
|
+
".rte-state-inspector-row.is-flagged .rte-state-inspector-row-val{color:#b91c1c;font-weight:750;}",
|
|
552
|
+
"@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;}}"
|
|
553
|
+
].join("");
|
|
554
|
+
hostDoc.head.appendChild(style);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function ensureShell() {
|
|
558
|
+
if (shell) return shell;
|
|
559
|
+
var container = config.container;
|
|
560
|
+
var hostDoc = container.ownerDocument;
|
|
561
|
+
shell = hostDoc.createElement("div");
|
|
562
|
+
shell.className = "rte-state-inspector-shell";
|
|
563
|
+
|
|
564
|
+
var host = hostDoc.createElement("div");
|
|
565
|
+
host.className = "rte-state-inspector-host";
|
|
566
|
+
|
|
567
|
+
container.parentNode.insertBefore(shell, container);
|
|
568
|
+
shell.appendChild(host);
|
|
569
|
+
host.appendChild(container);
|
|
570
|
+
|
|
571
|
+
panel = hostDoc.createElement("aside");
|
|
572
|
+
panel.className = "rte-state-inspector-panel";
|
|
573
|
+
panel.setAttribute("role", "complementary");
|
|
574
|
+
panel.setAttribute("aria-label", config.stateInspectorTitle);
|
|
575
|
+
|
|
576
|
+
var header = hostDoc.createElement("div");
|
|
577
|
+
header.className = "rte-state-inspector-header";
|
|
578
|
+
var kicker = hostDoc.createElement("div");
|
|
579
|
+
kicker.className = "rte-state-inspector-kicker";
|
|
580
|
+
kicker.innerText = "Developer";
|
|
581
|
+
var title = hostDoc.createElement("div");
|
|
582
|
+
title.className = "rte-state-inspector-title";
|
|
583
|
+
title.innerText = config.stateInspectorTitle;
|
|
584
|
+
var copy = hostDoc.createElement("div");
|
|
585
|
+
copy.className = "rte-state-inspector-copy";
|
|
586
|
+
copy.innerText = config.stateInspectorHint;
|
|
587
|
+
header.appendChild(kicker);
|
|
588
|
+
header.appendChild(title);
|
|
589
|
+
header.appendChild(copy);
|
|
590
|
+
|
|
591
|
+
var toolbar = hostDoc.createElement("div");
|
|
592
|
+
toolbar.className = "rte-state-inspector-toolbar";
|
|
593
|
+
summaryEl = hostDoc.createElement("div");
|
|
594
|
+
summaryEl.className = "rte-state-inspector-summary";
|
|
595
|
+
summaryEl.setAttribute("data-rte-inspector-summary", "1");
|
|
596
|
+
var actions = hostDoc.createElement("div");
|
|
597
|
+
actions.className = "rte-state-inspector-actions";
|
|
598
|
+
actions.appendChild(makeButton(hostDoc, "Copy", "Copy the state snapshot as JSON", copySnapshot));
|
|
599
|
+
actions.appendChild(makeButton(hostDoc, "Hide", "Hide the state inspector", closePanel));
|
|
600
|
+
toolbar.appendChild(summaryEl);
|
|
601
|
+
toolbar.appendChild(actions);
|
|
602
|
+
|
|
603
|
+
tabsBar = hostDoc.createElement("div");
|
|
604
|
+
tabsBar.className = "rte-state-inspector-tabs";
|
|
605
|
+
tabsBar.setAttribute("role", "tablist");
|
|
606
|
+
for (var i = 0; i < TABS.length; i++) {
|
|
607
|
+
tabsBar.appendChild(makeTab(hostDoc, TABS[i]));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
body = hostDoc.createElement("div");
|
|
611
|
+
body.className = "rte-state-inspector-body";
|
|
612
|
+
|
|
613
|
+
panel.appendChild(header);
|
|
614
|
+
panel.appendChild(toolbar);
|
|
615
|
+
panel.appendChild(tabsBar);
|
|
616
|
+
panel.appendChild(body);
|
|
617
|
+
shell.appendChild(panel);
|
|
618
|
+
return shell;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function makeButton(hostDoc, label, aria, handler) {
|
|
622
|
+
var btn = hostDoc.createElement("button");
|
|
623
|
+
btn.type = "button";
|
|
624
|
+
btn.className = "rte-state-inspector-btn";
|
|
625
|
+
btn.innerText = label;
|
|
626
|
+
btn.setAttribute("aria-label", aria);
|
|
627
|
+
btn.onclick = handler;
|
|
628
|
+
return btn;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function makeTab(hostDoc, tab) {
|
|
632
|
+
var btn = hostDoc.createElement("button");
|
|
633
|
+
btn.type = "button";
|
|
634
|
+
btn.className = "rte-state-inspector-tab";
|
|
635
|
+
btn.innerText = tab.label;
|
|
636
|
+
btn.setAttribute("role", "tab");
|
|
637
|
+
btn.setAttribute("data-rte-inspector-tab", tab.id);
|
|
638
|
+
btn.setAttribute("aria-selected", tab.id === activeTab ? "true" : "false");
|
|
639
|
+
btn.onclick = function () {
|
|
640
|
+
activeTab = tab.id;
|
|
641
|
+
syncTabs();
|
|
642
|
+
render();
|
|
643
|
+
};
|
|
644
|
+
return btn;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function syncTabs() {
|
|
648
|
+
if (!tabsBar) return;
|
|
649
|
+
var buttons = tabsBar.querySelectorAll("[data-rte-inspector-tab]");
|
|
650
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
651
|
+
var id = buttons[i].getAttribute("data-rte-inspector-tab");
|
|
652
|
+
buttons[i].setAttribute("aria-selected", id === activeTab ? "true" : "false");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function togglePanel() {
|
|
657
|
+
if (shell && shell.classList.contains("is-open")) closePanel();
|
|
658
|
+
else openPanel();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function openPanel() {
|
|
662
|
+
ensureShell();
|
|
663
|
+
shell.classList.add("is-open");
|
|
664
|
+
render();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function closePanel() {
|
|
668
|
+
if (!shell) return;
|
|
669
|
+
shell.classList.remove("is-open");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function scheduleRefresh() {
|
|
673
|
+
if (!shell || !shell.classList.contains("is-open")) return;
|
|
674
|
+
clearTimeout(refreshTimer);
|
|
675
|
+
refreshTimer = setTimeout(function () { render(); }, 120);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function copySnapshot() {
|
|
679
|
+
var json = JSON.stringify(buildSnapshot(), null, 2);
|
|
680
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
681
|
+
navigator.clipboard.writeText(json);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
var hostDoc = config.container.ownerDocument;
|
|
685
|
+
var area = hostDoc.createElement("textarea");
|
|
686
|
+
area.value = json;
|
|
687
|
+
area.style.position = "fixed";
|
|
688
|
+
area.style.opacity = "0";
|
|
689
|
+
hostDoc.body.appendChild(area);
|
|
690
|
+
area.select();
|
|
691
|
+
try { hostDoc.execCommand("copy"); } catch (e) { /* clipboard unavailable */ }
|
|
692
|
+
hostDoc.body.removeChild(area);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function clear(el) {
|
|
696
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function render() {
|
|
700
|
+
if (!shell || !body) return;
|
|
701
|
+
var tree = snapshotTree();
|
|
702
|
+
var analysis = analyze(tree);
|
|
703
|
+
var hostDoc = config.container.ownerDocument;
|
|
704
|
+
|
|
705
|
+
var errors = 0;
|
|
706
|
+
for (var i = 0; i < analysis.issues.length; i++) {
|
|
707
|
+
if (analysis.issues[i].severity === "error") errors++;
|
|
708
|
+
}
|
|
709
|
+
if (summaryEl) {
|
|
710
|
+
// Kept short on purpose: the toolbar row ellipsises, and "3 errors" is
|
|
711
|
+
// the part that must survive the truncation. Full counts are in the
|
|
712
|
+
// snapshot and the tree tab.
|
|
713
|
+
summaryEl.innerText = tree.counters.elements + " el · " +
|
|
714
|
+
analysis.issues.length + " issue" + (analysis.issues.length === 1 ? "" : "s") +
|
|
715
|
+
(errors ? " · " + errors + " error" + (errors === 1 ? "" : "s") : "");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
clear(body);
|
|
719
|
+
if (activeTab === "issues") renderIssues(hostDoc, analysis.issues);
|
|
720
|
+
else if (activeTab === "tree") renderTree(hostDoc, tree);
|
|
721
|
+
else if (activeTab === "selection") renderSelection(hostDoc);
|
|
722
|
+
else if (activeTab === "plugins") renderPlugins(hostDoc);
|
|
723
|
+
else if (activeTab === "log") renderLog(hostDoc);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function renderIssues(hostDoc, issues) {
|
|
727
|
+
if (!issues.length) {
|
|
728
|
+
var ok = hostDoc.createElement("div");
|
|
729
|
+
ok.className = "rte-state-inspector-ok";
|
|
730
|
+
ok.innerText = "No structural issues found.";
|
|
731
|
+
body.appendChild(ok);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (var i = 0; i < issues.length; i++) {
|
|
735
|
+
var item = issues[i];
|
|
736
|
+
var wrap = hostDoc.createElement("div");
|
|
737
|
+
wrap.className = "rte-state-inspector-issue is-" + (item.severity === "error" ? "error" : "warn");
|
|
738
|
+
wrap.setAttribute("data-rte-issue-id", item.id);
|
|
739
|
+
var msg = hostDoc.createElement("div");
|
|
740
|
+
msg.className = "rte-state-inspector-issue-msg";
|
|
741
|
+
msg.innerText = item.message;
|
|
742
|
+
var meta = hostDoc.createElement("div");
|
|
743
|
+
meta.className = "rte-state-inspector-issue-meta";
|
|
744
|
+
meta.innerText = (item.detail || "(document root)") + " · " + item.id;
|
|
745
|
+
wrap.appendChild(msg);
|
|
746
|
+
wrap.appendChild(meta);
|
|
747
|
+
body.appendChild(wrap);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function renderTree(hostDoc, tree) {
|
|
752
|
+
if (!tree.root || !tree.root.children.length) {
|
|
753
|
+
var empty = hostDoc.createElement("div");
|
|
754
|
+
empty.className = "rte-state-inspector-empty";
|
|
755
|
+
empty.innerText = config.stateInspectorEmptyText;
|
|
756
|
+
body.appendChild(empty);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
var pre = hostDoc.createElement("pre");
|
|
760
|
+
pre.className = "rte-state-inspector-pre";
|
|
761
|
+
pre.innerText = formatTree(tree.root, 0).join("\n");
|
|
762
|
+
body.appendChild(pre);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function formatTree(node, depth) {
|
|
766
|
+
var lines = [];
|
|
767
|
+
var pad = new Array(depth + 1).join(" ");
|
|
768
|
+
if (node.type === "text") {
|
|
769
|
+
lines.push(pad + "#text(" + node.length + ") " + JSON.stringify(node.text));
|
|
770
|
+
return lines;
|
|
771
|
+
}
|
|
772
|
+
if (node.type === "comment") {
|
|
773
|
+
lines.push(pad + "#comment(" + node.length + ")");
|
|
774
|
+
return lines;
|
|
775
|
+
}
|
|
776
|
+
var label = pad + "<" + node.name;
|
|
777
|
+
var keys = [];
|
|
778
|
+
for (var k in node.attrs) keys.push(k);
|
|
779
|
+
if (keys.length) {
|
|
780
|
+
keys.sort();
|
|
781
|
+
for (var i = 0; i < keys.length && i < 4; i++) {
|
|
782
|
+
label += " " + keys[i] + "=" + JSON.stringify(node.attrs[keys[i]]);
|
|
783
|
+
}
|
|
784
|
+
if (keys.length > 4) label += " …+" + (keys.length - 4);
|
|
785
|
+
}
|
|
786
|
+
label += ">";
|
|
787
|
+
if (node.truncated) label += " … " + node.childCount + " children (depth limit)";
|
|
788
|
+
lines.push(label);
|
|
789
|
+
for (var c = 0; c < (node.children || []).length; c++) {
|
|
790
|
+
lines = lines.concat(formatTree(node.children[c], depth + 1));
|
|
791
|
+
}
|
|
792
|
+
return lines;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function renderSelection(hostDoc) {
|
|
796
|
+
var info = describeSelection();
|
|
797
|
+
if (!info.available) {
|
|
798
|
+
var empty = hostDoc.createElement("div");
|
|
799
|
+
empty.className = "rte-state-inspector-empty";
|
|
800
|
+
empty.innerText = "No selection in the document.";
|
|
801
|
+
body.appendChild(empty);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
addRow(hostDoc, "Collapsed", String(info.collapsed));
|
|
805
|
+
addRow(hostDoc, "In editable", String(info.insideEditable), !info.insideEditable);
|
|
806
|
+
addRow(hostDoc, "Block path", info.block || "(root)");
|
|
807
|
+
addRow(hostDoc, "Anchor", formatBoundary(info.anchor), !!(info.anchor && info.anchor.zeroLength));
|
|
808
|
+
addRow(hostDoc, "Focus", formatBoundary(info.focus), !!(info.focus && info.focus.zeroLength));
|
|
809
|
+
addRow(hostDoc, "Selected", info.textLength + " chars");
|
|
810
|
+
if (info.text) addRow(hostDoc, "Text", info.text);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function formatBoundary(boundary) {
|
|
814
|
+
if (!boundary) return "(none)";
|
|
815
|
+
var out = boundary.node + " @ " + boundary.offset;
|
|
816
|
+
if (typeof boundary.length === "number") out += " (len " + boundary.length + ")";
|
|
817
|
+
if (typeof boundary.childCount === "number") out += " (" + boundary.childCount + " children)";
|
|
818
|
+
if (boundary.zeroLength) out += " ← zero-length text node";
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function addRow(hostDoc, key, value, flagged) {
|
|
823
|
+
var row = hostDoc.createElement("div");
|
|
824
|
+
row.className = "rte-state-inspector-row" + (flagged ? " is-flagged" : "");
|
|
825
|
+
var k = hostDoc.createElement("div");
|
|
826
|
+
k.className = "rte-state-inspector-row-key";
|
|
827
|
+
k.innerText = key;
|
|
828
|
+
var v = hostDoc.createElement("div");
|
|
829
|
+
v.className = "rte-state-inspector-row-val";
|
|
830
|
+
v.innerText = value;
|
|
831
|
+
row.appendChild(k);
|
|
832
|
+
row.appendChild(v);
|
|
833
|
+
body.appendChild(row);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function renderPlugins(hostDoc) {
|
|
837
|
+
if (!pluginOrder.length) {
|
|
838
|
+
var empty = hostDoc.createElement("div");
|
|
839
|
+
empty.className = "rte-state-inspector-empty";
|
|
840
|
+
empty.innerText = "No plugins registered.";
|
|
841
|
+
body.appendChild(empty);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
var note = hostDoc.createElement("div");
|
|
845
|
+
note.className = "rte-state-inspector-copy";
|
|
846
|
+
note.innerText = "Initialization order. A plugin can only register into another plugin's API if it appears below it.";
|
|
847
|
+
body.appendChild(note);
|
|
848
|
+
for (var i = 0; i < pluginOrder.length; i++) {
|
|
849
|
+
addRow(hostDoc, String(i + 1).padStart ? String(i + 1).padStart(3, "0") : String(i + 1), pluginOrder[i].name);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function renderLog(hostDoc) {
|
|
854
|
+
if (!eventLog.length) {
|
|
855
|
+
var empty = hostDoc.createElement("div");
|
|
856
|
+
empty.className = "rte-state-inspector-empty";
|
|
857
|
+
empty.innerText = "No editor events recorded yet.";
|
|
858
|
+
body.appendChild(empty);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
var start = eventLog[0].at;
|
|
862
|
+
for (var i = eventLog.length - 1; i >= 0; i--) {
|
|
863
|
+
addRow(hostDoc, "+" + (eventLog[i].at - start) + "ms", eventLog[i].event);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|