@bobfrankston/rmfmail 1.2.326 → 1.2.327

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/.commitmsg ADDED
@@ -0,0 +1,9 @@
1
+ Edit in Word: only the user's own part goes to Word; signature + quoted message are re-attached verbatim
2
+
3
+ html-to-docx flattens <blockquote> to plain paragraphs and mammoth brings back
4
+ only <p>/<a>/<br>, so a reply came home with the quoted message's formatting
5
+ gone and the bordered signature reduced to two lines. splitWordEditable()
6
+ (client/compose/compose.ts) cuts the body at the signature rule
7
+ (hr.mailx-sig-rule, legacy hr[style*=3cm]) or div.reply; the head goes to
8
+ Word, the tail is taken from the editor at apply time and appended unchanged.
9
+ Signature rule now carries class="mailx-sig-rule".
@@ -32,3 +32,15 @@ Session: Claude Fable 5.1, started 2026-09-13 ~17:00 EDT.
32
32
  one tick after paste; moves the caret just after (or before) the anchor when only whitespace/U+FEFF
33
33
  separates it from the edge. Quill's typed path NOT covered (not the configured editor).
34
34
  - Ships as rmfmail 1.2.326 via rebuild.cmd (running). Bob must restart the daemon for the new bundle.
35
+
36
+ ## Follow-on (~19:40): Edit in Word lost the quoted message's formatting
37
+ - Cause: whole body → html-to-docx (flattens blockquote) → Word → mammoth (only p/a/br back).
38
+ - Fix (v1.2.327, publishing): `splitWordEditable()` in client/compose/compose.ts — Word gets only the
39
+ part above the signature rule (`hr.mailx-sig-rule`, legacy `hr[style*=3cm]`) or `div.reply`; the
40
+ tail is re-read from the editor at apply time and appended unchanged.
41
+ - OPEN (Bob's direction): replace mammoth on the return path with his `y:\dev\utils\whts` converter.
42
+ whts today = whole-document CLI (`converter(docPath)` writes files, uses globals/logger/drive
43
+ paths, emits CSS classes via styleToClass, pulls sql/itemgen/pdf deps). Needs a lean exported
44
+ `docxToHtml()` core with inline styles before rmfmail can import it. Not touched — separate project.
45
+ - Checked: bundled TinyMCE 8.8.2 has NO Word-HTML cleaner (that is PowerPaste, paid); only the
46
+ generic paste path and `mceInsertClipboardContent`.
@@ -6259,7 +6259,7 @@ function applyInit(init) {
6259
6259
  sigHtml = acct.signature;
6260
6260
  }
6261
6261
  if (sigHtml) {
6262
- const sigRule = '<hr style="width:3cm;margin:0.8em 0 0.5em 0;border:0;border-top:1px solid #999;">';
6262
+ const sigRule = '<hr class="mailx-sig-rule" style="width:3cm;margin:0.8em 0 0.5em 0;border:0;border-top:1px solid #999;">';
6263
6263
  const sigBlock = `<br>${sigRule}${sigHtml}`;
6264
6264
  bodyToRender = isReplyForward ? `<br>${sigBlock}<br>${bodyToRender}` : `${bodyToRender}${sigBlock}`;
6265
6265
  }
@@ -7050,6 +7050,18 @@ var extEditHintClose = null;
7050
7050
  }
7051
7051
  }
7052
7052
  var wordEditOpening = false;
7053
+ function splitWordEditable(html) {
7054
+ const doc = new DOMParser().parseFromString(html, "text/html");
7055
+ const nodes = Array.from(doc.body.childNodes);
7056
+ const cutAt = nodes.findIndex((n) => n instanceof HTMLElement && n.matches('hr.mailx-sig-rule, hr[style*="3cm"], div.reply'));
7057
+ if (cutAt < 0) return { head: html, tail: "" };
7058
+ const serialize = (part) => {
7059
+ const box = doc.createElement("div");
7060
+ for (const n of part) box.appendChild(n.cloneNode(true));
7061
+ return box.innerHTML;
7062
+ };
7063
+ return { head: serialize(nodes.slice(0, cutAt)), tail: serialize(nodes.slice(cutAt)) };
7064
+ }
7053
7065
  document.getElementById("btn-edit-in-word")?.addEventListener("click", async () => {
7054
7066
  const btn = document.getElementById("btn-edit-in-word");
7055
7067
  if (wordEditOpening) return;
@@ -7061,7 +7073,9 @@ document.getElementById("btn-edit-in-word")?.addEventListener("click", async ()
7061
7073
  if (!wordEditId) wordEditId = `compose-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
7062
7074
  showDraftStatus("Opening in Word\u2026 (this takes a few seconds)", false);
7063
7075
  try {
7064
- const result = await openInWord(wordEditId, editor.getHtml(), popoutInitId || "");
7076
+ const { head, tail } = splitWordEditable(editor.getHtml());
7077
+ logClientEvent("word-edit-split", { headChars: head.length, tailChars: tail.length, editId: wordEditId });
7078
+ const result = await openInWord(wordEditId, head, popoutInitId || "");
7065
7079
  if (!result.ok || result.opener === "none") {
7066
7080
  showDraftStatus("Couldn't launch an editor. Install Word, LibreOffice, or set a default for .html.", true);
7067
7081
  return;
@@ -7221,8 +7235,9 @@ onEvent((ev) => {
7221
7235
  }
7222
7236
  try {
7223
7237
  extEditHintClose?.();
7224
- logClientEvent("word-edit-applied", { chars: (ev.html || "").length, editId: ev.editId });
7225
- editor.setHtml(ev.html || "");
7238
+ const { tail } = splitWordEditable(editor.getHtml());
7239
+ logClientEvent("word-edit-applied", { chars: (ev.html || "").length, tailChars: tail.length, editId: ev.editId });
7240
+ editor.setHtml((ev.html || "") + tail);
7226
7241
  showDraftStatus("Reloaded edits from external editor.", false);
7227
7242
  scheduleDraftSave();
7228
7243
  } catch (e) {