@bobfrankston/rmfmail 1.2.184 → 1.2.185

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.
@@ -2080,7 +2080,9 @@ async function createTinyMceEditor(container2, opts = {}) {
2080
2080
  plugins: "lists advlist link table code codesample image searchreplace autolink wordcount emoticons charmap insertdatetime quickbars nonbreaking directionality help",
2081
2081
  toolbar: [
2082
2082
  "undo redo | bold italic underline strikethrough | forecolor backcolor",
2083
- "bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help"
2083
+ // blockquote on the main bar (was quickbar-only) Bob
2084
+ // 2026-07-28: pasted/inserted text needs a one-click quote.
2085
+ "blockquote bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help"
2084
2086
  ].join(" | "),
2085
2087
  // Include "tools" so wordcount and searchreplace are reachable.
2086
2088
  menubar: "file edit view insert format tools",
@@ -2411,9 +2413,9 @@ async function createTinyMceEditor(container2, opts = {}) {
2411
2413
  onSubmit: (api) => {
2412
2414
  const data = api.getData();
2413
2415
  const lang = data.language || "text";
2414
- const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2416
+ const esc2 = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2415
2417
  const borderStyle = data.border ? ` style="${Object.entries(BORDER_STYLES).map(([k, v]) => `${k}:${v}`).join(";")}"` : "";
2416
- const html = `<pre class="language-${lang}"${borderStyle}><code>${esc(data.code || "")}</code></pre>`;
2418
+ const html = `<pre class="language-${lang}"${borderStyle}><code>${esc2(data.code || "")}</code></pre>`;
2417
2419
  if (preEl && preEl.parentNode) {
2418
2420
  ed.dom.setOuterHTML(preEl, html);
2419
2421
  } else {
@@ -3100,8 +3102,8 @@ function initHarperLint(ed) {
3100
3102
  const nat = ed.nativeEditor;
3101
3103
  if (nat?.selection?.setRng && nat?.insertContent) {
3102
3104
  nat.selection.setRng(p.range);
3103
- const esc = replacement.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3104
- nat.insertContent(esc);
3105
+ const esc2 = replacement.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3106
+ nat.insertContent(esc2);
3105
3107
  } else {
3106
3108
  const sel = (doc.defaultView || window).getSelection();
3107
3109
  if (!sel)
@@ -3369,6 +3371,7 @@ Switch editors via **Settings \u2192 Editor \u2192 Quill | tiptap**.
3369
3371
  | Undo / Redo | Ctrl+Z / Ctrl+Y | (no toolbar button) |
3370
3372
  | Spell-check | (browser native \u2014 red underlines) | right-click word |
3371
3373
  | Paste plain text | Ctrl+Shift+V | (browser native) |
3374
+ | Paste markdown as formatting | (automatic) | Pasted plain text that looks like markdown (headings, \`- \` lists, \`> \` quotes, \\*\\*bold\\*\\*, code fences, pipe tables) converts to real formatting \u2014 blockquotes, lists, tables \u2014 instead of flat text. Ordinary prose is never touched; rich (HTML) clipboards keep their own markup. |
3372
3375
 
3373
3376
  ## Quill-only
3374
3377
 
@@ -3440,6 +3443,152 @@ toolbar \u2014 sending still works.
3440
3443
  }
3441
3444
  });
3442
3445
 
3446
+ // client/compose/paste-markdown.js
3447
+ function looksLikeMarkdown(text) {
3448
+ if (!text || text.length > 2e5)
3449
+ return false;
3450
+ const lines = text.split(/\r?\n/);
3451
+ const fences = text.match(/^\s*```/gm);
3452
+ if (fences && fences.length >= 2)
3453
+ return true;
3454
+ for (let i = 1; i < lines.length; i++) {
3455
+ if (/^\s*\|/.test(lines[i - 1]) && /^\s*\|[\s\-:|]+\|?\s*$/.test(lines[i]) && lines[i].includes("-"))
3456
+ return true;
3457
+ }
3458
+ let signals = 0;
3459
+ if (lines.some((l) => /^#{1,6}\s+\S/.test(l)))
3460
+ signals++;
3461
+ if (lines.filter((l) => /^\s*[-*+]\s+\S/.test(l)).length >= 2)
3462
+ signals++;
3463
+ if (lines.filter((l) => /^\s*\d+[.)]\s+\S/.test(l)).length >= 2)
3464
+ signals++;
3465
+ if (/\*\*[^*\n]+\*\*/.test(text))
3466
+ signals++;
3467
+ if (/\[[^\]\n]+\]\([^)\s]+\)/.test(text))
3468
+ signals++;
3469
+ if (lines.filter((l) => /^>\s?/.test(l)).length >= 2)
3470
+ signals++;
3471
+ return signals >= 2;
3472
+ }
3473
+ function esc(s) {
3474
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
3475
+ }
3476
+ function inlineMd(s) {
3477
+ const codeSpans = [];
3478
+ const TOK = "\0";
3479
+ let t = esc(s).replace(/`([^`]+)`/g, (_m, code) => {
3480
+ codeSpans.push(`<code style="font-family:Consolas,monospace;background:#f4f4f4;padding:1px 4px;border-radius:3px;">${code}</code>`);
3481
+ return TOK + (codeSpans.length - 1) + TOK;
3482
+ });
3483
+ t = t.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>").replace(/(?<![\w*])\*([^*\n]+)\*(?![\w*])/g, "<i>$1</i>").replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_m, txt, url) => /^(https?|mailto):/i.test(url) ? `<a href="${url}">${txt}</a>` : `${txt} (${url})`).replace(/(?<!["'=>[\w])(https?:\/\/[^\s<>"')\]]+)/g, '<a href="$1">$1</a>');
3484
+ return t.replace(/(\d+)/g, (_m, i) => codeSpans[Number(i)] ?? "");
3485
+ }
3486
+ var BLOCKQUOTE_STYLE = "margin:4px 0 4px 0;padding:2px 0 2px 12px;border-left:3px solid #b8b8b8;";
3487
+ var PRE_STYLE = "font-family:Consolas,monospace;font-size:0.95em;background:#f4f4f4;padding:8px 10px;border-radius:4px;overflow-x:auto;";
3488
+ function markdownToEmailHtml(md) {
3489
+ const lines = md.split(/\r?\n/);
3490
+ const out = [];
3491
+ let i = 0;
3492
+ while (i < lines.length) {
3493
+ const line = lines[i];
3494
+ if (/^\s*```/.test(line)) {
3495
+ const buf = [];
3496
+ i++;
3497
+ while (i < lines.length && !/^\s*```/.test(lines[i])) {
3498
+ buf.push(lines[i]);
3499
+ i++;
3500
+ }
3501
+ i++;
3502
+ out.push(`<pre style="${PRE_STYLE}">${esc(buf.join("\n"))}</pre>`);
3503
+ continue;
3504
+ }
3505
+ const h = /^(#{1,6})\s+(.*)$/.exec(line);
3506
+ if (h) {
3507
+ const level = Math.min(6, Math.max(3, h[1].length + 2));
3508
+ out.push(`<h${level} style="margin:10px 0 4px 0;">${inlineMd(h[2])}</h${level}>`);
3509
+ i++;
3510
+ continue;
3511
+ }
3512
+ if (/^>\s?/.test(line)) {
3513
+ const buf = [];
3514
+ while (i < lines.length && /^>\s?/.test(lines[i])) {
3515
+ buf.push(lines[i].replace(/^>\s?/, ""));
3516
+ i++;
3517
+ }
3518
+ out.push(`<blockquote style="${BLOCKQUOTE_STYLE}">${markdownToEmailHtml(buf.join("\n"))}</blockquote>`);
3519
+ continue;
3520
+ }
3521
+ if (/^\s*\|/.test(line) && i + 1 < lines.length && /^\s*\|[\s\-:|]+\|?\s*$/.test(lines[i + 1])) {
3522
+ const rows = [];
3523
+ while (i < lines.length && /^\s*\|/.test(lines[i])) {
3524
+ rows.push(lines[i].trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim()));
3525
+ i++;
3526
+ }
3527
+ const head = rows[0];
3528
+ const data = rows.slice(2);
3529
+ const cellStyle = "border:1px solid #ccc;padding:4px 10px;text-align:left;";
3530
+ out.push(`<table style="border-collapse:collapse;margin:6px 0;"><thead><tr>${head.map((c) => `<th style="${cellStyle}">${inlineMd(c)}</th>`).join("")}</tr></thead><tbody>${data.map((r) => `<tr>${r.map((c) => `<td style="${cellStyle}">${inlineMd(c)}</td>`).join("")}</tr>`).join("")}</tbody></table>`);
3531
+ continue;
3532
+ }
3533
+ if (/^\s*[-*+]\s+\S/.test(line)) {
3534
+ const items = [];
3535
+ while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i])) {
3536
+ items.push(lines[i].replace(/^\s*[-*+]\s+/, ""));
3537
+ i++;
3538
+ }
3539
+ out.push(`<ul style="margin:4px 0 4px 24px;padding:0;">${items.map((it) => `<li>${inlineMd(it)}</li>`).join("")}</ul>`);
3540
+ continue;
3541
+ }
3542
+ if (/^\s*\d+[.)]\s+\S/.test(line)) {
3543
+ const items = [];
3544
+ while (i < lines.length && /^\s*\d+[.)]\s+/.test(lines[i])) {
3545
+ items.push(lines[i].replace(/^\s*\d+[.)]\s+/, ""));
3546
+ i++;
3547
+ }
3548
+ out.push(`<ol style="margin:4px 0 4px 24px;padding:0;">${items.map((it) => `<li>${inlineMd(it)}</li>`).join("")}</ol>`);
3549
+ continue;
3550
+ }
3551
+ if (/^\s*(---+|\*\*\*+|___+)\s*$/.test(line)) {
3552
+ out.push(`<hr style="border:none;border-top:1px solid #ccc;margin:8px 0;">`);
3553
+ i++;
3554
+ continue;
3555
+ }
3556
+ if (/^\s*$/.test(line)) {
3557
+ i++;
3558
+ continue;
3559
+ }
3560
+ {
3561
+ const buf = [];
3562
+ while (i < lines.length && !/^\s*$/.test(lines[i]) && !/^\s*```|^#{1,6}\s+\S|^>\s?|^\s*[-*+]\s+\S|^\s*\d+[.)]\s+\S/.test(lines[i]) && !(/^\s*\|/.test(lines[i]) && i + 1 < lines.length && /^\s*\|[\s\-:|]+\|?\s*$/.test(lines[i + 1]))) {
3563
+ buf.push(lines[i]);
3564
+ i++;
3565
+ }
3566
+ out.push(`<p style="margin:4px 0;">${buf.map(inlineMd).join("<br>")}</p>`);
3567
+ }
3568
+ }
3569
+ return out.join("\n");
3570
+ }
3571
+ function wireMarkdownPaste(target, insertHtml) {
3572
+ target.addEventListener("paste", (e) => {
3573
+ try {
3574
+ const cb = e.clipboardData;
3575
+ if (!cb)
3576
+ return;
3577
+ if (cb.getData("text/html"))
3578
+ return;
3579
+ if (Array.from(cb.items).some((it) => it.kind === "file"))
3580
+ return;
3581
+ const plain = cb.getData("text/plain");
3582
+ if (!plain || !looksLikeMarkdown(plain))
3583
+ return;
3584
+ e.preventDefault();
3585
+ e.stopImmediatePropagation();
3586
+ insertHtml(markdownToEmailHtml(plain));
3587
+ } catch {
3588
+ }
3589
+ }, true);
3590
+ }
3591
+
3443
3592
  // client/compose/editor.js
3444
3593
  function looksLikeUrl(s) {
3445
3594
  const t = s.trim();
@@ -3882,6 +4031,14 @@ function createQuillEditor(container2) {
3882
4031
  q.insertText(range.index, plain.trim(), { link: url });
3883
4032
  q.setSelection(range.index + plain.trim().length, 0);
3884
4033
  }
4034
+ return;
4035
+ }
4036
+ if (plain && looksLikeMarkdown(plain)) {
4037
+ consume();
4038
+ const range = q.getSelection(true) || { index: q.getLength(), length: 0 };
4039
+ if (range.length > 0)
4040
+ q.deleteText(range.index, range.length);
4041
+ q.clipboard.dangerouslyPasteHTML(range.index, markdownToEmailHtml(plain));
3885
4042
  }
3886
4043
  }, true);
3887
4044
  q.on("text-change", (delta, _old, source) => {
@@ -4157,6 +4314,14 @@ async function createTinyMceEditor2(container2, opts = {}) {
4157
4314
  const m = await Promise.resolve().then(() => (init_rmf_tiny(), rmf_tiny_exports));
4158
4315
  const cssPx = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--compose-font-size"));
4159
4316
  const ed = await m.createTinyMceEditor(container2, { cdnUrl, apiKey, onFiles: opts.onFiles, fontSizePx: Number.isFinite(cssPx) ? cssPx : void 0 });
4317
+ try {
4318
+ const native = ed.nativeEditor;
4319
+ const body = native?.getBody?.();
4320
+ if (native && body) {
4321
+ wireMarkdownPaste(body, (html) => native.insertContent(html));
4322
+ }
4323
+ } catch {
4324
+ }
4160
4325
  return ed;
4161
4326
  }
4162
4327
 
@@ -6032,8 +6197,8 @@ function showEditorHelpModal(md) {
6032
6197
  });
6033
6198
  }
6034
6199
  function renderMarkdownLite(md) {
6035
- const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6036
- const inline = (s) => esc(s).replace(/`([^`]+)`/g, '<code style="background:var(--color-bg-surface,#f3f3f3);padding:1px 4px;border-radius:3px;">$1</code>').replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(?<![*\w])\*([^*\n]+)\*(?!\w)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
6200
+ const esc2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6201
+ const inline = (s) => esc2(s).replace(/`([^`]+)`/g, '<code style="background:var(--color-bg-surface,#f3f3f3);padding:1px 4px;border-radius:3px;">$1</code>').replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(?<![*\w])\*([^*\n]+)\*(?!\w)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
6037
6202
  const lines = md.split(/\r?\n/);
6038
6203
  const out = [];
6039
6204
  let i = 0;