chore: Review fix
This commit is contained in:
@@ -91,6 +91,17 @@ const cutBlockAtMarker = (block, marker) => {
|
||||
if (!block.childNodes.length) block.remove();
|
||||
};
|
||||
|
||||
// Walk up to the nearest enclosing `<blockquote>` (including `block` itself).
|
||||
// Returns null when there is none below `root`.
|
||||
const findEnclosingBlockquote = (block, root) => {
|
||||
let cur = block;
|
||||
while (cur && cur !== root) {
|
||||
if (cur.tagName === 'BLOCKQUOTE') return cur;
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Walk up while `block` is the first substantive child of its parent.
|
||||
// Promotes the cut to the wrapper, so a divider `<div>` plus the body
|
||||
// siblings AFTER it strip together.
|
||||
@@ -151,13 +162,28 @@ const apply = root => {
|
||||
// 3. Trailing <blockquote> as the last top-level child.
|
||||
if (root.lastElementChild?.matches?.('blockquote'))
|
||||
root.lastElementChild.remove();
|
||||
// 4. Soft headers — same hard-cut, but walk up to the wrapper first.
|
||||
findBlocks(root, isSoftHeader).forEach(b =>
|
||||
cutBlockAtMarker(
|
||||
expandToWrapper(b, root),
|
||||
t => HEADER_LINE.test(t) || ATTRIBUTION.test(t)
|
||||
)
|
||||
);
|
||||
// 4. Soft headers. Three sub-cases:
|
||||
// (a) match sits inside a <blockquote> — remove the whole blockquote
|
||||
// (it wraps the entire quote: attribution + body). Apple Mail.
|
||||
// (b) match has >= 2 header lines (real Outlook attribution shape) —
|
||||
// hard-cut at the wrapper level so the body siblings go too.
|
||||
// (c) just an "On … wrote:" attribution → develop-style remove the
|
||||
// block, so a user reply sitting at root level isn't eaten.
|
||||
findBlocks(root, isSoftHeader).forEach(block => {
|
||||
const enclosingBq = findEnclosingBlockquote(block, root);
|
||||
if (enclosingBq) {
|
||||
enclosingBq.remove();
|
||||
return;
|
||||
}
|
||||
if (countHeaderLines(blockText(block)) >= 2) {
|
||||
cutBlockAtMarker(
|
||||
expandToWrapper(block, root),
|
||||
t => HEADER_LINE.test(t) || ATTRIBUTION.test(t)
|
||||
);
|
||||
return;
|
||||
}
|
||||
block.remove();
|
||||
});
|
||||
// 5. Top-level RFC `>` / header tail.
|
||||
const start = findTopLevelTailStart(root);
|
||||
if (start !== -1) [...root.childNodes].slice(start).forEach(n => n.remove());
|
||||
|
||||
@@ -310,6 +310,54 @@ describe('EmailQuoteExtractor', () => {
|
||||
expect(cleaned).not.toContain('From: Sam');
|
||||
});
|
||||
|
||||
it('strips Apple-Mail blockquote (attribution + body inside one <blockquote type="cite">)', () => {
|
||||
const html =
|
||||
'<div>Sounds good, see you Friday.</div>' +
|
||||
'<div><br><blockquote type="cite">' +
|
||||
'<div>On Apr 6, 2026, at 11:26 AM, Sam <sam@example.test> wrote:</div>' +
|
||||
'<br><div><div>Hi Pat,</div><div>Locking the Friday slot.</div><div>Sam</div></div>' +
|
||||
'</blockquote></div>';
|
||||
const c = document.createElement('div');
|
||||
c.innerHTML = EmailQuoteExtractor.extractQuotes(html);
|
||||
expect(c.textContent).toContain('Sounds good');
|
||||
expect(c.textContent).not.toContain('On Apr 6, 2026');
|
||||
expect(c.textContent).not.toContain('Hi Pat');
|
||||
expect(c.textContent).not.toContain('Locking the Friday slot');
|
||||
});
|
||||
|
||||
it('strips flat Outlook attribution + body (multi-line From/Sent/To/Subject)', () => {
|
||||
const html =
|
||||
'<p>Confirming I received this — will review tomorrow.</p>' +
|
||||
'<p>Thanks,<br>Pat</p>' +
|
||||
'<p>From: Sam <sam@example.test><br>Sent: Wednesday, December 4, 2024 5:15 PM<br>To: Pat <pat@example.test><br>Subject: Quotation</p>' +
|
||||
'<p>Hi Pat,<br>Quotation attached. Let me know if you need changes.<br>Sam</p>';
|
||||
const c = document.createElement('div');
|
||||
c.innerHTML = EmailQuoteExtractor.extractQuotes(html);
|
||||
expect(c.textContent).toContain('Confirming I received this');
|
||||
expect(c.textContent).toContain('Thanks,');
|
||||
expect(c.textContent).not.toContain('From: Sam');
|
||||
expect(c.textContent).not.toContain('Quotation attached');
|
||||
});
|
||||
|
||||
// Inline reply where a soft-header `<blockquote>` is followed by the
|
||||
// user's actual reply at the SAME level. The hard-cut for soft headers
|
||||
// would otherwise eat the reply.
|
||||
it('preserves user reply that follows a soft-header <blockquote>', () => {
|
||||
const html =
|
||||
'<blockquote>On Mon, Sep 22, Sam wrote:<br>Original quoted line.</blockquote><p>My actual reply.</p>';
|
||||
const c = document.createElement('div');
|
||||
c.innerHTML = EmailQuoteExtractor.extractQuotes(html);
|
||||
expect(c.textContent).toContain('My actual reply');
|
||||
});
|
||||
|
||||
it('preserves user reply that follows a wrapper div containing the soft-header block', () => {
|
||||
const html =
|
||||
'<div><blockquote>On Mon, Sam wrote:</blockquote></div><p>My reply outside the wrapper.</p>';
|
||||
const c = document.createElement('div');
|
||||
c.innerHTML = EmailQuoteExtractor.extractQuotes(html);
|
||||
expect(c.textContent).toContain('My reply outside the wrapper');
|
||||
});
|
||||
|
||||
it('preserves reply when the From-header sits inside a deep wrapper (Outlook WordSection1)', () => {
|
||||
const html = `
|
||||
<div class="WordSection1">
|
||||
|
||||
Reference in New Issue
Block a user