Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Build

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Check out ReadOnlyDom
uses: actions/checkout@v7.0.1
with:
path: AngleSharp.ReadOnlyDom

- name: Check out the matching AngleSharp source
uses: actions/checkout@v7.0.1
with:
repository: dv00d00/AngleSharp
ref: devel
path: AngleSharp

- name: Set up .NET
uses: actions/setup-dotnet@v6.0.0
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore AngleSharp.ReadOnlyDom.slnx --force --no-cache
working-directory: AngleSharp.ReadOnlyDom

- name: Build
run: dotnet build AngleSharp.ReadOnlyDom.slnx -c Release --no-restore -m:1
working-directory: AngleSharp.ReadOnlyDom

- name: Test
run: >-
dotnet test tests/AngleSharp.ReadOnlyDom.Tests/AngleSharp.ReadOnlyDom.Tests.csproj
-c Release -f net10.0 --no-build --no-restore --
--minimum-expected-tests 1 --progress off
working-directory: AngleSharp.ReadOnlyDom
43 changes: 28 additions & 15 deletions docs/UPSTREAM_ANGLESHARP_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
# AngleSharp generic construction notes
# AngleSharp upstream dependencies

The object and compact DOM projects exercise generic construction and byte-source APIs that are not in a released
AngleSharp package yet. Until they ship, `Directory.Build.targets` replaces the AngleSharp package reference with a
source checkout. The standalone streaming project does not use that override.

## Upstream tracking

Status snapshot: 2026-08-20. All three pull requests are open and mergeable; their Linux and Windows jobs compile the
changes and then hit the same Fallout/.NET SDK infrastructure failure described in the PR comments.

| Pull request | Relationship to this repository |
| --- | --- |
| [AngleSharp/AngleSharp#1285](https://github.com/AngleSharp/AngleSharp/pull/1285) | Handle-oriented HTML tree construction used by the compact arena |
| [AngleSharp/AngleSharp#1286](https://github.com/AngleSharp/AngleSharp/pull/1286) | Direct `ReadOnlyMemory<byte>` parsing in the retained DOM lanes |
| [AngleSharp/AngleSharp#1287](https://github.com/AngleSharp/AngleSharp/pull/1287) | Related opt-in custom-DOM whitespace contract; not enabled by default here |

Do not publish the retained DOM packages against the temporary source contract. After #1285 and #1286 land in an
AngleSharp release, remove the source override, update the central package version, and run the full multi-target suite
against the package before making either project packable. #1287 can land independently.

## Handle-oriented tree construction

Expand All @@ -8,19 +27,13 @@ FsCheck differential testing against AngleSharp 1.5.2 reduced a mismatch to malf
<table><div><span attr='unterminated>
```

The standard mutable DOM foster-parents `div` into `body` before `table`. A DOM built through
`IDomConstructionElementFactory<TDocument, TElement>` places it outside `html`.

The original `HtmlDomBuilder<TDocument, TElement>.AddElementWithFoster` checked `el is HtmlTemplateElement` and
`el is HtmlTableElement`. Those AngleSharp core concrete types prevented a custom constructable element from matching
mutable parser behavior.

The handle-oriented `HtmlTreeBuilder<TDocument, TNode>` now uses parser flags and HTML tag identity instead. Compact parsing
therefore handles foster parenting, templates, and formatting adoption directly through `ArenaHandle`, without a parallel
object tree. Differential and smoke coverage retains the malformed-table case above as a regression contract.
The standard mutable DOM foster-parents `div` into `body` before `table`. The old generic construction path placed it
outside `html` because foster-parenting checks depended on AngleSharp's concrete element types. The handle-oriented
tree builder uses parser flags and HTML tag identity instead, so compact parsing can handle foster parenting, templates,
and formatting adoption directly through `ArenaHandle`, without a parallel object tree. Differential and smoke coverage
keeps the malformed-table case as a regression contract.

## UTF-8 token-source adapter
## Standalone UTF-8 lane

The streaming product now owns its native UTF-8 tokenizer, WHATWG entity table, encoding-label table, limits, and sink
contract, and therefore has no runtime dependency on AngleSharp. The mutable-DOM token-source adapter remains core
specific; object and compact construction continue to consume the fork until their required construction APIs ship.
The streaming product owns its tokenizer, WHATWG entity table, encoding-label table, limits, and sink contract. It has
no runtime dependency on AngleSharp and can be built, tested, and published independently of the upstream work above.
13 changes: 8 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ serially because the solution and the fork share AngleSharp output paths:
```powershell
dotnet restore AngleSharp.ReadOnlyDom.slnx --force --no-cache
dotnet build AngleSharp.ReadOnlyDom.slnx -c Release --no-restore -m:1
dotnet test tests/AngleSharp.ReadOnlyDom.Tests/AngleSharp.ReadOnlyDom.Tests.csproj -c Release -f net10.0 --no-restore
dotnet test tests/AngleSharp.ReadOnlyDom.Tests/AngleSharp.ReadOnlyDom.Tests.csproj -c Release -f net10.0 --no-restore -- --minimum-expected-tests 1
```

The Release build output should contain an
Expand Down Expand Up @@ -345,13 +345,15 @@ dotnet run --project samples/AngleSharp.ReadOnlyDom.MarkdownProxy -c Release
```

The Hacker News reader folds a live list page into NDJSON one story at a time and unfurls a link-preview card per row
on scroll, abandoning each linked page's download at `</head>`. Targets `net11.0` with platform async, so it needs a
.NET 11 SDK:
on scroll, abandoning each linked page's download at `</head>`. It builds on the default .NET 10 lane:

```powershell
dotnet run --project samples/AngleSharp.ReadOnlyDom.HackerNews -c Release
```

With a .NET 11 SDK, pass `-p:Net11Lane=true -p:Net11Async=true` to opt the sample and streaming library into the
platform-async experiment.

Opinionated text/Markdown projections and safe local Markdown navigation remain runnable examples rather than library
surface:

Expand All @@ -360,10 +362,11 @@ dotnet run --project samples/AngleSharp.ReadOnlyDom.ExtractionExamples -c Releas
dotnet run --project samples/AngleSharp.ReadOnlyDom.MarkdownNavigation -c Release
```

Run the test suite from the repository root:
Run the test suite from the repository root. The minimum count guard prevents a broken test-host configuration from
reporting success after discovering no tests:

```powershell
dotnet test AngleSharp.ReadOnlyDom.slnx -c Release
dotnet test tests/AngleSharp.ReadOnlyDom.Tests/AngleSharp.ReadOnlyDom.Tests.csproj -c Release -f net10.0 -- --minimum-expected-tests 1
```

## Repository layout
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework Condition="'$(Net11Lane)' == 'true'">net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<Features>$(Features);runtime-async=on</Features>
<UseRuntimeAsync Condition="'$(Net11Async)' == 'true' and '$(TargetFramework)' == 'net11.0'">true</UseRuntimeAsync>
<LangVersion Condition="'$(Net11Async)' == 'true' and '$(TargetFramework)' == 'net11.0'">preview</LangVersion>
<Features Condition="'$(Net11Async)' == 'true' and '$(TargetFramework)' == 'net11.0'">$(Features);runtime-async=on</Features>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AngleSharp.ReadOnlyDom.Streaming\AngleSharp.ReadOnlyDom.Streaming.csproj" />
Expand Down
15 changes: 7 additions & 8 deletions samples/AngleSharp.ReadOnlyDom.HackerNews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
A Hacker News front end built on two streaming query plans. Nothing is buffered: list HTML is folded into NDJSON one
story per line, and each row unfurls a link-preview card as it scrolls into view.

Needs a .NET 11 SDK (`net11.0`, platform async on).
The default lane targets `net10.0`. A .NET 11 SDK can opt into the platform-async experiment without making the
repository's normal build depend on a preview SDK.

```powershell
dotnet run --project samples/AngleSharp.ReadOnlyDom.HackerNews -c Release

# with the library on the same async lane
# Optional: target net11.0 and put both the app and library on the platform-async lane.
dotnet run --project samples/AngleSharp.ReadOnlyDom.HackerNews -c Release -p:Net11Lane=true -p:Net11Async=true
```

Expand Down Expand Up @@ -52,12 +53,10 @@ socket goes through a `ConnectCallback` that refuses private, loopback, link-loc
time, not parse time. Card images are re-served from this origin. Feeds cache 15 s, cards 10 min. Cards load three
at a time.

**Platform async.** `Features=runtime-async=on` is the switch; `UseRuntimeAsync` alone is a no-op and SDK
11.0.100-preview.7 has no property for it. Restore does not flow `AdditionalProperties` across a project reference,
so `Net11Lane`/`Net11Async` must be global `-p:` properties or restore fails NETSDK1005. Emitted state machines: 0
vs 3 for this app (43,008 B vs 52,224 B), 0 vs 14 for the library (293,376 B vs 309,760 B). Whether it is *faster*
is unmeasured here — earlier preview-6 runs gained 1–4% on stream shapes while the preview JIT lost 8–12% on the
synchronous tokenizer loop.
**Platform async.** The checked-in default is the supported `net10.0` lane. Passing both `Net11Lane=true` and
`Net11Async=true` targets `net11.0` and adds `Features=runtime-async=on`; the properties must be global `-p:` values so
restore also applies them to the streaming project reference. The experiment previously emitted 0 compiler-generated
state machines versus 3 for this app, and 0 versus 14 for the library. Whether it is *faster* remains unmeasured here.

**Limits.** Lexical start/end-tag topology, not corrected tree topology: omitted end tags and foster parenting can
differ from a DOM converter. A page that declares card metadata below the head, or only from script, gets no card.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(Net11Lane)' == 'true'">net8.0;net10.0;net11.0</TargetFrameworks>
<UseRuntimeAsync Condition="'$(Net11Async)' == 'true'">true</UseRuntimeAsync>
<Features Condition="'$(Net11Async)' == 'true'">$(Features);runtime-async=on</Features>
<LangVersion Condition="'$(Net11Async)' == 'true'">preview</LangVersion>
<UseRuntimeAsync Condition="'$(Net11Async)' == 'true' and '$(TargetFramework)' == 'net11.0'">true</UseRuntimeAsync>
<Features Condition="'$(Net11Async)' == 'true' and '$(TargetFramework)' == 'net11.0'">$(Features);runtime-async=on</Features>
<LangVersion Condition="'$(Net11Async)' == 'true' and '$(TargetFramework)' == 'net11.0'">preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.IO.Pipelines" />
<PackageReference Include="System.IO.Pipelines" Condition="'$(TargetFramework)' == 'net8.0'" />
</ItemGroup>

<ItemGroup>
Expand Down
148 changes: 3 additions & 145 deletions tests/AngleSharp.ReadOnlyDom.Tests/AngleSharp.ReadOnlyDom.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0;net472</TargetFrameworks>
Expand Down Expand Up @@ -28,149 +28,7 @@
</ItemGroup>

<ItemGroup>
<None Update="TestData\html5lib-tokenizer\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\163.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\360.cn.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\aliexpress.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\amazon.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\ask.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\baidu.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\bing.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\blogspot.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\codeproject.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\ebay.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\en.wikipedia.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\flickr.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\florian-rappl.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\godaddy.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\google.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\html5rocks.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\html5test.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\huffingtonpost.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\imdb.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\kickass.to.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\linkedin.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\live.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\mail.ru.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\msn.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\myspace.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\nbc.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\neobux.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\netflix.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\news.google.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\nytimes.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\pcmag.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\peacekeeper.futuremark.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\pinterest.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\qq.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\reddit.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\sitepoint.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\spiegel.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\stackoverflow.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\taobao.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\tmall.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\tumblr.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\vk.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\w3.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\weibo.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\wordpress.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\yahoo.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="temp\youtube.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\html5lib-tokenizer\**\*" CopyToOutputDirectory="PreserveNewest" />
<None Update="temp\*.html" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>