@richscripts/richtexteditor 2.6.4 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1138,6 +1138,19 @@ function RTE_Plugin_DocumentImport() {
1138
1138
  // matches the old behaviour rather than inventing <ol>s.
1139
1139
  info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
1140
1140
  }
1141
+ // Word paragraph indentation. Nothing read this before 2026-09-04,
1142
+ // so every indented paragraph in every imported .docx arrived
1143
+ // flush left and the indentation was lost silently.
1144
+ // w:left is in twips; 15 twips = 1px.
1145
+ var ind = firstChildEl(ppr, "ind");
1146
+ if (ind) {
1147
+ var lft = ind.getAttributeNS(W, "left") || ind.getAttribute("w:left") || "";
1148
+ var tw = parseInt(lft, 10);
1149
+ // 720 twips alongside the Quote style is the blockquote's own
1150
+ // indent, already carried by the blockquote element itself.
1151
+ if (tw > 0 && !(info.quote && tw === 720))
1152
+ info.indentPx = Math.round(tw / 15);
1153
+ }
1141
1154
  var jc = firstChildEl(ppr, "jc");
1142
1155
  if (jc) {
1143
1156
  var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
@@ -1204,10 +1217,20 @@ function RTE_Plugin_DocumentImport() {
1204
1217
  continue;
1205
1218
  }
1206
1219
  closeListsTo(0);
1207
- if (st.heading) out.push("<h" + st.heading + ">" + (inner || "") + "</h" + st.heading + ">");
1220
+ if (st.heading) {
1221
+ // Headings carry w:ind too; without this an indented
1222
+ // heading imported flush left.
1223
+ var hs = st.indentPx ? ' style="margin-left:' + st.indentPx + 'px"' : "";
1224
+ out.push("<h" + st.heading + hs + ">" + (inner || "") + "</h" + st.heading + ">");
1225
+ }
1208
1226
  else if (st.quote) out.push("<blockquote>" + (inner || "<br>") + "</blockquote>");
1209
1227
  else {
1210
- var style = st.align ? ' style="text-align:' + st.align + '"' : "";
1228
+ var decl = [];
1229
+ if (st.align) decl.push("text-align:" + st.align);
1230
+ // indentUseMargin defaults to margin, and margin is what
1231
+ // w:ind means, so import writes margin-left to match.
1232
+ if (st.indentPx) decl.push("margin-left:" + st.indentPx + "px");
1233
+ var style = decl.length ? ' style="' + decl.join(";") + '"' : "";
1211
1234
  // A paragraph that contained ONLY a text box has no text of
1212
1235
  // its own; emitting an empty <p> before the box adds a blank
1213
1236
  // line that was never in the document.
@@ -612,6 +612,31 @@ function RTE_Plugin_DocxExport() {
612
612
  var revSeq = { n: 1 };
613
613
  var base = { bold: false, italic: false, underline: false, strike: false, sizeHalfPoints: 0, color: null, highlight: null, vertAlign: null, mono: false };
614
614
 
615
+
616
+ // A block indented in the editor carries margin-left (or padding-left,
617
+ // per indentUseMargin). Word expresses the same thing as w:ind, in
618
+ // TWIPS. Before 2026-09-04 indentation reached Word only when it was
619
+ // spelled <blockquote>, which dragged the Quote style along with it -
620
+ // so indented body text arrived italic and accent-coloured.
621
+ // 1px = 0.75pt and 1pt = 20 twips, hence px * 15.
622
+ function indentTwips(node) {
623
+ if (!node || !node.style) return 0;
624
+ var raw = node.style.marginLeft || node.style.paddingLeft || "";
625
+ var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
626
+ if (!m) return 0;
627
+ var n = parseFloat(m[1]);
628
+ if (!n || n <= 0) return 0;
629
+ var unit = (m[2] || "px").toLowerCase();
630
+ var px = unit === "px" ? n
631
+ : unit === "pt" ? n * (4 / 3)
632
+ : unit === "in" ? n * 96
633
+ : unit === "cm" ? n * 37.795
634
+ : unit === "mm" ? n * 3.7795
635
+ : (unit === "em" || unit === "rem") ? n * 16
636
+ : 0;
637
+ return Math.round(px * 15);
638
+ }
639
+
615
640
  function para(node, style, opts) {
616
641
  opts = opts || {};
617
642
  var pr = "";
@@ -620,6 +645,12 @@ function RTE_Plugin_DocxExport() {
620
645
  if (opts.pre) pr += '<w:pStyle w:val="Code"/>';
621
646
  if (opts.numId) pr += "<w:numPr><w:ilvl w:val=\"" + (opts.level || 0) + "\"/><w:numId w:val=\"" + opts.numId + "\"/></w:numPr>";
622
647
  if (opts.pageBreakBefore) pr += "<w:pageBreakBefore/>";
648
+ // Independent of the Quote style: a real blockquote already got
649
+ // its w:ind above, so only emit this for everything else.
650
+ if (!opts.quote) {
651
+ var indTw = indentTwips(node);
652
+ if (indTw > 0) pr += '<w:ind w:left="' + indTw + '"/>';
653
+ }
623
654
  var align = node && node.style ? node.style.textAlign : "";
624
655
  if (align === "center" || align === "right") pr += '<w:jc w:val="' + align + '"/>';
625
656
  else if (align === "justify") pr += '<w:jc w:val="both"/>';
@@ -0,0 +1,270 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-09-04 Inline code — CKEditor 5's "Code" feature (`<code>`), the one
4
+ // basic-style it ships that this editor had no command for. Markdown `x` was
5
+ // the only way to produce inline code here, which meant there was no way to
6
+ // REMOVE it and no toolbar affordance at all.
7
+ //
8
+ // The command is "inlinecode", NOT "code". "code" is already taken by the
9
+ // HTML-source-view toggle in the core (__Is_Cmd_Active case "code" ->
10
+ // ___Is_CodeMode()), so registering "code" here would have collided with source
11
+ // view. keyboarda11y.js has carried "inlinecode" in its TOGGLE_CMD list all
12
+ // along, with a comment that it matched nothing in this build; a core
13
+ // __Is_Cmd_Active case added alongside this plugin is what makes it match, so
14
+ // the button reports aria-pressed and not just a highlight.
15
+ //
16
+ // Design notes:
17
+ // - Toggling is done by SPLITTING the <code> element around the selection
18
+ // rather than by lifting text nodes out of it. Splitting preserves nested
19
+ // inline markup: un-coding the middle of <code>a <em>b</em> c</code> keeps
20
+ // the <em>. Lifting text nodes would have dropped it.
21
+ // - A collapsed selection acts on the word under the caret. That is the same
22
+ // choice changecase.js makes, and for the same reason: it makes the command
23
+ // usable from a keyboard shortcut without selecting first. The alternative -
24
+ // inserting an empty <code> and putting the caret inside - is the caret trap
25
+ // documented in the footnotes work, so it is deliberately not done.
26
+ // - Never nests inside <pre> or an existing <code>: both already mean
27
+ // "preformatted", and nesting produces markup no serializer round-trips.
28
+ // - Emits a bare <code> element and no rte-* class. That keeps the styling in
29
+ // the content stylesheet where authors can override it, and keeps the plugin
30
+ // out of check-css-contract.mjs, which only scans the four theme sheets.
31
+ RTE_DefaultConfig.plugin_inlinecode = RTE_Plugin_InlineCode;
32
+
33
+ if (!RTE_DefaultConfig.text_inlinecode) RTE_DefaultConfig.text_inlinecode = "Inline code";
34
+
35
+ if (!RTE_DefaultConfig.svgCode_inlinecode) {
36
+ RTE_DefaultConfig.svgCode_inlinecode = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="7 8 3 12 7 16"/><polyline points="17 8 21 12 17 16"/></svg>';
37
+ }
38
+
39
+ function RTE_Plugin_InlineCode() {
40
+ var obj = this;
41
+ var config, editor;
42
+
43
+ obj.PluginName = "InlineCode";
44
+
45
+ obj.InitConfig = function (argconfig) { config = argconfig; };
46
+
47
+ obj.InitEditor = function (argeditor) {
48
+ editor = argeditor;
49
+
50
+ editor.attachEvent("exec_command_inlinecode", function (state) {
51
+ state.returnValue = true;
52
+ obj.Toggle();
53
+ });
54
+
55
+ // Public API, matching the shape the other toggle plugins expose.
56
+ editor.toggleInlineCode = function () { return obj.Toggle(); };
57
+ editor.isInlineCode = function () { return obj.IsActive(); };
58
+ };
59
+
60
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
61
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
62
+
63
+ function getRange(doc) {
64
+ var sel = doc && doc.getSelection ? doc.getSelection() : null;
65
+ if (!sel || !sel.rangeCount) return null;
66
+ return sel.getRangeAt(0);
67
+ }
68
+
69
+ // Nearest <code> ancestor that is still inside the editable area.
70
+ function codeAncestor(node, editable) {
71
+ while (node && node !== editable) {
72
+ if (node.nodeType === 1 && node.nodeName === "CODE") return node;
73
+ node = node.parentNode;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function inPre(node, editable) {
79
+ while (node && node !== editable) {
80
+ if (node.nodeType === 1 && node.nodeName === "PRE") return true;
81
+ node = node.parentNode;
82
+ }
83
+ return false;
84
+ }
85
+
86
+ obj.IsActive = function () {
87
+ var doc = getDoc(), editable = getEditable();
88
+ if (!doc || !editable) return false;
89
+ var range = getRange(doc);
90
+ if (!range) return false;
91
+ return !!codeAncestor(range.startContainer, editable);
92
+ };
93
+
94
+ // ---- selection helpers -----------------------------------------------
95
+
96
+ // Expand a collapsed caret to the word under it. Returns false when the
97
+ // caret is not sitting in a word, in which case the command does nothing
98
+ // rather than creating an empty element.
99
+ function expandToWord(range) {
100
+ var node = range.startContainer;
101
+ if (!node || node.nodeType !== 3) return false;
102
+ var text = node.data || "";
103
+ var offset = range.startOffset;
104
+ var start = offset, end = offset;
105
+ while (start > 0 && /\S/.test(text.charAt(start - 1))) start--;
106
+ while (end < text.length && /\S/.test(text.charAt(end))) end++;
107
+ if (start === end) return false;
108
+ range.setStart(node, start);
109
+ range.setEnd(node, end);
110
+ return true;
111
+ }
112
+
113
+ // Every text node the range actually covers. Boundary text nodes are split
114
+ // first so a partially-selected node is not treated as fully selected.
115
+ function selectedTextNodes(doc, range, editable) {
116
+ if (range.startContainer.nodeType === 3 && range.startOffset > 0) {
117
+ var s = range.startContainer;
118
+ if (range.startOffset < s.data.length) {
119
+ var rest = s.splitText(range.startOffset);
120
+ var endSame = (range.endContainer === s);
121
+ var endOff = range.endOffset;
122
+ range.setStart(rest, 0);
123
+ if (endSame) range.setEnd(rest, endOff - s.data.length);
124
+ }
125
+ }
126
+ if (range.endContainer.nodeType === 3 && range.endOffset < range.endContainer.data.length && range.endOffset > 0) {
127
+ range.endContainer.splitText(range.endOffset);
128
+ }
129
+
130
+ var out = [];
131
+ var walker = doc.createTreeWalker(editable, 4 /* SHOW_TEXT */, null, false);
132
+ var n;
133
+ while ((n = walker.nextNode())) {
134
+ if (!n.data) continue;
135
+ if (range.intersectsNode ? range.intersectsNode(n) : true) {
136
+ // intersectsNode is true for merely-touching nodes, so confirm
137
+ // the node is genuinely within the boundaries.
138
+ var r = doc.createRange();
139
+ r.selectNodeContents(n);
140
+ if (range.compareBoundaryPoints(Range.END_TO_START, r) < 0 &&
141
+ range.compareBoundaryPoints(Range.START_TO_END, r) > 0) {
142
+ out.push(n);
143
+ }
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ // ---- apply / remove ---------------------------------------------------
150
+
151
+ // Split `code` around the selection and unwrap only the overlapping part,
152
+ // so partial removal keeps the untouched halves coded.
153
+ function unwrapPart(doc, code, range) {
154
+ var parent = code.parentNode;
155
+ if (!parent) return;
156
+
157
+ var startsInside = code.contains(range.startContainer);
158
+ var endsInside = code.contains(range.endContainer);
159
+
160
+ // Tail first: extracting it cannot disturb the head's boundaries.
161
+ var after = doc.createRange();
162
+ if (endsInside) after.setStart(range.endContainer, range.endOffset);
163
+ else after.setStart(code, code.childNodes.length);
164
+ after.setEnd(code, code.childNodes.length);
165
+ var afterFrag = after.extractContents();
166
+
167
+ var before = doc.createRange();
168
+ before.setStart(code, 0);
169
+ if (startsInside) before.setEnd(range.startContainer, range.startOffset);
170
+ else before.setEnd(code, 0);
171
+ var beforeFrag = before.extractContents();
172
+
173
+ if (beforeFrag && beforeFrag.textContent !== "") {
174
+ var head = doc.createElement("code");
175
+ head.appendChild(beforeFrag);
176
+ parent.insertBefore(head, code);
177
+ }
178
+
179
+ // What is left in `code` is exactly the selected part: unwrap it.
180
+ while (code.firstChild) parent.insertBefore(code.firstChild, code);
181
+
182
+ if (afterFrag && afterFrag.textContent !== "") {
183
+ var tail = doc.createElement("code");
184
+ tail.appendChild(afterFrag);
185
+ parent.insertBefore(tail, code);
186
+ }
187
+
188
+ parent.removeChild(code);
189
+ }
190
+
191
+ function wrapNodes(doc, nodes, editable) {
192
+ // Group contiguous siblings so a run of text under one parent becomes a
193
+ // single <code>, not one per text node.
194
+ var group = [], groups = [];
195
+ for (var i = 0; i < nodes.length; i++) {
196
+ var n = nodes[i];
197
+ if (inPre(n, editable) || codeAncestor(n, editable)) continue;
198
+ if (group.length && group[group.length - 1].parentNode === n.parentNode &&
199
+ group[group.length - 1].nextSibling === n) {
200
+ group.push(n);
201
+ } else {
202
+ if (group.length) groups.push(group);
203
+ group = [n];
204
+ }
205
+ }
206
+ if (group.length) groups.push(group);
207
+
208
+ for (var g = 0; g < groups.length; g++) {
209
+ var run = groups[g];
210
+ // Whitespace-only runs would produce a <code> containing nothing but
211
+ // a space, which is invisible formatting debris.
212
+ var joined = "";
213
+ for (var k = 0; k < run.length; k++) joined += run[k].data;
214
+ if (!/\S/.test(joined)) continue;
215
+
216
+ var code = doc.createElement("code");
217
+ run[0].parentNode.insertBefore(code, run[0]);
218
+ for (var j = 0; j < run.length; j++) code.appendChild(run[j]);
219
+ }
220
+ }
221
+
222
+ // Adjacent <code><code> pairs read as one to the user but serialize as two.
223
+ function mergeAdjacent(editable) {
224
+ var all = editable.querySelectorAll("code");
225
+ for (var i = 0; i < all.length; i++) {
226
+ var code = all[i];
227
+ var next = code.nextSibling;
228
+ while (next && next.nodeType === 1 && next.nodeName === "CODE") {
229
+ while (next.firstChild) code.appendChild(next.firstChild);
230
+ next.parentNode.removeChild(next);
231
+ next = code.nextSibling;
232
+ }
233
+ }
234
+ }
235
+
236
+ obj.Toggle = function () {
237
+ var doc = getDoc(), editable = getEditable();
238
+ if (!doc || !editable) return false;
239
+ var range = getRange(doc);
240
+ if (!range) return false;
241
+ if (!editable.contains(range.startContainer)) return false;
242
+
243
+ if (range.collapsed && !expandToWord(range)) return false;
244
+
245
+ var nodes = selectedTextNodes(doc, range, editable);
246
+ if (!nodes.length) return false;
247
+
248
+ // Remove when every selected text node is already coded; otherwise apply.
249
+ var allCoded = true;
250
+ for (var i = 0; i < nodes.length; i++) {
251
+ if (!/\S/.test(nodes[i].data)) continue;
252
+ if (!codeAncestor(nodes[i], editable)) { allCoded = false; break; }
253
+ }
254
+
255
+ if (allCoded) {
256
+ var codes = [], seen = [];
257
+ for (var n = 0; n < nodes.length; n++) {
258
+ var c = codeAncestor(nodes[n], editable);
259
+ if (c && seen.indexOf(c) < 0) { seen.push(c); codes.push(c); }
260
+ }
261
+ for (var x = 0; x < codes.length; x++) unwrapPart(doc, codes[x], range);
262
+ } else {
263
+ wrapNodes(doc, nodes, editable);
264
+ mergeAdjacent(editable);
265
+ }
266
+
267
+ try { editor.updateToolbarStatus && editor.updateToolbarStatus(); } catch (e) { }
268
+ return true;
269
+ };
270
+ }
@@ -393,6 +393,28 @@ function RTE_Plugin_PdfExport() {
393
393
  var self = this;
394
394
  var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
395
395
 
396
+ // A block indented in the editor carries margin-left (or padding-left, per
397
+ // indentUseMargin). Before 2026-09-04 the PDF honoured an indent only when
398
+ // it was spelled <blockquote>, because that was the only way the editor
399
+ // could express one. Points, since that is the PDF's unit: 1px = 0.75pt.
400
+ function cssIndentPt(node) {
401
+ if (!node || !node.style) return 0;
402
+ var raw = node.style.marginLeft || node.style.paddingLeft || "";
403
+ var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
404
+ if (!m) return 0;
405
+ var n = parseFloat(m[1]);
406
+ if (!n || n <= 0) return 0;
407
+ var unit = (m[2] || "px").toLowerCase();
408
+ var px = unit === "px" ? n
409
+ : unit === "pt" ? n * (4 / 3)
410
+ : unit === "in" ? n * 96
411
+ : unit === "cm" ? n * 37.795
412
+ : unit === "mm" ? n * 3.7795
413
+ : (unit === "em" || unit === "rem") ? n * 16
414
+ : 0;
415
+ return px * 0.75;
416
+ }
417
+
396
418
  function block(node, style, indent, listMarker, listId, listOrdered) {
397
419
  var tag = node.tagName.toLowerCase();
398
420
  var s = styleOf(node, style);
@@ -465,8 +487,9 @@ function RTE_Plugin_PdfExport() {
465
487
  continue;
466
488
  }
467
489
  if (isBlockTag(tag)) {
468
- if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0)); continue; }
469
- block(c, style, indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0), null);
490
+ var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
491
+ if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + own); continue; }
492
+ block(c, style, indent + own, null);
470
493
  continue;
471
494
  }
472
495
  // Inline content sitting directly under a container.
@@ -146,7 +146,9 @@ function RTE_Plugin_WordExport() {
146
146
  "table { border-collapse: collapse; }\r\n" +
147
147
  "td, th { border: 1px solid #999; padding: 4px 8px; }\r\n" +
148
148
  "img { max-width: 100%; height: auto; }\r\n" +
149
- "blockquote { border-left: 3px solid #ccc; margin-left: 0; padding-left: 12px; color: #444; }\r\n" +
149
+ // margin-left was zeroed here, which also flattened any indentation
150
+ // expressed as a margin. Keep the quote bar, keep the offset.
151
+ "blockquote { border-left: 3px solid #ccc; padding-left: 12px; color: #444; }\r\n" +
150
152
  "pre { font-family: Consolas, 'Courier New', monospace; background: #f4f4f4; padding: 8px; }\r\n" +
151
153
  "</style>\r\n" +
152
154
  "</head>\r\n" +