@bobfrankston/rmfmail 1.2.259 → 1.2.260

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,157 @@ 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;
2092
2239
  applyZoom(doc);
2240
+ refreshSearchHighlight(iframe, true);
2093
2241
  doc.addEventListener("keydown", (e) => {
2094
2242
  const target = e.target;
2095
2243
  if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)))
@@ -2136,6 +2284,16 @@ function installPreviewControls(iframe) {
2136
2284
  attach();
2137
2285
  else
2138
2286
  iframe.addEventListener("load", attach, { once: true });
2287
+ queueMicrotask(() => {
2288
+ const doc = iframe.contentDocument;
2289
+ if (!doc)
2290
+ return;
2291
+ if (doc.readyState === "loading") {
2292
+ doc.addEventListener("DOMContentLoaded", () => refreshSearchHighlight(iframe, true), { once: true });
2293
+ } else {
2294
+ refreshSearchHighlight(iframe, true);
2295
+ }
2296
+ });
2139
2297
  }
2140
2298
  function clearViewer() {
2141
2299
  currentMessage = null;
@@ -3144,6 +3302,8 @@ function appendTextProgressively(iframe, rest, gen) {
3144
3302
  const span = doc.createElement("span");
3145
3303
  span.innerHTML = linkifyText(chunks[i++]);
3146
3304
  host.appendChild(span);
3305
+ if (i >= chunks.length)
3306
+ refreshSearchHighlight(iframe);
3147
3307
  requestAnimationFrame(step);
3148
3308
  };
3149
3309
  let waits = 0;
@@ -3305,6 +3465,12 @@ ${csp}
3305
3465
  word-break: break-word;
3306
3466
  }
3307
3467
  blockquote { border-left: 3px solid #ccc; padding-left: 1rem; margin-left: 0; color: #666; }
3468
+ /* Search matches. Painted by the parent through the CSS Custom Highlight
3469
+ API (CSS.highlights) \u2014 no <mark> in the message DOM, so the letter the
3470
+ user copies, quotes or re-renders is byte-identical to what arrived.
3471
+ Both colors are explicit: a highlight that inherits the body color is
3472
+ invisible against its own background in one theme or the other. */
3473
+ ::highlight(mailx-find) { background: #ffd54a; color: #1a1a2e; }
3308
3474
  @media (prefers-color-scheme: dark) {
3309
3475
  body { color: #cdd6f4; background: #282840; }
3310
3476
  a { color: #89b4fa; }
@@ -3859,7 +4025,7 @@ function spawnDesktopPopout(msg, accountId) {
3859
4025
  function escapeHtmlLocal(s) {
3860
4026
  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3861
4027
  }
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;
4028
+ 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
4029
  var init_message_viewer = __esm({
3864
4030
  "client/components/message-viewer.js"() {
3865
4031
  "use strict";
@@ -3897,6 +4063,9 @@ var init_message_viewer = __esm({
3897
4063
  ZOOM_MAX = 3;
3898
4064
  ZOOM_STEP = 0.1;
3899
4065
  previewZoom = clampZoom(parseFloat(localStorage.getItem(ZOOM_KEY) || "1.15"));
4066
+ HIGHLIGHT_NAME = "mailx-find";
4067
+ HIGHLIGHT_MAX = 2e3;
4068
+ searchHighlightTerms = [];
3900
4069
  subscribeStore("*", (ev) => {
3901
4070
  if (ev?.kind !== "messageRemoved")
3902
4071
  return;
@@ -10691,6 +10860,7 @@ function renderSearchHighlight(settled) {
10691
10860
  }
10692
10861
  function updateSearchHighlight() {
10693
10862
  renderSearchHighlight(false);
10863
+ setSearchHighlightTerms(parseHighlightTerms(searchInput?.value || ""));
10694
10864
  if (searchRegexSettleTimer) clearTimeout(searchRegexSettleTimer);
10695
10865
  searchRegexSettleTimer = setTimeout(() => renderSearchHighlight(true), SEARCH_REGEX_SETTLE_MS);
10696
10866
  }
@@ -10703,6 +10873,7 @@ var SERVER_SEARCH_DALLY_MS = 700;
10703
10873
  var serverSearchTimer = null;
10704
10874
  function doSearch(immediate = false) {
10705
10875
  const query = searchInput.value.trim();
10876
+ setSearchHighlightTerms(parseHighlightTerms(query));
10706
10877
  if (query.length === 0) {
10707
10878
  if (serverSearchTimer) {
10708
10879
  clearTimeout(serverSearchTimer);