@bobfrankston/rmfmail 1.1.203 → 1.1.204

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.
@@ -81,6 +81,31 @@ export async function createTinyMceEditor(container, opts = {}) {
81
81
  target.id = `rmf-tiny-${Math.random().toString(36).slice(2, 10)}`;
82
82
  target.style.cssText = "width:100%;height:100%;";
83
83
  container.appendChild(target);
84
+ // Code-block languages — shared by the stock codesample dialog
85
+ // (codesample_languages) AND our custom "Insert code" dialog below, so a
86
+ // language added here shows up in both. "Text" first = default (no
87
+ // highlighting, which would mangle plain pasted snippets — Bob 2026-05-24).
88
+ // PowerShell added 2026-05-31 (Bob: ".ps1 is another file type").
89
+ const CODE_LANGS = [
90
+ { text: "Text", value: "text" },
91
+ { text: "HTML/XML", value: "markup" },
92
+ { text: "JavaScript", value: "javascript" },
93
+ { text: "TypeScript", value: "typescript" },
94
+ { text: "CSS", value: "css" },
95
+ { text: "JSON", value: "json" },
96
+ { text: "Python", value: "python" },
97
+ { text: "Java", value: "java" },
98
+ { text: "C", value: "c" },
99
+ { text: "C++", value: "cpp" },
100
+ { text: "C#", value: "csharp" },
101
+ { text: "Go", value: "go" },
102
+ { text: "Rust", value: "rust" },
103
+ { text: "Ruby", value: "ruby" },
104
+ { text: "PHP", value: "php" },
105
+ { text: "Shell", value: "bash" },
106
+ { text: "PowerShell", value: "powershell" },
107
+ { text: "SQL", value: "sql" },
108
+ ];
84
109
  const editor = await new Promise((resolve) => {
85
110
  tinymce.init({
86
111
  target,
@@ -93,7 +118,7 @@ export async function createTinyMceEditor(container, opts = {}) {
93
118
  plugins: "paste lists advlist link table code codesample image searchreplace autolink wordcount emoticons charmap insertdatetime quickbars nonbreaking directionality help",
94
119
  toolbar: [
95
120
  "undo redo | bold italic underline strikethrough | forecolor backcolor",
96
- "bullist numlist outdent indent | link table image code codesample | emoticons charmap | help",
121
+ "bullist numlist outdent indent | link table image code rmfcode | emoticons charmap | help",
97
122
  ].join(" | "),
98
123
  // Include "tools" so wordcount and searchreplace are reachable.
99
124
  menubar: "file edit view insert format tools",
@@ -120,25 +145,7 @@ export async function createTinyMceEditor(container, opts = {}) {
120
145
  // which mangles unrelated paste content (Bob 2026-05-24).
121
146
  // Adding Text first so it's the default; rest are the modern
122
147
  // languages we actually paste.
123
- codesample_languages: [
124
- { text: "Text", value: "text" },
125
- { text: "HTML/XML", value: "markup" },
126
- { text: "JavaScript", value: "javascript" },
127
- { text: "TypeScript", value: "typescript" },
128
- { text: "CSS", value: "css" },
129
- { text: "JSON", value: "json" },
130
- { text: "Python", value: "python" },
131
- { text: "Java", value: "java" },
132
- { text: "C", value: "c" },
133
- { text: "C++", value: "cpp" },
134
- { text: "C#", value: "csharp" },
135
- { text: "Go", value: "go" },
136
- { text: "Rust", value: "rust" },
137
- { text: "Ruby", value: "ruby" },
138
- { text: "PHP", value: "php" },
139
- { text: "Shell", value: "bash" },
140
- { text: "SQL", value: "sql" },
141
- ],
148
+ codesample_languages: CODE_LANGS,
142
149
  // WebView's native spell-check (red underlines, right-click
143
150
  // "Add to dictionary"). Free; same UX as Quill's spellcheck=true.
144
151
  // Premium tinymcespellchecker plugin would replace this with a
@@ -156,6 +163,17 @@ export async function createTinyMceEditor(container, opts = {}) {
156
163
  statusbar: false,
157
164
  branding: false,
158
165
  license_key: "gpl",
166
+ // Keep URLs verbatim. TinyMCE defaults to convert_urls:true +
167
+ // relative_urls:true, which rewrites an absolute href
168
+ // (https://github.com/…) to be RELATIVE to the editor's base URL
169
+ // (the msger custom-protocol page). A link that worked in the
170
+ // original message arrives in the reply quote rewritten/dead
171
+ // (Bob 2026-05-31: "URL works in the original but not in the
172
+ // reply"). false on both = hrefs travel into the sent message
173
+ // exactly as the sender wrote them.
174
+ convert_urls: false,
175
+ relative_urls: false,
176
+ remove_script_host: false,
159
177
  paste_data_images: true,
160
178
  // Permissive valid_elements — preserve as much of the source
161
179
  // formatting as possible. The default is more aggressive about
@@ -250,6 +268,67 @@ export async function createTinyMceEditor(container, opts = {}) {
250
268
  applyZoom();
251
269
  saveZoom();
252
270
  };
271
+ // Custom "Insert code" — wraps the stock codesample plugin
272
+ // (keeps its Prism highlighting + edit-in-place) but adds a
273
+ // "Box it in" border checkbox so a snippet is visually
274
+ // demarcated from surrounding prose. The border is applied as
275
+ // an INLINE style on the <pre>, NOT a content_style class, so
276
+ // it survives into the sent email where the recipient has none
277
+ // of our editor CSS. Bob 2026-05-31.
278
+ const BORDER_STYLES = { "border": "1px solid #888", "border-radius": "4px", "padding": "10px" };
279
+ const openCodeDialog = () => {
280
+ const cs = ed.plugins.codesample;
281
+ const preEl = ed.dom.getParent(ed.selection.getNode(), "pre");
282
+ let curLang = "text";
283
+ let curBorder = false;
284
+ if (preEl) {
285
+ const m = (preEl.className || "").match(/language-([\w-]+)/);
286
+ if (m)
287
+ curLang = m[1];
288
+ curBorder = !!(preEl.style && preEl.style.border && preEl.style.border !== "none");
289
+ }
290
+ const curCode = (cs && cs.getCurrentCode) ? (cs.getCurrentCode(ed) || "") : "";
291
+ ed.windowManager.open({
292
+ title: "Insert code",
293
+ size: "large",
294
+ body: {
295
+ type: "panel",
296
+ items: [
297
+ { type: "listbox", name: "language", label: "Language", items: CODE_LANGS },
298
+ { type: "textarea", name: "code", label: "Code" },
299
+ { type: "checkbox", name: "border", label: "Box it in (border around the code)" },
300
+ ],
301
+ },
302
+ initialData: { language: curLang, code: curCode, border: curBorder },
303
+ buttons: [
304
+ { type: "cancel", text: "Cancel" },
305
+ { type: "submit", text: "Save", primary: true },
306
+ ],
307
+ onSubmit: (api) => {
308
+ const data = api.getData();
309
+ if (cs && cs.insertCodeSample)
310
+ cs.insertCodeSample(data.language, data.code);
311
+ const pre = ed.dom.getParent(ed.selection.getNode(), "pre");
312
+ if (pre) {
313
+ if (data.border)
314
+ ed.dom.setStyles(pre, BORDER_STYLES);
315
+ else
316
+ ed.dom.setStyles(pre, { "border": "", "border-radius": "", "padding": "" });
317
+ }
318
+ api.close();
319
+ },
320
+ });
321
+ };
322
+ ed.ui.registry.addButton("rmfcode", {
323
+ icon: "code-sample",
324
+ tooltip: "Insert/edit code block",
325
+ onAction: openCodeDialog,
326
+ });
327
+ ed.ui.registry.addMenuItem("rmfcode", {
328
+ icon: "code-sample",
329
+ text: "Code block…",
330
+ onAction: openCodeDialog,
331
+ });
253
332
  ed.ui.registry.addMenuItem("zoomIn", {
254
333
  text: "Zoom in", shortcut: "Ctrl+=",
255
334
  onAction: () => bumpZoom(ZOOM_STEP),
@@ -326,11 +405,28 @@ export async function createTinyMceEditor(container, opts = {}) {
326
405
  }
327
406
  if (tail.toString().length > 0)
328
407
  return;
329
- const after = doc.createRange();
330
- after.setStartAfter(link);
331
- after.collapse(true);
332
- sel.removeAllRanges();
333
- sel.addRange(after);
408
+ // Move the caret to just after the link. Use TinyMCE's
409
+ // own selection API rather than a raw
410
+ // removeAllRanges/addRange: the raw mutation ran ahead
411
+ // of TinyMCE's beforeinput handling and desynced its
412
+ // UndoManager, leaving Ctrl+Z erratic (Bob 2026-05-31:
413
+ // "^z is very broken in TinyMCE"). Going through
414
+ // ed.selection keeps TinyMCE's selection + undo state
415
+ // consistent. Raw range as a fallback if it throws.
416
+ const parent = link.parentNode;
417
+ const idx = parent ? Array.prototype.indexOf.call(parent.childNodes, link) + 1 : -1;
418
+ if (parent && idx >= 0) {
419
+ try {
420
+ ed.selection.setCursorLocation(parent, idx);
421
+ }
422
+ catch {
423
+ const after = doc.createRange();
424
+ after.setStartAfter(link);
425
+ after.collapse(true);
426
+ sel.removeAllRanges();
427
+ sel.addRange(after);
428
+ }
429
+ }
334
430
  }, true);
335
431
  };
336
432
  ed.on("init", installLinkEscape);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.1.203",
3
+ "version": "1.1.204",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",