@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.
package/README.md CHANGED
@@ -446,4 +446,24 @@ In order to start using Rich Text Editor, configure or customize it further, ple
446
446
 
447
447
  ## License
448
448
 
449
- For licensing information please read [Licensing Info](http://richtexteditor.com/license.aspx).
449
+ For licensing information please read [Licensing Info](http://richtexteditor.com/license.aspx).
450
+
451
+ ### State inspector (developer tool)
452
+
453
+ A derived view of the document the editor is actually holding: a serializable
454
+ snapshot (node tree, selection, plugin initialization order) plus structural
455
+ checks for the faults that break documents downstream - zero-length text nodes,
456
+ empty inline wrappers, duplicate nested formatting, control characters that make
457
+ an exported .docx unreadable, unsafe URL schemes, `iframe[srcdoc]`, orphaned
458
+ list items, images with no alt text, and unprotected atomic content.
459
+
460
+ ```js
461
+ editor.stateInspector.open(); // panel with Issues / Tree / Selection / Plugins / Log
462
+ editor.stateInspector.issues(); // [{ id, severity, message, detail }]
463
+ editor.stateInspector.snapshot(); // plain JSON - attach this to a bug report
464
+ ```
465
+
466
+ It is **not** in any default toolbar. Add the button with
467
+ `stateInspectorToolbarButton: true`, open it from the API, or type `/inspect`.
468
+ Other options: `stateInspectorEnabled`, `stateInspectorAutoOpen`,
469
+ `stateInspectorMaxDepth`, `stateInspectorLogLimit`.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.6.4",
6
+ "version": "2.7.0",
7
7
  "description": "A full-featured JavaScript rich text editor (WYSIWYG) with toolbar, plugins, i18n support, a built-in AI Toolkit (Ask AI, AI Chat, AI Review drawer, BYOK resolver), structured content APIs (JSON/Markdown), and an optional collaboration / review stack: slash commands, @mentions, human-author Track Changes, threaded comments, revision history, and Yjs-backed presence. Ships first-class React, Vue, Angular and Svelte bindings, a headless \"bring your own UI\" core, and server-safe conversion helpers.",
8
8
  "main": "./richtexteditor/rte.js",
9
9
  "bin": {
@@ -133,7 +133,7 @@
133
133
  "ai-editor"
134
134
  ],
135
135
  "author": "CuteSoft Components Inc.",
136
- "license": "SEE LICENSE IN http://richtexteditor.com/license.aspx",
136
+ "license": "SEE LICENSE IN https://richtexteditor.com/license",
137
137
  "homepage": "https://richtexteditor.com",
138
138
  "repository": {
139
139
  "type": "git",
@@ -24520,6 +24520,19 @@ function RTE_Plugin_DocumentImport() {
24520
24520
  // matches the old behaviour rather than inventing <ol>s.
24521
24521
  info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
24522
24522
  }
24523
+ // Word paragraph indentation. Nothing read this before 2026-09-04,
24524
+ // so every indented paragraph in every imported .docx arrived
24525
+ // flush left and the indentation was lost silently.
24526
+ // w:left is in twips; 15 twips = 1px.
24527
+ var ind = firstChildEl(ppr, "ind");
24528
+ if (ind) {
24529
+ var lft = ind.getAttributeNS(W, "left") || ind.getAttribute("w:left") || "";
24530
+ var tw = parseInt(lft, 10);
24531
+ // 720 twips alongside the Quote style is the blockquote's own
24532
+ // indent, already carried by the blockquote element itself.
24533
+ if (tw > 0 && !(info.quote && tw === 720))
24534
+ info.indentPx = Math.round(tw / 15);
24535
+ }
24523
24536
  var jc = firstChildEl(ppr, "jc");
24524
24537
  if (jc) {
24525
24538
  var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
@@ -24586,10 +24599,20 @@ function RTE_Plugin_DocumentImport() {
24586
24599
  continue;
24587
24600
  }
24588
24601
  closeListsTo(0);
24589
- if (st.heading) out.push("<h" + st.heading + ">" + (inner || "") + "</h" + st.heading + ">");
24602
+ if (st.heading) {
24603
+ // Headings carry w:ind too; without this an indented
24604
+ // heading imported flush left.
24605
+ var hs = st.indentPx ? ' style="margin-left:' + st.indentPx + 'px"' : "";
24606
+ out.push("<h" + st.heading + hs + ">" + (inner || "") + "</h" + st.heading + ">");
24607
+ }
24590
24608
  else if (st.quote) out.push("<blockquote>" + (inner || "<br>") + "</blockquote>");
24591
24609
  else {
24592
- var style = st.align ? ' style="text-align:' + st.align + '"' : "";
24610
+ var decl = [];
24611
+ if (st.align) decl.push("text-align:" + st.align);
24612
+ // indentUseMargin defaults to margin, and margin is what
24613
+ // w:ind means, so import writes margin-left to match.
24614
+ if (st.indentPx) decl.push("margin-left:" + st.indentPx + "px");
24615
+ var style = decl.length ? ' style="' + decl.join(";") + '"' : "";
24593
24616
  // A paragraph that contained ONLY a text box has no text of
24594
24617
  // its own; emitting an empty <p> before the box adds a blank
24595
24618
  // line that was never in the document.
@@ -25612,6 +25635,31 @@ function RTE_Plugin_DocxExport() {
25612
25635
  var revSeq = { n: 1 };
25613
25636
  var base = { bold: false, italic: false, underline: false, strike: false, sizeHalfPoints: 0, color: null, highlight: null, vertAlign: null, mono: false };
25614
25637
 
25638
+
25639
+ // A block indented in the editor carries margin-left (or padding-left,
25640
+ // per indentUseMargin). Word expresses the same thing as w:ind, in
25641
+ // TWIPS. Before 2026-09-04 indentation reached Word only when it was
25642
+ // spelled <blockquote>, which dragged the Quote style along with it -
25643
+ // so indented body text arrived italic and accent-coloured.
25644
+ // 1px = 0.75pt and 1pt = 20 twips, hence px * 15.
25645
+ function indentTwips(node) {
25646
+ if (!node || !node.style) return 0;
25647
+ var raw = node.style.marginLeft || node.style.paddingLeft || "";
25648
+ var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
25649
+ if (!m) return 0;
25650
+ var n = parseFloat(m[1]);
25651
+ if (!n || n <= 0) return 0;
25652
+ var unit = (m[2] || "px").toLowerCase();
25653
+ var px = unit === "px" ? n
25654
+ : unit === "pt" ? n * (4 / 3)
25655
+ : unit === "in" ? n * 96
25656
+ : unit === "cm" ? n * 37.795
25657
+ : unit === "mm" ? n * 3.7795
25658
+ : (unit === "em" || unit === "rem") ? n * 16
25659
+ : 0;
25660
+ return Math.round(px * 15);
25661
+ }
25662
+
25615
25663
  function para(node, style, opts) {
25616
25664
  opts = opts || {};
25617
25665
  var pr = "";
@@ -25620,6 +25668,12 @@ function RTE_Plugin_DocxExport() {
25620
25668
  if (opts.pre) pr += '<w:pStyle w:val="Code"/>';
25621
25669
  if (opts.numId) pr += "<w:numPr><w:ilvl w:val=\"" + (opts.level || 0) + "\"/><w:numId w:val=\"" + opts.numId + "\"/></w:numPr>";
25622
25670
  if (opts.pageBreakBefore) pr += "<w:pageBreakBefore/>";
25671
+ // Independent of the Quote style: a real blockquote already got
25672
+ // its w:ind above, so only emit this for everything else.
25673
+ if (!opts.quote) {
25674
+ var indTw = indentTwips(node);
25675
+ if (indTw > 0) pr += '<w:ind w:left="' + indTw + '"/>';
25676
+ }
25623
25677
  var align = node && node.style ? node.style.textAlign : "";
25624
25678
  if (align === "center" || align === "right") pr += '<w:jc w:val="' + align + '"/>';
25625
25679
  else if (align === "justify") pr += '<w:jc w:val="both"/>';
@@ -29261,6 +29315,277 @@ function RTE_Plugin_ImportStyles() {
29261
29315
  }
29262
29316
  }
29263
29317
 
29318
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
29319
+
29320
+ // 2026-09-04 Inline code — CKEditor 5's "Code" feature (`<code>`), the one
29321
+ // basic-style it ships that this editor had no command for. Markdown `x` was
29322
+ // the only way to produce inline code here, which meant there was no way to
29323
+ // REMOVE it and no toolbar affordance at all.
29324
+ //
29325
+ // The command is "inlinecode", NOT "code". "code" is already taken by the
29326
+ // HTML-source-view toggle in the core (__Is_Cmd_Active case "code" ->
29327
+ // ___Is_CodeMode()), so registering "code" here would have collided with source
29328
+ // view. keyboarda11y.js has carried "inlinecode" in its TOGGLE_CMD list all
29329
+ // along, with a comment that it matched nothing in this build; a core
29330
+ // __Is_Cmd_Active case added alongside this plugin is what makes it match, so
29331
+ // the button reports aria-pressed and not just a highlight.
29332
+ //
29333
+ // Design notes:
29334
+ // - Toggling is done by SPLITTING the <code> element around the selection
29335
+ // rather than by lifting text nodes out of it. Splitting preserves nested
29336
+ // inline markup: un-coding the middle of <code>a <em>b</em> c</code> keeps
29337
+ // the <em>. Lifting text nodes would have dropped it.
29338
+ // - A collapsed selection acts on the word under the caret. That is the same
29339
+ // choice changecase.js makes, and for the same reason: it makes the command
29340
+ // usable from a keyboard shortcut without selecting first. The alternative -
29341
+ // inserting an empty <code> and putting the caret inside - is the caret trap
29342
+ // documented in the footnotes work, so it is deliberately not done.
29343
+ // - Never nests inside <pre> or an existing <code>: both already mean
29344
+ // "preformatted", and nesting produces markup no serializer round-trips.
29345
+ // - Emits a bare <code> element and no rte-* class. That keeps the styling in
29346
+ // the content stylesheet where authors can override it, and keeps the plugin
29347
+ // out of check-css-contract.mjs, which only scans the four theme sheets.
29348
+ RTE_DefaultConfig.plugin_inlinecode = RTE_Plugin_InlineCode;
29349
+
29350
+ if (!RTE_DefaultConfig.text_inlinecode) RTE_DefaultConfig.text_inlinecode = "Inline code";
29351
+
29352
+ if (!RTE_DefaultConfig.svgCode_inlinecode) {
29353
+ 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>';
29354
+ }
29355
+
29356
+ function RTE_Plugin_InlineCode() {
29357
+ var obj = this;
29358
+ var config, editor;
29359
+
29360
+ obj.PluginName = "InlineCode";
29361
+
29362
+ obj.InitConfig = function (argconfig) { config = argconfig; };
29363
+
29364
+ obj.InitEditor = function (argeditor) {
29365
+ editor = argeditor;
29366
+
29367
+ editor.attachEvent("exec_command_inlinecode", function (state) {
29368
+ state.returnValue = true;
29369
+ obj.Toggle();
29370
+ });
29371
+
29372
+ // Public API, matching the shape the other toggle plugins expose.
29373
+ editor.toggleInlineCode = function () { return obj.Toggle(); };
29374
+ editor.isInlineCode = function () { return obj.IsActive(); };
29375
+ };
29376
+
29377
+ function getDoc() { try { return editor.getDocument(); } catch (e) { return null; } }
29378
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
29379
+
29380
+ function getRange(doc) {
29381
+ var sel = doc && doc.getSelection ? doc.getSelection() : null;
29382
+ if (!sel || !sel.rangeCount) return null;
29383
+ return sel.getRangeAt(0);
29384
+ }
29385
+
29386
+ // Nearest <code> ancestor that is still inside the editable area.
29387
+ function codeAncestor(node, editable) {
29388
+ while (node && node !== editable) {
29389
+ if (node.nodeType === 1 && node.nodeName === "CODE") return node;
29390
+ node = node.parentNode;
29391
+ }
29392
+ return null;
29393
+ }
29394
+
29395
+ function inPre(node, editable) {
29396
+ while (node && node !== editable) {
29397
+ if (node.nodeType === 1 && node.nodeName === "PRE") return true;
29398
+ node = node.parentNode;
29399
+ }
29400
+ return false;
29401
+ }
29402
+
29403
+ obj.IsActive = function () {
29404
+ var doc = getDoc(), editable = getEditable();
29405
+ if (!doc || !editable) return false;
29406
+ var range = getRange(doc);
29407
+ if (!range) return false;
29408
+ return !!codeAncestor(range.startContainer, editable);
29409
+ };
29410
+
29411
+ // ---- selection helpers -----------------------------------------------
29412
+
29413
+ // Expand a collapsed caret to the word under it. Returns false when the
29414
+ // caret is not sitting in a word, in which case the command does nothing
29415
+ // rather than creating an empty element.
29416
+ function expandToWord(range) {
29417
+ var node = range.startContainer;
29418
+ if (!node || node.nodeType !== 3) return false;
29419
+ var text = node.data || "";
29420
+ var offset = range.startOffset;
29421
+ var start = offset, end = offset;
29422
+ while (start > 0 && /\S/.test(text.charAt(start - 1))) start--;
29423
+ while (end < text.length && /\S/.test(text.charAt(end))) end++;
29424
+ if (start === end) return false;
29425
+ range.setStart(node, start);
29426
+ range.setEnd(node, end);
29427
+ return true;
29428
+ }
29429
+
29430
+ // Every text node the range actually covers. Boundary text nodes are split
29431
+ // first so a partially-selected node is not treated as fully selected.
29432
+ function selectedTextNodes(doc, range, editable) {
29433
+ if (range.startContainer.nodeType === 3 && range.startOffset > 0) {
29434
+ var s = range.startContainer;
29435
+ if (range.startOffset < s.data.length) {
29436
+ var rest = s.splitText(range.startOffset);
29437
+ var endSame = (range.endContainer === s);
29438
+ var endOff = range.endOffset;
29439
+ range.setStart(rest, 0);
29440
+ if (endSame) range.setEnd(rest, endOff - s.data.length);
29441
+ }
29442
+ }
29443
+ if (range.endContainer.nodeType === 3 && range.endOffset < range.endContainer.data.length && range.endOffset > 0) {
29444
+ range.endContainer.splitText(range.endOffset);
29445
+ }
29446
+
29447
+ var out = [];
29448
+ var walker = doc.createTreeWalker(editable, 4 /* SHOW_TEXT */, null, false);
29449
+ var n;
29450
+ while ((n = walker.nextNode())) {
29451
+ if (!n.data) continue;
29452
+ if (range.intersectsNode ? range.intersectsNode(n) : true) {
29453
+ // intersectsNode is true for merely-touching nodes, so confirm
29454
+ // the node is genuinely within the boundaries.
29455
+ var r = doc.createRange();
29456
+ r.selectNodeContents(n);
29457
+ if (range.compareBoundaryPoints(Range.END_TO_START, r) < 0 &&
29458
+ range.compareBoundaryPoints(Range.START_TO_END, r) > 0) {
29459
+ out.push(n);
29460
+ }
29461
+ }
29462
+ }
29463
+ return out;
29464
+ }
29465
+
29466
+ // ---- apply / remove ---------------------------------------------------
29467
+
29468
+ // Split `code` around the selection and unwrap only the overlapping part,
29469
+ // so partial removal keeps the untouched halves coded.
29470
+ function unwrapPart(doc, code, range) {
29471
+ var parent = code.parentNode;
29472
+ if (!parent) return;
29473
+
29474
+ var startsInside = code.contains(range.startContainer);
29475
+ var endsInside = code.contains(range.endContainer);
29476
+
29477
+ // Tail first: extracting it cannot disturb the head's boundaries.
29478
+ var after = doc.createRange();
29479
+ if (endsInside) after.setStart(range.endContainer, range.endOffset);
29480
+ else after.setStart(code, code.childNodes.length);
29481
+ after.setEnd(code, code.childNodes.length);
29482
+ var afterFrag = after.extractContents();
29483
+
29484
+ var before = doc.createRange();
29485
+ before.setStart(code, 0);
29486
+ if (startsInside) before.setEnd(range.startContainer, range.startOffset);
29487
+ else before.setEnd(code, 0);
29488
+ var beforeFrag = before.extractContents();
29489
+
29490
+ if (beforeFrag && beforeFrag.textContent !== "") {
29491
+ var head = doc.createElement("code");
29492
+ head.appendChild(beforeFrag);
29493
+ parent.insertBefore(head, code);
29494
+ }
29495
+
29496
+ // What is left in `code` is exactly the selected part: unwrap it.
29497
+ while (code.firstChild) parent.insertBefore(code.firstChild, code);
29498
+
29499
+ if (afterFrag && afterFrag.textContent !== "") {
29500
+ var tail = doc.createElement("code");
29501
+ tail.appendChild(afterFrag);
29502
+ parent.insertBefore(tail, code);
29503
+ }
29504
+
29505
+ parent.removeChild(code);
29506
+ }
29507
+
29508
+ function wrapNodes(doc, nodes, editable) {
29509
+ // Group contiguous siblings so a run of text under one parent becomes a
29510
+ // single <code>, not one per text node.
29511
+ var group = [], groups = [];
29512
+ for (var i = 0; i < nodes.length; i++) {
29513
+ var n = nodes[i];
29514
+ if (inPre(n, editable) || codeAncestor(n, editable)) continue;
29515
+ if (group.length && group[group.length - 1].parentNode === n.parentNode &&
29516
+ group[group.length - 1].nextSibling === n) {
29517
+ group.push(n);
29518
+ } else {
29519
+ if (group.length) groups.push(group);
29520
+ group = [n];
29521
+ }
29522
+ }
29523
+ if (group.length) groups.push(group);
29524
+
29525
+ for (var g = 0; g < groups.length; g++) {
29526
+ var run = groups[g];
29527
+ // Whitespace-only runs would produce a <code> containing nothing but
29528
+ // a space, which is invisible formatting debris.
29529
+ var joined = "";
29530
+ for (var k = 0; k < run.length; k++) joined += run[k].data;
29531
+ if (!/\S/.test(joined)) continue;
29532
+
29533
+ var code = doc.createElement("code");
29534
+ run[0].parentNode.insertBefore(code, run[0]);
29535
+ for (var j = 0; j < run.length; j++) code.appendChild(run[j]);
29536
+ }
29537
+ }
29538
+
29539
+ // Adjacent <code><code> pairs read as one to the user but serialize as two.
29540
+ function mergeAdjacent(editable) {
29541
+ var all = editable.querySelectorAll("code");
29542
+ for (var i = 0; i < all.length; i++) {
29543
+ var code = all[i];
29544
+ var next = code.nextSibling;
29545
+ while (next && next.nodeType === 1 && next.nodeName === "CODE") {
29546
+ while (next.firstChild) code.appendChild(next.firstChild);
29547
+ next.parentNode.removeChild(next);
29548
+ next = code.nextSibling;
29549
+ }
29550
+ }
29551
+ }
29552
+
29553
+ obj.Toggle = function () {
29554
+ var doc = getDoc(), editable = getEditable();
29555
+ if (!doc || !editable) return false;
29556
+ var range = getRange(doc);
29557
+ if (!range) return false;
29558
+ if (!editable.contains(range.startContainer)) return false;
29559
+
29560
+ if (range.collapsed && !expandToWord(range)) return false;
29561
+
29562
+ var nodes = selectedTextNodes(doc, range, editable);
29563
+ if (!nodes.length) return false;
29564
+
29565
+ // Remove when every selected text node is already coded; otherwise apply.
29566
+ var allCoded = true;
29567
+ for (var i = 0; i < nodes.length; i++) {
29568
+ if (!/\S/.test(nodes[i].data)) continue;
29569
+ if (!codeAncestor(nodes[i], editable)) { allCoded = false; break; }
29570
+ }
29571
+
29572
+ if (allCoded) {
29573
+ var codes = [], seen = [];
29574
+ for (var n = 0; n < nodes.length; n++) {
29575
+ var c = codeAncestor(nodes[n], editable);
29576
+ if (c && seen.indexOf(c) < 0) { seen.push(c); codes.push(c); }
29577
+ }
29578
+ for (var x = 0; x < codes.length; x++) unwrapPart(doc, codes[x], range);
29579
+ } else {
29580
+ wrapNodes(doc, nodes, editable);
29581
+ mergeAdjacent(editable);
29582
+ }
29583
+
29584
+ try { editor.updateToolbarStatus && editor.updateToolbarStatus(); } catch (e) { }
29585
+ return true;
29586
+ };
29587
+ }
29588
+
29264
29589
 
29265
29590
  if (!RTE_DefaultConfig.svgCode_insertcode) {
29266
29591
  RTE_DefaultConfig.svgCode_insertcode = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 7 3 12 8 17"/><polyline points="16 7 21 12 16 17"/><line x1="14" y1="5" x2="10" y2="19"/></svg>';
@@ -35450,6 +35775,28 @@ function RTE_Plugin_PdfExport() {
35450
35775
  var self = this;
35451
35776
  var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
35452
35777
 
35778
+ // A block indented in the editor carries margin-left (or padding-left, per
35779
+ // indentUseMargin). Before 2026-09-04 the PDF honoured an indent only when
35780
+ // it was spelled <blockquote>, because that was the only way the editor
35781
+ // could express one. Points, since that is the PDF's unit: 1px = 0.75pt.
35782
+ function cssIndentPt(node) {
35783
+ if (!node || !node.style) return 0;
35784
+ var raw = node.style.marginLeft || node.style.paddingLeft || "";
35785
+ var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
35786
+ if (!m) return 0;
35787
+ var n = parseFloat(m[1]);
35788
+ if (!n || n <= 0) return 0;
35789
+ var unit = (m[2] || "px").toLowerCase();
35790
+ var px = unit === "px" ? n
35791
+ : unit === "pt" ? n * (4 / 3)
35792
+ : unit === "in" ? n * 96
35793
+ : unit === "cm" ? n * 37.795
35794
+ : unit === "mm" ? n * 3.7795
35795
+ : (unit === "em" || unit === "rem") ? n * 16
35796
+ : 0;
35797
+ return px * 0.75;
35798
+ }
35799
+
35453
35800
  function block(node, style, indent, listMarker, listId, listOrdered) {
35454
35801
  var tag = node.tagName.toLowerCase();
35455
35802
  var s = styleOf(node, style);
@@ -35522,8 +35869,9 @@ function RTE_Plugin_PdfExport() {
35522
35869
  continue;
35523
35870
  }
35524
35871
  if (isBlockTag(tag)) {
35525
- if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0)); continue; }
35526
- block(c, style, indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0), null);
35872
+ var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
35873
+ if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + own); continue; }
35874
+ block(c, style, indent + own, null);
35527
35875
  continue;
35528
35876
  }
35529
35877
  // Inline content sitting directly under a container.
@@ -43026,7 +43374,9 @@ function RTE_Plugin_WordExport() {
43026
43374
  "table { border-collapse: collapse; }\r\n" +
43027
43375
  "td, th { border: 1px solid #999; padding: 4px 8px; }\r\n" +
43028
43376
  "img { max-width: 100%; height: auto; }\r\n" +
43029
- "blockquote { border-left: 3px solid #ccc; margin-left: 0; padding-left: 12px; color: #444; }\r\n" +
43377
+ // margin-left was zeroed here, which also flattened any indentation
43378
+ // expressed as a margin. Keep the quote bar, keep the offset.
43379
+ "blockquote { border-left: 3px solid #ccc; padding-left: 12px; color: #444; }\r\n" +
43030
43380
  "pre { font-family: Consolas, 'Courier New', monospace; background: #f4f4f4; padding: 8px; }\r\n" +
43031
43381
  "</style>\r\n" +
43032
43382
  "</head>\r\n" +