---
title: What Eight Selectors Remove Before Your Agent Reads the Page — NanoParse Blog
url: https://nanoparse.app/blog/2026-11-04-what-a-parser-strips
description: A parse is a reduction. The page arrives as HTML; the agent receives markdown, a title, and a block of signals. Between those two states sits a fixed list of de…
---

A parse is a reduction. The page arrives as HTML; the agent receives markdown, a title, and a block of signals. Between those two states sits a fixed list of decisions about what to delete, and that list is the policy the reader never sees.

This post prints the policy out of the deployed source and measures it. Not described, not promised: eight selector strings, ten tags removed before them, nine container candidates, and one code path that bypasses the whole strip. Every claim below is either a quoted line of `index.ts` as it is live today or the output of running that same code over a fixture. The fixtures, the scripts, and the extracted functions are committed beside this post.

The reason to do this is narrow and practical. Extraction is the largest pipe of untrusted text most agents have. A page that wants an agent to do something only has to get the instruction into the text the agent reads, and the parser decides which text that is.

## The order of operations

Three removals run before any conversion. Quoted from the deployed route:

```
$("nav, footer, aside, header, script, style, noscript, svg, form, iframe").remove();
// ... a second pass for site chrome: wikipedia navigation, youtube comments, ads, cookie banners
// P0-3: Strip hidden content to prevent prompt injection.
```

Then the hidden-node pass, which is one call with eight selector strings:

```
[style*="display:none"], [style*="display: none"],
[style*="visibility:hidden"], [style*="visibility: hidden"],
[style*="opacity:0"], [style*="opacity: 0"],
[aria-hidden="true"], [hidden]
```

Then exactly one container is chosen, first non-empty match wins:

```
article > main > [role="main"] > #mw-content-text > .mw-parser-output
> .post-content > .article-content > .content > body
```

Only that container is converted. Everything outside it is dropped without being inspected, which is why a sidebar link never reaches the markdown even though none of the eight rules mentions sidebars.

## Five node classes, eight rules

The eight strings cover five ways of hiding content: no display, no visibility, zero opacity, an accessibility attribute, and the boolean `hidden` attribute. Two of the five are spelled twice because a site may write the style with or without a space after the colon, and a substring match cannot normalise whitespace.

That last detail is the whole design. These are substring tests against the `style` attribute plus two attribute selectors. No stylesheet is downloaded, cascaded or computed. The parser has the markup, not the render.

## What reaches the markdown

Seventeen fixtures, each one an ordinary fifty-word article with a single marked node appended. The last column is what the deployed functions actually produce from that input.

```
fixture markup in markdown
---------------------------------- ------------------------------ -----------
display none, no space style="display:none" no
display none, one space style="display: none" no
visibility hidden style="visibility:hidden" no
opacity zero style="opacity:0" no
aria-hidden aria-hidden="true" no
hidden attribute <p hidden> no
screen-reader-only class class="sr-only" YES
utility class class="hidden" YES
opacity 0.5, faded but visible style="opacity:0.5" no, removed
comment, no angle bracket <!-- text --> no
comment with an angle bracket <!-- if x > 5 then ... --> partial
image alt text <img alt="..."> no
template contents <template>...</template> YES
prose with < in it, parsed DOM <p>latency < 200ms</p> yes
prose with < in it, raw string <p>latency < 200ms</p> truncated
shell page + __NEXT_DATA__ prose <div id="root"></div> YES
same sentence in display:none style="display:none" no
```

Eleven of the seventeen behave exactly as the comment claims. Six do not, and each of the six is worth knowing before treating this list as protection.

## The rules that do less than they look like they do

### Class-based hiding is not covered

`.sr-only` and `.hidden` both reach the markdown, text intact. The parser never sees the rule that hides them, because the rule lives in a stylesheet — and the stylesheet is deleted a few lines earlier as a `style` tag. The evidence is removed along with the instruction. A page can hide text with `class="hidden"` and a single rule in its own
