@bobfrankston/rmfmail 1.2.259 → 1.2.261

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.
@@ -1653,12 +1653,15 @@ __export(message_viewer_exports, {
1653
1653
  getCurrentMessage: () => getCurrentMessage,
1654
1654
  initViewer: () => initViewer,
1655
1655
  invalidateParsedCache: () => invalidateParsedCache,
1656
+ parseHighlightTerms: () => parseHighlightTerms,
1656
1657
  popOutCurrentMessage: () => popOutCurrentMessage,
1657
1658
  popOutToWindow: () => popOutToWindow,
1658
1659
  printCurrentMessage: () => printCurrentMessage,
1659
1660
  printMessage: () => printMessage,
1661
+ refreshSearchHighlight: () => refreshSearchHighlight,
1660
1662
  saveCurrentMessageAsEml: () => saveCurrentMessageAsEml,
1661
1663
  saveCurrentMessageAsHtml: () => saveCurrentMessageAsHtml,
1664
+ setSearchHighlightTerms: () => setSearchHighlightTerms,
1662
1665
  showMessage: () => showMessage,
1663
1666
  showPreviewBodyMenu: () => showPreviewBodyMenu,
1664
1667
  spawnDesktopPopout: () => spawnDesktopPopout,
@@ -2084,12 +2087,160 @@ async function translateAndShow(text) {
2084
2087
  status.textContent = `Translate error: ${err?.message || ""}`;
2085
2088
  }
2086
2089
  }
2090
+ function parseHighlightTerms(query) {
2091
+ const parts = (query || "").match(/"[^"]*"|\S+/g) || [];
2092
+ const out = [];
2093
+ let negate = false;
2094
+ for (const raw of parts) {
2095
+ const part = raw.replace(/^"|"$/g, "");
2096
+ if (/^(AND|OR)$/i.test(part))
2097
+ continue;
2098
+ if (/^NOT$/i.test(part)) {
2099
+ negate = true;
2100
+ continue;
2101
+ }
2102
+ if (negate) {
2103
+ negate = false;
2104
+ continue;
2105
+ }
2106
+ if (part.startsWith("-"))
2107
+ continue;
2108
+ const q = part.match(/^([a-z]+):(.*)$/i);
2109
+ if (q) {
2110
+ const [, field, value] = q;
2111
+ if (/^subject$/i.test(field) && value)
2112
+ out.push(value.replace(/^"|"$/g, ""));
2113
+ continue;
2114
+ }
2115
+ if (part.length >= 2)
2116
+ out.push(part);
2117
+ }
2118
+ return Array.from(new Set(out.map((t) => t.toLowerCase()))).sort((a, b) => b.length - a.length);
2119
+ }
2120
+ function collectHighlightRanges(doc, terms) {
2121
+ const ranges = [];
2122
+ const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, {
2123
+ acceptNode(node) {
2124
+ const tag = (node.parentElement?.tagName || "").toUpperCase();
2125
+ if (tag === "SCRIPT" || tag === "STYLE" || tag === "NOSCRIPT")
2126
+ return NodeFilter.FILTER_REJECT;
2127
+ return node.nodeValue && node.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
2128
+ }
2129
+ });
2130
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
2131
+ const hay = n.data.toLowerCase();
2132
+ const taken = [];
2133
+ for (const term of terms) {
2134
+ let from = 0;
2135
+ for (; ; ) {
2136
+ const at = hay.indexOf(term, from);
2137
+ if (at < 0)
2138
+ break;
2139
+ const end = at + term.length;
2140
+ from = end;
2141
+ if (taken.some(([s, e]) => at < e && end > s))
2142
+ continue;
2143
+ taken.push([at, end]);
2144
+ const r = doc.createRange();
2145
+ r.setStart(n, at);
2146
+ r.setEnd(n, end);
2147
+ ranges.push(r);
2148
+ if (ranges.length >= HIGHLIGHT_MAX)
2149
+ return ranges;
2150
+ }
2151
+ }
2152
+ }
2153
+ return ranges;
2154
+ }
2155
+ function applySearchHighlight(iframe, scrollToFirst = false) {
2156
+ const doc = iframe.contentDocument;
2157
+ const win = iframe.contentWindow;
2158
+ if (!doc?.body || !win?.CSS?.highlights || typeof win.Highlight !== "function")
2159
+ return 0;
2160
+ try {
2161
+ win.CSS.highlights.delete(HIGHLIGHT_NAME);
2162
+ if (searchHighlightTerms.length === 0)
2163
+ return 0;
2164
+ const ranges = collectHighlightRanges(doc, searchHighlightTerms);
2165
+ if (ranges.length === 0)
2166
+ return 0;
2167
+ win.CSS.highlights.set(HIGHLIGHT_NAME, new win.Highlight(...ranges));
2168
+ if (scrollToFirst) {
2169
+ const rect = ranges[0].getBoundingClientRect();
2170
+ const h = doc.documentElement.clientHeight || 0;
2171
+ if (rect.height > 0 && (rect.top < 0 || rect.bottom > h)) {
2172
+ const target = (doc.scrollingElement || doc.documentElement).scrollTop + rect.top - h / 3;
2173
+ (doc.scrollingElement || doc.documentElement).scrollTop = Math.max(0, target);
2174
+ }
2175
+ }
2176
+ return ranges.length;
2177
+ } catch {
2178
+ return 0;
2179
+ }
2180
+ }
2181
+ function applyHeaderHighlight() {
2182
+ const anyWin = window;
2183
+ if (!anyWin.CSS?.highlights || typeof anyWin.Highlight !== "function")
2184
+ return;
2185
+ try {
2186
+ anyWin.CSS.highlights.delete(HIGHLIGHT_NAME);
2187
+ const subj = document.querySelector(".mv-subject");
2188
+ if (!subj || searchHighlightTerms.length === 0)
2189
+ return;
2190
+ const ranges = collectHighlightRangesIn(subj, searchHighlightTerms);
2191
+ if (ranges.length)
2192
+ anyWin.CSS.highlights.set(HIGHLIGHT_NAME, new anyWin.Highlight(...ranges));
2193
+ } catch {
2194
+ }
2195
+ }
2196
+ function collectHighlightRangesIn(root, terms) {
2197
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2198
+ const ranges = [];
2199
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
2200
+ const hay = n.data.toLowerCase();
2201
+ for (const term of terms) {
2202
+ let from = 0;
2203
+ for (; ; ) {
2204
+ const at = hay.indexOf(term, from);
2205
+ if (at < 0)
2206
+ break;
2207
+ from = at + term.length;
2208
+ const r = document.createRange();
2209
+ r.setStart(n, at);
2210
+ r.setEnd(n, from);
2211
+ ranges.push(r);
2212
+ }
2213
+ }
2214
+ }
2215
+ return ranges;
2216
+ }
2217
+ function setSearchHighlightTerms(terms) {
2218
+ const next = terms.filter(Boolean).map((t) => t.toLowerCase());
2219
+ const same = next.length === searchHighlightTerms.length && next.every((t, i) => t === searchHighlightTerms[i]);
2220
+ if (same)
2221
+ return;
2222
+ searchHighlightTerms = next;
2223
+ applyHeaderHighlight();
2224
+ for (const f of Array.from(document.querySelectorAll("iframe"))) {
2225
+ applySearchHighlight(f, true);
2226
+ }
2227
+ }
2228
+ function refreshSearchHighlight(iframe, scrollToFirst = false) {
2229
+ if (searchHighlightTerms.length === 0)
2230
+ return;
2231
+ applySearchHighlight(iframe, scrollToFirst);
2232
+ applyHeaderHighlight();
2233
+ }
2087
2234
  function installPreviewControls(iframe) {
2088
2235
  const attach = () => {
2089
2236
  const doc = iframe.contentDocument;
2090
2237
  if (!doc)
2091
2238
  return;
2239
+ if (doc.__mvControlsBound)
2240
+ return;
2241
+ doc.__mvControlsBound = true;
2092
2242
  applyZoom(doc);
2243
+ refreshSearchHighlight(iframe, true);
2093
2244
  doc.addEventListener("keydown", (e) => {
2094
2245
  const target = e.target;
2095
2246
  if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)))
@@ -2132,10 +2283,19 @@ function installPreviewControls(iframe) {
2132
2283
  setZoom(previewZoom + (e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP), doc);
2133
2284
  }, { passive: false });
2134
2285
  };
2286
+ iframe.addEventListener("load", attach);
2135
2287
  if (iframe.contentDocument?.readyState === "complete")
2136
2288
  attach();
2137
- else
2138
- iframe.addEventListener("load", attach, { once: true });
2289
+ queueMicrotask(() => {
2290
+ const doc = iframe.contentDocument;
2291
+ if (!doc)
2292
+ return;
2293
+ if (doc.readyState === "loading") {
2294
+ doc.addEventListener("DOMContentLoaded", () => refreshSearchHighlight(iframe, true), { once: true });
2295
+ } else {
2296
+ refreshSearchHighlight(iframe, true);
2297
+ }
2298
+ });
2139
2299
  }
2140
2300
  function clearViewer() {
2141
2301
  currentMessage = null;
@@ -3144,6 +3304,8 @@ function appendTextProgressively(iframe, rest, gen) {
3144
3304
  const span = doc.createElement("span");
3145
3305
  span.innerHTML = linkifyText(chunks[i++]);
3146
3306
  host.appendChild(span);
3307
+ if (i >= chunks.length)
3308
+ refreshSearchHighlight(iframe);
3147
3309
  requestAnimationFrame(step);
3148
3310
  };
3149
3311
  let waits = 0;
@@ -3305,6 +3467,12 @@ ${csp}
3305
3467
  word-break: break-word;
3306
3468
  }
3307
3469
  blockquote { border-left: 3px solid #ccc; padding-left: 1rem; margin-left: 0; color: #666; }
3470
+ /* Search matches. Painted by the parent through the CSS Custom Highlight
3471
+ API (CSS.highlights) \u2014 no <mark> in the message DOM, so the letter the
3472
+ user copies, quotes or re-renders is byte-identical to what arrived.
3473
+ Both colors are explicit: a highlight that inherits the body color is
3474
+ invisible against its own background in one theme or the other. */
3475
+ ::highlight(mailx-find) { background: #ffd54a; color: #1a1a2e; }
3308
3476
  @media (prefers-color-scheme: dark) {
3309
3477
  body { color: #cdd6f4; background: #282840; }
3310
3478
  a { color: #89b4fa; }
@@ -3859,7 +4027,7 @@ function spawnDesktopPopout(msg, accountId) {
3859
4027
  function escapeHtmlLocal(s) {
3860
4028
  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3861
4029
  }
3862
- var pendingImageCopies, imageCopySeq, IMAGE_COPY_TIMEOUT_MS, currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, WAIT_GIVE_UP_SECS, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey, PROGRESSIVE_TEXT_THRESHOLD, TEXT_CHUNK_BYTES;
4030
+ var pendingImageCopies, imageCopySeq, IMAGE_COPY_TIMEOUT_MS, currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, WAIT_GIVE_UP_SECS, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, HIGHLIGHT_NAME, HIGHLIGHT_MAX, searchHighlightTerms, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey, PROGRESSIVE_TEXT_THRESHOLD, TEXT_CHUNK_BYTES;
3863
4031
  var init_message_viewer = __esm({
3864
4032
  "client/components/message-viewer.js"() {
3865
4033
  "use strict";
@@ -3897,6 +4065,9 @@ var init_message_viewer = __esm({
3897
4065
  ZOOM_MAX = 3;
3898
4066
  ZOOM_STEP = 0.1;
3899
4067
  previewZoom = clampZoom(parseFloat(localStorage.getItem(ZOOM_KEY) || "1.15"));
4068
+ HIGHLIGHT_NAME = "mailx-find";
4069
+ HIGHLIGHT_MAX = 2e3;
4070
+ searchHighlightTerms = [];
3900
4071
  subscribeStore("*", (ev) => {
3901
4072
  if (ev?.kind !== "messageRemoved")
3902
4073
  return;
@@ -10691,6 +10862,7 @@ function renderSearchHighlight(settled) {
10691
10862
  }
10692
10863
  function updateSearchHighlight() {
10693
10864
  renderSearchHighlight(false);
10865
+ setSearchHighlightTerms(parseHighlightTerms(searchInput?.value || ""));
10694
10866
  if (searchRegexSettleTimer) clearTimeout(searchRegexSettleTimer);
10695
10867
  searchRegexSettleTimer = setTimeout(() => renderSearchHighlight(true), SEARCH_REGEX_SETTLE_MS);
10696
10868
  }
@@ -10703,6 +10875,7 @@ var SERVER_SEARCH_DALLY_MS = 700;
10703
10875
  var serverSearchTimer = null;
10704
10876
  function doSearch(immediate = false) {
10705
10877
  const query = searchInput.value.trim();
10878
+ setSearchHighlightTerms(parseHighlightTerms(query));
10706
10879
  if (query.length === 0) {
10707
10880
  if (serverSearchTimer) {
10708
10881
  clearTimeout(serverSearchTimer);