@bobfrankston/rmfmail 1.1.243 → 1.1.244

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.
@@ -948,8 +948,8 @@ var require_lib = __commonJS({
948
948
  "use strict";
949
949
  var buffer = require_is_buffer();
950
950
  var affix = require_affix();
951
- module.exports = NSpell3;
952
- var proto = NSpell3.prototype;
951
+ module.exports = NSpell2;
952
+ var proto = NSpell2.prototype;
953
953
  proto.correct = require_correct();
954
954
  proto.suggest = require_suggest();
955
955
  proto.spell = require_spell();
@@ -958,11 +958,11 @@ var require_lib = __commonJS({
958
958
  proto.wordCharacters = require_word_characters();
959
959
  proto.dictionary = require_dictionary2();
960
960
  proto.personal = require_personal();
961
- function NSpell3(aff, dic) {
961
+ function NSpell2(aff, dic) {
962
962
  var index = -1;
963
963
  var dictionaries;
964
- if (!(this instanceof NSpell3)) {
965
- return new NSpell3(aff, dic);
964
+ if (!(this instanceof NSpell2)) {
965
+ return new NSpell2(aff, dic);
966
966
  }
967
967
  if (typeof aff === "string" || buffer(aff)) {
968
968
  if (typeof dic === "string" || buffer(dic)) {
@@ -2329,438 +2329,12 @@ var spellcheck_exports = {};
2329
2329
  __export(spellcheck_exports, {
2330
2330
  wireSpellcheck: () => wireSpellcheck
2331
2331
  });
2332
- async function getSpell2() {
2333
- if (spellPromise2)
2334
- return spellPromise2;
2335
- spellPromise2 = (async () => {
2336
- const [affRes, dicRes] = await Promise.all([
2337
- fetch("../lib/dict/en.aff"),
2338
- fetch("../lib/dict/en.dic")
2339
- ]);
2340
- if (!affRes.ok || !dicRes.ok) {
2341
- throw new Error(`spellcheck: dict fetch failed (aff=${affRes.status} dic=${dicRes.status})`);
2342
- }
2343
- const [aff, dic] = await Promise.all([affRes.text(), dicRes.text()]);
2344
- const sp = new import_nspell2.default({ aff, dic });
2345
- try {
2346
- const raw = localStorage.getItem(USER_DICT_KEY2);
2347
- if (raw)
2348
- for (const w of JSON.parse(raw))
2349
- sp.add(w);
2350
- } catch {
2351
- }
2352
- getUserDict().then((cloud) => {
2353
- const cloudArr = Array.isArray(cloud) ? cloud : [];
2354
- for (const w of cloudArr)
2355
- sp.add(w);
2356
- let local = [];
2357
- try {
2358
- const raw = localStorage.getItem(USER_DICT_KEY2);
2359
- local = raw ? JSON.parse(raw) : [];
2360
- } catch {
2361
- local = [];
2362
- }
2363
- const cloudSet = new Set(cloudArr);
2364
- const localOnly = local.filter((w) => !cloudSet.has(w));
2365
- if (localOnly.length > 0) {
2366
- addUserDictWords(localOnly).catch((e) => console.error("[spell] reconcile:", e));
2367
- }
2368
- try {
2369
- const merged = [.../* @__PURE__ */ new Set([...local, ...cloudArr])];
2370
- localStorage.setItem(USER_DICT_KEY2, JSON.stringify(merged));
2371
- } catch {
2372
- }
2373
- }).catch(() => {
2374
- });
2375
- return sp;
2376
- })();
2377
- return spellPromise2;
2378
- }
2379
- function addToUserDict2(word, sp) {
2380
- try {
2381
- const raw = localStorage.getItem(USER_DICT_KEY2);
2382
- const arr = raw ? JSON.parse(raw) : [];
2383
- if (!arr.includes(word)) {
2384
- arr.push(word);
2385
- localStorage.setItem(USER_DICT_KEY2, JSON.stringify(arr));
2386
- }
2387
- } catch {
2388
- }
2389
- sp.add(word);
2390
- addUserDictWord(word).catch((e) => console.error("[spell] addUserDictWord:", e));
2391
- }
2392
- function decorate(editor2, sp) {
2393
- const body = editor2.getBody?.();
2394
- const doc = editor2.getDoc?.();
2395
- if (!body || !doc)
2396
- return;
2397
- const _activeSel = doc.getSelection();
2398
- if (_activeSel && _activeSel.rangeCount > 0 && !_activeSel.isCollapsed)
2399
- return;
2400
- let savedFocusNode = null;
2401
- let savedFocusOffset = 0;
2402
- const _preSel = doc.getSelection();
2403
- if (_preSel && _preSel.rangeCount > 0) {
2404
- savedFocusNode = _preSel.focusNode;
2405
- savedFocusOffset = _preSel.focusOffset;
2406
- }
2407
- const savedAbs = caretAbsOffsetFromBody(body);
2408
- const scroller = doc.scrollingElement || doc.documentElement;
2409
- const savedScrollTop = scroller?.scrollTop ?? 0;
2410
- const savedBodyScrollTop = body.scrollTop;
2411
- try {
2412
- editor2.undoManager?.ignore?.(() => {
2413
- const old = body.querySelectorAll(`span[${MARKER_ATTR2}]`);
2414
- for (const m of old) {
2415
- const parent2 = m.parentNode;
2416
- if (!parent2)
2417
- continue;
2418
- while (m.firstChild)
2419
- parent2.insertBefore(m.firstChild, m);
2420
- parent2.removeChild(m);
2421
- }
2422
- body.normalize();
2423
- const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
2424
- acceptNode(node) {
2425
- let p = node.parentNode;
2426
- while (p && p !== body) {
2427
- if (p.nodeType === Node.ELEMENT_NODE && SKIP_TAGS2.has(p.tagName)) {
2428
- return NodeFilter.FILTER_REJECT;
2429
- }
2430
- p = p.parentNode;
2431
- }
2432
- return NodeFilter.FILTER_ACCEPT;
2433
- }
2434
- });
2435
- let caretNode = null;
2436
- let caretOffset = 0;
2437
- const liveSel = doc.getSelection();
2438
- if (liveSel && liveSel.rangeCount > 0) {
2439
- const f = liveSel.focusNode;
2440
- if (f && f.nodeType === Node.TEXT_NODE) {
2441
- caretNode = f;
2442
- caretOffset = liveSel.focusOffset;
2443
- }
2444
- }
2445
- const hits = [];
2446
- let n = walker.nextNode();
2447
- const WORD_RE = /[\p{L}][\p{L}'’\-]*/gu;
2448
- const EMAIL_RE = /[^\s@<>()]+@[^\s@<>()]+\.[^\s@<>()]+/g;
2449
- while (n) {
2450
- const tn = n;
2451
- const text = tn.data;
2452
- const emailRanges = [];
2453
- EMAIL_RE.lastIndex = 0;
2454
- let em;
2455
- while ((em = EMAIL_RE.exec(text)) !== null) {
2456
- emailRanges.push([em.index, em.index + em[0].length]);
2457
- }
2458
- let m;
2459
- WORD_RE.lastIndex = 0;
2460
- while ((m = WORD_RE.exec(text)) !== null) {
2461
- const word = m[0];
2462
- if (word.length < MIN_WORD_LEN2)
2463
- continue;
2464
- const wStart = m.index, wEnd = m.index + word.length;
2465
- if (emailRanges.some(([s, e]) => wStart < e && wEnd > s))
2466
- continue;
2467
- if (caretNode === tn && caretOffset >= m.index && caretOffset <= m.index + word.length) {
2468
- continue;
2469
- }
2470
- if (sp.correct(word))
2471
- continue;
2472
- hits.push({ node: tn, start: m.index, end: m.index + word.length });
2473
- }
2474
- n = walker.nextNode();
2475
- }
2476
- hits.reverse();
2477
- for (const h of hits) {
2478
- const range = doc.createRange();
2479
- range.setStart(h.node, h.start);
2480
- range.setEnd(h.node, h.end);
2481
- const span = doc.createElement("span");
2482
- span.setAttribute(MARKER_ATTR2, "1");
2483
- try {
2484
- range.surroundContents(span);
2485
- } catch {
2486
- }
2487
- }
2488
- });
2489
- } finally {
2490
- let restored = false;
2491
- if (savedFocusNode && body.contains(savedFocusNode)) {
2492
- try {
2493
- const range = doc.createRange();
2494
- const maxOffset = savedFocusNode.nodeType === Node.TEXT_NODE ? savedFocusNode.data.length : savedFocusNode.childNodes.length;
2495
- range.setStart(savedFocusNode, Math.min(savedFocusOffset, maxOffset));
2496
- range.collapse(true);
2497
- const sel = doc.getSelection();
2498
- if (sel) {
2499
- sel.removeAllRanges();
2500
- sel.addRange(range);
2501
- restored = true;
2502
- }
2503
- } catch {
2504
- }
2505
- }
2506
- if (!restored && savedAbs != null)
2507
- restoreCaretFromAbsOffset(body, savedAbs);
2508
- if (scroller && scroller.scrollTop !== savedScrollTop)
2509
- scroller.scrollTop = savedScrollTop;
2510
- if (body.scrollTop !== savedBodyScrollTop)
2511
- body.scrollTop = savedBodyScrollTop;
2512
- }
2513
- }
2514
- function caretAbsOffsetFromBody(body) {
2515
- const doc = body.ownerDocument;
2516
- const sel = doc.getSelection();
2517
- if (!sel || sel.rangeCount === 0)
2518
- return null;
2519
- const focusNode = sel.focusNode;
2520
- const focusOffset = sel.focusOffset;
2521
- if (!focusNode)
2522
- return null;
2523
- if (focusNode.nodeType !== Node.TEXT_NODE) {
2524
- let abs2 = 0;
2525
- const walker2 = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
2526
- let n2 = walker2.nextNode();
2527
- while (n2) {
2528
- if (focusNode.contains(n2))
2529
- break;
2530
- const cmp = focusNode.compareDocumentPosition(n2);
2531
- if (cmp & Node.DOCUMENT_POSITION_PRECEDING)
2532
- abs2 += n2.data.length;
2533
- n2 = walker2.nextNode();
2534
- }
2535
- return abs2;
2536
- }
2537
- let abs = 0;
2538
- const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
2539
- let n = walker.nextNode();
2540
- while (n) {
2541
- if (n === focusNode)
2542
- return abs + focusOffset;
2543
- abs += n.data.length;
2544
- n = walker.nextNode();
2545
- }
2546
- return null;
2547
- }
2548
- function restoreCaretFromAbsOffset(body, abs) {
2549
- const doc = body.ownerDocument;
2550
- const sel = doc.getSelection();
2551
- if (!sel)
2552
- return;
2553
- const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
2554
- let acc = 0;
2555
- let n = walker.nextNode();
2556
- while (n) {
2557
- const len = n.data.length;
2558
- if (acc + len >= abs) {
2559
- const range = doc.createRange();
2560
- range.setStart(n, Math.max(0, abs - acc));
2561
- range.collapse(true);
2562
- sel.removeAllRanges();
2563
- sel.addRange(range);
2564
- return;
2565
- }
2566
- acc += len;
2567
- n = walker.nextNode();
2568
- }
2569
- const last = walker.previousNode();
2570
- if (last) {
2571
- const range = doc.createRange();
2572
- range.setStart(last, last.data.length);
2573
- range.collapse(true);
2574
- sel.removeAllRanges();
2575
- sel.addRange(range);
2576
- }
2577
- }
2578
- function installDecorationStyle(editor2) {
2579
- const doc = editor2.getDoc?.();
2580
- if (!doc)
2581
- return;
2582
- if (doc.getElementById("mailx-spell-style"))
2583
- return;
2584
- const style = doc.createElement("style");
2585
- style.id = "mailx-spell-style";
2586
- style.textContent = `
2587
- span[${MARKER_ATTR2}] {
2588
- text-decoration: underline wavy #d33;
2589
- text-decoration-skip-ink: none;
2590
- text-underline-offset: 2px;
2591
- /* No background \u2014 keeps the styling subtle, like a native
2592
- * spell underline, not a Find-highlight. */
2593
- background: transparent;
2594
- }
2595
- `;
2596
- doc.head.appendChild(style);
2597
- }
2598
- function installSerializerFilter(editor2) {
2599
- if (editor2.__mailxSpellSerializerWired)
2600
- return;
2601
- editor2.__mailxSpellSerializerWired = true;
2602
- try {
2603
- editor2.serializer.addAttributeFilter(MARKER_ATTR2, (nodes) => {
2604
- for (const node of nodes) {
2605
- if (typeof node.unwrap === "function")
2606
- node.unwrap();
2607
- }
2608
- });
2609
- } catch (e) {
2610
- console.warn("[spellcheck] serializer filter setup failed:", e);
2611
- }
2612
- }
2613
- function showSuggestionsMenu2(parentDoc, x, y, items) {
2614
- parentDoc.getElementById("mailx-spell-menu")?.remove();
2615
- const menu = parentDoc.createElement("div");
2616
- menu.id = "mailx-spell-menu";
2617
- menu.style.cssText = `
2618
- position: fixed;
2619
- left: ${x}px; top: ${y}px;
2620
- z-index: 10000;
2621
- background: var(--color-bg, #fff);
2622
- color: var(--color-text, #222);
2623
- border: 1px solid var(--color-border, #ccc);
2624
- border-radius: 6px;
2625
- box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2626
- padding: 4px 0;
2627
- font: 13px system-ui, sans-serif;
2628
- min-width: 180px;
2629
- max-width: 320px;
2630
- `;
2631
- for (const it of items) {
2632
- if (it.separator) {
2633
- const sep = parentDoc.createElement("div");
2634
- sep.style.cssText = "border-top:1px solid var(--color-border,#ddd); margin: 4px 0;";
2635
- menu.appendChild(sep);
2636
- continue;
2637
- }
2638
- const btn = parentDoc.createElement("button");
2639
- btn.type = "button";
2640
- btn.textContent = it.label;
2641
- btn.style.cssText = `
2642
- display: block; width: 100%; text-align: left;
2643
- padding: 5px 12px; border: none; background: none;
2644
- color: inherit; cursor: pointer; font: inherit;
2645
- ${it.emphasized ? "font-weight: 600;" : ""}
2646
- `;
2647
- btn.addEventListener("mouseenter", () => {
2648
- btn.style.background = "var(--color-bg-hover, #eef)";
2649
- });
2650
- btn.addEventListener("mouseleave", () => {
2651
- btn.style.background = "none";
2652
- });
2653
- btn.addEventListener("click", () => {
2654
- try {
2655
- it.action();
2656
- } finally {
2657
- menu.remove();
2658
- }
2659
- });
2660
- menu.appendChild(btn);
2661
- }
2662
- parentDoc.body.appendChild(menu);
2663
- const r = menu.getBoundingClientRect();
2664
- if (r.right > window.innerWidth)
2665
- menu.style.left = `${Math.max(8, window.innerWidth - r.width - 8)}px`;
2666
- if (r.bottom > window.innerHeight)
2667
- menu.style.top = `${Math.max(8, window.innerHeight - r.height - 8)}px`;
2668
- const docs = [parentDoc];
2669
- try {
2670
- const composeWin = parentDoc.defaultView;
2671
- if (composeWin?.frameElement && composeWin.parent?.document && composeWin.parent.document !== parentDoc) {
2672
- docs.push(composeWin.parent.document);
2673
- }
2674
- } catch {
2675
- }
2676
- try {
2677
- const editorIframe = parentDoc.querySelector("iframe.tox-edit-area__iframe") || parentDoc.querySelector("iframe");
2678
- const editorDoc = editorIframe?.contentDocument;
2679
- if (editorDoc && editorDoc !== parentDoc)
2680
- docs.push(editorDoc);
2681
- } catch {
2682
- }
2683
- const dismiss2 = (e) => {
2684
- if (e.type === "keydown" && e.key !== "Escape")
2685
- return;
2686
- if (e.type === "mousedown" && menu.contains(e.target))
2687
- return;
2688
- menu.remove();
2689
- for (const d of docs) {
2690
- d.removeEventListener("mousedown", dismiss2, true);
2691
- d.removeEventListener("keydown", dismiss2, true);
2692
- }
2693
- };
2694
- setTimeout(() => {
2695
- for (const d of docs) {
2696
- d.addEventListener("mousedown", dismiss2, true);
2697
- d.addEventListener("keydown", dismiss2, true);
2698
- }
2699
- }, 0);
2700
- }
2701
- function replaceMarker(editor2, marker, replacement) {
2702
- try {
2703
- editor2.focus();
2704
- editor2.selection.select(marker);
2705
- editor2.insertContent(editor2.dom.encode(replacement));
2706
- } catch {
2707
- const doc = editor2.getDoc();
2708
- const range = doc.createRange();
2709
- range.selectNode(marker);
2710
- range.deleteContents();
2711
- range.insertNode(doc.createTextNode(replacement));
2712
- }
2713
- }
2714
- function cleanupCorrected(editor2, sp) {
2715
- const body = editor2.getBody?.();
2716
- const doc = editor2.getDoc?.();
2717
- if (!body || !doc)
2718
- return;
2719
- const activeSel = doc.getSelection();
2720
- if (activeSel && activeSel.rangeCount > 0 && !activeSel.isCollapsed)
2721
- return;
2722
- const markers = body.querySelectorAll(`span[${MARKER_ATTR2}]`);
2723
- if (markers.length === 0)
2724
- return;
2725
- let caretMarker = null;
2726
- const sel = doc.getSelection();
2727
- if (sel && sel.rangeCount > 0) {
2728
- let p = sel.focusNode;
2729
- while (p && p !== body) {
2730
- if (p.nodeType === Node.ELEMENT_NODE && p.hasAttribute?.(MARKER_ATTR2)) {
2731
- caretMarker = p;
2732
- break;
2733
- }
2734
- p = p.parentNode;
2735
- }
2736
- }
2737
- const stale = [];
2738
- for (const m of markers) {
2739
- const word = m.textContent || "";
2740
- const isStale = !word || /\s/.test(word) || sp.correct(word);
2741
- if (!isStale && m === caretMarker)
2742
- continue;
2743
- if (isStale)
2744
- stale.push(m);
2745
- }
2746
- if (stale.length === 0)
2747
- return;
2748
- editor2.undoManager?.ignore?.(() => {
2749
- for (const m of stale) {
2750
- const parent2 = m.parentNode;
2751
- if (!parent2)
2752
- continue;
2753
- while (m.firstChild)
2754
- parent2.insertBefore(m.firstChild, m);
2755
- parent2.removeChild(m);
2756
- }
2757
- });
2758
- }
2759
2332
  function wireSpellcheck(editor2) {
2760
2333
  if (editor2.__mailxSpellWired)
2761
2334
  return;
2762
2335
  editor2.__mailxSpellWired = true;
2763
- const killNativeSpellcheck = () => {
2336
+ let sp = null;
2337
+ const killNative = () => {
2764
2338
  try {
2765
2339
  const body = editor2.getBody?.();
2766
2340
  if (body && body.getAttribute("spellcheck") !== "false") {
@@ -2769,129 +2343,182 @@ function wireSpellcheck(editor2) {
2769
2343
  } catch {
2770
2344
  }
2771
2345
  };
2772
- killNativeSpellcheck();
2346
+ killNative();
2773
2347
  try {
2774
2348
  const body = editor2.getBody?.();
2775
2349
  if (body)
2776
- new MutationObserver(killNativeSpellcheck).observe(body, { attributes: true, attributeFilter: ["spellcheck"] });
2350
+ new MutationObserver(killNative).observe(body, { attributes: true, attributeFilter: ["spellcheck"] });
2777
2351
  } catch {
2778
2352
  }
2779
- let sp = null;
2780
- let decorateTimer = null;
2781
- const scheduleDecorate = () => {
2353
+ const ensureOverlay = () => {
2354
+ const body = editor2.getBody?.();
2355
+ const doc = editor2.getDoc?.();
2356
+ if (!body || !doc)
2357
+ return null;
2358
+ let ov = doc.getElementById(OVERLAY_ID);
2359
+ if (!ov || ov.parentNode !== body) {
2360
+ ov?.remove();
2361
+ ov = doc.createElement("div");
2362
+ ov.id = OVERLAY_ID;
2363
+ ov.setAttribute("contenteditable", "false");
2364
+ ov.setAttribute("data-mce-bogus", "all");
2365
+ ov.style.cssText = "position:absolute;top:0;left:0;pointer-events:none;user-select:none;";
2366
+ body.appendChild(ov);
2367
+ }
2368
+ return ov;
2369
+ };
2370
+ const scan = () => {
2782
2371
  if (!sp)
2783
2372
  return;
2784
- if (decorateTimer)
2785
- clearTimeout(decorateTimer);
2786
- decorateTimer = setTimeout(() => {
2787
- decorateTimer = null;
2788
- if (sp)
2789
- decorate(editor2, sp);
2790
- }, DECORATE_DEBOUNCE_MS);
2373
+ const body = editor2.getBody?.();
2374
+ const doc = editor2.getDoc?.();
2375
+ const ov = ensureOverlay();
2376
+ if (!body || !doc || !ov)
2377
+ return;
2378
+ const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
2379
+ acceptNode(node) {
2380
+ let p = node.parentNode;
2381
+ while (p && p !== body) {
2382
+ if (p.nodeType === Node.ELEMENT_NODE) {
2383
+ const el = p;
2384
+ if (el.id === OVERLAY_ID)
2385
+ return NodeFilter.FILTER_REJECT;
2386
+ if (SKIP_TAGS.has(el.tagName))
2387
+ return NodeFilter.FILTER_REJECT;
2388
+ }
2389
+ p = p.parentNode;
2390
+ }
2391
+ return NodeFilter.FILTER_ACCEPT;
2392
+ }
2393
+ });
2394
+ const bodyRect = body.getBoundingClientRect();
2395
+ const sx = body.scrollLeft;
2396
+ const sy = body.scrollTop;
2397
+ const frag = doc.createDocumentFragment();
2398
+ const wordRe = /[\p{L}][\p{L}'’-]*/gu;
2399
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
2400
+ const text = n.data;
2401
+ if (!text || text.length < MIN_WORD_LEN)
2402
+ continue;
2403
+ wordRe.lastIndex = 0;
2404
+ let m;
2405
+ while (m = wordRe.exec(text)) {
2406
+ const w = m[0];
2407
+ if (w.length < MIN_WORD_LEN)
2408
+ continue;
2409
+ if (sp.correct(w))
2410
+ continue;
2411
+ const r = doc.createRange();
2412
+ r.setStart(n, m.index);
2413
+ r.setEnd(n, m.index + w.length);
2414
+ const rects = r.getClientRects();
2415
+ for (let i = 0; i < rects.length; i++) {
2416
+ const rect = rects[i];
2417
+ if (rect.width < 1)
2418
+ continue;
2419
+ const sq = doc.createElement("div");
2420
+ sq.style.cssText = `position:absolute;left:${(rect.left - bodyRect.left + sx).toFixed(1)}px;top:${(rect.bottom - bodyRect.top + sy - 3).toFixed(1)}px;width:${rect.width.toFixed(1)}px;height:3px;background:${WAVE} repeat-x left bottom;`;
2421
+ frag.appendChild(sq);
2422
+ }
2423
+ }
2424
+ }
2425
+ ov.textContent = "";
2426
+ ov.appendChild(frag);
2791
2427
  };
2792
- let cleanupTimer = null;
2793
- const scheduleCleanup = () => {
2428
+ let scanTimer = null;
2429
+ const scheduleScan = () => {
2794
2430
  if (!sp)
2795
2431
  return;
2796
- if (cleanupTimer)
2797
- clearTimeout(cleanupTimer);
2798
- cleanupTimer = setTimeout(() => {
2799
- cleanupTimer = null;
2800
- if (sp)
2801
- cleanupCorrected(editor2, sp);
2802
- }, CLEANUP_DEBOUNCE_MS);
2432
+ if (scanTimer)
2433
+ clearTimeout(scanTimer);
2434
+ scanTimer = setTimeout(() => {
2435
+ scanTimer = null;
2436
+ scan();
2437
+ }, SCAN_DEBOUNCE_MS);
2803
2438
  };
2804
- getSpell2().then((loaded) => {
2439
+ getSpell().then((loaded) => {
2805
2440
  sp = loaded;
2806
- installDecorationStyle(editor2);
2807
- installSerializerFilter(editor2);
2808
- decorate(editor2, loaded);
2809
- }).catch((err) => {
2810
- console.error("[spellcheck] dict load failed:", err);
2811
- });
2812
- editor2.on("input nodechange setcontent paste keyup", scheduleDecorate);
2813
- editor2.on("input nodechange setcontent paste keyup", scheduleCleanup);
2441
+ scan();
2442
+ }).catch((e) => console.error("[spellcheck] dict load failed:", e));
2443
+ editor2.on("input keyup paste SetContent Undo Redo", scheduleScan);
2444
+ editor2.on("ResizeEditor", scheduleScan);
2445
+ try {
2446
+ editor2.getDoc()?.addEventListener("scroll", scheduleScan, { passive: true });
2447
+ } catch {
2448
+ }
2449
+ const apply = (node, start, end, replacement) => {
2450
+ try {
2451
+ const doc = editor2.getDoc();
2452
+ const range = doc.createRange();
2453
+ range.setStart(node, start);
2454
+ range.setEnd(node, end);
2455
+ editor2.focus();
2456
+ editor2.selection.setRng(range);
2457
+ editor2.insertContent(editor2.dom.encode(replacement));
2458
+ } catch {
2459
+ try {
2460
+ const doc = editor2.getDoc();
2461
+ const range = doc.createRange();
2462
+ range.setStart(node, start);
2463
+ range.setEnd(node, end);
2464
+ range.deleteContents();
2465
+ range.insertNode(doc.createTextNode(replacement));
2466
+ } catch {
2467
+ }
2468
+ }
2469
+ scheduleScan();
2470
+ };
2814
2471
  const iframeDoc = editor2.getDoc();
2815
2472
  iframeDoc.addEventListener("contextmenu", (ev) => {
2816
2473
  const e = ev;
2817
- const target = e.target;
2818
- if (!target)
2474
+ const body = editor2.getBody?.();
2475
+ if (!body || !sp)
2819
2476
  return;
2820
- const marker = target.closest?.(`span[${MARKER_ATTR2}]`);
2821
- if (!marker)
2477
+ const hit = getWordAtPoint(body, e.clientX, e.clientY);
2478
+ if (!hit)
2822
2479
  return;
2823
- const word = marker.textContent || "";
2824
- if (!word || !sp)
2480
+ if (sp.correct(hit.word))
2825
2481
  return;
2826
2482
  e.preventDefault();
2827
2483
  e.stopPropagation();
2828
- const transposed = [];
2829
- for (let i = 0; i < word.length - 1; i++) {
2830
- const swapped = word.slice(0, i) + word[i + 1] + word[i] + word.slice(i + 2);
2831
- if (swapped !== word && sp.correct(swapped) && !transposed.includes(swapped)) {
2832
- transposed.push(swapped);
2833
- }
2834
- }
2835
- const nspellSugs = sp.suggest(word);
2836
- const sugs = [];
2837
- for (const s of [...transposed, ...nspellSugs]) {
2838
- if (!sugs.includes(s))
2839
- sugs.push(s);
2840
- if (sugs.length >= 7)
2841
- break;
2842
- }
2843
- const iframeEl = editor2.iframeElement;
2844
- const iframeRect = iframeEl ? iframeEl.getBoundingClientRect() : { left: 0, top: 0 };
2845
- const items = [];
2846
- if (sugs.length === 0) {
2847
- items.push({ label: "(no suggestions)", action: () => {
2848
- } });
2849
- } else {
2850
- for (const s of sugs) {
2851
- items.push({
2852
- label: s,
2853
- emphasized: true,
2854
- action: () => {
2855
- replaceMarker(editor2, marker, s);
2856
- scheduleDecorate();
2857
- }
2858
- });
2859
- }
2860
- }
2484
+ const sugs = buildSuggestionList(hit.word, sp);
2485
+ const items = sugs.length === 0 ? [{ label: "(no suggestions)", action: () => {
2486
+ } }] : sugs.map((s) => ({
2487
+ label: s,
2488
+ emphasized: true,
2489
+ action: () => apply(hit.node, hit.start, hit.end, s)
2490
+ }));
2861
2491
  items.push({ label: "", action: () => {
2862
2492
  }, separator: true });
2863
2493
  items.push({
2864
- label: `Add "${word}" to dictionary`,
2494
+ label: `Add "${hit.word}" to dictionary`,
2865
2495
  action: () => {
2866
2496
  if (sp)
2867
- addToUserDict2(word, sp);
2868
- scheduleDecorate();
2497
+ addToUserDict(hit.word, sp);
2498
+ scheduleScan();
2869
2499
  }
2870
2500
  });
2871
2501
  items.push({
2872
2502
  label: "Ignore (this session)",
2873
2503
  action: () => {
2874
2504
  if (sp)
2875
- sp.add(word);
2876
- scheduleDecorate();
2505
+ sp.add(hit.word);
2506
+ scheduleScan();
2877
2507
  }
2878
2508
  });
2879
- showSuggestionsMenu2(document, iframeRect.left + e.clientX, iframeRect.top + e.clientY, items);
2509
+ const iframeEl = editor2.iframeElement;
2510
+ const rect = iframeEl ? iframeEl.getBoundingClientRect() : { left: 0, top: 0 };
2511
+ showSuggestionsMenu(document, rect.left + e.clientX, rect.top + e.clientY, items, [iframeDoc]);
2880
2512
  }, true);
2881
2513
  }
2882
- var import_nspell2, USER_DICT_KEY2, MARKER_ATTR2, DECORATE_DEBOUNCE_MS, CLEANUP_DEBOUNCE_MS, MIN_WORD_LEN2, SKIP_TAGS2, spellPromise2;
2514
+ var SCAN_DEBOUNCE_MS, WAVE, OVERLAY_ID;
2883
2515
  var init_spellcheck = __esm({
2884
2516
  "client/compose/spellcheck.js"() {
2885
2517
  "use strict";
2886
- import_nspell2 = __toESM(require_lib(), 1);
2887
- init_api_client();
2888
- USER_DICT_KEY2 = "mailx-user-dict";
2889
- MARKER_ATTR2 = "data-mailx-spellerror";
2890
- DECORATE_DEBOUNCE_MS = 1200;
2891
- CLEANUP_DEBOUNCE_MS = 300;
2892
- MIN_WORD_LEN2 = 3;
2893
- SKIP_TAGS2 = /* @__PURE__ */ new Set(["BLOCKQUOTE", "CODE", "PRE", "A", "SCRIPT", "STYLE", "KBD", "SAMP", "VAR"]);
2894
- spellPromise2 = null;
2518
+ init_spellcheck_core();
2519
+ SCAN_DEBOUNCE_MS = 600;
2520
+ WAVE = `url("data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="6" height="3"><path d="M0 2 Q1.5 0 3 2 T6 2" stroke="#d33" fill="none" stroke-width="1"/></svg>')}")`;
2521
+ OVERLAY_ID = "mailx-spell-overlay";
2895
2522
  }
2896
2523
  });
2897
2524