@bobfrankston/rmfmail 1.0.678 → 1.0.679
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/client/compose/compose.bundle.js +189 -88
- package/client/compose/compose.bundle.js.map +3 -3
- package/client/compose/spellcheck.js +257 -150
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +252 -136
- package/docs/accounts.md +9 -1
- package/package.json +3 -3
- package/packages/mailx-service/index.d.ts +1 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +26 -6
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +24 -4
|
@@ -1729,52 +1729,120 @@ function addToUserDict(word, sp) {
|
|
|
1729
1729
|
}
|
|
1730
1730
|
sp.add(word);
|
|
1731
1731
|
}
|
|
1732
|
-
function
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
caretOffset = p.offset;
|
|
1748
|
-
} else {
|
|
1749
|
-
return null;
|
|
1732
|
+
function decorate(editor2, sp) {
|
|
1733
|
+
const body = editor2.getBody?.();
|
|
1734
|
+
const doc = editor2.getDoc?.();
|
|
1735
|
+
if (!body || !doc)
|
|
1736
|
+
return;
|
|
1737
|
+
const sel = doc.getSelection();
|
|
1738
|
+
if (sel && sel.rangeCount > 0) {
|
|
1739
|
+
const focus = sel.focusNode;
|
|
1740
|
+
let p = focus;
|
|
1741
|
+
while (p && p !== body) {
|
|
1742
|
+
if (p.nodeType === Node.ELEMENT_NODE && p.hasAttribute?.(MARKER_ATTR)) {
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
p = p.parentNode;
|
|
1746
|
+
}
|
|
1750
1747
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1748
|
+
const bookmark = editor2.selection?.getBookmark?.(2);
|
|
1749
|
+
try {
|
|
1750
|
+
editor2.undoManager?.ignore?.(() => {
|
|
1751
|
+
const old = body.querySelectorAll(`span[${MARKER_ATTR}]`);
|
|
1752
|
+
for (const m of old) {
|
|
1753
|
+
const parent2 = m.parentNode;
|
|
1754
|
+
if (!parent2)
|
|
1755
|
+
continue;
|
|
1756
|
+
while (m.firstChild)
|
|
1757
|
+
parent2.insertBefore(m.firstChild, m);
|
|
1758
|
+
parent2.removeChild(m);
|
|
1759
|
+
}
|
|
1760
|
+
body.normalize();
|
|
1761
|
+
const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
|
|
1762
|
+
acceptNode(node) {
|
|
1763
|
+
let p = node.parentNode;
|
|
1764
|
+
while (p && p !== body) {
|
|
1765
|
+
if (p.nodeType === Node.ELEMENT_NODE && SKIP_TAGS.has(p.tagName)) {
|
|
1766
|
+
return NodeFilter.FILTER_REJECT;
|
|
1767
|
+
}
|
|
1768
|
+
p = p.parentNode;
|
|
1769
|
+
}
|
|
1770
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
const hits = [];
|
|
1774
|
+
let n = walker.nextNode();
|
|
1775
|
+
const WORD_RE = /[\p{L}][\p{L}'’\-]*/gu;
|
|
1776
|
+
while (n) {
|
|
1777
|
+
const tn = n;
|
|
1778
|
+
const text = tn.data;
|
|
1779
|
+
let m;
|
|
1780
|
+
WORD_RE.lastIndex = 0;
|
|
1781
|
+
while ((m = WORD_RE.exec(text)) !== null) {
|
|
1782
|
+
const word = m[0];
|
|
1783
|
+
if (word.length < MIN_WORD_LEN)
|
|
1784
|
+
continue;
|
|
1785
|
+
if (sp.correct(word))
|
|
1786
|
+
continue;
|
|
1787
|
+
hits.push({ node: tn, start: m.index, end: m.index + word.length });
|
|
1788
|
+
}
|
|
1789
|
+
n = walker.nextNode();
|
|
1790
|
+
}
|
|
1791
|
+
hits.reverse();
|
|
1792
|
+
for (const h of hits) {
|
|
1793
|
+
const range = doc.createRange();
|
|
1794
|
+
range.setStart(h.node, h.start);
|
|
1795
|
+
range.setEnd(h.node, h.end);
|
|
1796
|
+
const span = doc.createElement("span");
|
|
1797
|
+
span.setAttribute(MARKER_ATTR, "1");
|
|
1798
|
+
try {
|
|
1799
|
+
range.surroundContents(span);
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
} finally {
|
|
1805
|
+
if (bookmark)
|
|
1806
|
+
try {
|
|
1807
|
+
editor2.selection?.moveToBookmark?.(bookmark);
|
|
1808
|
+
} catch {
|
|
1809
|
+
}
|
|
1767
1810
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1811
|
+
}
|
|
1812
|
+
function installDecorationStyle(editor2) {
|
|
1813
|
+
const doc = editor2.getDoc?.();
|
|
1814
|
+
if (!doc)
|
|
1815
|
+
return;
|
|
1816
|
+
if (doc.getElementById("mailx-spell-style"))
|
|
1817
|
+
return;
|
|
1818
|
+
const style = doc.createElement("style");
|
|
1819
|
+
style.id = "mailx-spell-style";
|
|
1820
|
+
style.textContent = `
|
|
1821
|
+
span[${MARKER_ATTR}] {
|
|
1822
|
+
text-decoration: underline wavy #d33;
|
|
1823
|
+
text-decoration-skip-ink: none;
|
|
1824
|
+
text-underline-offset: 2px;
|
|
1825
|
+
/* No background \u2014 keeps the styling subtle, like a native
|
|
1826
|
+
* spell underline, not a Find-highlight. */
|
|
1827
|
+
background: transparent;
|
|
1828
|
+
}
|
|
1829
|
+
`;
|
|
1830
|
+
doc.head.appendChild(style);
|
|
1831
|
+
}
|
|
1832
|
+
function installSerializerFilter(editor2) {
|
|
1833
|
+
if (editor2.__mailxSpellSerializerWired)
|
|
1834
|
+
return;
|
|
1835
|
+
editor2.__mailxSpellSerializerWired = true;
|
|
1836
|
+
try {
|
|
1837
|
+
editor2.serializer.addAttributeFilter(MARKER_ATTR, (nodes) => {
|
|
1838
|
+
for (const node of nodes) {
|
|
1839
|
+
if (typeof node.unwrap === "function")
|
|
1840
|
+
node.unwrap();
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
console.warn("[spellcheck] serializer filter setup failed:", e);
|
|
1771
1845
|
}
|
|
1772
|
-
if (!word)
|
|
1773
|
-
return null;
|
|
1774
|
-
const range = doc.createRange();
|
|
1775
|
-
range.setStart(caretNode, start);
|
|
1776
|
-
range.setEnd(caretNode, end);
|
|
1777
|
-
return { word, range };
|
|
1778
1846
|
}
|
|
1779
1847
|
function showSuggestionsMenu(parentDoc, x, y, items) {
|
|
1780
1848
|
parentDoc.getElementById("mailx-spell-menu")?.remove();
|
|
@@ -1791,11 +1859,11 @@ function showSuggestionsMenu(parentDoc, x, y, items) {
|
|
|
1791
1859
|
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
1792
1860
|
padding: 4px 0;
|
|
1793
1861
|
font: 13px system-ui, sans-serif;
|
|
1794
|
-
min-width:
|
|
1862
|
+
min-width: 180px;
|
|
1795
1863
|
max-width: 320px;
|
|
1796
1864
|
`;
|
|
1797
1865
|
for (const it of items) {
|
|
1798
|
-
if (it.
|
|
1866
|
+
if (it.separator) {
|
|
1799
1867
|
const sep = parentDoc.createElement("div");
|
|
1800
1868
|
sep.style.cssText = "border-top:1px solid var(--color-border,#ddd); margin: 4px 0;";
|
|
1801
1869
|
menu.appendChild(sep);
|
|
@@ -1845,7 +1913,10 @@ function showSuggestionsMenu(parentDoc, x, y, items) {
|
|
|
1845
1913
|
parentDoc.addEventListener("keydown", dismiss2, true);
|
|
1846
1914
|
}, 0);
|
|
1847
1915
|
}
|
|
1848
|
-
function
|
|
1916
|
+
function replaceMarker(editor2, marker, replacement) {
|
|
1917
|
+
const doc = editor2.getDoc();
|
|
1918
|
+
const range = doc.createRange();
|
|
1919
|
+
range.selectNode(marker);
|
|
1849
1920
|
const sel = doc.getSelection();
|
|
1850
1921
|
if (!sel)
|
|
1851
1922
|
return;
|
|
@@ -1865,62 +1936,92 @@ function wireSpellcheck(editor2) {
|
|
|
1865
1936
|
if (editor2.__mailxSpellWired)
|
|
1866
1937
|
return;
|
|
1867
1938
|
editor2.__mailxSpellWired = true;
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1939
|
+
let sp = null;
|
|
1940
|
+
let decorateTimer = null;
|
|
1941
|
+
const scheduleDecorate = () => {
|
|
1942
|
+
if (!sp)
|
|
1943
|
+
return;
|
|
1944
|
+
if (decorateTimer)
|
|
1945
|
+
clearTimeout(decorateTimer);
|
|
1946
|
+
decorateTimer = setTimeout(() => {
|
|
1947
|
+
decorateTimer = null;
|
|
1948
|
+
if (sp)
|
|
1949
|
+
decorate(editor2, sp);
|
|
1950
|
+
}, DECORATE_DEBOUNCE_MS);
|
|
1951
|
+
};
|
|
1952
|
+
getSpell().then((loaded) => {
|
|
1953
|
+
sp = loaded;
|
|
1954
|
+
installDecorationStyle(editor2);
|
|
1955
|
+
installSerializerFilter(editor2);
|
|
1956
|
+
decorate(editor2, loaded);
|
|
1957
|
+
}).catch((err) => {
|
|
1958
|
+
console.error("[spellcheck] dict load failed:", err);
|
|
1959
|
+
});
|
|
1960
|
+
editor2.on("input nodechange setcontent paste keyup", scheduleDecorate);
|
|
1961
|
+
const iframeDoc = editor2.getDoc();
|
|
1871
1962
|
iframeDoc.addEventListener("contextmenu", (ev) => {
|
|
1872
1963
|
const e = ev;
|
|
1873
|
-
const
|
|
1874
|
-
if (!
|
|
1964
|
+
const target = e.target;
|
|
1965
|
+
if (!target)
|
|
1875
1966
|
return;
|
|
1876
|
-
const
|
|
1877
|
-
if (!
|
|
1967
|
+
const marker = target.closest?.(`span[${MARKER_ATTR}]`);
|
|
1968
|
+
if (!marker)
|
|
1878
1969
|
return;
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
return;
|
|
1882
|
-
const sugs = sp.suggest(found.word).slice(0, 7);
|
|
1883
|
-
const iframeRect = iframeEl.getBoundingClientRect();
|
|
1884
|
-
const menuX = iframeRect.left + e.clientX;
|
|
1885
|
-
const menuY = iframeRect.top + e.clientY;
|
|
1886
|
-
const items = [];
|
|
1887
|
-
if (sugs.length === 0) {
|
|
1888
|
-
items.push({ label: "(no suggestions)", action: () => {
|
|
1889
|
-
} });
|
|
1890
|
-
} else {
|
|
1891
|
-
for (const s of sugs) {
|
|
1892
|
-
items.push({
|
|
1893
|
-
label: s,
|
|
1894
|
-
emphasized: true,
|
|
1895
|
-
action: () => replaceRange(iframeDoc, found.range, s)
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
items.push({ label: "---", action: () => {
|
|
1900
|
-
} });
|
|
1901
|
-
items.push({
|
|
1902
|
-
label: `Add "${found.word}" to dictionary`,
|
|
1903
|
-
action: () => addToUserDict(found.word, sp)
|
|
1904
|
-
});
|
|
1905
|
-
items.push({
|
|
1906
|
-
label: "Ignore (this session)",
|
|
1907
|
-
action: () => sp.add(found.word)
|
|
1908
|
-
});
|
|
1909
|
-
showSuggestionsMenu(document, menuX, menuY, items);
|
|
1910
|
-
}).catch((err) => {
|
|
1911
|
-
console.error("[spellcheck] dict load failed:", err);
|
|
1970
|
+
const word = marker.textContent || "";
|
|
1971
|
+
if (!word || !sp)
|
|
1912
1972
|
return;
|
|
1913
|
-
});
|
|
1914
1973
|
e.preventDefault();
|
|
1915
1974
|
e.stopPropagation();
|
|
1975
|
+
const sugs = sp.suggest(word).slice(0, 7);
|
|
1976
|
+
const iframeEl = editor2.iframeElement;
|
|
1977
|
+
const iframeRect = iframeEl ? iframeEl.getBoundingClientRect() : { left: 0, top: 0 };
|
|
1978
|
+
const items = [];
|
|
1979
|
+
if (sugs.length === 0) {
|
|
1980
|
+
items.push({ label: "(no suggestions)", action: () => {
|
|
1981
|
+
} });
|
|
1982
|
+
} else {
|
|
1983
|
+
for (const s of sugs) {
|
|
1984
|
+
items.push({
|
|
1985
|
+
label: s,
|
|
1986
|
+
emphasized: true,
|
|
1987
|
+
action: () => {
|
|
1988
|
+
replaceMarker(editor2, marker, s);
|
|
1989
|
+
scheduleDecorate();
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
items.push({ label: "", action: () => {
|
|
1995
|
+
}, separator: true });
|
|
1996
|
+
items.push({
|
|
1997
|
+
label: `Add "${word}" to dictionary`,
|
|
1998
|
+
action: () => {
|
|
1999
|
+
if (sp)
|
|
2000
|
+
addToUserDict(word, sp);
|
|
2001
|
+
scheduleDecorate();
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
items.push({
|
|
2005
|
+
label: "Ignore (this session)",
|
|
2006
|
+
action: () => {
|
|
2007
|
+
if (sp)
|
|
2008
|
+
sp.add(word);
|
|
2009
|
+
scheduleDecorate();
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
showSuggestionsMenu(document, iframeRect.left + e.clientX, iframeRect.top + e.clientY, items);
|
|
1916
2013
|
}, true);
|
|
1917
2014
|
}
|
|
1918
|
-
var import_nspell, USER_DICT_KEY, spellPromise;
|
|
2015
|
+
var import_nspell, USER_DICT_KEY, MARKER_ATTR, DECORATE_DEBOUNCE_MS, MIN_WORD_LEN, SKIP_TAGS, spellPromise;
|
|
1919
2016
|
var init_spellcheck = __esm({
|
|
1920
2017
|
"client/compose/spellcheck.js"() {
|
|
1921
2018
|
"use strict";
|
|
1922
2019
|
import_nspell = __toESM(require_lib(), 1);
|
|
1923
2020
|
USER_DICT_KEY = "mailx-user-dict";
|
|
2021
|
+
MARKER_ATTR = "data-mailx-spellerror";
|
|
2022
|
+
DECORATE_DEBOUNCE_MS = 500;
|
|
2023
|
+
MIN_WORD_LEN = 3;
|
|
2024
|
+
SKIP_TAGS = /* @__PURE__ */ new Set(["BLOCKQUOTE", "CODE", "PRE", "A", "SCRIPT", "STYLE", "KBD", "SAMP", "VAR"]);
|
|
1924
2025
|
spellPromise = null;
|
|
1925
2026
|
}
|
|
1926
2027
|
});
|