diff --git a/src/distill.js b/src/distill.js index 64cde67..13b4088 100644 --- a/src/distill.js +++ b/src/distill.js @@ -293,7 +293,7 @@ export function distill(html, url = '', { view = null, type = '' } = {}) { const hidden = (el) => el.getAttribute('hidden') !== null || el.getAttribute('aria-hidden') === 'true' || - /display:\s*none/.test(el.getAttribute('style') ?? ''); + DISPLAY_NONE.test(el.getAttribute('style') ?? ''); /** Subtree already emitted, skipped when the rest of the page is walked. */ let done = null; @@ -616,6 +616,9 @@ function number(blocks) { */ const bodyOf = (document) => document.querySelector('body') ?? document.documentElement; +/** CSS property names and keywords are case-insensitive: `DISPLAY : NONE` hides too. */ +const DISPLAY_NONE = /display\s*:\s*none/i; + /** * HTML attribute names are ASCII case-insensitive, but linkedom keeps them as * written, so `` had no href, `` no type and @@ -661,7 +664,7 @@ function cleanDocument(html, url = '', type = '') { if ((el.getAttribute('type') ?? '').toLowerCase() === 'hidden') el.remove(); } for (const el of [...document.querySelectorAll('[style]')]) { - if (/display:\s*none/.test(el.getAttribute('style') ?? '')) el.remove(); + if (DISPLAY_NONE.test(el.getAttribute('style') ?? '')) el.remove(); } return { document, title }; } diff --git a/tests/distill.test.js b/tests/distill.test.js index e6ca5b3..987d2ed 100644 --- a/tests/distill.test.js +++ b/tests/distill.test.js @@ -81,6 +81,25 @@ test('attribute names are matched case-insensitively, as HTML does', () => { } }); +test('a display:none style is matched case-insensitively, as CSS does', () => { + // CSS property names and keywords are case-insensitive and may have space + // before the colon, so each of these hides its content in a browser. + const html = `Styles +

Styles

+

shown

+

secret-upper

+

secret-mixed

+

secret-spaced

+ `; + const view = render(distill(html, 'https://example.com/s'), { budget: 5000 }).text; + for (const out of [view, toMarkdown(html), toHTML(html)]) { + assert.ok(out.includes('shown')); + for (const secret of ['secret-upper', 'secret-mixed', 'secret-spaced']) { + assert.ok(!out.includes(secret), `${secret} leaked`); + } + } +}); + test('raw mode emits real markdown with hrefs an agent can follow', () => { const md = toMarkdown(html); assert.ok(md.startsWith('# Fixture News'));