@bobfrankston/rmfmail 1.2.286 → 1.2.288

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.
@@ -2686,7 +2686,15 @@ async function createTinyMceEditor(container2, opts = {}) {
2686
2686
  // window; TinyMCE's fullscreen plugin re-positions the container in
2687
2687
  // ways that produce a blank body, so it's not in the plugin list.
2688
2688
  menu: {
2689
- view: { title: "View", items: "rmfsource | visualaid visualchars visualblocks | preview | zoomIn zoomOut zoomReset" }
2689
+ view: { title: "View", items: "rmfsource | visualaid visualchars visualblocks | preview | zoomIn zoomOut zoomReset" },
2690
+ // Insert menu — TinyMCE's default list minus the items whose
2691
+ // plugins we don't load (media, pagebreak, anchor, accordion),
2692
+ // plus the two quoting items. Listed in full because naming a
2693
+ // menu REPLACES its item list; the defaults do not carry over.
2694
+ insert: {
2695
+ title: "Insert",
2696
+ items: "image link inserttable | rmfblockquote rmfquotepaste | rmfcode codesample charmap emoticons | hr nonbreaking insertdatetime"
2697
+ }
2690
2698
  },
2691
2699
  // Disable the "Get all features" upsell badge that TinyMCE 6+
2692
2700
  // injects automatically when no premium plugins are listed.
@@ -3107,6 +3115,18 @@ async function createTinyMceEditor(container2, opts = {}) {
3107
3115
  text: "Code block\u2026",
3108
3116
  onAction: openCodeDialog
3109
3117
  });
3118
+ ed.ui.registry.addMenuItem("rmfblockquote", {
3119
+ icon: "quote",
3120
+ text: "Blockquote",
3121
+ onAction: () => ed.execCommand("mceBlockQuote")
3122
+ });
3123
+ ed.ui.registry.addMenuItem("rmfquotepaste", {
3124
+ icon: "quote",
3125
+ text: "Quoted text (paste)\u2026",
3126
+ onAction: () => {
3127
+ void pasteAsQuote(ed);
3128
+ }
3129
+ });
3110
3130
  ed.ui.registry.addMenuItem("rmfsource", {
3111
3131
  icon: "sourcecode",
3112
3132
  text: "HTML source\u2026",
@@ -3345,6 +3365,48 @@ async function createTinyMceEditor(container2, opts = {}) {
3345
3365
  nativeEditor: editor2
3346
3366
  };
3347
3367
  }
3368
+ async function pasteAsQuote(ed) {
3369
+ const notify = (text, type) => {
3370
+ try {
3371
+ ed.notificationManager.open({ text, type, timeout: 4e3 });
3372
+ } catch {
3373
+ }
3374
+ };
3375
+ const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3376
+ const fragment = (html2) => {
3377
+ const marked = html2.match(/<!--\s*StartFragment\s*-->([\s\S]*?)<!--\s*EndFragment\s*-->/i);
3378
+ const inner = marked ? marked[1] : html2.replace(/[\s\S]*<body[^>]*>|<\/body>[\s\S]*/gi, "");
3379
+ return inner.trim();
3380
+ };
3381
+ let html = "";
3382
+ try {
3383
+ const clip = navigator.clipboard;
3384
+ if (clip?.read) {
3385
+ const items = await clip.read();
3386
+ for (const item of items) {
3387
+ if (!item.types.includes("text/html"))
3388
+ continue;
3389
+ html = fragment(await (await item.getType("text/html")).text());
3390
+ break;
3391
+ }
3392
+ }
3393
+ if (!html) {
3394
+ const text = await navigator.clipboard?.readText?.() || "";
3395
+ if (text.trim()) {
3396
+ html = text.trim().split(/\n\s*\n/).map((para) => `<p>${escape(para).replace(/\n/g, "<br>")}</p>`).join("");
3397
+ }
3398
+ }
3399
+ } catch (e) {
3400
+ notify(`Could not read the clipboard: ${e?.message || e}`, "error");
3401
+ return;
3402
+ }
3403
+ if (!html) {
3404
+ notify("There is nothing on the clipboard to quote.", "warning");
3405
+ return;
3406
+ }
3407
+ ed.insertContent(`<blockquote>${html}</blockquote>`);
3408
+ ed.undoManager.add();
3409
+ }
3348
3410
  function openSourceDialog(editor2) {
3349
3411
  const SENTINEL = "\uE000";
3350
3412
  let offset = -1;
@@ -3710,7 +3772,13 @@ var init_spellcheck = __esm({
3710
3772
  // client/compose/harper-lint.js
3711
3773
  var harper_lint_exports = {};
3712
3774
  __export(harper_lint_exports, {
3713
- initHarperLint: () => initHarperLint
3775
+ blockText: () => blockText,
3776
+ caretOffset: () => caretOffset,
3777
+ collectBlocks: () => collectBlocks,
3778
+ initHarperLint: () => initHarperLint,
3779
+ lineBox: () => lineBox,
3780
+ sentenceStart: () => sentenceStart,
3781
+ spanToRange: () => spanToRange
3714
3782
  });
3715
3783
  function initHarperLint(ed) {
3716
3784
  let disposed = false;
@@ -3735,42 +3803,6 @@ function initHarperLint(ed) {
3735
3803
  doc.getElementById(OVERLAY_ID2)?.remove();
3736
3804
  doc.getElementById(POPUP_ID)?.remove();
3737
3805
  };
3738
- const collectBlocks = (root) => {
3739
- const kids = Array.from(root.children).filter((c) => c instanceof HTMLElement);
3740
- const blocks = (kids.length ? kids : [root]).filter((b) => b.tagName !== "BLOCKQUOTE" && !b.closest("blockquote") && !b.querySelector("blockquote"));
3741
- return blocks;
3742
- };
3743
- const blockText = (block, doc) => {
3744
- const walker = doc.createTreeWalker(block, NodeFilter.SHOW_TEXT);
3745
- const nodes = [];
3746
- let text = "";
3747
- for (let n = walker.nextNode(); n; n = walker.nextNode()) {
3748
- nodes.push({ node: n, from: text.length });
3749
- text += n.data;
3750
- }
3751
- return { text, nodes };
3752
- };
3753
- const spanToRange = (doc, nodes, start, end) => {
3754
- let a = null;
3755
- let b = null;
3756
- for (const e of nodes) {
3757
- const len = e.node.data.length;
3758
- if (!a && start >= e.from && start <= e.from + len)
3759
- a = { node: e.node, off: start - e.from };
3760
- if (end >= e.from && end <= e.from + len)
3761
- b = { node: e.node, off: end - e.from };
3762
- }
3763
- if (!a || !b)
3764
- return null;
3765
- try {
3766
- const r = doc.createRange();
3767
- r.setStart(a.node, a.off);
3768
- r.setEnd(b.node, b.off);
3769
- return r;
3770
- } catch {
3771
- return null;
3772
- }
3773
- };
3774
3806
  const paint = () => {
3775
3807
  if (disposed)
3776
3808
  return;
@@ -3834,9 +3866,13 @@ function initHarperLint(ed) {
3834
3866
  const tx = texts[i];
3835
3867
  if (!tx)
3836
3868
  return;
3869
+ const caret = caretOffset(t.doc, tx.nodes);
3870
+ const writingFrom = caret < 0 ? -1 : sentenceStart(tx.text, caret);
3837
3871
  for (const l of lints || []) {
3838
3872
  if (SKIP_KINDS.has(l.kind))
3839
3873
  continue;
3874
+ if (writingFrom >= 0 && l.end > writingFrom && l.start <= caret)
3875
+ continue;
3840
3876
  const range = spanToRange(t.doc, tx.nodes, l.start, l.end);
3841
3877
  if (range)
3842
3878
  next.push({ lint: l, range });
@@ -3966,7 +4002,7 @@ function initHarperLint(ed) {
3966
4002
  }
3967
4003
  };
3968
4004
  }
3969
- var DEBOUNCE_MS, OVERLAY_ID2, POPUP_ID, SKIP_KINDS;
4005
+ var DEBOUNCE_MS, OVERLAY_ID2, POPUP_ID, SKIP_KINDS, collectBlocks, LINE_BREAKING, blockText, lineBox, caretOffset, sentenceStart, spanToRange;
3970
4006
  var init_harper_lint = __esm({
3971
4007
  "client/compose/harper-lint.js"() {
3972
4008
  "use strict";
@@ -3975,6 +4011,100 @@ var init_harper_lint = __esm({
3975
4011
  OVERLAY_ID2 = "harper-overlay";
3976
4012
  POPUP_ID = "harper-popup";
3977
4013
  SKIP_KINDS = /* @__PURE__ */ new Set(["Spelling"]);
4014
+ collectBlocks = (root) => {
4015
+ const kids = Array.from(root.children).filter((c) => c instanceof HTMLElement);
4016
+ const blocks = (kids.length ? kids : [root]).filter((b) => b.tagName !== "BLOCKQUOTE" && !b.closest("blockquote") && !b.querySelector("blockquote"));
4017
+ return blocks;
4018
+ };
4019
+ LINE_BREAKING = /* @__PURE__ */ new Set([
4020
+ "BR",
4021
+ "P",
4022
+ "DIV",
4023
+ "LI",
4024
+ "UL",
4025
+ "OL",
4026
+ "TR",
4027
+ "TD",
4028
+ "TH",
4029
+ "TABLE",
4030
+ "PRE",
4031
+ "HR",
4032
+ "BLOCKQUOTE",
4033
+ "H1",
4034
+ "H2",
4035
+ "H3",
4036
+ "H4",
4037
+ "H5",
4038
+ "H6",
4039
+ "SECTION",
4040
+ "ARTICLE",
4041
+ "FIGURE"
4042
+ ]);
4043
+ blockText = (block, doc) => {
4044
+ const walker = doc.createTreeWalker(block, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
4045
+ const nodes = [];
4046
+ let text = "";
4047
+ let pendingBreak = false;
4048
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
4049
+ if (n.nodeType === Node.ELEMENT_NODE) {
4050
+ if (LINE_BREAKING.has(n.tagName))
4051
+ pendingBreak = true;
4052
+ continue;
4053
+ }
4054
+ const data = n.data;
4055
+ if (!data)
4056
+ continue;
4057
+ if (pendingBreak && text)
4058
+ text += "\n";
4059
+ pendingBreak = false;
4060
+ nodes.push({ node: n, from: text.length });
4061
+ text += data;
4062
+ }
4063
+ return { text, nodes };
4064
+ };
4065
+ lineBox = (node) => {
4066
+ for (let e = node; e; e = e.parentNode)
4067
+ if (e.nodeType === Node.ELEMENT_NODE && LINE_BREAKING.has(e.tagName))
4068
+ return e;
4069
+ return null;
4070
+ };
4071
+ caretOffset = (doc, nodes) => {
4072
+ const sel = doc.getSelection?.();
4073
+ if (!sel || !sel.focusNode)
4074
+ return -1;
4075
+ for (const e of nodes)
4076
+ if (e.node === sel.focusNode)
4077
+ return e.from + sel.focusOffset;
4078
+ return -1;
4079
+ };
4080
+ sentenceStart = (text, caret) => {
4081
+ const before = text.slice(0, caret);
4082
+ const m = before.match(/[.!?…\n][^.!?…\n]*$/);
4083
+ return m ? caret - (m[0].length - 1) : 0;
4084
+ };
4085
+ spanToRange = (doc, nodes, start, end) => {
4086
+ let a = null;
4087
+ let b = null;
4088
+ for (const e of nodes) {
4089
+ const len = e.node.data.length;
4090
+ if (!a && start >= e.from && start <= e.from + len)
4091
+ a = { node: e.node, off: start - e.from };
4092
+ if (end >= e.from && end <= e.from + len)
4093
+ b = { node: e.node, off: end - e.from };
4094
+ }
4095
+ if (!a || !b)
4096
+ return null;
4097
+ if (lineBox(a.node) !== lineBox(b.node))
4098
+ return null;
4099
+ try {
4100
+ const r = doc.createRange();
4101
+ r.setStart(a.node, a.off);
4102
+ r.setEnd(b.node, b.off);
4103
+ return r;
4104
+ } catch {
4105
+ return null;
4106
+ }
4107
+ };
3978
4108
  }
3979
4109
  });
3980
4110