@richscripts/richtexteditor 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/migration/README.md +100 -0
  2. package/migration/cli.js +294 -0
  3. package/migration/index.d.ts +18 -0
  4. package/migration/index.js +242 -0
  5. package/migration/report-schema-v2.json +127 -0
  6. package/package.json +11 -1
  7. package/richtexteditor/CHANGELOG.md +326 -1
  8. package/richtexteditor/all_plugins.js +14959 -4832
  9. package/richtexteditor/all_plugins.min.js +62 -42
  10. package/richtexteditor/plugins/accessibilitychecker.js +8 -7
  11. package/richtexteditor/plugins/aitoolkit.css +17 -8
  12. package/richtexteditor/plugins/aitoolkit.js +8 -8
  13. package/richtexteditor/plugins/all_plugins.js +14959 -4832
  14. package/richtexteditor/plugins/all_plugins.min.js +62 -42
  15. package/richtexteditor/plugins/autocorrect.js +353 -0
  16. package/richtexteditor/plugins/bookmarkcard.js +12 -1
  17. package/richtexteditor/plugins/changecase.js +238 -0
  18. package/richtexteditor/plugins/comments.js +15 -28
  19. package/richtexteditor/plugins/contentminimap.js +6 -5
  20. package/richtexteditor/plugins/crossreference.js +408 -0
  21. package/richtexteditor/plugins/documentimport.js +915 -21
  22. package/richtexteditor/plugins/documentoutline.js +5 -4
  23. package/richtexteditor/plugins/docxexport.js +960 -0
  24. package/richtexteditor/plugins/draghandle.js +309 -0
  25. package/richtexteditor/plugins/footnotes.js +398 -0
  26. package/richtexteditor/plugins/formatpainter.js +298 -0
  27. package/richtexteditor/plugins/formattingmarks.js +246 -0
  28. package/richtexteditor/plugins/html2pdf.js +108 -4
  29. package/richtexteditor/plugins/imageeditor.js +37 -7
  30. package/richtexteditor/plugins/insertgallery.js +553 -170
  31. package/richtexteditor/plugins/keyboarda11y.js +316 -0
  32. package/richtexteditor/plugins/linenumbers.js +352 -0
  33. package/richtexteditor/plugins/linkchecker.js +396 -0
  34. package/richtexteditor/plugins/mentionplugin.js +4 -3
  35. package/richtexteditor/plugins/mergefields.js +328 -0
  36. package/richtexteditor/plugins/multilevellist.js +137 -0
  37. package/richtexteditor/plugins/pagination.js +505 -0
  38. package/richtexteditor/plugins/pdfexport.js +1137 -0
  39. package/richtexteditor/plugins/permanentpen.js +211 -0
  40. package/richtexteditor/plugins/revisionhistory.js +47 -34
  41. package/richtexteditor/plugins/rtlui.js +150 -0
  42. package/richtexteditor/plugins/sanitizer.js +486 -0
  43. package/richtexteditor/plugins/slashcommand.js +142 -17
  44. package/richtexteditor/plugins/spellcheck.js +6 -3
  45. package/richtexteditor/plugins/tableofcontents.js +353 -0
  46. package/richtexteditor/plugins/tabletools.js +342 -0
  47. package/richtexteditor/plugins/textdirection.js +313 -0
  48. package/richtexteditor/plugins/trackedchanges.js +37 -6
  49. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
  50. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
  51. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
  52. package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
  53. package/richtexteditor/plugins/watermark.js +214 -0
  54. package/richtexteditor/plugins/wordexport.js +0 -0
  55. package/richtexteditor/plugins/yjscollab.js +49 -11
  56. package/richtexteditor/rte-config.js +44 -44
  57. package/richtexteditor/rte.js +91 -63
  58. package/richtexteditor/rte_theme_default.css +462 -384
@@ -0,0 +1,328 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-27 Merge fields. Placeholders that stand in for data — recipient name,
4
+ // invoice total, order date — so one document becomes a template you can render
5
+ // against many records. The basis of mail merge, invoice runs, personalised
6
+ // letters and contract generation.
7
+ //
8
+ // Premium in both majors:
9
+ // - CKEditor 5 merge fields: "Unlock this feature with selected CKEditor Plans".
10
+ // - TinyMCE mergetags: "This plugin is only available for paid TinyMCE
11
+ // subscriptions".
12
+ //
13
+ // Design notes:
14
+ // - A field is an ATOMIC contenteditable=false span. The caret steps over it,
15
+ // backspace removes the whole thing, and it can never be half-deleted into
16
+ // "{{customer.na}}" — which would silently stop resolving at merge time.
17
+ // - PREVIEW MODE IS PRESENTATIONAL AND MUST NEVER REACH THE SAVED HTML. This
18
+ // is the whole risk of the feature: if previewing with sample data and then
19
+ // saving wrote "Ada Lovelace" into the template where {{Customer name}} used
20
+ // to be, the user would destroy their template by looking at it. So the
21
+ // serializers are wrapped and every chip is restored to its label for the
22
+ // duration of the call — the same contract pagination.js uses for its
23
+ // overlay.
24
+ // - renderMergeFields(data) works on a DETACHED CLONE. Producing the merged
25
+ // output must not touch the document being edited.
26
+ RTE_DefaultConfig.plugin_mergefields = RTE_Plugin_MergeFields;
27
+
28
+ // Field definitions. Replace these with your own — they are examples, not a
29
+ // meaningful default schema. `sample` may be a string or a function.
30
+ if (typeof RTE_DefaultConfig.mergeFields === "undefined") {
31
+ RTE_DefaultConfig.mergeFields = [
32
+ { id: "recipient.name", label: "Recipient name", sample: "Ada Lovelace" },
33
+ { id: "recipient.email", label: "Recipient email", sample: "ada@example.com" },
34
+ { id: "company.name", label: "Company name", sample: "Analytical Engines Ltd" },
35
+ { id: "document.title", label: "Document title", sample: "Statement of Work" },
36
+ { id: "date.today", label: "Today's date", sample: function () { return new Date().toLocaleDateString(); } }
37
+ ];
38
+ }
39
+ // Wrappers shown around the label so a field is visually obvious as a placeholder.
40
+ if (typeof RTE_DefaultConfig.mergeFieldPrefix === "undefined") RTE_DefaultConfig.mergeFieldPrefix = "{{";
41
+ if (typeof RTE_DefaultConfig.mergeFieldSuffix === "undefined") RTE_DefaultConfig.mergeFieldSuffix = "}}";
42
+ // What renderMergeFields does with a field the data has no value for:
43
+ // "placeholder" leaves it visible, "empty" removes it. Defaults to leaving it
44
+ // visible, because a silently blank invoice line is worse than an obvious one.
45
+ if (typeof RTE_DefaultConfig.mergeFieldMissing === "undefined") RTE_DefaultConfig.mergeFieldMissing = "placeholder";
46
+
47
+ function RTE_Plugin_MergeFields() {
48
+ var obj = this;
49
+ var config, editor;
50
+ var boundDoc = null;
51
+ var preview = false;
52
+ var wrapped = false;
53
+
54
+ obj.PluginName = "MergeFields";
55
+
56
+ obj.InitConfig = function (argconfig) { config = argconfig; };
57
+
58
+ obj.InitEditor = function (argeditor) {
59
+ editor = argeditor;
60
+
61
+ editor.attachEvent("exec_command_insertmergefield", function (state) {
62
+ state.returnValue = true;
63
+ obj.Insert(state && state.value);
64
+ });
65
+
66
+ setup();
67
+ try { editor.attachEvent("ready", setup); } catch (e) {}
68
+ try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
69
+ setTimeout(setup, 0);
70
+
71
+ // Public API.
72
+ editor.getMergeFieldDefinitions = function () { return definitions().slice(); };
73
+ editor.insertMergeField = function (id) { return obj.Insert(id); };
74
+ editor.listMergeFields = function () { return obj.List(); };
75
+ editor.setMergeFieldPreview = function (on) { return applyPreview(!!on); };
76
+ editor.toggleMergeFieldPreview = function () { return applyPreview(!preview); };
77
+ editor.isMergeFieldPreview = function () { return preview; };
78
+ editor.renderMergeFields = function (data) { return obj.Render(data); };
79
+ editor.getMergeFieldCss = function () { return css(); };
80
+ };
81
+
82
+ function setup() {
83
+ var doc = getDoc();
84
+ if (!doc) return;
85
+ injectStyles(doc);
86
+ wrapSerializers();
87
+ if (doc === boundDoc) return;
88
+ boundDoc = doc;
89
+ // Content replaced wholesale: any chips that arrived with it should show
90
+ // whatever mode we are currently in.
91
+ refreshAll();
92
+ }
93
+
94
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
95
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
96
+
97
+ function definitions() {
98
+ var list = config.mergeFields;
99
+ return Object.prototype.toString.call(list) === "[object Array]" ? list : [];
100
+ }
101
+
102
+ function findDefinition(id) {
103
+ var list = definitions();
104
+ for (var i = 0; i < list.length; i++) if (list[i] && list[i].id === id) return list[i];
105
+ return null;
106
+ }
107
+
108
+ function labelFor(id) {
109
+ var def = findDefinition(id);
110
+ var text = def && def.label ? def.label : id;
111
+ return String(config.mergeFieldPrefix || "") + text + String(config.mergeFieldSuffix || "");
112
+ }
113
+
114
+ function sampleFor(id) {
115
+ var def = findDefinition(id);
116
+ if (!def) return "";
117
+ var s = def.sample;
118
+ if (typeof s === "function") {
119
+ try { return String(s(id)); } catch (e) { return ""; }
120
+ }
121
+ return s == null ? "" : String(s);
122
+ }
123
+
124
+ // ---- insert ----------------------------------------------------------
125
+
126
+ obj.Insert = function (id) {
127
+ var doc = getDoc();
128
+ var editable = getEditable();
129
+ if (!doc || !editable) return false;
130
+ if (!id) {
131
+ var first = definitions()[0];
132
+ if (!first) return false;
133
+ id = first.id;
134
+ }
135
+ var span = doc.createElement("span");
136
+ span.className = "rte-merge-field";
137
+ span.setAttribute("data-merge-field", id);
138
+ span.setAttribute("contenteditable", "false");
139
+ span.textContent = preview ? sampleFor(id) : labelFor(id);
140
+ if (preview) span.className += " rte-merge-preview";
141
+
142
+ var placed = false;
143
+ try {
144
+ if (typeof editor.insertElement === "function") { editor.insertElement(span); placed = true; }
145
+ } catch (e) {}
146
+ if (!placed) {
147
+ try {
148
+ var sel = editor.getSelection();
149
+ if (sel && sel.rangeCount) {
150
+ var r = sel.getRangeAt(0);
151
+ r.collapse(false);
152
+ r.insertNode(span);
153
+ r.setStartAfter(span); r.collapse(true);
154
+ sel.removeAllRanges(); sel.addRange(r);
155
+ placed = true;
156
+ }
157
+ } catch (e) {}
158
+ }
159
+ if (!placed) editable.appendChild(span);
160
+ fireChange();
161
+ return id;
162
+ };
163
+
164
+ // ---- inspect ---------------------------------------------------------
165
+
166
+ function chips() {
167
+ var editable = getEditable();
168
+ if (!editable) return [];
169
+ return Array.prototype.slice.call(editable.querySelectorAll("span.rte-merge-field[data-merge-field]"));
170
+ }
171
+
172
+ obj.List = function () {
173
+ var out = [];
174
+ var seen = {};
175
+ var list = chips();
176
+ for (var i = 0; i < list.length; i++) {
177
+ var id = list[i].getAttribute("data-merge-field");
178
+ var def = findDefinition(id);
179
+ out.push({
180
+ id: id,
181
+ label: def && def.label ? def.label : id,
182
+ known: !!def, // a chip pasted from a template built elsewhere may not be defined here
183
+ count: (seen[id] = (seen[id] || 0) + 1)
184
+ });
185
+ }
186
+ return out;
187
+ };
188
+
189
+ // ---- preview ---------------------------------------------------------
190
+
191
+ function applyPreview(on) {
192
+ preview = !!on;
193
+ refreshAll();
194
+ return preview;
195
+ }
196
+
197
+ function refreshAll() {
198
+ var list = chips();
199
+ for (var i = 0; i < list.length; i++) setChipText(list[i]);
200
+ }
201
+
202
+ function setChipText(chip) {
203
+ var id = chip.getAttribute("data-merge-field");
204
+ if (preview) {
205
+ chip.textContent = sampleFor(id);
206
+ if (chip.classList) chip.classList.add("rte-merge-preview");
207
+ } else {
208
+ chip.textContent = labelFor(id);
209
+ if (chip.classList) chip.classList.remove("rte-merge-preview");
210
+ }
211
+ }
212
+
213
+ // ---- render ----------------------------------------------------------
214
+
215
+ // Produce merged HTML for one record. Works on a clone: rendering output
216
+ // must never mutate the template being edited.
217
+ obj.Render = function (data) {
218
+ var editable = getEditable();
219
+ if (!editable) return "";
220
+ var clone = editable.cloneNode(true);
221
+ var list = clone.querySelectorAll("span.rte-merge-field[data-merge-field]");
222
+ for (var i = 0; i < list.length; i++) {
223
+ var chip = list[i];
224
+ var id = chip.getAttribute("data-merge-field");
225
+ var value = lookup(data, id);
226
+ if (value == null) {
227
+ if (String(config.mergeFieldMissing) === "empty") {
228
+ if (chip.parentNode) chip.parentNode.removeChild(chip);
229
+ } else {
230
+ // Leave it obvious rather than silently blank.
231
+ chip.textContent = labelFor(id);
232
+ }
233
+ continue;
234
+ }
235
+ var text = chip.ownerDocument.createTextNode(String(value));
236
+ if (chip.parentNode) chip.parentNode.replaceChild(text, chip);
237
+ }
238
+ return clone.innerHTML;
239
+ };
240
+
241
+ // Supports both flat keys ({"recipient.name": "..."}) and nested objects
242
+ // ({recipient: {name: "..."}}), because both are natural shapes for callers.
243
+ function lookup(data, id) {
244
+ if (!data || typeof data !== "object") return null;
245
+ if (Object.prototype.hasOwnProperty.call(data, id)) return data[id];
246
+ var parts = String(id).split(".");
247
+ var cur = data;
248
+ for (var i = 0; i < parts.length; i++) {
249
+ if (cur == null || typeof cur !== "object") return null;
250
+ if (!Object.prototype.hasOwnProperty.call(cur, parts[i])) return null;
251
+ cur = cur[parts[i]];
252
+ }
253
+ return cur == null ? null : cur;
254
+ }
255
+
256
+ // ---- serialization safety -------------------------------------------
257
+
258
+ // Restore every chip to its LABEL for the duration of a serialize call, then
259
+ // put the preview text back. Without this, saving while preview is on writes
260
+ // sample data into the template permanently.
261
+ function stripFor() {
262
+ if (!preview) return function () {};
263
+ var list = chips();
264
+ var parked = [];
265
+ for (var i = 0; i < list.length; i++) {
266
+ parked.push({ chip: list[i], text: list[i].textContent });
267
+ list[i].textContent = labelFor(list[i].getAttribute("data-merge-field"));
268
+ if (list[i].classList) list[i].classList.remove("rte-merge-preview");
269
+ }
270
+ return function restore() {
271
+ for (var j = 0; j < parked.length; j++) {
272
+ parked[j].chip.textContent = parked[j].text;
273
+ if (parked[j].chip.classList) parked[j].chip.classList.add("rte-merge-preview");
274
+ }
275
+ };
276
+ }
277
+
278
+ function wrapSerializers() {
279
+ if (wrapped) return;
280
+ var names = ["getHTMLCode", "getJSON", "getHTMLContent", "getText"];
281
+ var did = false;
282
+ for (var i = 0; i < names.length; i++) {
283
+ (function (name) {
284
+ var orig = editor[name];
285
+ if (typeof orig !== "function" || orig.__rteMfWrapped) return;
286
+ var w = function () {
287
+ var restore = stripFor();
288
+ try { return orig.apply(editor, arguments); } finally { restore(); }
289
+ };
290
+ w.__rteMfWrapped = true;
291
+ editor[name] = w;
292
+ did = true;
293
+ })(names[i]);
294
+ }
295
+ if (did) wrapped = true;
296
+ }
297
+
298
+ function fireChange() {
299
+ try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
300
+ try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
301
+ }
302
+
303
+ // ---- styles ----------------------------------------------------------
304
+
305
+ function css() {
306
+ return (
307
+ "span.rte-merge-field{display:inline-block;padding:0 .35em;border-radius:3px;" +
308
+ "background:#e8f0fe;color:#1a4fa0;border:1px solid #c3d8f7;" +
309
+ "font-size:.94em;white-space:nowrap;user-select:none;cursor:default;}" +
310
+ "span.rte-merge-field.rte-merge-preview{background:#eef7ed;color:#1e6b32;border-color:#c6e3c8;}"
311
+ );
312
+ }
313
+
314
+ function injectStyles(doc) {
315
+ if (!doc) return;
316
+ var existing = doc.getElementById("rte-merge-field-styles");
317
+ var text = css();
318
+ if (existing) {
319
+ if (existing.getAttribute("data-css") === text) return;
320
+ existing.parentNode && existing.parentNode.removeChild(existing);
321
+ }
322
+ var st = doc.createElement("style");
323
+ st.id = "rte-merge-field-styles";
324
+ st.setAttribute("data-css", text);
325
+ st.appendChild(doc.createTextNode(text));
326
+ (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
327
+ }
328
+ }
@@ -0,0 +1,137 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-07-17 Multi-level / legal list numbering. Turns an ordered list into
4
+ // legal-style hierarchical numbering — 1, 1.1, 1.1.1 — the numbering contracts,
5
+ // specifications, statutes and policy documents are written in.
6
+ //
7
+ // CKEditor sells this as a premium "Multi-level list" feature; TinyMCE, Froala,
8
+ // Tiptap, Lexical and Quill ship nothing equivalent.
9
+ //
10
+ // Implementation is CSS counters, not rewritten text:
11
+ // - the numbering is produced by counters(rte-legal, ".") on ::before, so the
12
+ // document keeps clean semantic <ol>/<li> markup with no injected number
13
+ // text to fall out of sync when items are added, removed or re-ordered
14
+ // - the only thing stored is one class on the root <ol>, which IS content (a
15
+ // deliberate formatting choice) and so is meant to persist in saved HTML
16
+ // - because it is real CSS, the browser print pipeline renders it, which means
17
+ // html2pdf and Print preview reproduce the numbering
18
+ //
19
+ // The matching stylesheet is injected into the editor document at runtime and is
20
+ // also exposed as editor.getLegalListCss() so hosts can drop the same rules into
21
+ // their public page, their print stylesheet, or an export template.
22
+ RTE_DefaultConfig.plugin_multilevellist = RTE_Plugin_MultiLevelList;
23
+
24
+ // Class applied to the root <ol>. Kept configurable so a host with an existing
25
+ // convention (or a CSS-module build) can point it at their own class.
26
+ if (typeof RTE_DefaultConfig.legalListClass === "undefined") RTE_DefaultConfig.legalListClass = "rte-legal-list";
27
+ // Separator between levels: "1.1.1" (default) or e.g. ")" / "-".
28
+ if (typeof RTE_DefaultConfig.legalListSeparator === "undefined") RTE_DefaultConfig.legalListSeparator = ".";
29
+ // Trailing string after the last number: "1.1." vs "1.1)".
30
+ if (typeof RTE_DefaultConfig.legalListSuffix === "undefined") RTE_DefaultConfig.legalListSuffix = ".";
31
+
32
+ function RTE_Plugin_MultiLevelList() {
33
+ var obj = this;
34
+ var config, editor;
35
+ var boundDoc = null;
36
+
37
+ obj.PluginName = "MultiLevelList";
38
+
39
+ obj.InitConfig = function (argconfig) { config = argconfig; };
40
+
41
+ obj.InitEditor = function (argeditor) {
42
+ editor = argeditor;
43
+ setup();
44
+ try { editor.attachEvent("ready", setup); } catch (e) {}
45
+ try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
46
+ setTimeout(setup, 0);
47
+
48
+ // Public API.
49
+ editor.setLegalNumbering = function (on) { return applyToSelection(!!on); };
50
+ editor.toggleLegalNumbering = function () { return applyToSelection(!isLegal(nearestRootList())); };
51
+ editor.isLegalNumbering = function () { return isLegal(nearestRootList()); };
52
+ editor.getLegalListCss = function () { return css(); };
53
+ };
54
+
55
+ function setup() {
56
+ var doc = getDoc();
57
+ if (doc && doc !== boundDoc) { injectStyles(doc); boundDoc = doc; }
58
+ else if (doc) injectStyles(doc);
59
+ }
60
+
61
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
62
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
63
+
64
+ // ---- selection helpers ----------------------------------------------
65
+
66
+ // The <ol> that owns the caret, walked up to the OUTERMOST list so the class
67
+ // lands on the root — counters() needs a single root scope to number from.
68
+ function nearestRootList() {
69
+ try {
70
+ var sel = editor.getSelection();
71
+ if (!sel || sel.rangeCount === 0) return null;
72
+ var editable = getEditable();
73
+ var n = sel.getRangeAt(0).startContainer;
74
+ if (n && n.nodeType === 3) n = n.parentNode;
75
+ var root = null;
76
+ while (n && n !== editable) {
77
+ if (n.nodeName === "OL") root = n; // keep climbing: last wins = outermost
78
+ n = n.parentNode;
79
+ }
80
+ return root;
81
+ } catch (e) { return null; }
82
+ }
83
+
84
+ function isLegal(ol) {
85
+ return !!(ol && ol.classList && ol.classList.contains(config.legalListClass));
86
+ }
87
+
88
+ function applyToSelection(on) {
89
+ var ol = nearestRootList();
90
+ if (!ol) return false;
91
+ if (on) ol.classList.add(config.legalListClass);
92
+ else ol.classList.remove(config.legalListClass);
93
+ // Let the host know the document changed so undo/save stay honest.
94
+ try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
95
+ try { editor.fireEvent && editor.fireEvent("change"); } catch (e) {}
96
+ return on;
97
+ }
98
+
99
+ // ---- styles ----------------------------------------------------------
100
+
101
+ function css() {
102
+ var cls = config.legalListClass;
103
+ var sep = String(config.legalListSeparator || ".").replace(/"/g, '\\"');
104
+ var suffix = String(config.legalListSuffix == null ? "." : config.legalListSuffix).replace(/"/g, '\\"');
105
+ // One counter name, reset at every level; counters() joins the whole
106
+ // ancestor chain, which is what yields 1 / 1.1 / 1.1.1 automatically.
107
+ return (
108
+ "ol." + cls + ",ol." + cls + " ol{counter-reset:rte-legal;list-style:none;}" +
109
+ "ol." + cls + ">li,ol." + cls + " ol>li{counter-increment:rte-legal;position:relative;}" +
110
+ // Hide any native marker that a UA or reset stylesheet still draws.
111
+ "ol." + cls + ">li::marker,ol." + cls + " ol>li::marker{content:\"\";}" +
112
+ "ol." + cls + ">li::before,ol." + cls + " ol>li::before{" +
113
+ "content:counters(rte-legal,\"" + sep + "\")\"" + suffix + "\";" +
114
+ "position:absolute;left:-3.2em;width:3em;text-align:right;" +
115
+ "font-variant-numeric:tabular-nums;}" +
116
+ // Room for the widest marker; nested levels indent further.
117
+ "ol." + cls + "{padding-left:3.6em;}" +
118
+ "ol." + cls + " ol{padding-left:3.2em;margin-top:.25em;}"
119
+ );
120
+ }
121
+
122
+ function injectStyles(doc) {
123
+ if (!doc) return;
124
+ var existing = doc.getElementById("rte-legal-list-styles");
125
+ var text = css();
126
+ if (existing) {
127
+ // Config can change at runtime (separator/suffix) — keep it current.
128
+ if (existing.getAttribute("data-css") === text) return;
129
+ existing.parentNode && existing.parentNode.removeChild(existing);
130
+ }
131
+ var st = doc.createElement("style");
132
+ st.id = "rte-legal-list-styles";
133
+ st.setAttribute("data-css", text);
134
+ st.appendChild(doc.createTextNode(text));
135
+ (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
136
+ }
137
+ }