@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.
- package/migration/README.md +100 -0
- package/migration/cli.js +294 -0
- package/migration/index.d.ts +18 -0
- package/migration/index.js +242 -0
- package/migration/report-schema-v2.json +127 -0
- package/package.json +11 -1
- package/richtexteditor/CHANGELOG.md +326 -1
- package/richtexteditor/all_plugins.js +14959 -4832
- package/richtexteditor/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/accessibilitychecker.js +8 -7
- package/richtexteditor/plugins/aitoolkit.css +17 -8
- package/richtexteditor/plugins/aitoolkit.js +8 -8
- package/richtexteditor/plugins/all_plugins.js +14959 -4832
- package/richtexteditor/plugins/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/autocorrect.js +353 -0
- package/richtexteditor/plugins/bookmarkcard.js +12 -1
- package/richtexteditor/plugins/changecase.js +238 -0
- package/richtexteditor/plugins/comments.js +15 -28
- package/richtexteditor/plugins/contentminimap.js +6 -5
- package/richtexteditor/plugins/crossreference.js +408 -0
- package/richtexteditor/plugins/documentimport.js +915 -21
- package/richtexteditor/plugins/documentoutline.js +5 -4
- package/richtexteditor/plugins/docxexport.js +960 -0
- package/richtexteditor/plugins/draghandle.js +309 -0
- package/richtexteditor/plugins/footnotes.js +398 -0
- package/richtexteditor/plugins/formatpainter.js +298 -0
- package/richtexteditor/plugins/formattingmarks.js +246 -0
- package/richtexteditor/plugins/html2pdf.js +108 -4
- package/richtexteditor/plugins/imageeditor.js +37 -7
- package/richtexteditor/plugins/insertgallery.js +553 -170
- package/richtexteditor/plugins/keyboarda11y.js +316 -0
- package/richtexteditor/plugins/linenumbers.js +352 -0
- package/richtexteditor/plugins/linkchecker.js +396 -0
- package/richtexteditor/plugins/mentionplugin.js +4 -3
- package/richtexteditor/plugins/mergefields.js +328 -0
- package/richtexteditor/plugins/multilevellist.js +137 -0
- package/richtexteditor/plugins/pagination.js +505 -0
- package/richtexteditor/plugins/pdfexport.js +1137 -0
- package/richtexteditor/plugins/permanentpen.js +211 -0
- package/richtexteditor/plugins/revisionhistory.js +47 -34
- package/richtexteditor/plugins/rtlui.js +150 -0
- package/richtexteditor/plugins/sanitizer.js +486 -0
- package/richtexteditor/plugins/slashcommand.js +142 -17
- package/richtexteditor/plugins/spellcheck.js +6 -3
- package/richtexteditor/plugins/tableofcontents.js +353 -0
- package/richtexteditor/plugins/tabletools.js +342 -0
- package/richtexteditor/plugins/textdirection.js +313 -0
- package/richtexteditor/plugins/trackedchanges.js +37 -6
- package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
- package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
- package/richtexteditor/plugins/watermark.js +214 -0
- package/richtexteditor/plugins/wordexport.js +0 -0
- package/richtexteditor/plugins/yjscollab.js +49 -11
- package/richtexteditor/rte-config.js +44 -44
- package/richtexteditor/rte.js +91 -63
- package/richtexteditor/rte_theme_default.css +462 -384
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-07-27 Autocorrect and typography. Fixes common typos as you type,
|
|
4
|
+
// capitalises the start of sentences, and turns typed approximations into the
|
|
5
|
+
// real characters — straight quotes into curly ones, -- into an em dash, ...
|
|
6
|
+
// into an ellipsis, (c) into (C).
|
|
7
|
+
//
|
|
8
|
+
// TinyMCE sells this as TWO premium plugins: Autocorrect ("This plugin is only
|
|
9
|
+
// available for paid TinyMCE subscriptions", typo correction + capitalisation)
|
|
10
|
+
// and a separate Advanced Typography plugin. CKEditor 5 does include automatic
|
|
11
|
+
// text transformation in its open-source core, so this is parity there rather
|
|
12
|
+
// than a gap — do not claim otherwise.
|
|
13
|
+
//
|
|
14
|
+
// Design notes:
|
|
15
|
+
// - Corrections are made by rewriting the caret's TEXT NODE, never by DOM
|
|
16
|
+
// surgery. Everything happens inside one node, so inline markup around the
|
|
17
|
+
// word is untouched and the caret is trivially restorable by offset.
|
|
18
|
+
// - Nothing fires inside <code>, <pre>, or any [data-rte-no-autocorrect]
|
|
19
|
+
// subtree. Curling the quotes inside a code sample corrupts it, and that is
|
|
20
|
+
// the single most damaging thing a feature like this can do.
|
|
21
|
+
// - Pressing Backspace immediately after a correction reverts it and leaves
|
|
22
|
+
// what you actually typed, the way Word and Docs behave. Without an escape
|
|
23
|
+
// hatch, autocorrect fights users who meant the thing it "fixed".
|
|
24
|
+
RTE_DefaultConfig.plugin_autocorrect = RTE_Plugin_Autocorrect;
|
|
25
|
+
|
|
26
|
+
if (typeof RTE_DefaultConfig.autocorrectEnabled === "undefined") RTE_DefaultConfig.autocorrectEnabled = true;
|
|
27
|
+
// Capitalise the first letter of a sentence.
|
|
28
|
+
if (typeof RTE_DefaultConfig.autocorrectCapitalize === "undefined") RTE_DefaultConfig.autocorrectCapitalize = true;
|
|
29
|
+
// Smart quotes, dashes, ellipsis, symbol and fraction substitutions.
|
|
30
|
+
if (typeof RTE_DefaultConfig.autocorrectTypography === "undefined") RTE_DefaultConfig.autocorrectTypography = true;
|
|
31
|
+
// Fix common misspellings from the table below.
|
|
32
|
+
if (typeof RTE_DefaultConfig.autocorrectSpelling === "undefined") RTE_DefaultConfig.autocorrectSpelling = true;
|
|
33
|
+
// Extra or overriding word replacements: { "recieve": "receive" }. Merged over
|
|
34
|
+
// the defaults, so a host can also DISABLE one by mapping it to itself.
|
|
35
|
+
if (typeof RTE_DefaultConfig.autocorrectReplacements === "undefined") RTE_DefaultConfig.autocorrectReplacements = null;
|
|
36
|
+
|
|
37
|
+
function RTE_Plugin_Autocorrect() {
|
|
38
|
+
var obj = this;
|
|
39
|
+
var config, editor;
|
|
40
|
+
var boundDoc = null;
|
|
41
|
+
var enabled = true;
|
|
42
|
+
var lastFix = null; // {node, start, typed, fixed} for the Backspace escape hatch
|
|
43
|
+
|
|
44
|
+
// Deliberately conservative. Every entry here must be a string that is never
|
|
45
|
+
// a word a user could legitimately want: "ill", "lets", "its" and "hes" are
|
|
46
|
+
// all excluded for that reason, even though they are frequent typos of
|
|
47
|
+
// "I'll", "let's", "it's" and "he's" — guessing wrong on a real word is far
|
|
48
|
+
// more annoying than missing a correction.
|
|
49
|
+
var DEFAULT_WORDS = {
|
|
50
|
+
"teh": "the", "adn": "and", "taht": "that", "thier": "their", "freind": "friend",
|
|
51
|
+
"recieve": "receive", "recieved": "received", "seperate": "separate",
|
|
52
|
+
"seperated": "separated", "occured": "occurred", "occuring": "occurring",
|
|
53
|
+
"definately": "definitely", "accomodate": "accommodate", "acommodate": "accommodate",
|
|
54
|
+
"wich": "which", "becuase": "because", "beacuse": "because", "becasue": "because",
|
|
55
|
+
"arguement": "argument", "existance": "existence", "maintainance": "maintenance",
|
|
56
|
+
"neccessary": "necessary", "necessery": "necessary", "occassion": "occasion",
|
|
57
|
+
"publically": "publicly", "reccommend": "recommend", "refered": "referred",
|
|
58
|
+
"relevent": "relevant", "succesful": "successful", "successfull": "successful",
|
|
59
|
+
"untill": "until", "wierd": "weird", "acheive": "achieve", "beleive": "believe",
|
|
60
|
+
"concious": "conscious", "embarass": "embarrass", "goverment": "government",
|
|
61
|
+
"independant": "independent", "occurence": "occurrence", "perseverence": "perseverance",
|
|
62
|
+
"priviledge": "privilege", "supercede": "supersede", "threshhold": "threshold",
|
|
63
|
+
"tommorow": "tomorrow", "tomorow": "tomorrow", "truely": "truly",
|
|
64
|
+
"alot": "a lot", "infront": "in front", "inspite": "in spite",
|
|
65
|
+
"dont": "don't", "doesnt": "doesn't", "didnt": "didn't", "cant": "can't",
|
|
66
|
+
"isnt": "isn't", "wasnt": "wasn't", "arent": "aren't", "werent": "weren't",
|
|
67
|
+
"havent": "haven't", "hasnt": "hasn't", "hadnt": "hadn't",
|
|
68
|
+
"couldnt": "couldn't", "shouldnt": "shouldn't", "wouldnt": "wouldn't",
|
|
69
|
+
"youre": "you're", "theyre": "they're", "wouldve": "would've",
|
|
70
|
+
"couldve": "could've", "shouldve": "should've",
|
|
71
|
+
"i": "I", "im": "I'm", "ive": "I've", "id": "I'd"
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Multi-character sequences replaced the moment they are completed.
|
|
75
|
+
var SEQUENCES = [
|
|
76
|
+
{ from: "...", to: "…" }, // ellipsis
|
|
77
|
+
{ from: "-->", to: "⟶" }, // long right arrow
|
|
78
|
+
{ from: "->", to: "→" },
|
|
79
|
+
{ from: "<-", to: "←" },
|
|
80
|
+
{ from: "<=", to: "≤" },
|
|
81
|
+
{ from: ">=", to: "≥" },
|
|
82
|
+
{ from: "!=", to: "≠" },
|
|
83
|
+
{ from: "(c)", to: "©" },
|
|
84
|
+
{ from: "(r)", to: "®" },
|
|
85
|
+
{ from: "(tm)", to: "™" },
|
|
86
|
+
{ from: "1/2", to: "½" },
|
|
87
|
+
{ from: "1/4", to: "¼" },
|
|
88
|
+
{ from: "3/4", to: "¾" },
|
|
89
|
+
{ from: "+-", to: "±" }
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
obj.PluginName = "Autocorrect";
|
|
93
|
+
|
|
94
|
+
obj.InitConfig = function (argconfig) {
|
|
95
|
+
config = argconfig;
|
|
96
|
+
enabled = config.autocorrectEnabled !== false;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
obj.InitEditor = function (argeditor) {
|
|
100
|
+
editor = argeditor;
|
|
101
|
+
setup();
|
|
102
|
+
try { editor.attachEvent("ready", setup); } catch (e) {}
|
|
103
|
+
try { editor.attachEvent("aftersethtml", setup); } catch (e) {}
|
|
104
|
+
setTimeout(setup, 0);
|
|
105
|
+
|
|
106
|
+
editor.setAutocorrectEnabled = function (on) { enabled = !!on; return enabled; };
|
|
107
|
+
editor.isAutocorrectEnabled = function () { return enabled; };
|
|
108
|
+
editor.addAutocorrectRule = function (from, to) {
|
|
109
|
+
if (!from) return false;
|
|
110
|
+
words()[String(from).toLowerCase()] = String(to);
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
113
|
+
editor.getAutocorrectRules = function () {
|
|
114
|
+
var w = words(), out = {};
|
|
115
|
+
for (var k in w) if (w.hasOwnProperty(k)) out[k] = w[k];
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
// Exposed mainly so a host can run the same normalisation over pasted or
|
|
119
|
+
// imported text rather than only over live typing.
|
|
120
|
+
editor.applyTypography = function (text) { return typographyPass(String(text)); };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
var wordTable = null;
|
|
124
|
+
function words() {
|
|
125
|
+
if (wordTable) return wordTable;
|
|
126
|
+
wordTable = {};
|
|
127
|
+
for (var k in DEFAULT_WORDS) if (DEFAULT_WORDS.hasOwnProperty(k)) wordTable[k] = DEFAULT_WORDS[k];
|
|
128
|
+
var extra = config.autocorrectReplacements;
|
|
129
|
+
if (extra) for (var j in extra) if (extra.hasOwnProperty(j)) wordTable[String(j).toLowerCase()] = extra[j];
|
|
130
|
+
return wordTable;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setup() {
|
|
134
|
+
var doc = getDoc();
|
|
135
|
+
if (!doc || doc === boundDoc) return;
|
|
136
|
+
boundDoc = doc;
|
|
137
|
+
var editable = getEditable();
|
|
138
|
+
if (!editable) return;
|
|
139
|
+
editable.addEventListener("input", onInput);
|
|
140
|
+
editable.addEventListener("keydown", onKeyDown, true);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
144
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
145
|
+
|
|
146
|
+
// ---- guards ----------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
// Code is the one place these substitutions are actively destructive.
|
|
149
|
+
function inProtectedContext(node) {
|
|
150
|
+
var editable = getEditable();
|
|
151
|
+
var n = node && node.nodeType === 3 ? node.parentNode : node;
|
|
152
|
+
while (n && n !== editable) {
|
|
153
|
+
var name = n.nodeName;
|
|
154
|
+
if (name === "CODE" || name === "PRE" || name === "KBD" || name === "SAMP" || name === "VAR") return true;
|
|
155
|
+
if (n.getAttribute && n.getAttribute("data-rte-no-autocorrect") !== null &&
|
|
156
|
+
n.getAttribute("data-rte-no-autocorrect") !== "false") return true;
|
|
157
|
+
n = n.parentNode;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function caret() {
|
|
163
|
+
try {
|
|
164
|
+
var sel = editor.getSelection();
|
|
165
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
166
|
+
var r = sel.getRangeAt(0);
|
|
167
|
+
if (!r.collapsed) return null; // only while typing
|
|
168
|
+
if (!r.startContainer || r.startContainer.nodeType !== 3) return null;
|
|
169
|
+
return { node: r.startContainer, offset: r.startOffset };
|
|
170
|
+
} catch (e) { return null; }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function setCaret(node, offset) {
|
|
174
|
+
try {
|
|
175
|
+
var doc = getDoc();
|
|
176
|
+
var r = doc.createRange();
|
|
177
|
+
var max = node.nodeValue.length;
|
|
178
|
+
r.setStart(node, Math.max(0, Math.min(offset, max)));
|
|
179
|
+
r.collapse(true);
|
|
180
|
+
var sel = editor.getSelection();
|
|
181
|
+
sel.removeAllRanges();
|
|
182
|
+
sel.addRange(r);
|
|
183
|
+
} catch (e) {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- the Backspace escape hatch --------------------------------------
|
|
187
|
+
|
|
188
|
+
function onKeyDown(e) {
|
|
189
|
+
if (e.key !== "Backspace" || !lastFix) return;
|
|
190
|
+
var c = caret();
|
|
191
|
+
if (!c || c.node !== lastFix.node) { lastFix = null; return; }
|
|
192
|
+
// Only when the caret is still exactly where the correction left it —
|
|
193
|
+
// NOT start+fixed.length, because a word correction fires on the boundary
|
|
194
|
+
// character, which is already sitting after the replaced range.
|
|
195
|
+
if (c.offset !== lastFix.caretAfter) { lastFix = null; return; }
|
|
196
|
+
var text = c.node.nodeValue;
|
|
197
|
+
c.node.nodeValue = text.slice(0, lastFix.start) + lastFix.typed +
|
|
198
|
+
text.slice(lastFix.start + lastFix.fixed.length);
|
|
199
|
+
setCaret(c.node, lastFix.start + lastFix.typed.length + (lastFix.tail || 0));
|
|
200
|
+
lastFix = null;
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
e.stopPropagation();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---- main ------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
function onInput(e) {
|
|
208
|
+
if (!enabled) return;
|
|
209
|
+
var ch = e && typeof e.data === "string" ? e.data : null;
|
|
210
|
+
if (!ch) return; // deletion, paste, composition end
|
|
211
|
+
var c = caret();
|
|
212
|
+
if (!c) return;
|
|
213
|
+
if (inProtectedContext(c.node)) return;
|
|
214
|
+
|
|
215
|
+
if (config.autocorrectTypography !== false && applyInline(c, ch)) return;
|
|
216
|
+
if (isBoundary(ch)) applyWordRules(c, ch);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isBoundary(ch) {
|
|
220
|
+
return ch === " " || ch === " " || ".,;:!?)]}\"'’”".indexOf(ch) >= 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Substitutions that resolve as soon as the triggering character lands.
|
|
224
|
+
function applyInline(c, ch) {
|
|
225
|
+
var text = c.node.nodeValue;
|
|
226
|
+
var at = c.offset;
|
|
227
|
+
|
|
228
|
+
// Straight quote -> curly. Direction depends on what precedes it: after
|
|
229
|
+
// whitespace or an opening bracket it opens, otherwise it closes.
|
|
230
|
+
if (ch === '"' || ch === "'") {
|
|
231
|
+
var prev = at >= 2 ? text.charAt(at - 2) : "";
|
|
232
|
+
var opening = !prev || /[\s(\[{—–\-]/.test(prev);
|
|
233
|
+
var repl = ch === '"' ? (opening ? "“" : "”") : (opening ? "‘" : "’");
|
|
234
|
+
return replaceRange(c, at - 1, at, repl, ch);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Em dash from "--", in both the tight form ("word--word") and the
|
|
238
|
+
// spaced one ("word -- word"), which is how most people actually type it.
|
|
239
|
+
// Both require real text to the left, so a "-- item" list marker at the
|
|
240
|
+
// start of a line and a "--flag" are left alone.
|
|
241
|
+
if (ch === "-" && at >= 2 && text.charAt(at - 2) === "-") {
|
|
242
|
+
var before = at >= 3 ? text.charAt(at - 3) : "";
|
|
243
|
+
var ok = /[\w\)\]"'’]/.test(before);
|
|
244
|
+
if (!ok && /\s/.test(before)) ok = /[\w\)\]"'’]\s*$/.test(text.slice(0, at - 3));
|
|
245
|
+
if (ok) return replaceRange(c, at - 2, at, "—", "--");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (var i = 0; i < SEQUENCES.length; i++) {
|
|
249
|
+
var s = SEQUENCES[i];
|
|
250
|
+
if (s.from.charAt(s.from.length - 1) !== ch) continue;
|
|
251
|
+
var start = at - s.from.length;
|
|
252
|
+
if (start < 0) continue;
|
|
253
|
+
if (text.substring(start, at).toLowerCase() !== s.from) continue;
|
|
254
|
+
// A fraction or arrow glued to a word is probably not one.
|
|
255
|
+
if (/^\d/.test(s.from) && start > 0 && /[\w.]/.test(text.charAt(start - 1))) continue;
|
|
256
|
+
return replaceRange(c, start, at, s.to, s.from);
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Rules that need a completed word: spelling and sentence capitalisation.
|
|
262
|
+
function applyWordRules(c, ch) {
|
|
263
|
+
var text = c.node.nodeValue;
|
|
264
|
+
var end = c.offset - 1; // the boundary char we just typed
|
|
265
|
+
var start = end;
|
|
266
|
+
while (start > 0 && /[A-Za-z'’]/.test(text.charAt(start - 1))) start--;
|
|
267
|
+
if (start === end) return;
|
|
268
|
+
var word = text.substring(start, end);
|
|
269
|
+
|
|
270
|
+
// Spelling THEN capitalisation, applied in sequence to the same word.
|
|
271
|
+
// Treating them as alternatives means "teh cat" opening a sentence comes
|
|
272
|
+
// out as "the cat" — the typo fixed but the sentence left lowercase.
|
|
273
|
+
var fixed = word;
|
|
274
|
+
if (config.autocorrectSpelling !== false) {
|
|
275
|
+
var hit = words()[fixed.toLowerCase()];
|
|
276
|
+
if (hit) fixed = matchCase(fixed, hit);
|
|
277
|
+
// The table stores plain apostrophes so it stays easy to extend, but
|
|
278
|
+
// typing ' produces a curly one — emit the same character here or
|
|
279
|
+
// "don't" ends up with a different apostrophe than "it's".
|
|
280
|
+
if (config.autocorrectTypography !== false) fixed = fixed.replace(/'/g, "’");
|
|
281
|
+
}
|
|
282
|
+
if (config.autocorrectCapitalize !== false && startsSentence(text, start, c.node)) {
|
|
283
|
+
var first = fixed.charAt(0);
|
|
284
|
+
if (first !== first.toUpperCase()) fixed = first.toUpperCase() + fixed.substring(1);
|
|
285
|
+
}
|
|
286
|
+
if (fixed === word) return;
|
|
287
|
+
replaceRange(c, start, end, fixed, word);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// "teh" -> "the", "Teh" -> "The", "TEH" -> "THE".
|
|
291
|
+
function matchCase(typed, fixed) {
|
|
292
|
+
if (typed === typed.toUpperCase() && typed.length > 1) return fixed.toUpperCase();
|
|
293
|
+
if (typed.charAt(0) === typed.charAt(0).toUpperCase()) {
|
|
294
|
+
return fixed.charAt(0).toUpperCase() + fixed.substring(1);
|
|
295
|
+
}
|
|
296
|
+
return fixed;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function startsSentence(text, start, node) {
|
|
300
|
+
for (var i = start - 1; i >= 0; i--) {
|
|
301
|
+
var ch = text.charAt(i);
|
|
302
|
+
if (/\s/.test(ch)) continue;
|
|
303
|
+
if (ch === '"' || ch === "'" || ch === "“" || ch === "‘" || ch === "(") continue;
|
|
304
|
+
return ".!?".indexOf(ch) >= 0;
|
|
305
|
+
}
|
|
306
|
+
// Nothing before it in this node: only a sentence start if nothing
|
|
307
|
+
// precedes the node in its block either.
|
|
308
|
+
return !hasTextBefore(node);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function hasTextBefore(node) {
|
|
312
|
+
var editable = getEditable();
|
|
313
|
+
var n = node;
|
|
314
|
+
while (n && n !== editable) {
|
|
315
|
+
var sib = n.previousSibling;
|
|
316
|
+
while (sib) {
|
|
317
|
+
if ((sib.textContent || "").trim()) return true;
|
|
318
|
+
sib = sib.previousSibling;
|
|
319
|
+
}
|
|
320
|
+
n = n.parentNode;
|
|
321
|
+
// Stop at the block: a new paragraph starts a new sentence.
|
|
322
|
+
if (n && /^(P|LI|TD|TH|H[1-6]|BLOCKQUOTE|DIV)$/.test(n.nodeName)) return false;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function replaceRange(c, start, end, replacement, typed) {
|
|
328
|
+
var node = c.node;
|
|
329
|
+
var text = node.nodeValue;
|
|
330
|
+
// Characters already typed after the range being replaced — for a word
|
|
331
|
+
// rule this is the boundary character (usually a space), because the
|
|
332
|
+
// boundary is what triggered the correction in the first place.
|
|
333
|
+
var tail = c.offset - end;
|
|
334
|
+
node.nodeValue = text.slice(0, start) + replacement + text.slice(end);
|
|
335
|
+
var caretAfter = start + replacement.length + tail;
|
|
336
|
+
setCaret(node, caretAfter);
|
|
337
|
+
lastFix = { node: node, start: start, typed: typed, fixed: replacement, caretAfter: caretAfter, tail: tail };
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Standalone typography pass, for pasted or imported text.
|
|
342
|
+
function typographyPass(text) {
|
|
343
|
+
var out = text;
|
|
344
|
+
for (var i = 0; i < SEQUENCES.length; i++) {
|
|
345
|
+
out = out.split(SEQUENCES[i].from).join(SEQUENCES[i].to);
|
|
346
|
+
}
|
|
347
|
+
out = out.replace(/(^|[\s(\[{])"/g, "$1“").replace(/"/g, "”");
|
|
348
|
+
out = out.replace(/(^|[\s(\[{])'/g, "$1‘").replace(/'/g, "’");
|
|
349
|
+
out = out.replace(/(\w)--(\w)/g, "$1—$2");
|
|
350
|
+
out = out.replace(/(\w)\s--\s(\w)/g, "$1 — $2");
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -15,8 +15,19 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
|
15
15
|
// };
|
|
16
16
|
RTE_DefaultConfig.plugin_bookmarkcard = RTE_Plugin_BookmarkCard;
|
|
17
17
|
RTE_DefaultConfig.bookmarkResolver = RTE_DefaultConfig.bookmarkResolver || null;
|
|
18
|
+
// 2026-07-31 Favicon lookup is OFF by default.
|
|
19
|
+
//
|
|
20
|
+
// This used to default to a third-party favicon service, which meant every
|
|
21
|
+
// bookmark card quietly sent the bookmarked domain to someone else's server —
|
|
22
|
+
// on a self-hosted editor whose whole position is that it makes no outbound
|
|
23
|
+
// calls. That is a privacy leak the host never opted into, and it also puts a
|
|
24
|
+
// public URL in a file that security-scanned deployments have to ship.
|
|
25
|
+
//
|
|
26
|
+
// Opt in by setting the service prefix explicitly, e.g.
|
|
27
|
+
// RTE_DefaultConfig.bookmarkFaviconService = "/favicon-proxy?domain=";
|
|
28
|
+
// or supply favicons through bookmarkResolver, which already returns one.
|
|
18
29
|
RTE_DefaultConfig.bookmarkFaviconService = (typeof RTE_DefaultConfig.bookmarkFaviconService === "undefined")
|
|
19
|
-
? "
|
|
30
|
+
? ""
|
|
20
31
|
: RTE_DefaultConfig.bookmarkFaviconService;
|
|
21
32
|
|
|
22
33
|
function RTE_Plugin_BookmarkCard() {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-07-27 Change case. UPPERCASE / lowercase / Title Case / Sentence case /
|
|
4
|
+
// tOGGLE cASE over the selection.
|
|
5
|
+
//
|
|
6
|
+
// CKEditor 5 case change: "Unlock this feature with selected CKEditor Plans".
|
|
7
|
+
//
|
|
8
|
+
// Design notes:
|
|
9
|
+
// - This rewrites TEXT NODES in place, so inline markup survives: change the
|
|
10
|
+
// case of "the <strong>Data</strong> Act" and the <strong> is still there.
|
|
11
|
+
// A naive getSelectedText()/insertHTML() round-trip would flatten it.
|
|
12
|
+
// - The selected text nodes are joined into one string, transformed, then
|
|
13
|
+
// written back slice by slice. Doing it per-node would break the modes that
|
|
14
|
+
// need context across a node boundary: Sentence case has to know it is at
|
|
15
|
+
// the start of the selection, and Title Case has to see a whole word even
|
|
16
|
+
// when <strong> splits it down the middle.
|
|
17
|
+
// - A collapsed selection operates on the word under the caret, which is what
|
|
18
|
+
// makes the feature usable from a keyboard shortcut without selecting first.
|
|
19
|
+
// - "text-transform" is deliberately NOT used: that is a presentational lie
|
|
20
|
+
// that disappears the moment the HTML is consumed somewhere else. This
|
|
21
|
+
// changes the actual characters.
|
|
22
|
+
RTE_DefaultConfig.plugin_changecase = RTE_Plugin_ChangeCase;
|
|
23
|
+
|
|
24
|
+
// Words left lowercase by Title Case unless they are first or last.
|
|
25
|
+
if (typeof RTE_DefaultConfig.titleCaseSmallWords === "undefined") {
|
|
26
|
+
RTE_DefaultConfig.titleCaseSmallWords =
|
|
27
|
+
"a an and as at but by for if in nor of on or per so the to via vs with";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function RTE_Plugin_ChangeCase() {
|
|
31
|
+
var obj = this;
|
|
32
|
+
var config, editor;
|
|
33
|
+
|
|
34
|
+
obj.PluginName = "ChangeCase";
|
|
35
|
+
|
|
36
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
37
|
+
|
|
38
|
+
obj.InitEditor = function (argeditor) {
|
|
39
|
+
editor = argeditor;
|
|
40
|
+
|
|
41
|
+
editor.attachEvent("exec_command_changecase", function (state) {
|
|
42
|
+
state.returnValue = true;
|
|
43
|
+
obj.Apply(state && state.value ? state.value : "upper");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Public API.
|
|
47
|
+
editor.changeCase = function (mode) { return obj.Apply(mode); };
|
|
48
|
+
editor.getChangeCaseModes = function () {
|
|
49
|
+
return ["upper", "lower", "title", "sentence", "toggle"];
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
|
|
54
|
+
function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
|
|
55
|
+
|
|
56
|
+
// ---- transforms ------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function smallWordSet() {
|
|
59
|
+
var set = {};
|
|
60
|
+
String(config.titleCaseSmallWords || "").split(/\s+/).forEach(function (w) {
|
|
61
|
+
if (w) set[w.toLowerCase()] = true;
|
|
62
|
+
});
|
|
63
|
+
return set;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function transform(text, mode) {
|
|
67
|
+
switch (mode) {
|
|
68
|
+
case "lower": return text.toLowerCase();
|
|
69
|
+
case "upper": return text.toUpperCase();
|
|
70
|
+
case "toggle":
|
|
71
|
+
return text.replace(/\S/g, function (ch) {
|
|
72
|
+
var up = ch.toUpperCase();
|
|
73
|
+
// Characters with no case (digits, CJK, punctuation) compare
|
|
74
|
+
// equal both ways — leave them alone.
|
|
75
|
+
return ch === up ? ch.toLowerCase() : up;
|
|
76
|
+
});
|
|
77
|
+
case "sentence": return sentenceCase(text);
|
|
78
|
+
case "title": return titleCase(text);
|
|
79
|
+
default: return text;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sentenceCase(text) {
|
|
84
|
+
var lower = text.toLowerCase();
|
|
85
|
+
var out = "";
|
|
86
|
+
var startOfSentence = true;
|
|
87
|
+
for (var i = 0; i < lower.length; i++) {
|
|
88
|
+
var ch = lower[i];
|
|
89
|
+
if (startOfSentence && /\S/.test(ch)) {
|
|
90
|
+
out += ch.toUpperCase();
|
|
91
|
+
// A letter opens the sentence; an opening quote or bracket does
|
|
92
|
+
// not, so keep waiting for the real first letter.
|
|
93
|
+
if (/[\p{L}\p{N}]/u.test(ch)) startOfSentence = false;
|
|
94
|
+
} else {
|
|
95
|
+
out += ch;
|
|
96
|
+
}
|
|
97
|
+
if (/[.!?]/.test(ch)) startOfSentence = true;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function titleCase(text) {
|
|
103
|
+
var small = smallWordSet();
|
|
104
|
+
// Split on whitespace but KEEP it, so the original spacing is preserved
|
|
105
|
+
// exactly rather than normalised to single spaces.
|
|
106
|
+
var parts = text.split(/(\s+)/);
|
|
107
|
+
var wordIndex = 0;
|
|
108
|
+
var wordCount = 0;
|
|
109
|
+
var i;
|
|
110
|
+
for (i = 0; i < parts.length; i++) if (!/^\s*$/.test(parts[i])) wordCount++;
|
|
111
|
+
for (i = 0; i < parts.length; i++) {
|
|
112
|
+
if (/^\s*$/.test(parts[i])) continue;
|
|
113
|
+
var isEdge = (wordIndex === 0 || wordIndex === wordCount - 1);
|
|
114
|
+
parts[i] = titleWord(parts[i], small, isEdge);
|
|
115
|
+
wordIndex++;
|
|
116
|
+
}
|
|
117
|
+
return parts.join("");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function titleWord(word, small, isEdge) {
|
|
121
|
+
var lower = word.toLowerCase();
|
|
122
|
+
// Compare on letters only so "the," and "(the" still match "the".
|
|
123
|
+
var bare = lower.replace(/[^\p{L}\p{N}']/gu, "");
|
|
124
|
+
if (!isEdge && small[bare]) return lower;
|
|
125
|
+
// A hyphenated compound is title-cased segment by segment, and the small
|
|
126
|
+
// word rule applies inside it too — "state-of-the-art" is conventionally
|
|
127
|
+
// "State-of-the-Art", not "State-Of-The-Art". Splitting on a capturing
|
|
128
|
+
// group keeps the separators, so odd indices are the separators and even
|
|
129
|
+
// indices are the segments.
|
|
130
|
+
var segs = lower.split(/([-\/])/);
|
|
131
|
+
var last = segs.length - 1;
|
|
132
|
+
for (var i = 0; i <= last; i += 2) {
|
|
133
|
+
var seg = segs[i];
|
|
134
|
+
if (!seg) continue;
|
|
135
|
+
var segBare = seg.replace(/[^\p{L}\p{N}']/gu, "");
|
|
136
|
+
if (i !== 0 && i !== last && small[segBare]) continue; // stays lowercase
|
|
137
|
+
segs[i] = seg.replace(/[\p{L}\p{N}]/u, function (ch) { return ch.toUpperCase(); });
|
|
138
|
+
}
|
|
139
|
+
return segs.join("");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- selection plumbing ----------------------------------------------
|
|
143
|
+
|
|
144
|
+
// Grow a collapsed caret to cover the word it sits in, so the command is
|
|
145
|
+
// useful without a prior selection.
|
|
146
|
+
function expandToWord(range) {
|
|
147
|
+
var node = range.startContainer;
|
|
148
|
+
if (!node || node.nodeType !== 3) return false;
|
|
149
|
+
var text = node.nodeValue || "";
|
|
150
|
+
var start = range.startOffset;
|
|
151
|
+
var end = start;
|
|
152
|
+
while (start > 0 && /[\p{L}\p{N}'']/u.test(text[start - 1])) start--;
|
|
153
|
+
while (end < text.length && /[\p{L}\p{N}'']/u.test(text[end])) end++;
|
|
154
|
+
if (start === end) return false;
|
|
155
|
+
range.setStart(node, start);
|
|
156
|
+
range.setEnd(node, end);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Split the boundary text nodes so the range covers whole nodes only, then
|
|
161
|
+
// collect every text node inside it.
|
|
162
|
+
function textNodesInRange(range, doc) {
|
|
163
|
+
if (range.startContainer.nodeType === 3 && range.startOffset > 0) {
|
|
164
|
+
var s = range.startContainer.splitText(range.startOffset);
|
|
165
|
+
range.setStart(s, 0);
|
|
166
|
+
}
|
|
167
|
+
if (range.endContainer.nodeType === 3 && range.endOffset < range.endContainer.nodeValue.length) {
|
|
168
|
+
range.endContainer.splitText(range.endOffset);
|
|
169
|
+
}
|
|
170
|
+
var root = range.commonAncestorContainer;
|
|
171
|
+
if (root.nodeType === 3) return [root];
|
|
172
|
+
var nodes = [];
|
|
173
|
+
var walker = doc.createTreeWalker(root, 4 /* SHOW_TEXT */, null, false);
|
|
174
|
+
var n;
|
|
175
|
+
while ((n = walker.nextNode())) {
|
|
176
|
+
if (!n.nodeValue) continue;
|
|
177
|
+
if (range.intersectsNode ? range.intersectsNode(n) : true) {
|
|
178
|
+
// intersectsNode is true for the boundary nodes that sit just
|
|
179
|
+
// outside a zero-width touch, so confirm with a real comparison.
|
|
180
|
+
var r = doc.createRange();
|
|
181
|
+
r.selectNodeContents(n);
|
|
182
|
+
if (range.compareBoundaryPoints(Range.END_TO_START, r) < 0 &&
|
|
183
|
+
range.compareBoundaryPoints(Range.START_TO_END, r) > 0) {
|
|
184
|
+
nodes.push(n);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return nodes;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
obj.Apply = function (mode) {
|
|
192
|
+
mode = String(mode || "upper").toLowerCase();
|
|
193
|
+
var doc = getDoc();
|
|
194
|
+
var sel;
|
|
195
|
+
try { sel = editor.getSelection(); } catch (e) { return false; }
|
|
196
|
+
if (!doc || !sel || sel.rangeCount === 0) return false;
|
|
197
|
+
|
|
198
|
+
var range = sel.getRangeAt(0);
|
|
199
|
+
if (range.collapsed && !expandToWord(range)) return false;
|
|
200
|
+
|
|
201
|
+
var nodes = textNodesInRange(range, doc);
|
|
202
|
+
if (!nodes.length) return false;
|
|
203
|
+
|
|
204
|
+
// Join -> transform -> write back, so context-sensitive modes see the
|
|
205
|
+
// whole selection rather than one <strong>'s worth of it.
|
|
206
|
+
var joined = "";
|
|
207
|
+
for (var i = 0; i < nodes.length; i++) joined += nodes[i].nodeValue;
|
|
208
|
+
var result = transform(joined, mode);
|
|
209
|
+
if (result === joined) return false;
|
|
210
|
+
// Defensive: every mode is length-preserving, but if a future mode is
|
|
211
|
+
// not, bail rather than corrupt the document by mis-slicing.
|
|
212
|
+
if (result.length !== joined.length) return false;
|
|
213
|
+
|
|
214
|
+
var at = 0;
|
|
215
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
216
|
+
var len = nodes[j].nodeValue.length;
|
|
217
|
+
nodes[j].nodeValue = result.substr(at, len);
|
|
218
|
+
at += len;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Restore the selection over the transformed run.
|
|
222
|
+
try {
|
|
223
|
+
var r2 = doc.createRange();
|
|
224
|
+
r2.setStart(nodes[0], 0);
|
|
225
|
+
r2.setEnd(nodes[nodes.length - 1], nodes[nodes.length - 1].nodeValue.length);
|
|
226
|
+
sel.removeAllRanges();
|
|
227
|
+
sel.addRange(r2);
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
|
|
230
|
+
fireChange();
|
|
231
|
+
return true;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
function fireChange() {
|
|
235
|
+
try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
|
|
236
|
+
try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
|
|
237
|
+
}
|
|
238
|
+
}
|