From f1d38e3fecea513e4fd424b76b17fcd8d66eb90e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 20:49:14 +0000 Subject: [PATCH 1/7] Initial plan From c5015ad025ed61be929778129e9627a5f573f7ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 21:00:48 +0000 Subject: [PATCH 2/7] Fix scraper character drops by extracting CSS pseudo-element content Sites like DW.com use ::before/::after pseudo-elements to inject characters (e.g. the letter 't') as an anti-scraping measure. The previous implementation cloned the DOM and used textContent, which never sees pseudo-element content. The new getAllTextContent() works on the live DOM and uses getComputedStyle() to read ::before/::after content for every element, ensuring characters rendered only through CSS pseudo-elements are included in the extracted text. Noise filtering and fallback logic are preserved. Agent-Logs-Url: https://github.com/philffm/ai-summary-helper/sessions/c31ded7c-9ceb-4868-b60e-fb3c2d3bcdab Co-authored-by: philffm <6079545+philffm@users.noreply.github.com> --- chrome-extension/content.js | 89 +++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 29 deletions(-) diff --git a/chrome-extension/content.js b/chrome-extension/content.js index 19141e7..ac292ad 100644 --- a/chrome-extension/content.js +++ b/chrome-extension/content.js @@ -314,9 +314,6 @@ function chunkText(text, chunkSize) { // Function to extract all relevant text content from the page function getAllTextContent() { console.log('Getting all text content'); - - // Create a clone of the document body so we don't mutate the actual page - const clone = document.body.cloneNode(true); // List of selectors for elements that usually contain non-article noise const noiseSelectors = [ @@ -326,45 +323,79 @@ function getAllTextContent() { '#comments', '.comments', '.sidebar', '#sidebar' ]; - for (const selector of noiseSelectors) { - const nodes = clone.querySelectorAll(selector); - for (const node of nodes) { - node.remove(); + const blockTags = new Set([ + 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'li', 'article', 'section', 'blockquote', 'br', 'tr' + ]); + + // Check whether an element (or any of its ancestors) matches a noise selector. + // We work on the live DOM so getComputedStyle is available for pseudo-elements. + function isNoise(el) { + return noiseSelectors.some(sel => { + try { return el.closest(sel) !== null; } catch (e) { return false; } + }); + } + + // Extract the rendered text injected by a CSS pseudo-element (::before / ::after). + // Many sites use pseudo-elements to insert characters as an anti-scraping measure; + // textContent on a detached clone never sees this content, but getComputedStyle does. + function getPseudoContent(el, pseudo) { + try { + const content = window.getComputedStyle(el, pseudo).getPropertyValue('content'); + if (!content || content === 'none' || content === 'normal') return ''; + // Strip the surrounding CSS quotes from the string value. + const stripped = content.replace(/^["']|["']$/g, ''); + // Discard likely icon-font characters (Unicode Private Use Area). + return stripped.replace(/[\uE000-\uF8FF]/g, ''); + } catch (e) { + return ''; } } - // To prevent block elements from running together when reading textContent, - // we can insert newlines before and after them. - const blockElements = ['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'article', 'section', 'blockquote']; - for (const tag of blockElements) { - const nodes = clone.querySelectorAll(tag); - for (const node of nodes) { - node.prepend(document.createTextNode('\\n\\n')); - node.append(document.createTextNode('\\n\\n')); + // Recursively build plain text from a live DOM element, capturing both text + // nodes and CSS pseudo-element content that textContent would normally miss. + function extractText(el) { + let result = ''; + result += getPseudoContent(el, '::before'); + for (const child of el.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + result += child.textContent; + } else if (child.nodeType === Node.ELEMENT_NODE) { + if (isNoise(child)) continue; + const tag = child.tagName.toLowerCase(); + if (blockTags.has(tag)) { + result += '\n\n' + extractText(child) + '\n\n'; + } else { + result += extractText(child); + } + } } + result += getPseudoContent(el, '::after'); + return result; } - // Fallback chain: look for article, or main, or use the whole body clone - let mainNode = clone.querySelector('article') || - clone.querySelector('main') || - clone.querySelector('[role="main"]') || - clone; + // Fallback chain: look for article, or main, or use the whole body + const mainEl = + document.querySelector('article') || + document.querySelector('main') || + document.querySelector('[role="main"]') || + document.body; + + let text = extractText(mainEl); - let text = mainNode.textContent || ''; - // Clean up whitespace: replace multiple spaces with one, and multiple newlines with double newlines let cleanText = text - .replace(/[ \\t]+/g, ' ') - .replace(/\\n\\s*\\n/g, '\\n\\n') + .replace(/[ \t]+/g, ' ') + .replace(/\n\s*\n/g, '\n\n') .trim(); // If it's suspiciously short (e.g. they put the main article outside
), fallback - if (cleanText.length < 500 && mainNode !== clone) { - console.log("Extracted content too short, falling back to full body container."); - text = clone.textContent || ''; + if (cleanText.length < 500 && mainEl !== document.body) { + console.log('Extracted content too short, falling back to full body container.'); + text = extractText(document.body); cleanText = text - .replace(/[ \\t]+/g, ' ') - .replace(/\\n\\s*\\n/g, '\\n\\n') + .replace(/[ \t]+/g, ' ') + .replace(/\n\s*\n/g, '\n\n') .trim(); } From 659e8ff1fd52ac835ee4b8f1414b0c4f96fbe023 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 21:02:43 +0000 Subject: [PATCH 3/7] Address code review feedback: add warning log and improve comment clarity Agent-Logs-Url: https://github.com/philffm/ai-summary-helper/sessions/c31ded7c-9ceb-4868-b60e-fb3c2d3bcdab Co-authored-by: philffm <6079545+philffm@users.noreply.github.com> --- chrome-extension/content.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chrome-extension/content.js b/chrome-extension/content.js index ac292ad..48d9fe8 100644 --- a/chrome-extension/content.js +++ b/chrome-extension/content.js @@ -332,7 +332,7 @@ function getAllTextContent() { // We work on the live DOM so getComputedStyle is available for pseudo-elements. function isNoise(el) { return noiseSelectors.some(sel => { - try { return el.closest(sel) !== null; } catch (e) { return false; } + try { return el.closest(sel) !== null; } catch (e) { console.warn("isNoise: invalid selector", sel, e); return false; } }); } @@ -345,7 +345,7 @@ function getAllTextContent() { if (!content || content === 'none' || content === 'normal') return ''; // Strip the surrounding CSS quotes from the string value. const stripped = content.replace(/^["']|["']$/g, ''); - // Discard likely icon-font characters (Unicode Private Use Area). + // Discard likely icon-font characters (U+E000–U+F8FF: Unicode Private Use Area). return stripped.replace(/[\uE000-\uF8FF]/g, ''); } catch (e) { return ''; From 66b337bc28dacf48f95fcdd5064b4734f0d9c1a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 21:04:46 +0000 Subject: [PATCH 4/7] Improve isNoise with Set-based pre-computation, enhance comments, fix XSS in debug panel Agent-Logs-Url: https://github.com/philffm/ai-summary-helper/sessions/c31ded7c-9ceb-4868-b60e-fb3c2d3bcdab Co-authored-by: philffm <6079545+philffm@users.noreply.github.com> --- chrome-extension/content.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/chrome-extension/content.js b/chrome-extension/content.js index 48d9fe8..24fd5c3 100644 --- a/chrome-extension/content.js +++ b/chrome-extension/content.js @@ -330,10 +330,24 @@ function getAllTextContent() { // Check whether an element (or any of its ancestors) matches a noise selector. // We work on the live DOM so getComputedStyle is available for pseudo-elements. + // Pre-compute the set of noise container elements once for efficient ancestry checks. + const noiseSet = new Set(); + for (const sel of noiseSelectors) { + try { + document.querySelectorAll(sel).forEach(el => noiseSet.add(el)); + } catch (e) { + console.warn('getAllTextContent: skipping invalid noise selector', sel, e); + } + } + + // Returns true if el or any ancestor is a known noise container. function isNoise(el) { - return noiseSelectors.some(sel => { - try { return el.closest(sel) !== null; } catch (e) { console.warn("isNoise: invalid selector", sel, e); return false; } - }); + let node = el; + while (node && node !== document.body) { + if (noiseSet.has(node)) return true; + node = node.parentElement; + } + return false; } // Extract the rendered text injected by a CSS pseudo-element (::before / ::after). @@ -345,7 +359,8 @@ function getAllTextContent() { if (!content || content === 'none' || content === 'normal') return ''; // Strip the surrounding CSS quotes from the string value. const stripped = content.replace(/^["']|["']$/g, ''); - // Discard likely icon-font characters (U+E000–U+F8FF: Unicode Private Use Area). + // Discard icon-font characters: fonts like Font Awesome map glyphs to the + // Unicode Private Use Area (U+E000–U+F8FF), which would appear as garbage symbols. return stripped.replace(/[\uE000-\uF8FF]/g, ''); } catch (e) { return ''; @@ -557,6 +572,9 @@ function updateDebugPanel(text, apiUrl) { panel.style.cssText = `position:fixed;right:10px;bottom:10px;width:350px;max-height:40vh;overflow:auto;background:#222;color:#0f0;padding:10px;z-index:10000;font-family:monospace;font-size:11px;border-radius:5px;box-shadow:0 0 10px rgba(0,0,0,0.5);`; document.body.appendChild(panel); } - panel.innerHTML = `Debug (${apiUrl})
${text}
`; + // Set content via textContent to avoid XSS from API responses or URL strings. + panel.innerHTML = '
';
+  panel.querySelector('strong').textContent = `Debug (${apiUrl})`;
+  panel.querySelector('pre').textContent = text;
 }
 

From 6de36496799cba7692335f013d8a79c770ae1f1f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 May 2026 21:06:54 +0000
Subject: [PATCH 5/7] Refine quote stripping, extend PUA filter to
 supplementary planes, simplify isNoise to O(1)

Agent-Logs-Url: https://github.com/philffm/ai-summary-helper/sessions/c31ded7c-9ceb-4868-b60e-fb3c2d3bcdab

Co-authored-by: philffm <6079545+philffm@users.noreply.github.com>
---
 chrome-extension/content.js | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/chrome-extension/content.js b/chrome-extension/content.js
index 24fd5c3..ea6dacf 100644
--- a/chrome-extension/content.js
+++ b/chrome-extension/content.js
@@ -340,14 +340,11 @@ function getAllTextContent() {
     }
   }
 
-  // Returns true if el or any ancestor is a known noise container.
+  // Returns true if el is a known noise container root. Because extractText() never
+  // recurses into skipped subtrees, the parent context is already confirmed clean,
+  // so a direct Set lookup is sufficient (no ancestor walk needed).
   function isNoise(el) {
-    let node = el;
-    while (node && node !== document.body) {
-      if (noiseSet.has(node)) return true;
-      node = node.parentElement;
-    }
-    return false;
+    return noiseSet.has(el);
   }
 
   // Extract the rendered text injected by a CSS pseudo-element (::before / ::after).
@@ -357,11 +354,14 @@ function getAllTextContent() {
     try {
       const content = window.getComputedStyle(el, pseudo).getPropertyValue('content');
       if (!content || content === 'none' || content === 'normal') return '';
-      // Strip the surrounding CSS quotes from the string value.
-      const stripped = content.replace(/^["']|["']$/g, '');
+      // Strip the outermost CSS quotes from the string value (browser always wraps in double quotes).
+      const q = content[0];
+      const stripped = (q === '"' || q === "'") && content[content.length - 1] === q
+        ? content.slice(1, -1)
+        : content;
       // Discard icon-font characters: fonts like Font Awesome map glyphs to the
       // Unicode Private Use Area (U+E000–U+F8FF), which would appear as garbage symbols.
-      return stripped.replace(/[\uE000-\uF8FF]/g, '');
+      return stripped.replace(/[\uE000-\uF8FF]|[\uDB80-\uDBFF][\uDC00-\uDFFF]/g, '');
     } catch (e) {
       return '';
     }

From 3d6f3c93bd016987690a4a7610024a5c9e37bdb7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 May 2026 21:08:07 +0000
Subject: [PATCH 6/7] Add explanatory comment for supplementary PUA surrogate
 pair filter

Agent-Logs-Url: https://github.com/philffm/ai-summary-helper/sessions/c31ded7c-9ceb-4868-b60e-fb3c2d3bcdab

Co-authored-by: philffm <6079545+philffm@users.noreply.github.com>
---
 chrome-extension/content.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/chrome-extension/content.js b/chrome-extension/content.js
index ea6dacf..b77784e 100644
--- a/chrome-extension/content.js
+++ b/chrome-extension/content.js
@@ -359,8 +359,10 @@ function getAllTextContent() {
       const stripped = (q === '"' || q === "'") && content[content.length - 1] === q
         ? content.slice(1, -1)
         : content;
-      // Discard icon-font characters: fonts like Font Awesome map glyphs to the
-      // Unicode Private Use Area (U+E000–U+F8FF), which would appear as garbage symbols.
+      // Discard icon-font characters: fonts like Font Awesome map glyphs to Unicode
+      // Private Use Area code points, which would appear as garbage symbols in output.
+      // The regex covers BMP PUA (U+E000–U+F8FF) and supplementary PUA planes
+      // (U+F0000–U+FFFFD, U+100000–U+10FFFD) via their UTF-16 surrogate pairs.
       return stripped.replace(/[\uE000-\uF8FF]|[\uDB80-\uDBFF][\uDC00-\uDFFF]/g, '');
     } catch (e) {
       return '';

From 16c00f47cf7836521b2cd55f9f7fa22b3395c3b6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 May 2026 21:40:21 +0000
Subject: [PATCH 7/7] Fix /\\n/g bug (literal backslash+n), refactor markdown
 parser into markdownToHtml() with list and inline-code support

Agent-Logs-Url: https://github.com/philffm/ai-summary-helper/sessions/7a278b63-2038-4ef9-8ebd-ac4e23839dae

Co-authored-by: philffm <6079545+philffm@users.noreply.github.com>
---
 chrome-extension/content.js | 48 ++++++++++++++++++++++---------------
 1 file changed, 29 insertions(+), 19 deletions(-)

diff --git a/chrome-extension/content.js b/chrome-extension/content.js
index b77784e..8c93294 100644
--- a/chrome-extension/content.js
+++ b/chrome-extension/content.js
@@ -67,6 +67,29 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
   }
 });
 
+// Convert a small subset of Markdown to HTML.
+// Handles bold, italic, headings, inline code, unordered lists, and code fences.
+function markdownToHtml(text) {
+  return text
+    // Strip wrapping code fences (```html ... ``` or ``` ... ```)
+    .replace(/^```(?:html)?\n?/gi, '').replace(/\n?```$/g, '')
+    // Headings
+    .replace(/^### (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^# (.*$)/gim, '

$1

') + // Bold and italic + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + // Inline code + .replace(/`([^`]+)`/g, '$1') + // Unordered list items (lines starting with "* " or "- ") + .replace(/^[*-] (.+)$/gim, '
  • $1
  • ') + // Wrap consecutive
  • elements in a