From 5245cc4245d49b9e07917f57f334628ae688a720 Mon Sep 17 00:00:00 2001 From: iamsivin Date: Wed, 29 Apr 2026 23:01:20 +0530 Subject: [PATCH] chore: Review fix --- .../dashboard/helper/emailQuoteExtractor.js | 40 +++++++++++++--- .../helper/specs/emailQuoteExtractor.spec.js | 48 +++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/app/javascript/dashboard/helper/emailQuoteExtractor.js b/app/javascript/dashboard/helper/emailQuoteExtractor.js index a660b735f..cf21183bb 100644 --- a/app/javascript/dashboard/helper/emailQuoteExtractor.js +++ b/app/javascript/dashboard/helper/emailQuoteExtractor.js @@ -91,6 +91,17 @@ const cutBlockAtMarker = (block, marker) => { if (!block.childNodes.length) block.remove(); }; +// Walk up to the nearest enclosing `
` (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 `
` plus the body // siblings AFTER it strip together. @@ -151,13 +162,28 @@ const apply = root => { // 3. Trailing
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
— 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()); diff --git a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js index 5c94e2136..51d5a12a6 100644 --- a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js +++ b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js @@ -310,6 +310,54 @@ describe('EmailQuoteExtractor', () => { expect(cleaned).not.toContain('From: Sam'); }); + it('strips Apple-Mail blockquote (attribution + body inside one
)', () => { + const html = + '
Sounds good, see you Friday.
' + + '

' + + '
On Apr 6, 2026, at 11:26 AM, Sam <sam@example.test> wrote:
' + + '
Hi Pat,
Locking the Friday slot.
Sam
' + + '
'; + 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 = + '

Confirming I received this — will review tomorrow.

' + + '

Thanks,
Pat

' + + '

From: Sam <sam@example.test>
Sent: Wednesday, December 4, 2024 5:15 PM
To: Pat <pat@example.test>
Subject: Quotation

' + + '

Hi Pat,
Quotation attached. Let me know if you need changes.
Sam

'; + 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 `
` 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
', () => { + const html = + '
On Mon, Sep 22, Sam wrote:
Original quoted line.

My actual reply.

'; + 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 = + '
On Mon, Sam wrote:

My reply outside the wrapper.

'; + 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 = `