@bobfrankston/rmfmail 1.2.286 → 1.2.287
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/.commitmsg +28 -38
- package/client/compose/compose.bundle.js +106 -38
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/harper-lint.js +125 -42
- package/client/compose/harper-lint.js.map +1 -1
- package/client/compose/harper-lint.ts +120 -40
- package/npmchanges.md +46 -0
- package/package.json +6 -6
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +20 -3
- package/packages/mailx-service/index.js.map +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-50896 → node_modules.npmglobalize-stash-38644}/.package-lock.json +0 -0
package/.commitmsg
CHANGED
|
@@ -1,43 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
Grammar squiggles: stop inventing words, stop underlining white space
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
newsletters Bob subscribed to (Constant Contact, Adobe MAX, Mokin, ActBlue)
|
|
6
|
-
scored 2.2 to 10.9 and two of them raised "This message is not what it says
|
|
7
|
-
it is" — on mail that passes DMARC aligned on a published reject policy,
|
|
8
|
-
where that headline is factually false. What scored them was bulk-mail rules
|
|
9
|
-
and third-party blocklist entries about hosts they merely link to: Mokin's
|
|
10
|
-
6.0 URIBL_SBL is the Spamhaus listing of the IP of the nameserver of a linked
|
|
11
|
-
domain, fourth-order guilt by association, more than the whole 4.5 threshold
|
|
12
|
-
on its own.
|
|
3
|
+
Three complaints from one compose session (Bob 2026-08-27, forwarding a
|
|
4
|
+
message), all of them the checker arguing with prose that was fine:
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
1. "keypoint". Harper's CompoundNouns rule fires on any noun+noun pair whose
|
|
7
|
+
joined form is in its dictionary, so "One key point in my recent efforts"
|
|
8
|
+
came back as an error suggesting a word he did not write. Disabled in the
|
|
9
|
+
daemon next to AvoidCurses — same objection as that one: the tool
|
|
10
|
+
proofreads, it does not get a vote on word choice. Its siblings (MergeWords,
|
|
11
|
+
MakeupCompoundNoun, OvertimeCompoundNoun, ThereAfterCompound,
|
|
12
|
+
EasyGoingCompoundAdjective) are named in a comment there, one line each to
|
|
13
|
+
turn off if they start nagging.
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
2. A blue dotted line across two blank lines. blockText concatenated a block's
|
|
16
|
+
text nodes with no separator, so the last word of a paragraph was glued to
|
|
17
|
+
the first word of the next: "…of the network" + "To: x@y.com" reached Harper
|
|
18
|
+
as "networkTo", and it reported — correctly, for the string it was given —
|
|
19
|
+
that the word should be split. The span then covered a junction that on
|
|
20
|
+
screen is two empty lines, and getClientRects painted a full-width underline
|
|
21
|
+
over the whitespace. Verified both halves against the real engine: gluing
|
|
22
|
+
produces `networkTo` and `FrankstonSubject` lints that vanish once the
|
|
23
|
+
breaks are there. blockText now emits "\n" at every line-breaking element,
|
|
24
|
+
and spanToRange refuses any span whose ends sit in different line boxes, so
|
|
25
|
+
no squiggle can ever be drawn across a paragraph boundary again.
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
score is all bulk, and both name their evidence ("8.0 SH_DBL_HEADERS (A domain
|
|
33
|
-
found in headers is listed in DBL)") instead of showing a bare number. The
|
|
34
|
-
`threshold/2` chip floor is untouched — see docs/spam-false-positives.md on
|
|
35
|
-
why tuning it is not the fix.
|
|
27
|
+
3. Lints on the sentence being typed. The unfinished sentence under the caret
|
|
28
|
+
is now exempt — a thought that is not out yet is not an error.
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
and one naming a different From).
|
|
42
|
-
|
|
43
|
-
TODO C165 first cut. Sender trust via allowlist.jsonc still open.
|
|
30
|
+
The pure helpers (collectBlocks, blockText, lineBox, caretOffset,
|
|
31
|
+
sentenceStart, spanToRange) moved to module scope and are exported, because
|
|
32
|
+
they hold the decisions worth testing; tests/dom/harper-lint.test.ts covers all
|
|
33
|
+
three rules in jsdom, using the forward markup client/app.ts actually builds.
|
|
@@ -3710,7 +3710,13 @@ var init_spellcheck = __esm({
|
|
|
3710
3710
|
// client/compose/harper-lint.js
|
|
3711
3711
|
var harper_lint_exports = {};
|
|
3712
3712
|
__export(harper_lint_exports, {
|
|
3713
|
-
|
|
3713
|
+
blockText: () => blockText,
|
|
3714
|
+
caretOffset: () => caretOffset,
|
|
3715
|
+
collectBlocks: () => collectBlocks,
|
|
3716
|
+
initHarperLint: () => initHarperLint,
|
|
3717
|
+
lineBox: () => lineBox,
|
|
3718
|
+
sentenceStart: () => sentenceStart,
|
|
3719
|
+
spanToRange: () => spanToRange
|
|
3714
3720
|
});
|
|
3715
3721
|
function initHarperLint(ed) {
|
|
3716
3722
|
let disposed = false;
|
|
@@ -3735,42 +3741,6 @@ function initHarperLint(ed) {
|
|
|
3735
3741
|
doc.getElementById(OVERLAY_ID2)?.remove();
|
|
3736
3742
|
doc.getElementById(POPUP_ID)?.remove();
|
|
3737
3743
|
};
|
|
3738
|
-
const collectBlocks = (root) => {
|
|
3739
|
-
const kids = Array.from(root.children).filter((c) => c instanceof HTMLElement);
|
|
3740
|
-
const blocks = (kids.length ? kids : [root]).filter((b) => b.tagName !== "BLOCKQUOTE" && !b.closest("blockquote") && !b.querySelector("blockquote"));
|
|
3741
|
-
return blocks;
|
|
3742
|
-
};
|
|
3743
|
-
const blockText = (block, doc) => {
|
|
3744
|
-
const walker = doc.createTreeWalker(block, NodeFilter.SHOW_TEXT);
|
|
3745
|
-
const nodes = [];
|
|
3746
|
-
let text = "";
|
|
3747
|
-
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
3748
|
-
nodes.push({ node: n, from: text.length });
|
|
3749
|
-
text += n.data;
|
|
3750
|
-
}
|
|
3751
|
-
return { text, nodes };
|
|
3752
|
-
};
|
|
3753
|
-
const spanToRange = (doc, nodes, start, end) => {
|
|
3754
|
-
let a = null;
|
|
3755
|
-
let b = null;
|
|
3756
|
-
for (const e of nodes) {
|
|
3757
|
-
const len = e.node.data.length;
|
|
3758
|
-
if (!a && start >= e.from && start <= e.from + len)
|
|
3759
|
-
a = { node: e.node, off: start - e.from };
|
|
3760
|
-
if (end >= e.from && end <= e.from + len)
|
|
3761
|
-
b = { node: e.node, off: end - e.from };
|
|
3762
|
-
}
|
|
3763
|
-
if (!a || !b)
|
|
3764
|
-
return null;
|
|
3765
|
-
try {
|
|
3766
|
-
const r = doc.createRange();
|
|
3767
|
-
r.setStart(a.node, a.off);
|
|
3768
|
-
r.setEnd(b.node, b.off);
|
|
3769
|
-
return r;
|
|
3770
|
-
} catch {
|
|
3771
|
-
return null;
|
|
3772
|
-
}
|
|
3773
|
-
};
|
|
3774
3744
|
const paint = () => {
|
|
3775
3745
|
if (disposed)
|
|
3776
3746
|
return;
|
|
@@ -3834,9 +3804,13 @@ function initHarperLint(ed) {
|
|
|
3834
3804
|
const tx = texts[i];
|
|
3835
3805
|
if (!tx)
|
|
3836
3806
|
return;
|
|
3807
|
+
const caret = caretOffset(t.doc, tx.nodes);
|
|
3808
|
+
const writingFrom = caret < 0 ? -1 : sentenceStart(tx.text, caret);
|
|
3837
3809
|
for (const l of lints || []) {
|
|
3838
3810
|
if (SKIP_KINDS.has(l.kind))
|
|
3839
3811
|
continue;
|
|
3812
|
+
if (writingFrom >= 0 && l.end > writingFrom && l.start <= caret)
|
|
3813
|
+
continue;
|
|
3840
3814
|
const range = spanToRange(t.doc, tx.nodes, l.start, l.end);
|
|
3841
3815
|
if (range)
|
|
3842
3816
|
next.push({ lint: l, range });
|
|
@@ -3966,7 +3940,7 @@ function initHarperLint(ed) {
|
|
|
3966
3940
|
}
|
|
3967
3941
|
};
|
|
3968
3942
|
}
|
|
3969
|
-
var DEBOUNCE_MS, OVERLAY_ID2, POPUP_ID, SKIP_KINDS;
|
|
3943
|
+
var DEBOUNCE_MS, OVERLAY_ID2, POPUP_ID, SKIP_KINDS, collectBlocks, LINE_BREAKING, blockText, lineBox, caretOffset, sentenceStart, spanToRange;
|
|
3970
3944
|
var init_harper_lint = __esm({
|
|
3971
3945
|
"client/compose/harper-lint.js"() {
|
|
3972
3946
|
"use strict";
|
|
@@ -3975,6 +3949,100 @@ var init_harper_lint = __esm({
|
|
|
3975
3949
|
OVERLAY_ID2 = "harper-overlay";
|
|
3976
3950
|
POPUP_ID = "harper-popup";
|
|
3977
3951
|
SKIP_KINDS = /* @__PURE__ */ new Set(["Spelling"]);
|
|
3952
|
+
collectBlocks = (root) => {
|
|
3953
|
+
const kids = Array.from(root.children).filter((c) => c instanceof HTMLElement);
|
|
3954
|
+
const blocks = (kids.length ? kids : [root]).filter((b) => b.tagName !== "BLOCKQUOTE" && !b.closest("blockquote") && !b.querySelector("blockquote"));
|
|
3955
|
+
return blocks;
|
|
3956
|
+
};
|
|
3957
|
+
LINE_BREAKING = /* @__PURE__ */ new Set([
|
|
3958
|
+
"BR",
|
|
3959
|
+
"P",
|
|
3960
|
+
"DIV",
|
|
3961
|
+
"LI",
|
|
3962
|
+
"UL",
|
|
3963
|
+
"OL",
|
|
3964
|
+
"TR",
|
|
3965
|
+
"TD",
|
|
3966
|
+
"TH",
|
|
3967
|
+
"TABLE",
|
|
3968
|
+
"PRE",
|
|
3969
|
+
"HR",
|
|
3970
|
+
"BLOCKQUOTE",
|
|
3971
|
+
"H1",
|
|
3972
|
+
"H2",
|
|
3973
|
+
"H3",
|
|
3974
|
+
"H4",
|
|
3975
|
+
"H5",
|
|
3976
|
+
"H6",
|
|
3977
|
+
"SECTION",
|
|
3978
|
+
"ARTICLE",
|
|
3979
|
+
"FIGURE"
|
|
3980
|
+
]);
|
|
3981
|
+
blockText = (block, doc) => {
|
|
3982
|
+
const walker = doc.createTreeWalker(block, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
|
3983
|
+
const nodes = [];
|
|
3984
|
+
let text = "";
|
|
3985
|
+
let pendingBreak = false;
|
|
3986
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
3987
|
+
if (n.nodeType === Node.ELEMENT_NODE) {
|
|
3988
|
+
if (LINE_BREAKING.has(n.tagName))
|
|
3989
|
+
pendingBreak = true;
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
const data = n.data;
|
|
3993
|
+
if (!data)
|
|
3994
|
+
continue;
|
|
3995
|
+
if (pendingBreak && text)
|
|
3996
|
+
text += "\n";
|
|
3997
|
+
pendingBreak = false;
|
|
3998
|
+
nodes.push({ node: n, from: text.length });
|
|
3999
|
+
text += data;
|
|
4000
|
+
}
|
|
4001
|
+
return { text, nodes };
|
|
4002
|
+
};
|
|
4003
|
+
lineBox = (node) => {
|
|
4004
|
+
for (let e = node; e; e = e.parentNode)
|
|
4005
|
+
if (e.nodeType === Node.ELEMENT_NODE && LINE_BREAKING.has(e.tagName))
|
|
4006
|
+
return e;
|
|
4007
|
+
return null;
|
|
4008
|
+
};
|
|
4009
|
+
caretOffset = (doc, nodes) => {
|
|
4010
|
+
const sel = doc.getSelection?.();
|
|
4011
|
+
if (!sel || !sel.focusNode)
|
|
4012
|
+
return -1;
|
|
4013
|
+
for (const e of nodes)
|
|
4014
|
+
if (e.node === sel.focusNode)
|
|
4015
|
+
return e.from + sel.focusOffset;
|
|
4016
|
+
return -1;
|
|
4017
|
+
};
|
|
4018
|
+
sentenceStart = (text, caret) => {
|
|
4019
|
+
const before = text.slice(0, caret);
|
|
4020
|
+
const m = before.match(/[.!?…\n][^.!?…\n]*$/);
|
|
4021
|
+
return m ? caret - (m[0].length - 1) : 0;
|
|
4022
|
+
};
|
|
4023
|
+
spanToRange = (doc, nodes, start, end) => {
|
|
4024
|
+
let a = null;
|
|
4025
|
+
let b = null;
|
|
4026
|
+
for (const e of nodes) {
|
|
4027
|
+
const len = e.node.data.length;
|
|
4028
|
+
if (!a && start >= e.from && start <= e.from + len)
|
|
4029
|
+
a = { node: e.node, off: start - e.from };
|
|
4030
|
+
if (end >= e.from && end <= e.from + len)
|
|
4031
|
+
b = { node: e.node, off: end - e.from };
|
|
4032
|
+
}
|
|
4033
|
+
if (!a || !b)
|
|
4034
|
+
return null;
|
|
4035
|
+
if (lineBox(a.node) !== lineBox(b.node))
|
|
4036
|
+
return null;
|
|
4037
|
+
try {
|
|
4038
|
+
const r = doc.createRange();
|
|
4039
|
+
r.setStart(a.node, a.off);
|
|
4040
|
+
r.setEnd(b.node, b.off);
|
|
4041
|
+
return r;
|
|
4042
|
+
} catch {
|
|
4043
|
+
return null;
|
|
4044
|
+
}
|
|
4045
|
+
};
|
|
3978
4046
|
}
|
|
3979
4047
|
});
|
|
3980
4048
|
|