---
title: "Coming from AngleSharp: where the trees differ"
url: https://recursiveai.org/onehtml/cookbook/migrate-from-anglesharp/
description: "If you are moving HTML parsing from AngleSharp to onehtml, this is what changes in the trees you get. On a corpus of real pages the two agree exactly on…"
version: "1.0.0"
---

# Coming from AngleSharp: where the trees differ

If you are moving HTML parsing from AngleSharp to onehtml, this is what changes in the trees you get. On a corpus of real pages the two agree exactly on about nine in ten pages, and every difference in the rest comes from one of a short list of documented causes.

onehtml’s query members follow the DOM standard: `QuerySelector`, `QuerySelectorAll`, `Matches`, `Closest`, `GetAttribute`, `TextContent`, and `Children` for element children. Code written against those DOM names maps directly. The onehtml repository compares the two parsers on 13,857 crawled pages. 12,469 (89.98%) give identical trees, and every difference in the rest is traced to the HTML, Encoding or DOM standard in [AngleSharp differential](https://recursiveai.org/onehtml/docs/anglesharp-differential/).

This program runs the documented inputs through onehtml:

```csharp
using System.Text;
using RecursiveAI.OneHtml;

void Show(string label, string html, string selector)
{
    Document doc = Document.Parse(html);
    Node node = doc.Root.QuerySelector(selector);
    Console.WriteLine($"{label,-26} {doc.SerializeNode(node)}");
}

// SVG keeps the xlink prefix on adjusted foreign attributes
Document svg = Document.Parse("<svg><use xlink:href=\"#icon\"></use></svg>");
foreach (Attr attr in svg.Root.QuerySelector("use").Attributes)
{
    Console.WriteLine($"{"xlink attribute",-26} qualified {attr.QualifiedName}, local {attr.LocalName}, namespace {attr.Namespace}");
}

// A bare & before ; is text, and the ; stays
Show("bare ampersand", "<p>a&;b</p>", "p");

// select content is parsed as in body: other elements stay inside the select
Show("select content", "<select><option>One<b>!</b></option></select>", "select");

// A parsed document keeps declarative shadow root templates as template elements
Show("shadowrootmode", "<div><template shadowrootmode=open><b>x</b></template></div>", "div");

// search closes an open p and its end tag pops to it
Show("search element", "<div><search><div></search>x</div>", "body");

// </td> does not close a th
Show("cell end tag", "<table><tr><th></td>x</table>", "tr");

// Encodings: gb2312 is GBK, and undeclared pages fall back to windows-1252
Console.WriteLine($"{"gb2312 label",-26} {Document.Parse(Encoding.ASCII.GetBytes("<meta charset=gb2312><p>x")).Source.Encoding.Name}");
Console.WriteLine($"{"no declaration",-26} {Document.Parse(Encoding.ASCII.GetBytes("<p>x")).Source.Encoding.Name}");
```

```text
xlink attribute            qualified xlink:href, local href, namespace XLink
bare ampersand             <p>a&amp;;b</p>
select content             <select><option>One<b>!</b></option></select>
shadowrootmode             <div><template shadowrootmode="open"><b>x</b></template></div>
search element             <body><div><search><div></div></search>x</div></body>
cell end tag               <tr><th>x</th></tr>
gb2312 label               GBK
no declaration             windows-1252
```

The same inputs in AngleSharp, as that document records them:

| Input | onehtml | AngleSharp |
|---|---|---|
| `<use xlink:href>` in SVG | prefix `xlink`, local name `href` in the XLink namespace | plain `href` (namespace kept, prefix dropped). With both `href` and `xlink:href`, keeps one |
| `a&;b` | text `a&;b` | drops the `;` |
| elements inside `<select>` | kept, as the current standard parses `select` content as in body | dropped or moved (the older “in select” mode) |
| `<template shadowrootmode>` in a parsed document | stays a `template` element with its content | attaches a shadow root, so the host has no child |
| `<search>` | closes an open `p` and its end tag pops to it | unknown element, so `</search>` is ignored |
| `</td>` while a `th` is open | ignored | closes the cell |
| `gb2312` / `gbk` labels | GBK | gb18030 (the decoded text is the same) |
| no BOM, header, `<meta>` or XML declaration | windows-1252, the standard’s recommended default | UTF-8 |
| `<?xml … encoding="Shift_JIS"?>` with no other declaration | Shift_JIS | UTF-8 |
| uppercase `</C` at the end of a `<style>` | text `</C` | text `</c` |
| thousands of unclosed elements | nests up to 4,096, then throws `HtmlLimitExceededException` | stops nesting at 512 and appends siblings |

Other differences to plan for:

- **Undeclared pages decode differently.** This is the change most likely to show up in your output. Where AngleSharp assumed UTF-8, onehtml uses windows-1252. If your sources are UTF-8 without a declaration, set `ParseOptions.DefaultEncoding = WhatwgEncoding.Utf8` (see [Detect the encoding](https://recursiveai.org/onehtml/cookbook/detect-encoding/)).
- **Nothing is fetched and no script runs.** onehtml never opens a URL or a file. Fetch the bytes yourself, pass the `Content-Type` header as `ParseOptions.ContentType`, and supply linked stylesheets through a resolver (see [Compute CSS styles](https://recursiveai.org/onehtml/cookbook/computed-style-csharp/)).
- **Documents are immutable.** You don’t change a node in place. You build edits and `Apply` them, which returns a new snapshot and leaves every untouched byte of the original as it was (see [Edit HTML without reformatting it](https://recursiveai.org/onehtml/cookbook/edit-html-csharp-lossless/)).
- **`Node` is a struct.** A missing result is the null node (`IsNull`), not `null`.
- **The target framework is `net10.0` only.**

For a measured comparison of the trees each parser builds: [HtmlAgilityPack vs AngleSharp](https://recursiveai.org/onehtml/articles/htmlagilitypack-vs-anglesharp/). Coming from HtmlAgilityPack instead: [Migrate from HtmlAgilityPack](https://recursiveai.org/onehtml/articles/migrate-from-htmlagilitypack/).
