Skip to content

fix(content): decode HTML entities in one pass - #394

Merged
steipete merged 1 commit into
steipete:mainfrom
devYRPauli:fix/decode-entities-single-pass
Aug 31, 2026
Merged

fix(content): decode HTML entities in one pass#394
steipete merged 1 commit into
steipete:mainfrom
devYRPauli:fix/decode-entities-single-pass

Conversation

@devYRPauli

@devYRPauli devYRPauli commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Problem

decodeHtmlEntities decodes with a chain of replaceAll calls, and & is decoded first:

return input
  .replaceAll("&", "&")
  .replaceAll("&lt;", "<")
  ...

Each call runs over the output of the previous one, so a single-escaped &amp;lt; becomes &lt; at step one and step two decodes it again into <. One level of escaping is unwrapped twice.

Measured on the helper:

input before correct
&amp;lt;div&amp;gt; <div> &lt;div&gt;
&amp;quot;x&amp;quot; "x" &quot;x&quot;
&amp;amp; &amp; &amp;
&lt;p&gt; <p> <p>

Only entities after &amp; in the chain are affected. The last two rows show ordinary input is unaffected, so this is specific to the chain order.

Which callers this actually affects

This matters only where the helper is handed RAW html, meaning entities are still in their source form. Callers that regex-strip tags and then decode:

caller affected
content/browser-html.ts:61 decodeHtmlEntities(withBreaks.replace(/<[^>]+>/g, " ")...) yes
content/browser-html.ts:88 decodeHtmlEntities(value.replace(/<[^>]+>/g, " ")) yes
link-preview/content/article.ts:161 inside extractPlainText yes
link-preview/content/firecrawl.ts:30 calls extractPlainText(html) directly yes
article.ts:48 and :106, inside the sanitize-html textFilter no
parsers.ts:75 and :90, on element.textContent no

The last four are not affected because sanitize-html and the DOM have already decoded one level before the helper runs, so it only ever sees single-escaped text there.

I checked this by running the real extraction paths rather than reasoning about them:

input: <p>To escape a tag write &amp;lt;div&amp;gt; here.</p>

                          before                                  after
extractArticleContent     To escape a tag write <div> here.       To escape a tag write <div> here.
extractPlainText          To escape a tag write <div> here.       To escape a tag write &lt;div&gt; here.

extractArticleContent is unchanged, because it goes through sanitize-html. extractPlainText is fixed. An earlier version of this description claimed the sanitize-html path was affected; that was wrong and the table above replaces it.

Change

One regex pass with a lookup table, so each entity is decoded exactly once. The set of entities and their replacements is unchanged, including the case-sensitive &#x27; and &#x2F;. Unknown entities are still left untouched.

Proof

vitest run tests/cleaner.test.ts
Tests  9 passed (9)

Source change reverted, tests kept:

FAIL  tests/cleaner.test.ts > decodes each entity once, so an escaped entity stays escaped
AssertionError: expected '<div>' to be '&lt;div&gt;'

Full suite, unchanged from before this commit:

vitest run
Test Files  556 passed | 29 skipped (585)
     Tests  3017 passed | 43 skipped (3060)

oxlint and oxfmt --check on both changed files: exit 0.

The existing test at tests/cleaner.test.ts:27 still passes. It did not catch this because its &amp; is standalone and surrounded by spaces, so decoding it never produces a new entity for a later call to consume.

decodeHtmlEntities chained replaceAll calls and decoded "&amp;" first. A
single-escaped "&amp;lt;" became "&lt;", which the next call in the same
chain decoded again into "<".

Text that shows an entity to the reader, which pages about HTML syntax and
double-encoding CMS feeds both produce, therefore had real markup injected
into the extracted article text before the model saw it.

Replace the chain with one regex pass and a lookup table, so each entity is
decoded exactly once. Unknown entities are still left untouched.
@clawsweeper

clawsweeper Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 24, 2026
@clawsweeper

clawsweeper Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 31, 2026, 1:04 AM ET / 05:04 UTC.

ClawSweeper review

What this changes

This PR replaces sequential HTML-entity substitutions in link-preview content cleaning with a one-pass lookup and adds regression coverage for escaped and unknown entities.

Merge readiness

⚠️ Ready for maintainer review - 1 item remains

Keep open for ordinary maintainer merge review: current main still has the sequential decoder, while this focused patch fixes the demonstrated double-decoding behavior with sufficient production-path evidence and no blocking finding.

Priority: P2
Reviewed head: 719ee87fb508d28aeb133495e1af92285ac05386

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused, well-scoped correctness fix with credible production-path proof and direct regression coverage.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (terminal): The changed production owner is the core content cleaner used by raw HTML extraction; the PR body provides a real extractPlainText before/after trace showing escaped markup is preserved after the change, with focused regression and full-suite results as supplemental evidence.
Patch quality 🦞 diamond lobster (5/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The changed production owner is the core content cleaner used by raw HTML extraction; the PR body provides a real extractPlainText before/after trace showing escaped markup is preserved after the change, with focused regression and full-suite results as supplemental evidence.
Evidence reviewed 5 items Current-main behavior: The fetched current main revision still implements entity decoding as ordered sequential replacements, so the central defect is not already fixed upstream.
Introduced patch: The pinned PR delta changes only the cleaner and its focused tests; its single global scan preserves the existing supported entity mapping while preventing later substitutions from consuming newly decoded text.
Affected production path: Raw HTML and extracted text flow through this helper from browser HTML normalization and link-preview plain-text extraction, making the escaped-entity scenario relevant to production content processing.
Findings None None.
Security None None.

How this fits together

Summarize's link-preview pipeline turns fetched page HTML into normalized article text for downstream summarization. The changed core helper decodes supported HTML entities after extraction while preserving text that intentionally displays an escaped entity.

flowchart LR
  A[Page HTML] --> B[Link preview extraction]
  B --> C[Content cleaner]
  C --> D{Recognized HTML entity}
  D -->|Decode once| E[Normalized article text]
  D -->|Leave unchanged| E
  E --> F[Summarization input]
Loading

Before merge

  • Complete next step (P2) - No mechanical repair is identified; the focused, proof-backed patch needs only ordinary maintainer merge review.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Focused code and test delta production +17/-9, tests +11 across 2 files The implementation is a contained replacement of one helper with direct regression coverage.

Technical review

Best possible solution:

Merge the narrow one-pass decoder and its regression tests so intentionally escaped markup remains literal text in raw HTML extraction.

Do we have a high-confidence way to reproduce the issue?

Yes—source-reproducible with high confidence: current main's ordered replacements directly turn &lt; into <, and the PR supplies a retained regression failure plus an extraction-path comparison.

Is this the best way to solve the issue?

Yes. A single global replacement pass retains the existing supported entity set and removes the cascade without adding a second decoding policy or configuration surface.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 66202d92f055.

Labels

Label justifications:

  • P2: This fixes a bounded content-extraction correctness defect without evidence of an urgent availability, security, or data-loss impact.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The changed production owner is the core content cleaner used by raw HTML extraction; the PR body provides a real extractPlainText before/after trace showing escaped markup is preserved after the change, with focused regression and full-suite results as supplemental evidence.
  • proof: sufficient: Contributor real behavior proof is sufficient. The changed production owner is the core content cleaner used by raw HTML extraction; the PR body provides a real extractPlainText before/after trace showing escaped markup is preserved after the change, with focused regression and full-suite results as supplemental evidence.

Evidence

What I checked:

Likely related people:

  • Peter Steinberger: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (24 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-30T01:21:33.749Z sha 719ee87 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T07:59:25.714Z sha 719ee87 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T13:03:32.711Z sha 719ee87 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T14:39:31.623Z sha 719ee87 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T17:38:00.796Z sha 719ee87 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-30T19:42:14.820Z sha 719ee87 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-31T01:32:42.577Z sha 719ee87 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-31T03:04:43.965Z sha 719ee87 :: needs maintainer review before merge. :: none

@devYRPauli

Copy link
Copy Markdown
Contributor Author

Went to get the extraction-path proof and it corrected the PR rather than confirming it.

Running the real paths on <p>To escape a tag write &amp;lt;div&amp;gt; here.</p>:

                          before                                  after
extractArticleContent     To escape a tag write <div> here.       To escape a tag write <div> here.
extractPlainText          To escape a tag write <div> here.       To escape a tag write &lt;div&gt; here.

extractArticleContent does not change. sanitize-html already decodes one level before the textFilter runs, so the helper only ever sees single-escaped text there. My original description claimed that path was affected. It was wrong and I have replaced it.

What the fix does change is every caller that hands the helper raw html: browser-html.ts:61 and :88, which regex-strip tags and then decode, article.ts:161 inside extractPlainText, and firecrawl.ts:30, which calls extractPlainText(html) directly rather than only as a fallback.

The description now lists which call sites are affected and which are not, with that measurement.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. labels Aug 24, 2026
@steipete

Copy link
Copy Markdown
Owner

Triage recommendation: LAND. I reproduced the cascade on current main and verified this PR through the built core package's exported extractBrowserHtmlContent() API, using HTML fetched from a real local HTTP fixture server.

Input: &amp;lt;div&amp;gt;
main:    title="Escaping <div>"
patched: title="Escaping &lt;div&gt;"
main:    text="Escaping <div> To escape a tag write <div> here."
patched: text="Escaping &lt;div&gt; To escape a tag write &lt;div&gt; here."

The sequential replacements consume text produced by the first &amp; replacement. The single-pass lookup preserves the current supported entity set and prevents that second decode. This proof covers raw browser-HTML extraction; it does not claim to repair every already-decoded DOM/sanitize-html caller.

Reviewed commit: 719ee87fb508d28aeb133495e1af92285ac05386.

Both content PRs also apply together cleanly on current main and pass all 12 cleaner regression tests. The baseline package build passed on Node 24.20.0, and each changed cleaner was compiled into the built core package for the integration checks. Local tests reused the installed dependency tree (Vitest 4.1.10); each original PR's exact-head CI is green. Codex autoreview was scoped-clean at the default P0 threshold.

No source repair or branch rewrite was needed. No merge performed. Preserve Co-authored-by: Yash Raj Pandey <yashpn62@gmail.com> when squashing.

Suggested landing changelog: “Content extraction: decode HTML entities once so deliberately escaped markup remains literal text (#394, thanks @devYRPauli).”

@steipete
steipete merged commit fd185fc into steipete:main Aug 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants