Recursive AI

Coming from AngleSharp: where the trees differ

All onehtml recipes

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.

This program runs the documented inputs through onehtml:

C#
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:

InputonehtmlAngleSharp
<use xlink:href> in SVGprefix xlink, local name href in the XLink namespaceplain href (namespace kept, prefix dropped). With both href and xlink:href, keeps one
a&;btext a&;bdrops the ;
elements inside <select>kept, as the current standard parses select content as in bodydropped or moved (the older “in select” mode)
<template shadowrootmode> in a parsed documentstays a template element with its contentattaches a shadow root, so the host has no child
<search>closes an open p and its end tag pops to itunknown element, so </search> is ignored
</td> while a th is openignoredcloses the cell
gb2312 / gbk labelsGBKgb18030 (the decoded text is the same)
no BOM, header, <meta> or XML declarationwindows-1252, the standard’s recommended defaultUTF-8
<?xml … encoding="Shift_JIS"?> with no other declarationShift_JISUTF-8
uppercase </C at the end of a <style>text </Ctext </c
thousands of unclosed elementsnests up to 4,096, then throws HtmlLimitExceededExceptionstops 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).
  • 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).
  • 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).
  • 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. Coming from HtmlAgilityPack instead: Migrate from HtmlAgilityPack.