mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 08:15:38 +00:00
fix: keep a mail full of bare blockquotes from freezing the reader
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
import { setupQuoteCollapse, collapsePlainTextQuotes, QUOTE_TOGGLE_ATTR, QUOTE_COLLAPSED_ATTR } from '@/lib/quote-collapse';
|
||||
import { plainTextToSafeHtml, sanitizeEmailHtml, sanitizePlainTextRenderedHtml } from '@/lib/email-sanitization';
|
||||
@@ -162,6 +162,23 @@ describe('setupQuoteCollapse', () => {
|
||||
expect(toggle()).toBeNull();
|
||||
});
|
||||
|
||||
it('looks for an attribution in one pass over many bare blockquotes', () => {
|
||||
// Every outermost blockquote used to walk all text before it again, so a
|
||||
// body of a few ten thousand blockquotes froze the reader for minutes.
|
||||
const n = 2000;
|
||||
document.body.innerHTML = '<p>Intro</p>' + '<blockquote>x</blockquote>'.repeat(n) + '<p>Earlier:</p><blockquote>late quote</blockquote>';
|
||||
const nodes = document.body.querySelectorAll('*').length + n + 3;
|
||||
const walkerSpy = vi.spyOn(Node.prototype, 'compareDocumentPosition');
|
||||
try {
|
||||
expect(setupQuoteCollapse(document, labels)).toBe(true);
|
||||
expect(walkerSpy.mock.calls.length).toBeLessThan(nodes * 6);
|
||||
} finally {
|
||||
walkerSpy.mockRestore();
|
||||
}
|
||||
expect(document.body.lastElementChild!.textContent).toBe('late quote');
|
||||
expect((document.body.lastElementChild as HTMLElement).style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('collapses a nested wrapper case: separator inside a wrapper div hides content outside it too', () => {
|
||||
document.body.innerHTML = `
|
||||
<div>answer</div>
|
||||
|
||||
+13
-16
@@ -86,25 +86,22 @@ function hasVisibleContent(body: HTMLElement, marker: Element, side: 'before' |
|
||||
* ending with a colon ("On …, X wrote:", "Am … schrieb X:", "… a écrit :").
|
||||
*/
|
||||
function findAttributedBlockquote(body: HTMLElement): Element | null {
|
||||
for (const bq of Array.from(body.querySelectorAll('blockquote'))) {
|
||||
if (bq.parentElement?.closest('blockquote')) continue; // outermost only
|
||||
if (precedingTextEndsWithColon(body, bq)) return bq;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** True when the last non-blank text node before `el` ends with a colon. */
|
||||
function precedingTextEndsWithColon(body: HTMLElement, el: Element): boolean {
|
||||
const walker = body.ownerDocument.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
||||
// One walk in document order, remembering the last non-blank text: a
|
||||
// blockquote is reached before its own text, so that is the text preceding
|
||||
// it. Walking from the start again per blockquote cost quadratic time.
|
||||
const walker = body.ownerDocument.createTreeWalker(body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
||||
let last: string | null = null;
|
||||
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
||||
const rel = el.compareDocumentPosition(node);
|
||||
if (rel & Node.DOCUMENT_POSITION_CONTAINED_BY) continue;
|
||||
if (!(rel & Node.DOCUMENT_POSITION_PRECEDING)) break; // reached/passed el
|
||||
const text = (node.nodeValue || '').trim();
|
||||
if (text !== '') last = text;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = (node.nodeValue || '').trim();
|
||||
if (text !== '') last = text;
|
||||
continue;
|
||||
}
|
||||
const el = node as Element;
|
||||
if (el.tagName !== 'BLOCKQUOTE' || el.parentElement?.closest('blockquote')) continue; // outermost only
|
||||
if (last !== null && last.endsWith(':')) return el;
|
||||
}
|
||||
return last !== null && last.endsWith(':');
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Hide an element, remembering its original inline display for restore. */
|
||||
|
||||
Reference in New Issue
Block a user