@richscripts/richtexteditor 2.6.3 → 2.6.4
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/angular/component.ts +16 -3
- package/integrations/shared.js +27 -0
- package/package.json +1 -1
- package/react/index.d.ts +7 -0
- package/react/index.js +25 -3
- package/richtexteditor/CHANGELOG.md +87 -0
- package/richtexteditor/all_plugins.js +366 -48
- package/richtexteditor/all_plugins.min.js +57 -57
- package/richtexteditor/lang/rte-lang-de.js +96 -0
- package/richtexteditor/lang/rte-lang-zh-Hans.js +181 -0
- package/richtexteditor/lang/rte-lang-zh-Hant.js +181 -0
- package/richtexteditor/plugins/all_plugins.js +366 -48
- package/richtexteditor/plugins/all_plugins.min.js +57 -57
- package/richtexteditor/plugins/comments.js +11 -1
- package/richtexteditor/plugins/crdt-engine.js +244 -17
- package/richtexteditor/plugins/crdt-engine.min.js +5 -5
- package/richtexteditor/plugins/documentimport.js +6 -11
- package/richtexteditor/plugins/insertcode.js +2 -2
- package/richtexteditor/plugins/readabilitystats.js +6 -11
- package/richtexteditor/plugins/sanitizer.js +136 -10
- package/richtexteditor/plugins/shadowdom.js +11 -1
- package/richtexteditor/plugins/slashcommand.js +78 -7
- package/richtexteditor/plugins/yjscollab.js +116 -5
- package/richtexteditor/rte-webcomponent.js +15 -0
- package/richtexteditor/rte.js +2 -2
- package/richtexteditor/rte_theme_default.css +13 -4
- package/richtexteditor/rte_theme_default.min.css +1 -1
- package/vue/index.js +12 -2
|
@@ -21718,7 +21718,17 @@ function RTE_Plugin_Comments() {
|
|
|
21718
21718
|
|
|
21719
21719
|
var id = "cmt-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
|
21720
21720
|
var user = getCurrentUser();
|
|
21721
|
-
var anchor = options.anchor;
|
|
21721
|
+
var anchor = options.anchor || options.range || null;
|
|
21722
|
+
// Accept a plain DOM Range (or Range-like) as the anchor: it has the
|
|
21723
|
+
// container/offset fields but no `.text`, so until 2026-09-02 the common
|
|
21724
|
+
// call add({ anchor: range }) silently recorded the comment WITHOUT a
|
|
21725
|
+
// highlight. Derive the text from the range instead.
|
|
21726
|
+
if (anchor && anchor.startContainer && anchor.endContainer && !anchor.text) {
|
|
21727
|
+
var derived = "";
|
|
21728
|
+
try { derived = typeof anchor.toString === "function" ? String(anchor.toString()) : ""; } catch (e) { derived = ""; }
|
|
21729
|
+
if (!derived) { try { derived = anchor.cloneContents ? (anchor.cloneContents().textContent || "") : ""; } catch (e2) { derived = ""; } }
|
|
21730
|
+
anchor = { startContainer: anchor.startContainer, startOffset: anchor.startOffset, endContainer: anchor.endContainer, endOffset: anchor.endOffset, text: derived };
|
|
21731
|
+
}
|
|
21722
21732
|
var wrappedText = "";
|
|
21723
21733
|
|
|
21724
21734
|
if (anchor && anchor.startContainer && anchor.endContainer && anchor.text) {
|
|
@@ -23437,17 +23447,12 @@ function RTE_Plugin_DocumentImport() {
|
|
|
23437
23447
|
openPicker({});
|
|
23438
23448
|
});
|
|
23439
23449
|
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
keywords: ["import", "open", "file", "word", "markdown", "upload"],
|
|
23447
|
-
action: function () { openPicker({}); }
|
|
23448
|
-
});
|
|
23449
|
-
} catch (e) {}
|
|
23450
|
-
}
|
|
23450
|
+
// NOTE: do NOT call editor.slashCommands.register() from here. Plugins
|
|
23451
|
+
// initialise in bundle (alphabetical) order and this file sorts before
|
|
23452
|
+
// "slashcommand", so editor.slashCommands does not exist yet and the guarded
|
|
23453
|
+
// call silently did nothing - the "import-document" slash entry never appeared for any
|
|
23454
|
+
// customer until 2026-09-02. The entry now lives in slashcommand.js, gated on
|
|
23455
|
+
// typeof editor.openImportDialog === "function" (same pattern as exportToPdf).
|
|
23451
23456
|
};
|
|
23452
23457
|
|
|
23453
23458
|
function kindFromName(name) {
|
|
@@ -29588,7 +29593,7 @@ function RTE_Plugin_InsertCode() {
|
|
|
29588
29593
|
sel_lang.options.add(new Option(aliases, brush));
|
|
29589
29594
|
|
|
29590
29595
|
|
|
29591
|
-
var b = sessionStorage.getItem("rte-insertcode-lang")
|
|
29596
|
+
var b = null; try { b = sessionStorage.getItem("rte-insertcode-lang"); } catch (e) { } // storage throws in sandboxed / opaque-origin embeds
|
|
29592
29597
|
if (b) sel_lang.value = b;
|
|
29593
29598
|
}
|
|
29594
29599
|
|
|
@@ -29608,7 +29613,7 @@ function RTE_Plugin_InsertCode() {
|
|
|
29608
29613
|
btn.onclick = function () {
|
|
29609
29614
|
dialoginner.close();
|
|
29610
29615
|
|
|
29611
|
-
sessionStorage.setItem("rte-insertcode-lang", sel_lang.value)
|
|
29616
|
+
try { sessionStorage.setItem("rte-insertcode-lang", sel_lang.value); } catch (e) { }
|
|
29612
29617
|
|
|
29613
29618
|
if (sel_lang.value != "") {
|
|
29614
29619
|
var b = dp.sh.Brushes[sel_lang.value];
|
|
@@ -36441,17 +36446,12 @@ function RTE_Plugin_ReadabilityStats() {
|
|
|
36441
36446
|
openDialog();
|
|
36442
36447
|
});
|
|
36443
36448
|
|
|
36444
|
-
|
|
36445
|
-
|
|
36446
|
-
|
|
36447
|
-
|
|
36448
|
-
|
|
36449
|
-
|
|
36450
|
-
keywords: ["readability", "flesch", "grade", "reading", "stats", "score"],
|
|
36451
|
-
action: function () { openDialog(); }
|
|
36452
|
-
});
|
|
36453
|
-
} catch (e) {}
|
|
36454
|
-
}
|
|
36449
|
+
// NOTE: do NOT call editor.slashCommands.register() from here. Plugins
|
|
36450
|
+
// initialise in bundle (alphabetical) order and this file sorts before
|
|
36451
|
+
// "slashcommand", so editor.slashCommands does not exist yet and the guarded
|
|
36452
|
+
// call silently did nothing - the "readability-stats" slash entry never appeared for any
|
|
36453
|
+
// customer until 2026-09-02. The entry now lives in slashcommand.js, gated on
|
|
36454
|
+
// typeof editor.getReadabilityStats === "function" (same pattern as exportToPdf).
|
|
36455
36455
|
};
|
|
36456
36456
|
|
|
36457
36457
|
function currentText() {
|
|
@@ -37877,8 +37877,9 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
|
37877
37877
|
// config.sanitizerAllowStyleTags = true // keep <style> in content
|
|
37878
37878
|
// config.sanitizerAllowIframes = false // drop <iframe> outright
|
|
37879
37879
|
// config.sanitizerAllowedIframeHosts = ["www.youtube.com"]
|
|
37880
|
-
// config.sanitizerAllowTags = ["custom-el"]
|
|
37881
|
-
// config.sanitizerAllowAttributes = ["my-attr"]
|
|
37880
|
+
// config.sanitizerAllowTags = ["custom-el", "x-*", /^ui-/]
|
|
37881
|
+
// config.sanitizerAllowAttributes = ["my-attr", "data-cms-*"]
|
|
37882
|
+
// exact names, "*" wildcards, or RegExp -- see buildMatcher()
|
|
37882
37883
|
RTE_DefaultConfig.plugin_sanitizer = RTE_Plugin_Sanitizer;
|
|
37883
37884
|
if (typeof RTE_DefaultConfig.contentSanitizer === "undefined") RTE_DefaultConfig.contentSanitizer = true;
|
|
37884
37885
|
|
|
@@ -38065,12 +38066,63 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38065
38066
|
return false;
|
|
38066
38067
|
}
|
|
38067
38068
|
|
|
38069
|
+
// Build a name matcher from a config list. Entries may be:
|
|
38070
|
+
// "my-widget" an exact name
|
|
38071
|
+
// "my-*" a wildcard, where * matches any run of characters
|
|
38072
|
+
// /^my-/ a RegExp
|
|
38073
|
+
// The wildcard form is what makes this a workable answer to CKEditor's General
|
|
38074
|
+
// HTML Support: without it an integrator has to enumerate every tag and
|
|
38075
|
+
// attribute a CMS might emit, by name, which nobody does -- so the content gets
|
|
38076
|
+
// unwrapped instead and the integrator concludes the editor destroys it.
|
|
38077
|
+
//
|
|
38078
|
+
// A pattern widens what is KEPT, never what is executed: on* handlers,
|
|
38079
|
+
// javascript: URLs, <script> and iframe[srcdoc] are all handled before this is
|
|
38080
|
+
// consulted, so allowing a tag or attribute by pattern cannot re-open those.
|
|
38081
|
+
//
|
|
38082
|
+
// Wildcards are matched by hand rather than compiled to a RegExp, so a config
|
|
38083
|
+
// value can never be a regex-injection vector and no escaping table has to be
|
|
38084
|
+
// kept correct.
|
|
38085
|
+
function buildMatcher(list) {
|
|
38086
|
+
var literal = {};
|
|
38087
|
+
var globs = [];
|
|
38088
|
+
var regexps = [];
|
|
38089
|
+
(list || []).forEach(function (entry) {
|
|
38090
|
+
if (entry instanceof RegExp) { regexps.push(entry); return; }
|
|
38091
|
+
var t = String(entry).toLowerCase();
|
|
38092
|
+
if (t.indexOf("*") === -1) { literal[t] = true; return; }
|
|
38093
|
+
globs.push(t.split("*"));
|
|
38094
|
+
});
|
|
38095
|
+
function globMatches(parts, name) {
|
|
38096
|
+
// parts came from splitting on "*": they must appear in order, the first
|
|
38097
|
+
// anchored at the start and the last at the end.
|
|
38098
|
+
var first = parts[0];
|
|
38099
|
+
if (name.lastIndexOf(first, 0) !== 0) return false;
|
|
38100
|
+
var last = parts[parts.length - 1];
|
|
38101
|
+
if (last) {
|
|
38102
|
+
if (name.length < last.length) return false;
|
|
38103
|
+
if (name.indexOf(last, name.length - last.length) === -1) return false;
|
|
38104
|
+
}
|
|
38105
|
+
var pos = first.length;
|
|
38106
|
+
for (var i = 1; i < parts.length; i++) {
|
|
38107
|
+
if (!parts[i]) continue;
|
|
38108
|
+
var at = name.indexOf(parts[i], pos);
|
|
38109
|
+
if (at === -1) return false;
|
|
38110
|
+
pos = at + parts[i].length;
|
|
38111
|
+
}
|
|
38112
|
+
return true;
|
|
38113
|
+
}
|
|
38114
|
+
return function (name) {
|
|
38115
|
+
if (literal[name]) return true;
|
|
38116
|
+
for (var i = 0; i < globs.length; i++) if (globMatches(globs[i], name)) return true;
|
|
38117
|
+
for (var j = 0; j < regexps.length; j++) if (regexps[j].test(name)) return true;
|
|
38118
|
+
return false;
|
|
38119
|
+
};
|
|
38120
|
+
}
|
|
38121
|
+
|
|
38068
38122
|
// --------------------------------------------------------------- engine
|
|
38069
38123
|
function sanitizeNode(root, report) {
|
|
38070
|
-
var allowExtra =
|
|
38071
|
-
|
|
38072
|
-
var attrExtra = {};
|
|
38073
|
-
(config.sanitizerAllowAttributes || []).forEach(function (a) { attrExtra[String(a).toLowerCase()] = true; });
|
|
38124
|
+
var allowExtra = buildMatcher(config.sanitizerAllowTags);
|
|
38125
|
+
var attrExtra = buildMatcher(config.sanitizerAllowAttributes);
|
|
38074
38126
|
var allowStyleTag = config.sanitizerAllowStyleTags === true;
|
|
38075
38127
|
|
|
38076
38128
|
// Collect first: removing nodes while walking a live list skips siblings.
|
|
@@ -38079,7 +38131,7 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38079
38131
|
for (var i = 0; i < n.childNodes.length; i++) {
|
|
38080
38132
|
var c = n.childNodes[i];
|
|
38081
38133
|
if (c.nodeType === 1) { all.push(c); collect(c); }
|
|
38082
|
-
else if (c.nodeType === 8) all.push(c); //
|
|
38134
|
+
else if (c.nodeType === 8) all.push(c); // checked, see commentEscapes()
|
|
38083
38135
|
}
|
|
38084
38136
|
})(root);
|
|
38085
38137
|
|
|
@@ -38087,7 +38139,33 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38087
38139
|
var el = all[i];
|
|
38088
38140
|
if (!el.parentNode) continue; // already removed with an ancestor
|
|
38089
38141
|
|
|
38090
|
-
if (el.nodeType === 8) {
|
|
38142
|
+
if (el.nodeType === 8) {
|
|
38143
|
+
// Comments used to be removed wholesale, on the stated grounds that
|
|
38144
|
+
// "comments can hide markup". That reason does not survive testing:
|
|
38145
|
+
// serialise <!--<script>alert(1)</script>--> and re-parse it and the
|
|
38146
|
+
// script is inert -- a well-formed comment is just text.
|
|
38147
|
+
//
|
|
38148
|
+
// What IS unsafe is a narrow, enumerable set of comment DATA that
|
|
38149
|
+
// cannot survive serialisation, because the serialiser writes the
|
|
38150
|
+
// data verbatim between <!-- and -->. Re-parsing then ends the
|
|
38151
|
+
// comment early and the remainder becomes live markup:
|
|
38152
|
+
//
|
|
38153
|
+
// "--> <img src=x onerror=...>" -> <!----> <img ...>--> ESCAPES
|
|
38154
|
+
// "--!> <img ...>" -> <!----!> <img ...>--> ESCAPES
|
|
38155
|
+
// "> <img ...>" -> <!--> <img ...>--> ESCAPES
|
|
38156
|
+
// "-> <img ...>" -> <!---> <img ...>--> ESCAPES
|
|
38157
|
+
//
|
|
38158
|
+
// That is the WHATWG serialisation constraint, not a heuristic, so
|
|
38159
|
+
// it can be enumerated rather than guessed at. Everything else is
|
|
38160
|
+
// kept, which is what lets a CMS round-trip its own markers and lets
|
|
38161
|
+
// an email template keep its Outlook conditional comments -- the
|
|
38162
|
+
// mechanism emailtoolkit.js needs and could not previously rely on.
|
|
38163
|
+
if (commentEscapes(el.data)) {
|
|
38164
|
+
note(report.removedTags, "#comment");
|
|
38165
|
+
el.parentNode.removeChild(el);
|
|
38166
|
+
}
|
|
38167
|
+
continue;
|
|
38168
|
+
}
|
|
38091
38169
|
|
|
38092
38170
|
var tag = (el.localName || el.nodeName || "").toLowerCase();
|
|
38093
38171
|
|
|
@@ -38115,7 +38193,7 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38115
38193
|
note(report.removedTags, tag);
|
|
38116
38194
|
el.parentNode.removeChild(el);
|
|
38117
38195
|
continue;
|
|
38118
|
-
} else if (!ALLOWED_TAGS[tag] && !allowExtra
|
|
38196
|
+
} else if (!ALLOWED_TAGS[tag] && !allowExtra(tag)) {
|
|
38119
38197
|
// Unknown element: unwrap rather than delete, so the words a
|
|
38120
38198
|
// user typed inside a stray tag are not silently lost.
|
|
38121
38199
|
note(report.removedTags, tag);
|
|
@@ -38144,7 +38222,7 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38144
38222
|
if (!isSafeUrl(value, tag, name)) { note(report.removedAttributes, tag + "[" + name + "]"); el.removeAttribute(attrs[k].name); }
|
|
38145
38223
|
continue;
|
|
38146
38224
|
}
|
|
38147
|
-
if (!ALLOWED_ATTRS[name] && !attrExtra
|
|
38225
|
+
if (!ALLOWED_ATTRS[name] && !attrExtra(name)) {
|
|
38148
38226
|
note(report.removedAttributes, name);
|
|
38149
38227
|
el.removeAttribute(attrs[k].name);
|
|
38150
38228
|
}
|
|
@@ -38257,6 +38335,44 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38257
38335
|
// Measured: the payload ran locally AND replicated to the peer with its
|
|
38258
38336
|
// onerror attribute intact.
|
|
38259
38337
|
//
|
|
38338
|
+
// THREAT MODEL -- read this before tightening or loosening the rule below.
|
|
38339
|
+
//
|
|
38340
|
+
// An escaping comment CANNOT BE AUTHORED AS AN HTML STRING. Feed
|
|
38341
|
+
// `<!--x--> <img src=q onerror=...>-->` to setHTMLCode and the parser yields
|
|
38342
|
+
// comment("x") + text + a normal <img> element, which the element/attribute
|
|
38343
|
+
// filters already handle. The ONLY way to obtain a comment node whose data
|
|
38344
|
+
// escapes is programmatic DOM construction -- document.createComment(), or a
|
|
38345
|
+
// sync layer building nodes directly.
|
|
38346
|
+
//
|
|
38347
|
+
// That is why the guard lives on the DOM-level paths (the sanitise pass and the
|
|
38348
|
+
// live-DOM MutationObserver) and why the string paths need nothing extra. Both
|
|
38349
|
+
// string directions were measured: sender getHTMLCode, receiver setHTMLCode, and
|
|
38350
|
+
// the receiver's live DOM all yield zero executable attributes, because
|
|
38351
|
+
// wrapSerializers() and wrapSetters() filter on the way out and on the way in.
|
|
38352
|
+
//
|
|
38353
|
+
// The previous rule here deleted EVERY comment, justified only by the remark
|
|
38354
|
+
// "comments can hide markup". That is false -- serialise
|
|
38355
|
+
// <!--<script>alert(1)</script>--> and re-parse it and the script is inert -- and
|
|
38356
|
+
// it cost the product CMS round-tripping and Outlook conditional comments (the
|
|
38357
|
+
// mechanism emailtoolkit.js needs) for as long as nobody re-checked it. An
|
|
38358
|
+
// unexplained restriction outlives its reason, so this one is written down.
|
|
38359
|
+
//
|
|
38360
|
+
// A comment's data is written verbatim between <!-- and -->, so data that
|
|
38361
|
+
// contains a terminator (or opens with one) breaks out of the comment when the
|
|
38362
|
+
// serialised output is re-parsed, turning the remainder into live markup.
|
|
38363
|
+
// Module scope on purpose: both the sanitiser pass and the live-DOM
|
|
38364
|
+
// MutationObserver below need it, and they must agree exactly on what is unsafe.
|
|
38365
|
+
// Deliberately conservative -- it rejects rather than rewrites, because silently
|
|
38366
|
+
// editing someone's comment text is its own kind of surprise.
|
|
38367
|
+
function commentEscapes(data) {
|
|
38368
|
+
var d = String(data == null ? "" : data);
|
|
38369
|
+
if (d.indexOf("-->") !== -1) return true;
|
|
38370
|
+
if (d.indexOf("--!>") !== -1) return true;
|
|
38371
|
+
if (d.charAt(0) === ">") return true;
|
|
38372
|
+
if (d.charAt(0) === "-" && d.charAt(1) === ">") return true;
|
|
38373
|
+
return false;
|
|
38374
|
+
}
|
|
38375
|
+
|
|
38260
38376
|
// A MutationObserver is the only place to catch every arrival path — sync,
|
|
38261
38377
|
// drop, a third-party plugin, host-page code. It is honest to say this
|
|
38262
38378
|
// narrows the window rather than closing it completely: the observer runs
|
|
@@ -38307,6 +38423,16 @@ function RTE_Plugin_Sanitizer() {
|
|
|
38307
38423
|
}
|
|
38308
38424
|
for (var a = 0; a < rec.addedNodes.length; a++) {
|
|
38309
38425
|
var node = rec.addedNodes[a];
|
|
38426
|
+
// Comments are preserved now, so an injected comment whose data
|
|
38427
|
+
// escapes on re-parse has to wake the sanitiser. This loop used
|
|
38428
|
+
// to skip every non-element node, which was safe only while
|
|
38429
|
+
// comments were being deleted unconditionally. The CRDT sync
|
|
38430
|
+
// path is exactly how a node arrives here without passing the
|
|
38431
|
+
// input filter -- that is the path that produced the stored XSS.
|
|
38432
|
+
if (node.nodeType === 8) {
|
|
38433
|
+
if (commentEscapes(node.data)) { suspect = true; break; }
|
|
38434
|
+
continue;
|
|
38435
|
+
}
|
|
38310
38436
|
if (node.nodeType !== 1) continue; // typing inserts text: free
|
|
38311
38437
|
if (looksDangerous(node)) { suspect = true; break; }
|
|
38312
38438
|
}
|
|
@@ -38495,7 +38621,17 @@ function RTE_Plugin_ShadowDom() {
|
|
|
38495
38621
|
var id = String(node.id || "");
|
|
38496
38622
|
if (/^rte[-_]|^__rte/.test(id)) return true;
|
|
38497
38623
|
var href = node.href || "";
|
|
38498
|
-
if (!href)
|
|
38624
|
+
if (!href) {
|
|
38625
|
+
// An inline <style> with no id and no href is what bundlers (Vite,
|
|
38626
|
+
// webpack style-loader) inject for an imported theme. Recognise it by
|
|
38627
|
+
// its content, or a shadow-root editor is unstyled (39,726 px toolbar,
|
|
38628
|
+
// Times New Roman) exactly as before this plugin existed. 2026-09-02.
|
|
38629
|
+
if (node.tagName === "STYLE") {
|
|
38630
|
+
var css = node.textContent || "";
|
|
38631
|
+
return /[.]richtexteditor|rte-toolbar|rte-editor|rte-dialog|[.]rte-a11y/.test(css);
|
|
38632
|
+
}
|
|
38633
|
+
return false;
|
|
38634
|
+
}
|
|
38499
38635
|
var base = assetBase();
|
|
38500
38636
|
if (base && href.indexOf(base) === 0) return true;
|
|
38501
38637
|
// Fall back to the file names this product actually ships, so an
|
|
@@ -38643,7 +38779,7 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38643
38779
|
editor.slashCommands = {
|
|
38644
38780
|
register: registerCommand,
|
|
38645
38781
|
remove: removeCommand,
|
|
38646
|
-
list: function () { return commands.slice(); },
|
|
38782
|
+
list: function () { refreshDefaults(); return commands.slice(); },
|
|
38647
38783
|
open: function () { openPopup(true); },
|
|
38648
38784
|
close: closePopup,
|
|
38649
38785
|
isOpen: function () { return !!popupEl; }
|
|
@@ -38655,6 +38791,15 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38655
38791
|
registerCommand(config.slashCommands[i]);
|
|
38656
38792
|
}
|
|
38657
38793
|
}
|
|
38794
|
+
// Plugins initialise synchronously in bundle (alphabetical) order, so at
|
|
38795
|
+
// this point every plugin sorting AFTER "slashcommand" (tabletools,
|
|
38796
|
+
// textdirection, watermark, tableofcontents, typewriter, ...) has not
|
|
38797
|
+
// defined its editor.* API yet and the typeof gates in
|
|
38798
|
+
// buildDefaultCommands() were false: nine entries (sort asc/desc, rtl/
|
|
38799
|
+
// ltr/autodir, watermark, toc, typewriter, focusmode) were missing for
|
|
38800
|
+
// every customer until 2026-09-02. Rebuild once the constructor has
|
|
38801
|
+
// finished (setTimeout 0 runs after all plugin inits) and again on open.
|
|
38802
|
+
setTimeout(refreshDefaults, 0);
|
|
38658
38803
|
|
|
38659
38804
|
injectStyles();
|
|
38660
38805
|
|
|
@@ -38677,7 +38822,26 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38677
38822
|
else commands.push(normalizeCommand(def));
|
|
38678
38823
|
}
|
|
38679
38824
|
|
|
38825
|
+
var removedIds = {};
|
|
38826
|
+
// Rebuild the gated default entries (their typeof checks may only be true
|
|
38827
|
+
// now) while keeping every custom registration and honouring remove().
|
|
38828
|
+
function refreshDefaults() {
|
|
38829
|
+
if (!editor) return;
|
|
38830
|
+
var custom = [];
|
|
38831
|
+
for (var i = 0; i < commands.length; i++) if (!commands[i]._default) custom.push(commands[i]);
|
|
38832
|
+
var defaults = buildDefaultCommands();
|
|
38833
|
+
var next = [];
|
|
38834
|
+
for (var d = 0; d < defaults.length; d++) {
|
|
38835
|
+
var id = defaults[d].id;
|
|
38836
|
+
if (removedIds[id]) continue;
|
|
38837
|
+
var overridden = false;
|
|
38838
|
+
for (var c = 0; c < custom.length; c++) if (custom[c].id === id) { overridden = true; break; }
|
|
38839
|
+
if (!overridden) next.push(defaults[d]);
|
|
38840
|
+
}
|
|
38841
|
+
commands = next.concat(custom);
|
|
38842
|
+
}
|
|
38680
38843
|
function removeCommand(id) {
|
|
38844
|
+
removedIds[id] = true;
|
|
38681
38845
|
var idx = findCommandIndex(id);
|
|
38682
38846
|
if (idx >= 0) commands.splice(idx, 1);
|
|
38683
38847
|
}
|
|
@@ -38687,12 +38851,25 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38687
38851
|
return -1;
|
|
38688
38852
|
}
|
|
38689
38853
|
|
|
38854
|
+
// Localization with an English fallback for EVERY entry (defaults, plugin
|
|
38855
|
+
// register() calls, AI actions, config.slashCommands): the lang packs set
|
|
38856
|
+
// config.text_<key>; __GetLangText returns the raw KEY when one is missing,
|
|
38857
|
+
// so resolve only when a translation exists. Keys: text_slash_<id> (title),
|
|
38858
|
+
// text_slash_<id>_desc (description), text_slash_section_<section>.
|
|
38859
|
+
function L(key, fallback) {
|
|
38860
|
+
try {
|
|
38861
|
+
var k = String(key).toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
38862
|
+
if (config && config["text_" + k] && editor && typeof editor.getLangText === "function") return editor.getLangText(k);
|
|
38863
|
+
} catch (e) { }
|
|
38864
|
+
return fallback;
|
|
38865
|
+
}
|
|
38690
38866
|
function normalizeCommand(def) {
|
|
38867
|
+
var section = def.section || "Blocks";
|
|
38691
38868
|
return {
|
|
38692
38869
|
id: def.id,
|
|
38693
|
-
section:
|
|
38694
|
-
title: def.title || def.id,
|
|
38695
|
-
description: def.description || "",
|
|
38870
|
+
section: L("slash_section_" + section, section),
|
|
38871
|
+
title: L("slash_" + def.id, def.title || def.id),
|
|
38872
|
+
description: L("slash_" + def.id + "_desc", def.description || ""),
|
|
38696
38873
|
keywords: (def.keywords || []).slice(),
|
|
38697
38874
|
icon: def.icon || "",
|
|
38698
38875
|
iconSvg: def.iconSvg || "",
|
|
@@ -38743,11 +38920,13 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38743
38920
|
var list = [];
|
|
38744
38921
|
|
|
38745
38922
|
function push(section, id, title, description, keywords, iconSvg, run) {
|
|
38746
|
-
|
|
38923
|
+
var cmd = normalizeCommand({ // normalizeCommand localizes title/description/section
|
|
38747
38924
|
id: id, section: section, title: title,
|
|
38748
38925
|
description: description, keywords: keywords,
|
|
38749
38926
|
iconSvg: iconSvg, run: run
|
|
38750
|
-
})
|
|
38927
|
+
});
|
|
38928
|
+
cmd._default = true;
|
|
38929
|
+
list.push(cmd);
|
|
38751
38930
|
}
|
|
38752
38931
|
|
|
38753
38932
|
push("Blocks", "heading1", "Heading 1", "Large section heading", ["h1", "title"], iconHeading("1"),
|
|
@@ -38963,7 +39142,8 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38963
39142
|
// Gated on the API rather than registered by the plugin itself: plugins
|
|
38964
39143
|
// initialise in bundle order, so anything sorting before "slashcommand"
|
|
38965
39144
|
// would call slashCommands.register() before it exists and vanish from
|
|
38966
|
-
// this menu without any error.
|
|
39145
|
+
// this menu without any error. These checks are re-run by refreshDefaults()
|
|
39146
|
+
// after all plugins have initialised and again each time the menu opens.
|
|
38967
39147
|
if (typeof editor.exportToPdf === "function") {
|
|
38968
39148
|
push("Tools", "exportpdf", "Export to PDF", "Real text, not a screenshot — selectable, searchable and screen-reader friendly", ["pdf", "export", "download", "save", "print", "text", "accessible", "searchable"], iconPdfText(),
|
|
38969
39149
|
function () { editor.exportToPdf(); });
|
|
@@ -38972,6 +39152,32 @@ function RTE_Plugin_SlashCommand() {
|
|
|
38972
39152
|
push("Tools", "exportdocx", "Export to Word (.docx)", "A real OOXML document, built in the browser with no upload", ["word", "docx", "ooxml", "export", "download", "save", "office"], iconWordFile(),
|
|
38973
39153
|
function () { editor.exportToDocx(); });
|
|
38974
39154
|
}
|
|
39155
|
+
// documentimport.js and readabilitystats.js sort before this file, so they
|
|
39156
|
+
// cannot register at init (editor.slashCommands does not exist yet); their
|
|
39157
|
+
// guarded calls silently did nothing and these two entries were missing for
|
|
39158
|
+
// every customer until 2026-09-02. Declared here, gated on the plugin API.
|
|
39159
|
+
if (typeof editor.openImportDialog === "function") {
|
|
39160
|
+
push("Tools", "import-document", "Import document", "Open a Markdown, HTML, text, or Word file into the editor", ["import", "open", "file", "word", "docx", "markdown", "upload"], iconWordFile(),
|
|
39161
|
+
function () { editor.openImportDialog({}); });
|
|
39162
|
+
}
|
|
39163
|
+
if (typeof editor.getReadabilityStats === "function") {
|
|
39164
|
+
push("Tools", "readability-stats", "Readability statistics", "Flesch reading ease, grade level, and reading time", ["readability", "flesch", "grade", "reading", "stats", "score"], iconSpell(),
|
|
39165
|
+
function () { editor.execCommand("readability"); });
|
|
39166
|
+
}
|
|
39167
|
+
// Marketed features that had NO toolbar button and NO slash entry (reachable
|
|
39168
|
+
// only via the API) until 2026-09-02. All gated on the owning plugin.
|
|
39169
|
+
if (typeof editor.insertCrossReference === "function") {
|
|
39170
|
+
push("Insert", "crossreference", "Cross-reference", "Insert a live reference to a heading, figure, table, or bookmark", ["cross", "reference", "xref", "see", "figure", "heading"], iconXref(),
|
|
39171
|
+
function () { editor.execCommand("insertcrossreference"); });
|
|
39172
|
+
}
|
|
39173
|
+
if (editor.restrictedEditing) {
|
|
39174
|
+
push("Tools", "restrictedediting", "Restricted editing", "Lock the document and mark only the regions others may edit", ["restrict", "lock", "protect", "editable", "region", "permission"], iconPen(),
|
|
39175
|
+
function () { editor.execCommand("restrictedediting"); });
|
|
39176
|
+
}
|
|
39177
|
+
if (editor.dictation) {
|
|
39178
|
+
push("Tools", "dictation", "Dictation", "Speak and have your words typed into the document", ["dictate", "speech", "voice", "microphone", "talk"], iconReadAloud(),
|
|
39179
|
+
function () { editor.execCommand("dictation"); });
|
|
39180
|
+
}
|
|
38975
39181
|
if (typeof editor.copyAsMarkdown === "function") {
|
|
38976
39182
|
push("Tools", "copymarkdown", "Copy as Markdown", "Copy the whole document to the clipboard as Markdown", ["markdown", "md", "copy", "clipboard", "export"], iconMarkdown(),
|
|
38977
39183
|
function () { editor.copyAsMarkdown(); });
|
|
@@ -39103,6 +39309,7 @@ function RTE_Plugin_SlashCommand() {
|
|
|
39103
39309
|
}
|
|
39104
39310
|
|
|
39105
39311
|
function openPopup(manual) {
|
|
39312
|
+
refreshDefaults();
|
|
39106
39313
|
closePopup();
|
|
39107
39314
|
var sel = editor.getSelection();
|
|
39108
39315
|
if (!sel || sel.rangeCount === 0) return;
|
|
@@ -43018,9 +43225,42 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43018
43225
|
// and the review ledger bridge.
|
|
43019
43226
|
if (session.textSyncMode === "crdt") {
|
|
43020
43227
|
try {
|
|
43228
|
+
// 2026-09-02: the engine bundles its OWN Yjs and builds nodes with it.
|
|
43229
|
+
// A doc built by the host's Yjs (npm/esm "yjs" + y-websocket - the
|
|
43230
|
+
// documented path) is a FOREIGN instance: inserting engine-built nodes
|
|
43231
|
+
// into it throws "Unexpected content type in insert operation" and text
|
|
43232
|
+
// never syncs, while presence still works and the status still said
|
|
43233
|
+
// "crdt". Yjs updates are instance-independent bytes, so bind the
|
|
43234
|
+
// engine to an inner doc built with ITS Yjs and bridge updates both
|
|
43235
|
+
// ways: host->inner with the engine's applyUpdate, inner->host with the
|
|
43236
|
+
// host's module (options.Y or window.Y). Without the host module the
|
|
43237
|
+
// outbound leg cannot be applied safely, so fail LOUDLY instead.
|
|
43238
|
+
var engineY = window.RichTextEditorCrdt.Y || null;
|
|
43239
|
+
var hostY = options.Y || (typeof window !== "undefined" ? window.Y : null) || null;
|
|
43240
|
+
var bindDoc = doc;
|
|
43241
|
+
var foreign = !!(engineY && engineY.Doc && !(doc instanceof engineY.Doc));
|
|
43242
|
+
if (foreign) {
|
|
43243
|
+
if (!hostY || typeof hostY.applyUpdate !== "function" || typeof hostY.encodeStateAsUpdate !== "function") {
|
|
43244
|
+
throw new Error("yjscollab: the Y.Doc was created by a different Yjs instance than crdt-engine.js bundles. Pass your Yjs module to attach({ Y }) (or build the doc with window.RichTextEditorCrdt.Y) so text can be bridged.");
|
|
43245
|
+
}
|
|
43246
|
+
var BRIDGE = { bridge: "rte-yjs-bridge" };
|
|
43247
|
+
var inner = new engineY.Doc();
|
|
43248
|
+
engineY.applyUpdate(inner, hostY.encodeStateAsUpdate(doc), BRIDGE);
|
|
43249
|
+
var LOCAL = window.RichTextEditorCrdt.LOCAL_ORIGIN;
|
|
43250
|
+
// A host-doc write the plugin tagged as LOCAL (ledger entries) must not
|
|
43251
|
+
// make the engine re-render the editable (see flush race in the engine).
|
|
43252
|
+
var hostToInner = function (update, origin) { if (origin === BRIDGE) return; try { engineY.applyUpdate(inner, update, (LOCAL && origin === LOCAL) ? LOCAL : BRIDGE); } catch (e) { if (window.console) console.error("yjscollab bridge host->inner:", e); } };
|
|
43253
|
+
var innerToHost = function (update, origin) { if (origin === BRIDGE) return; try { hostY.applyUpdate(doc, update, BRIDGE); } catch (e) { if (window.console) console.error("yjscollab bridge inner->host:", e); } };
|
|
43254
|
+
doc.on("update", hostToInner);
|
|
43255
|
+
inner.on("update", innerToHost);
|
|
43256
|
+
session.cleanup.push(function () { try { doc.off("update", hostToInner); } catch (e) { } try { inner.off("update", innerToHost); } catch (e) { } try { inner.destroy(); } catch (e) { } });
|
|
43257
|
+
session.yjsBridge = true;
|
|
43258
|
+
bindDoc = inner;
|
|
43259
|
+
if (window.console && console.info) console.info("yjscollab: bridging a foreign Yjs doc into the CRDT engine (updates relayed both ways).");
|
|
43260
|
+
}
|
|
43021
43261
|
session.crdtBinding = window.RichTextEditorCrdt.attachCrdtBinding({
|
|
43022
43262
|
editable: editor.getEditable(),
|
|
43023
|
-
ydoc:
|
|
43263
|
+
ydoc: bindDoc,
|
|
43024
43264
|
provider: provider,
|
|
43025
43265
|
awareness: provider.awareness,
|
|
43026
43266
|
fragmentName: options.fragmentName || "default",
|
|
@@ -43033,6 +43273,24 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43033
43273
|
try { session.crdtBinding && session.crdtBinding.dispose(); }
|
|
43034
43274
|
catch (ignore) { }
|
|
43035
43275
|
});
|
|
43276
|
+
// Per-author undo. The engine ships a Y.UndoManager scoped to LOCAL_ORIGIN
|
|
43277
|
+
// (undo.ts) but nothing attached it, so Ctrl+Z ran the core DOM-snapshot
|
|
43278
|
+
// undo and reverted OTHER people's edits on every peer (2026-09-02). Route
|
|
43279
|
+
// undo/redo to the engine while crdt mode is active.
|
|
43280
|
+
try {
|
|
43281
|
+
if (typeof window.RichTextEditorCrdt.attachUndoManager === "function" && session.crdtBinding && session.crdtBinding.fragment) {
|
|
43282
|
+
session.undoManager = window.RichTextEditorCrdt.attachUndoManager({ fragment: session.crdtBinding.fragment });
|
|
43283
|
+
var undoHook = function (state) { if (!session || !session.undoManager) return; state.returnValue = true; state.stopBubble = true; try { session.undoManager.undo(); } catch (e) { } };
|
|
43284
|
+
var redoHook = function (state) { if (!session || !session.undoManager) return; state.returnValue = true; state.stopBubble = true; try { session.undoManager.redo(); } catch (e) { } };
|
|
43285
|
+
editor.attachEvent("exec_command_undo", undoHook);
|
|
43286
|
+
editor.attachEvent("exec_command_redo", redoHook);
|
|
43287
|
+
session.cleanup.push(function () {
|
|
43288
|
+
try { if (typeof editor.detachEvent === "function") { editor.detachEvent("exec_command_undo", undoHook); editor.detachEvent("exec_command_redo", redoHook); } } catch (e) { }
|
|
43289
|
+
try { session.undoManager && session.undoManager.destroy(); } catch (e) { }
|
|
43290
|
+
session.undoManager = null;
|
|
43291
|
+
});
|
|
43292
|
+
}
|
|
43293
|
+
} catch (undoErr) { if (window.console) console.warn("yjscollab: undo manager not attached:", undoErr); }
|
|
43036
43294
|
} catch (err) {
|
|
43037
43295
|
session.fallbackReason = "crdt-attach-failed";
|
|
43038
43296
|
session.crdtError = err;
|
|
@@ -43065,7 +43323,7 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43065
43323
|
}
|
|
43066
43324
|
return {
|
|
43067
43325
|
attached: true,
|
|
43068
|
-
textSyncMode: session.textSyncMode,
|
|
43326
|
+
textSyncMode: session.textSyncMode, yjsBridge: !!session.yjsBridge,
|
|
43069
43327
|
requestedTextSync: session.requestedTextSync,
|
|
43070
43328
|
fallbackReason: session.fallbackReason || null,
|
|
43071
43329
|
peerCount: getRemotePeers().length
|
|
@@ -43310,6 +43568,52 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43310
43568
|
lastKnownHtml = localInitial;
|
|
43311
43569
|
}
|
|
43312
43570
|
|
|
43571
|
+
// --- IME composition gate (legacy snapshot mode) ---------------------
|
|
43572
|
+
// setEditorHtml() replaces the whole document. Doing that while an IME
|
|
43573
|
+
// is composing swaps the live text node out from under it, so the
|
|
43574
|
+
// commit lands at a stale offset and the raw buffer survives: composing
|
|
43575
|
+
// "shi" and committing a character while a collaborator typed produced
|
|
43576
|
+
// "Xbasesh<char>i" on BOTH peers (2026-09-03 — the same corruption the
|
|
43577
|
+
// crdt binding had, on the deprecated-but-supported path v2.0 customers
|
|
43578
|
+
// still run). Hold the repaint until the composition commits.
|
|
43579
|
+
var composing = false;
|
|
43580
|
+
var pendingRemoteHtml = null;
|
|
43581
|
+
var compositionDoc = (editor.getDocument && editor.getDocument()) || null;
|
|
43582
|
+
if (compositionDoc) {
|
|
43583
|
+
var onCompStart = function () { composing = true; };
|
|
43584
|
+
var onCompEnd = function () {
|
|
43585
|
+
if (!composing) return;
|
|
43586
|
+
composing = false;
|
|
43587
|
+
setTimeout(function () {
|
|
43588
|
+
if (pendingRemoteHtml !== null) {
|
|
43589
|
+
// Deliberately NOT repainting: that would discard the
|
|
43590
|
+
// character the IME just committed. Adopt the remote text as
|
|
43591
|
+
// the diff baseline instead, so the local push that follows
|
|
43592
|
+
// is computed against what the peers actually hold. Legacy
|
|
43593
|
+
// mode is documented last-write-wins on same-paragraph
|
|
43594
|
+
// conflicts, so the local commit winning is correct here —
|
|
43595
|
+
// corrupted text was not.
|
|
43596
|
+
lastKnownHtml = pendingRemoteHtml;
|
|
43597
|
+
pendingRemoteHtml = null;
|
|
43598
|
+
}
|
|
43599
|
+
// Push explicitly. The commit's DOM mutation is delivered to
|
|
43600
|
+
// the MutationObserver BEFORE compositionend fires, so it was
|
|
43601
|
+
// dropped by the `composing` guard — without this the
|
|
43602
|
+
// committed character never reaches the peers at all
|
|
43603
|
+
// (measured: A "base<char>", B "base").
|
|
43604
|
+
onEditorMutation();
|
|
43605
|
+
}, 0);
|
|
43606
|
+
};
|
|
43607
|
+
// Bound on the DOCUMENT, not the body: some skins remount the body
|
|
43608
|
+
// (see attachMutationObserver below) and composition events bubble.
|
|
43609
|
+
compositionDoc.addEventListener("compositionstart", onCompStart, true);
|
|
43610
|
+
compositionDoc.addEventListener("compositionend", onCompEnd, true);
|
|
43611
|
+
session.cleanup.push(function () {
|
|
43612
|
+
compositionDoc.removeEventListener("compositionstart", onCompStart, true);
|
|
43613
|
+
compositionDoc.removeEventListener("compositionend", onCompEnd, true);
|
|
43614
|
+
});
|
|
43615
|
+
}
|
|
43616
|
+
|
|
43313
43617
|
// Y.Text → editor.
|
|
43314
43618
|
var onTextChange = function (event, transaction) {
|
|
43315
43619
|
if (pushing) return;
|
|
@@ -43317,6 +43621,7 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43317
43621
|
if (transaction && transaction.local) return;
|
|
43318
43622
|
var fresh = session.textMap.toString();
|
|
43319
43623
|
if (fresh === lastKnownHtml) return;
|
|
43624
|
+
if (composing) { pendingRemoteHtml = fresh; return; }
|
|
43320
43625
|
setEditorHtml(fresh);
|
|
43321
43626
|
lastKnownHtml = fresh;
|
|
43322
43627
|
// 2026-05-28 setEditorHtml writes into editable.innerHTML; if that
|
|
@@ -43331,6 +43636,9 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43331
43636
|
// Editor DOM → Y.Text. Debounced so bursts collapse to one CRDT diff.
|
|
43332
43637
|
var onEditorMutation = function () {
|
|
43333
43638
|
if (applying) return;
|
|
43639
|
+
// Intermediate IME buffers are not real edits; the commit fires its
|
|
43640
|
+
// own mutation. Pushing them also races the composition gate above.
|
|
43641
|
+
if (composing) return;
|
|
43334
43642
|
if (pendingPushTimer) return;
|
|
43335
43643
|
// 2026-05-28 Re-attach observer if the body was remounted since last
|
|
43336
43644
|
// mutation. Cheap no-op when body is unchanged.
|
|
@@ -43418,17 +43726,17 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43418
43726
|
|
|
43419
43727
|
ledger.add = function (entry) {
|
|
43420
43728
|
var result = originalAdd.call(ledger, entry);
|
|
43421
|
-
if (result && !echoGuard) session.ledgerMap.set(result.id, cloneEntry(result));
|
|
43729
|
+
if (result && !echoGuard) ledgerWrite(function () { session.ledgerMap.set(result.id, cloneEntry(result)); });
|
|
43422
43730
|
return result;
|
|
43423
43731
|
};
|
|
43424
43732
|
ledger.update = function (id, patch) {
|
|
43425
43733
|
var result = originalUpdate.call(ledger, id, patch);
|
|
43426
|
-
if (result && !echoGuard) session.ledgerMap.set(result.id, cloneEntry(result));
|
|
43734
|
+
if (result && !echoGuard) ledgerWrite(function () { session.ledgerMap.set(result.id, cloneEntry(result)); });
|
|
43427
43735
|
return result;
|
|
43428
43736
|
};
|
|
43429
43737
|
ledger.remove = function (id) {
|
|
43430
43738
|
var result = originalRemove.call(ledger, id);
|
|
43431
|
-
if (result && !echoGuard) session.ledgerMap.delete(id);
|
|
43739
|
+
if (result && !echoGuard) ledgerWrite(function () { session.ledgerMap.delete(id); });
|
|
43432
43740
|
return result;
|
|
43433
43741
|
};
|
|
43434
43742
|
|
|
@@ -43462,6 +43770,16 @@ function RTE_Plugin_YjsCollab() {
|
|
|
43462
43770
|
});
|
|
43463
43771
|
}
|
|
43464
43772
|
|
|
43773
|
+
// Ledger writes are the plugin's own (non-text) Y.Doc transactions. In crdt
|
|
43774
|
+
// mode they are tagged with the engine's LOCAL_ORIGIN so the engine does not
|
|
43775
|
+
// re-render the editable for them - a re-render between a DOM mutation and
|
|
43776
|
+
// the observer flush discarded that mutation (2026-09-02: comment anchors
|
|
43777
|
+
// vanished the instant the ledger entry was written).
|
|
43778
|
+
function ledgerWrite(fn) {
|
|
43779
|
+
var origin = (session && session.textSyncMode === "crdt" && window.RichTextEditorCrdt) ? window.RichTextEditorCrdt.LOCAL_ORIGIN : null;
|
|
43780
|
+
if (origin && session && session.doc && typeof session.doc.transact === "function") { session.doc.transact(fn, origin); }
|
|
43781
|
+
else { fn(); }
|
|
43782
|
+
}
|
|
43465
43783
|
function seedLedgerFromRemote() {
|
|
43466
43784
|
if (!editor.reviewLedger || !session.ledgerMap) return;
|
|
43467
43785
|
echoGuard = true;
|