Jint.HtmlParser: owned markup, DOM and CSS infrastructure
Status: historical migration proposal, 2026-09-23. The AngleSharp dependency and parser-thread inventory below describes the pre-migration revisions named here, not the current runtime. The shipped native ownership and integration contract are documented in the parser package README and Browser's contributor instructions. Design worktree base: 73f9220b1b48e6da755cc1ae32b058f6e846b599. Evidence refreshed against the coordinator's integration base d78d25137526b4a7b35ccdcfc8522730acd392d1; current-state statements below refer to that newer revision, not the design worktree's older checkout. Implementation work targets main.
1. Decision and scope
Create a standalone, managed Jint.HtmlParser project and NuGet package. It owns the HTML parser, the mutable document tree used by HTML/XML/SVG, CSS syntax and CSSOM, selector matching, serialization, and the mutation machinery that makes those facilities coherent. Jint.Browser owns JavaScript bindings, browser scheduling, networking, resource activation, DOM event dispatch, and rendering approximations. The dependency is Jint.Browser → Jint.HtmlParser; the parser has no dependency on Jint, Jint.DevTools, a JavaScript engine, or AngleSharp.
The user's explicit request supersedes the existing AngleSharp-only principle for this campaign. It does not waive existing script-visible behavior, performance, identity, resource limits, or conformance requirements. Update the affected guidance when the implementation switches ownership; this proposal leaves instructions governing the current runtime intact.
Completion requires both the standalone construct APIs in §4 and removal of all four AngleSharp packages from the shipped browser dependency graph, including transitive references through generator/runtime projects. The standalone gates cover creation, document/fragment parsing, CSS constructs, selectors, intrinsic element state, mutation tracking and serialization without a Browser. A parser that reads simple HTML while the browser still needs AngleSharp's CSSOM, node classes or XPath navigator is an intermediate artifact, not completion. AngleSharp can remain a pinned test/benchmark oracle in development-only projects.
The public API is new. Do not reproduce AngleSharp interfaces, its configuration/service container, or its [DomName] metadata merely to minimize edits. Preserve web-facing contracts through generated bindings and explicit semantic adapters. The existing public Browser/Page surface predominantly returns snapshots and values, so most CLR migration is internal; a public API snapshot remains a required gate rather than assuming that no dependency type escaped.
Package and implementation boundaries
| Component | Owner | Responsibility |
|---|---|---|
Jint.HtmlParser/Dom | New package | Node identity, namespaces, tree/attribute operations, ranges, collections, change tracking |
Dom/Html | New package | Intrinsic element reflection, form ownership, control values/dirty flags, checkedness, selectedness, disabledness and validity |
Html | New package | Tokenizer, tree builder, document/fragment/resumable parsing, character references |
Xml and Svg | New package | XML well-formedness, shared-tree construction, SVG document entry and namespace handling |
Css and Selectors | New package | CSS tokens/rules/declarations, selector programs, property grammar, cascade inputs |
Serialization and XPath | New package | HTML/XML serializers and a navigator over the same nodes |
Dom/Generated and generator | Browser/tooling | WebIDL branding, conversion, shapes and calls into the new native model |
| Event, script, frame, resource and lifecycle algorithms | Browser | Existing Jint event bus and page loop; no second scheduler in the parser |
| CSS environment and layout | Browser | Media state, pseudo-class state, UA rules, resolved values, flat layout and rule-use reporting |
Target net8.0;net10.0, matching the browser and its tests. A standalone package does not require a netstandard2.0 asset; adding one now would constrain hot-path APIs without a demonstrated consumer. Use repository signing/versioning/central-package conventions. Enable trimming/AOT analyzers for the new project and verify a published standalone consumer; promote the browser's AOT claim only after its own complete native smoke test. No reflection-based service discovery or runtime code generation.
This is document infrastructure, not a renderer. SVG parsing includes XML namespaces, foreign content, DOM and attribute preservation; it does not promise painting, fonts, filters or animation. Existing SVG DOM interfaces used by the browser still require explicit bindings and semantic implementations. HTML also requires MathML foreign-content handling even though no separate MathML entry point was requested. CSS syntax support is not equivalent to implementing every CSS property's computed value.
2. What actually has to move
The integration tree has 194 Jint.Browser .cs files mentioning AngleSharp (including generated files). The dependency pins are AngleSharp 1.8.2, AngleSharp.Css 1.1.2, AngleSharp.Xml 1.2.0, and AngleSharp.XPath 2.0.6. The first two also feed tools/dom-bindings/pin.json.
| Current seam | Evidence in this repository | Replacement obligation |
|---|---|---|
| Navigation parser | Runtime/Parsing/ParserDriver.cs, ParserBaton.cs | Scripting flag, incremental tree, script insertion points, readiness, bounded parsing |
| Navigation input | Runtime/DocumentFetch.cs | Bounded byte accumulation, decode, MIME dispatch, URL/response/error preservation |
| XML navigation routing | PageDocumentFactory.cs, DomContentType.cs | Principal and frame XML/XHTML/+xml routing, SVG document branding |
| Subresources | PageResourceLoader, PageStylingService, PageScriptingService | Discovery and activation of scripts/styles/images/frames; load/error ordering |
| Detached parsing | Dom/Views/JsDomParser.cs, DomConstructors.cs | Inert HTML/XML documents, parsererror mapping, correct document brands |
| Bindings | tools/dom-bindings/Jint.Browser.BindingGenerator, Dom/Generated/*.g.cs | Replace assembly metadata input and interface casts, retain checked-in generated shapes |
| Tree state | DomHostHooks, DomNodeMembers, collections, forms, custom elements | Every insertion, adoption, attribute, reflection, range and traversal operation |
| Detached dynamic markup | Dom/DynamicMarkupInsertion.cs | Existing secondary-document open/write/close behavior and document identity |
| Mutations | Observers/JsMutationObserver.cs, MutationObserverLane.cs, DevTools/DomNodeTracker.cs | Shared mutation source, observer options and records, microtask delivery, protocol tracking |
| Attribute hooks | CustomElementAttributeObserver, FileInputAttributeObserver, FrameAttributeObserver | Cover detached nodes, namespaced writes and native mutations, not just connected descendants |
| CSS | Dom/Views/CssCascade.cs, ResolvedStyle.cs, JsCssNamespace.cs, Runtime/PageRenderDevice.cs, CSS protocol domain | Selector/property parsing, CSSOM mutation, cascade, supports/media checks, rule-use tracing |
| XPath | JsXPath.cs, DevTools/DomDomain.Events.cs | Navigator plus node identity; BCL XPath evaluation can remain |
| Consumers of tree/CSS | Layout, Accessibility, Extraction, Events, CustomElements, Media, FrameWindows | One new tree, no cross-model node conversion and no stale second state |
| Layout invalidation | Layout/PageLayout.Invalidation.cs, DomFailures.GuardMutation | Entry/exit mutation scopes, reentrant reads, version saturation and unknown-writer fallbacks |
Generated code currently consumes AngleSharp-specific interfaces, extension methods, enums, nullability, collection accessors and legacy named properties. Renaming namespaces is insufficient. Build a generated inventory of every emitted member and every manually consumed native member before migration starts. Each inventory row must name its new owner, implementation task, regression tests and disposition of the old workaround. The existing divergence register is evidence to migrate, not behavior to discard.
The current document fetch already reads a bounded stream into a byte array, decodes it, and hands a complete string to the parser. The separate parser thread exists because AngleSharp's async parsing and scripting hooks can resume on pool threads; a blocking baton enforces exclusive DOM ownership. This is a scheduling adaptation, not evidence that network streaming is already supported. FetchedDocument now carries Markup and ContentType: top-level XML navigation and XHTML/+xml routing already work through PageDocumentFactory. XML navigation scripts remain inert, a documented browser divergence. The migration must preserve that routing and separately account for script support. Current selector fixes in DomSelectors/DomForgivingSelectors, namespace/element factories and processing-instruction attributes also belong in the inventory; do not regress them to an older baseline.
3. Recovery, strictness and what Lightpanda teaches
Conformance is not rejection of malformed input
The default must implement specified HTML error recovery. Missing end tags, foster parenting and misnested formatting have observable results. Rejecting them is an authoring validator policy, not more faithful browser parsing. CSS similarly recovers at defined boundaries, with different rules for different grammar contexts. WPT tests observable algorithms; it is not an XML-style cleanliness test for HTML. See the HTML parsing standard, CSS error handling, and the repository's Jint.Tests.Browser/Wpt/AGENTS.md for its real-harness and exclusion rules.
Use these separate concepts:
| Concept | Proposed behavior |
|---|---|
| HTML/CSS recovery | One standard algorithm, always the default |
| Diagnostics | Optional bounded collection of error codes and source positions; does not change the tree |
| XML well-formedness | Required; failure is a parse failure, not HTML recovery |
| HTML quirks mode | Determined by doctype and parsing context, not a user strictness toggle |
| Missing implementation | Explicit capability/debt entry and failing or excluded test; never silently successful parsing |
| Headless approximations | Browser policy, independently documented, such as synthetic geometry; not a parser option |
A ParseSvg call uses XML rules; an inline <svg> in HTML uses HTML's foreign-content rules. There is no input sniffing that silently swaps them.
The browser and WPT use the same parser defaults. There is no strictness option, feature-enable list, or planned profile system. Required parse context (such as a fragment's element and HTML scripting flag) selects the algorithm the caller is invoking; resource limits bound its work. Neither disables otherwise implemented features. Diagnostics observe that algorithm without changing its result.
Lightpanda source findings
Inspected source commit: 1dd33ee7ec779033bf8d3b4707277deddf667202. These are observations of that revision, not claims about every release or proof of conformance.
| Observation and primary source | Design consequence |
|---|---|
| Rust wrapper uses html5ever 0.40.0 and xml5ever 0.40.0; HTML document and streaming constructors use default parser options. Manifest, wrapper | HTML recovery comes from a standards-oriented parser, not a home-grown globally tolerant grammar. Keep a real tree builder. |
HTML parseErrorCallback ignores diagnostics; XML callback marks errors except two message-prefix cases. Parser callbacks | Diagnostics can be cheap when disabled. Do not copy message-based XML exceptions without normative and test evidence. |
Streaming.read guards reentrant feeds and queues input; done warns and drops writes made during finish. Streaming implementation | Reentrancy needs an explicit protocol. These choices are not an oracle for document.write timing. |
| Pending text accumulates chunks and flushes before operations that expose the tree; comments identify an earlier quadratic concatenation problem. Text accumulation | Coalesce parser text without repeated immutable-string concatenation; flush before script/reaction observation. |
units.zig distinguishes its permissive attribute lexer from fail-closed media queries. Unit parser | Share lexical utilities, not permissiveness across unrelated grammar contexts. |
The source also provides a tokenizer-only preload scan and periodic termination checks. Both are useful architectural ideas; a preload scan is optional later work, and its result must remain a hint. Cancellation in Jint must also cover long tokens and tree-builder loops, not only node append counts.
This evidence supports selective, explicit engineering tradeoffs. It does not establish that Lightpanda has a universal strict/lenient switch, nor that copying its shortcuts preserves WPT behavior. Use its failure cases as test ideas and its architecture as inspiration. Implement from specifications and independently written tests rather than copying its AGPL-licensed implementation into this package.
4. Native API contract
The following is the proposed public shape, not existing compiling code. It includes the standalone construct entry points required for this campaign, independent of what Browser happens to call. Implement and snapshot each surface with its owning feature, rather than freezing a broad speculative assembly snapshot or publishing throwing placeholders at project creation. Parser internals and browser integration hooks remain internal with a signed friend grant until an external consumer demonstrates that they need promotion.
using Jint.HtmlParser;
using Jint.HtmlParser.Css;
Document html = MarkupParser.ParseHtml("<p>Hello <b>world</b>");
Document xml = MarkupParser.ParseXml("<items><item id='a'/></items>");
Document svg = MarkupParser.ParseSvg("<svg xmlns='http://www.w3.org/2000/svg'/>");
CssStyleSheetSyntax css = MarkupParser.ParseCss("p { color: red }");
DocumentFragment fragment = MarkupParser.ParseHtmlFragment("<td>x", tableRow);
CssDeclarationBlock style = MarkupParser.ParseCssDeclarations("color:red; width:2em");
Element? first = html.QuerySelector("p > b");
string text = first?.TextContent ?? "";
string serialized = MarkupSerializer.ToHtml(html);Proposed signatures:
public static class MarkupParser
{
public static Document ParseHtml(string source, HtmlParseOptions? options = null,
CancellationToken cancellationToken = default);
public static DocumentFragment ParseHtmlFragment(string source, Element context,
HtmlParseOptions? options = null, CancellationToken cancellationToken = default);
public static Document ParseXml(string source, XmlParseOptions? options = null,
CancellationToken cancellationToken = default);
public static DocumentFragment ParseXmlFragment(string source, Element context,
XmlParseOptions? options = null, CancellationToken cancellationToken = default);
public static Document ParseSvg(string source, XmlParseOptions? options = null,
CancellationToken cancellationToken = default);
public static CssStyleSheetSyntax ParseCss(string source, CssParseOptions? options = null,
CancellationToken cancellationToken = default);
public static CssDeclarationBlock ParseCssDeclarations(string source,
CssParseOptions? options = null, CancellationToken cancellationToken = default);
public static CssRuleSyntax ParseCssRule(string source, CssParseOptions? options = null,
CancellationToken cancellationToken = default);
public static CssDeclarationSyntax ParseCssDeclaration(string source,
CssParseOptions? options = null, CancellationToken cancellationToken = default);
public static CssComponentValue ParseCssComponentValue(string source,
CssParseOptions? options = null, CancellationToken cancellationToken = default);
public static CssComponentValueList ParseCssComponentValues(string source,
CssParseOptions? options = null, CancellationToken cancellationToken = default);
public static CssValueParseResult ParseCssValue(string propertyName, string source,
CssParseOptions? options = null, CancellationToken cancellationToken = default);
public static SelectorProgram ParseSelector(string source,
SelectorParseContext? context = null, CancellationToken cancellationToken = default);
}null source/context throws ArgumentNullException; empty HTML is a valid empty HTML document; empty XML/SVG fails. HTML returns its implied html/head/body, doctype and document mode. Parsing never fetches, executes script or fires DOM events. Fragment parsing uses the supplied element's document, namespace, local name, form/template ancestry and relevant flags; it returns a detached fragment without replacing the context's children. It must handle table/select/script/style/template/SVG/MathML contexts.
ParseSvg parses an XML document and requires an svg root in the SVG namespace; <svg/> with no namespace is not silently repaired. Consumers wanting HTML's inline SVG recovery call ParseHtml or ParseHtmlFragment with an SVG context. ParseXmlFragment is a required entry point: it parses an XML fragment with the context's in-scope namespace bindings and owner document, allows multiple roots/text, and returns detached children without modifying the context. An empty fragment succeeds; an XML declaration, doctype, undeclared prefix or malformed markup fails with MarkupParseException. It does not infer namespaces from tag names. SVG snippets use this method with an SVG context for XML semantics, or ParseHtmlFragment for HTML semantics. X1 owns XML document and fragment parsing; X2 owns SVG checks.
CSS and selector construct contracts
All entry points consume the whole input according to the named grammar; no successful single-item parse silently leaves a second item unread. Syntax results own their retained data and are usable after the source/session is released. CssStyleSheetSyntax, CssRuleSyntax and CssDeclarationSyntax are syntax objects, distinct from mutable CSSOM rules and validated declarations. This makes an unknown at-rule or property available to a standalone consumer without claiming the browser implements its semantics.
| Entry point | Return and failure contract | Owner |
|---|---|---|
ParseCss | Immutable CssStyleSheetSyntax with exact Source and read-only Rules; standard whole-sheet CSS Syntax recovery, unknown rules and unvalidated property contents retained; empty source succeeds | C1 |
ParseCssDeclarations | Mutable validated CssDeclarationBlock; declaration-list recovery and importance/cascade ordering; empty source succeeds | C4/C5 |
ParseCssRule | One CssRuleSyntax, including opaque unknown at-rule syntax; CssParseException if the single-rule grammar returns failure or trailing non-whitespace input remains | C1 |
ParseCssDeclaration | One CssDeclarationSyntax containing property name, component values and importance; CssParseException on structural failure; does not assert property support | C1 |
ParseCssComponentValue | One token, function or simple block; CSS Syntax recovery still applies inside it; CssParseException on no value or extra non-whitespace values | C1 |
ParseCssComponentValues | Immutable sequence, empty allowed; CSS Syntax token/block recovery, optionally diagnosed; no property grammar claim | C1 |
ParseCssValue | CssValueParseResult with Valid, Invalid or UnsupportedProperty; Value exists only for Valid, with deferred substitution represented explicitly; parses property grammar, does not compute a style | C5 |
ParseSelector | Immutable SelectorProgram for a selector list; SelectorParseException for invalid syntax, unresolved namespace prefix or unsupported selector construct; no forgiving fallback outside grammar-defined forgiving lists | C2 |
ParseCss is a cold syntax facade, amending the original proposal of a mutable validated stylesheet return. Its sealed result has no public constructor and owns the completed rules behind a genuinely read-only collection; nested components and spans remain the existing public syntax objects. It retains the immutable original source without retaining the parser, options, diagnostic collector or host. The existing limits, diagnostics, checkpoints and cancellation apply, including a final cancellation check before publication. It adds no strictness mode, selector compilation, property validation, cascade, geometry, attachment, URL resolution or loading. Public mutable CSSOM promotion remains C4 work, and the equivalent CSSOM benchmark row below remains separate; this syntax facade does not establish that parity or a replacement performance claim.
The standalone single-declaration wrapper permits one terminating semicolon and surrounding whitespace, but rejects a second declaration or other trailing content. It wraps CSS Syntax's declaration consumer with that whole-input check; callers parsing style text use ParseCssDeclarations. Tokenization recovery is not a failure policy toggle. CSSOM insertRule additionally validates supported rule kind and insertion hierarchy, using the syntax parser internally. CSS.supports consults the same property/condition grammar as the native validated path, not success from ParseCssComponentValues. Syntactically legal deferred var() values are not rejected for lacking a computed environment. Unknown ordinary properties report UnsupportedProperty; custom properties use their own defined grammar and preserve case.
SelectorParseContext supplies namespace-prefix bindings and parse limits; an absent context means no declared prefixes and ordinary query-selector grammar. SelectorProgram.Matches(Element) and QuerySelector/QuerySelectorAll evaluate against the current tree. Intrinsic HTML state is always available from native nodes. A browser may additionally supply its environment state internally. An absent host means no focus/hover/active target, not disabled selector parsing or false answers for :checked, :disabled and :valid. Context is not a feature configuration registry.
Creation without parsing
Document.CreateHtml() and Document.CreateXml() create empty documents of the named kind, without implied nodes, parser session or host; this differs deliberately from parsing empty HTML. Factories below create detached objects owned by that document. Creation never connects a node, loads a resource or executes a callback. Names are validated before publication. Invalid names/namespaces use a native DomException with stable DOM error names, which Browser maps to its existing JS exception type.
| Factory/member | Contract | Owner |
|---|---|---|
Document.CreateElement(string localName) | HTML document: ASCII-lowercase HTML-namespace element; XML document: case-preserving null-namespace element | D2 |
Document.CreateElementNS(string? namespaceUri, string qualifiedName) | Validated namespace/prefix/local-name tuple; no HTML case folding | D2 |
Document.CreateTextNode(string data), CreateComment(string data) | Owned text/comment; empty data allowed; serialization applies the format's separate restrictions | D2 |
Document.CreateDocumentFragment() | Empty fragment owned by the document | D1 |
Document.CreateAttribute(string name), CreateAttributeNS(string? namespaceUri, string qualifiedName) | Detached attribute with empty value and no owner element; same name rules as the corresponding element factory | D2 |
Document.CreateProcessingInstruction(string target, string data) | Validate target and forbidden PI terminator; detached PI | D2 |
Document.CreateCDataSection(string data) | XML only; rejects forbidden CDATA terminator; HTML reports NotSupportedError | D2 |
Document.CreateDocumentType(string qualifiedName, string publicId = "", string systemId = "") | Detached doctype with validated name; attachment checks document hierarchy separately | D2 |
Node.AppendChild, InsertBefore, RemoveChild, ReplaceChild | Validate before structural change, preserve identity, use common mutation algorithms; cross-document insertion adopts where DOM permits | D1/D3 |
Document.AdoptNode(Node), ImportNode(Node, bool deep = false) | Adoption moves identity/ownership; import creates a new detached copy; unsupported node kinds fail explicitly | D3 |
Null required strings/nodes fail with ArgumentNullException. Nullable namespace is intentional. Document/node factories and their small API snapshots land with D1/D2/D3; the parser project bootstrap does not reserve every future method in a shipped assembly. There is no runtime element-factory plugin or per-tag class hierarchy. Named CSS and selector constructors above likewise land with C1/C2/C4/C5. The native follow-up contract specifies document metadata, D3b explicit adoption/replace-all and the required template-content ownership commit after D3a clone/import.
Options carry required context, resource limits and diagnostics, not feature enablement or alternate recovery algorithms. They are small immutable values, snapshotted at session creation. No builder/container chain and no per-element factories. HtmlParseOptions contains base URL, the HTML scripting flag (default false; affects noscript, does not execute code), shared ParseLimits, and an optional diagnostic collector. Declarative-shadow behavior is selected by browser call context through internal creation parameters, not exposed as a dozen interacting public booleans. XML and CSS options contain only applicable fields. Base URL metadata is not authorization to fetch anything. Browser URL resolution continues to use the repository's WHATWG URL implementation; the standalone package must not pretend BCL URI resolution is automatically browser-equivalent.
Limits cover input characters, cumulative parser work/created nodes, token/attribute text, open-element depth, CSS nesting, XML entity expansion, and accumulated inserted input. Zero means unbounded where consistent with the repository. Ordinary standalone parsing defaults to no arbitrary document-size ceiling; the browser passes only bounds with equivalent counting semantics (see §5); it must not reinterpret its existing MaxDomNodes as a cumulative allocation/work limit. Cancellation is always available. Exhaustion is a distinct ParseLimitException with a stable limit kind and observed count; XML syntax errors are MarkupParseException with code/position. Cancellation remains OperationCanceledException. Malformed HTML/CSS recovers by default. Infrastructure exceptions are never relabeled parse errors.
Diagnostics are optional, capped, and use stable codes plus UTF-16 offsets for string inputs; line and column calculation is enabled only when requested. The collector reports truncation. A byte decoder later adds byte offsets without redefining existing offsets. Dynamic input needs source-segment identity as well as offset. Diagnostic collection must not retain the entire input through a tiny record.
Nodes, storage and ownership
Use ordinary GC-owned reference objects with stable identity: Node, Document, Element, Text, Comment, DocumentType, DocumentFragment, and XML processing-instruction/CDATA nodes. Document.Kind distinguishes HTML/XML; SVG is an XML document with SVG elements, not a second tree implementation. Elements carry namespace/local-name/prefix and an internal known-element discriminator. Avoid a public class for every HTML tag; bindings select web interface brands from that discriminator and semantic state.
Start with parent/first/last/previous/next links, child count, and a compact ordered attribute store that promotes to an index only when profitable. Measure before selecting thresholds. Typical small elements should not allocate dictionaries, observer lists or property bags. Intern known names statically; use document/session-scoped atoms for unknown names, never an immortal process-wide user-string table.
Owned text strings allow input buffers to be released. Tokens use borrowed spans only within a drive call; any token retained across a pause owns its data. Pool scratch buffers/stacks, never externally reachable nodes. Do not require Document.Dispose() to make its nodes valid or invalidate a node when its parse session ends. This accommodates detached nodes, adoption, JS wrappers and retained ranges. An arena that retains a whole page through one detached node is not the initial storage design.
Documents and parse sessions are single-owner mutable objects. Independent documents may be parsed on different threads; concurrent access to one document requires the host to serialize it. Public read-only snapshots are explicitly named. QuerySelectorAll returns a new static result; Children/ChildNodes are live views. Collection caches may use mutation versions but must not cache wrappers by live index. Adoption preserves node identity and moves owner-document state while creation-realm identity remains the browser wrapper's responsibility. Cloning creates new identity.
Intrinsic HTML state belongs to the native document
Jint.HtmlParser.Dom.Html owns HTML reflected attributes and control algorithms: value/defaultValue and dirty flags, checked/defaultChecked, selected/defaultSelected, radio groups, option selection, form ownership, disabledness including fieldset/legend and optgroup rules, constraints and validity. This state exists without Jint or a Browser. Standalone setters and parser attribute initialization reach the same algorithms that generated JS bindings call. For example an unchecked checkbox with a stale checked attribute after its dirty flag is set must match according to current checkedness; :disabled must include a disabled fieldset's effect, not just the element's own attribute.
Expose native semantic access through a lazily created HtmlElementState view on applicable elements, with control-specific state only where the element kind requires it; do not allocate a browser service or JS wrapper to read a property. Its properties/operations are implemented in D7's finite control families, with scoped API snapshots. Validity is a pure native read; browser checkValidity additionally dispatches the required events. Reflected values, selectors, form serialization and bindings all read the native owner. Browser supplies environment-dependent focus/hover/activation, user input, navigation, script execution and event dispatch. It must not maintain a second authoritative checkedness/validity store. Mutations and adoption reset/recompute affected native state under the shared DOM algorithms.
5. Resumability first; transport streaming later
A complete string is the initial input API and initial browser transport. The parser core is nevertheless resumable from its first tree-building milestone. Tokenizer state, open elements, formatting list, template modes, pending table text, character-reference state and insertion points belong to the session, not the CLR call stack. No await occurs inside tree mutation.
| Approach | Benefits | Cost | Decision |
|---|---|---|---|
| One-shot string parser | Simplest caller, contiguous scanning | Cannot expose partial DOM at parser-blocking scripts if it builds the whole tree first | Public convenience only, drives the resumable core |
| Resumable string parser | Correct script boundaries, insertion and cancellation; matches current transport | Explicit state machine and host protocol | Required first architecture |
| Incremental UTF-16 chunks | Tests chunk boundaries and bounds retained input | Carry partial tokens/CRLF/surrogates and input ownership | Core contract tested early; public API can wait |
| Incremental network bytes | Earlier first-script latency, less input duplication | Encoding prescan/restart, transport backpressure and lifecycle changes | Separate phase after browser parity |
Proposed internal protocol:
CreateDocumentSession(document, parsingContext, limits)
AppendInput(ReadOnlyMemory<char>, isFinal) // owned or copied under an explicit lease
Drive(workQuota, cancellationToken) -> ParseStep
ParseStep = NeedInput | Yielded | HostAction(requestId, action) | Complete
HostAction = PrepareScript | ConstructCustomElement | ProcessTreeEffect
CompleteHostAction(requestId, result)
InsertAtInsertionPoint(text, insertionFrame)
DriveInsertedInput(insertionFrame, cancellationToken)
Abort(reason)This protocol is a design contract, not a requirement to expose all actions as heap objects. Use a small tagged result and reusable request storage where measurement supports it. Drive returns only with a coherent tree and flushed text. A request is completed once; stale IDs, driving while the same frame is running, or appending after final completion are rejected. Final network input does not mean EOF has already been consumed: scripts can still insert before pending EOF. Complete occurs only when EOF and all parser work have been handled, and differs from the browser's window load.
On PrepareScript, Browser decides whether execution blocks parsing, starts or reuses the fetch, and runs JavaScript only on PageLoop. A returned parse step releases the parser's CLR stack before user code runs. Async/defer/module scheduling, stylesheet blocking, import maps and ready-state transitions belong to a browser script coordinator. They do not belong to a generic syntax parser. Tree-effect hooks cover script/style/link/image/frame activation and relevant mutations, including src set after insertion. Inert parser entry points install no browser activation host. R1 exercises this protocol with controlled resource completions and implements classic parser blockers; it does not claim stylesheet-blocking integration. R2 adds real stylesheet parse/attachment completion and applicable script-blocking conditions after B5's CSS integration, including media/disable changes, fetch failures and event ordering. A style fetch completing is not sufficient to unblock a script before the associated CSS processing is complete.
document.write is a synchronous insertion protocol
Appending writes to the end of the source, parsing them as a detached fragment, or waiting until the outer script returns is wrong. A script can write an element and query it in the very next statement. Nested written scripts may themselves write. The parser needs saved/restored insertion frames and the HTML parser-pause/script-nesting rules. The dynamic markup specification is authoritative; a generic reentrancy lock cannot substitute for those rules.
When a parser script is executing, the outer Drive has already returned. Browser's write hook inserts before the saved unread tail and drives eligible inserted input synchronously up to that frame's marker or a specified blocking boundary. Nested host requests get nested frames, with an explicit depth limit. The same active parser frame is never entered recursively while its tree mutation is running. Do not pump arbitrary tasks/microtasks simply because the parser yielded inside running JavaScript. Reactions and checkpoints happen at their specified algorithm boundaries. Split writes may intentionally leave a partial token that later writes complete.
Initial migration preserves the current explicit refusal of post-parse implicit document.open on a displayed document while providing the core machinery for it. Secondary HTML documents already implement open/write/close through DynamicMarkupInsertion, reparsing accumulated input and replacing children after each write. Preserve that supported surface, then replace its identity-losing reparse with the resumable session. A separate task implements displayed-document replacement/listener clearing, new parser generations and lifecycle behavior. Do not claim complete dynamic-markup conformance until that task passes its WPT cases. XML documents continue rejecting these operations.
Parser CPU quotas return Yielded only at safe states; they let the loop observe close/cancellation without allowing unrelated DOM operations halfway through a mutation. The host decides which browser tasks may run at a parsing yield. Poll input scans as well as emitted tokens and long tree algorithms, so a huge unterminated comment cannot evade cancellation. A separately named cumulative parser-work limit can count every created node before allocation, including transient/reparented work. That is not the browser's existing MaxDomNodes: today its parse leg checks the finished document, while its independent wrapper leg counts nodes projected to script. During migration keep that final-document check and independent wrapper count. A node that is created then discarded or removed before the final check must not unexpectedly spend the existing DOM ceiling. Any live-tree allocation counter used internally must likewise not redefine the public cap as a peak or cumulative count.
Do not populate the new work limit from MaxDomNodes. Standalone callers may explicitly set the parser-work bound; Browser adoption of a new bound requires its own named option/contract change, not a silent mapping. Cancellation and cooperative work quotas provide responsiveness independently of size limits. Tests must distinguish final/live membership, cumulative creation and wrapper counts, including parser recovery that creates transient nodes and scripts that remove nodes during parsing.
Byte streaming has a separate acceptance gate
Change FetchedDocument to a bounded byte source only after resumability works with complete strings. Networking produces bytes without touching DOM or Jint values; the page loop owns decoding/session advance, or receives decoded immutable chunks from a stateful decoder. Backpressure caps outstanding chunks. Preserve response metadata, redirects, cookies, MIME behavior and close/cancellation semantics. Use encoding-label and HTML sniffing rules, including BOM/header/meta precedence and restart constraints. No externally visible script executes twice because a decoder restarted. Test multibyte splits, CRLF splits, surrogate pairs, EOF in every token state, early cancellation and redirects before committing. Do not advertise a byte-stream API that assumes each network chunk is independently valid UTF-8.
6. Mutation and event integration
Every write path goes through the same DOM algorithms: parser attachment/reparenting, host methods, binding methods, Attr.Value, namespaced attributes, TextContent, innerHTML, replacement, clone, adoption, shadow/template operations and CSSOM mutation. Tree links and attribute storage are not publicly writable. Fast parser insertions may skip redundant validation when their preconditions hold, but cannot skip required semantic hooks, collection invalidation, range updates or observable records.
Use three separate mechanisms, with explicit ownership:
- Synchronous internal semantic hooks. Narrow before/after operation hooks update live ranges, iterators, form state, slotting, style versions and browser state before the next read. Attribute hooks run even on detached elements and receive namespace/local name and old/new values. Custom element reaction scopes are supplied by Browser; the DOM core does not run arbitrary JS itself.
- Mutation record production. Per-node observer registration lazily records the DOM-standard child-list/attribute/character-data information. It matches ancestors, filters, old-value requests and transient registrations; replace-all and fragments produce their specified record structure. It does not flatten records into one generic “document changed” event.
- Host scheduling and delivery. A cheap internal “pending observers” signal schedules one Browser microtask.
MutationObserverLaneretains its delivery job, ordering and exception policy. Protocol mutation consumers use the same source, and notifications occur on the page loop. CSS changes carry separate style versions/notifications; DOM MutationObserver does not invent CSSOM records.
For standalone .NET consumers, expose a disposable pull subscription: document.ObserveMutations(target, options) returns MutationSubscription; the D5 dispatch defines its exact options/record surface and lifetime. TakeRecords() drains only; TakeRecordsForDelivery() also completes transient-registration cleanup, providing standalone callers an explicit delivery boundary. Disconnect() clears the subscription. Records contain stable node references and immutable added/removed snapshots. No hidden thread or event loop is started. The browser adds its own callback scheduling over this machinery. Draining one subscription never drains another. Disconnect clears its registrations and records; records retain removed nodes only while queued or retained by a consumer. Add callback convenience only with a documented scheduling/reentrancy contract.
With no observer and no browser host, mutation recording should require an inexpensive empty-state check and no record allocation. Internal change stamps are still maintained as needed for correctness. Observer old strings and added/removed lists are materialized only when an interested observer needs them. Detaching a subtree must not lose the transient registrations required until the next delivery.
DOM event dispatch stays in Jint.WebApi.Events/Jint.Browser.Events. Native nodes provide parent, shadow-root/slot, ownership and activation state to the existing dispatcher. Preserve capture/bubble, retargeting, once/passive/abort behavior, listener identity, handler ordering and default-action rollback. Mutation observation is not an event bus. The new package does not emit legacy mutation events or fire load just because it finished tokenizing. Browser translates parser/resource milestones to its existing readiness/resource event algorithms.
7. XML, SVG, selectors and CSS are migration work
XML and SVG
The XML dispatch decision evaluates System.Xml.XmlReader and selects an owned scanner because the source-token, expansion-cancellation and failure contracts require control the reader API does not expose. Build native nodes directly, retaining one DOM. Preserve XML node kinds, qualified names, namespaces, doctype and document metadata; XML input never goes through HTML. Never initiate network/file access. Internal DTD/entity support, a pinned local character-entity catalog and finite expansion limits are required and tested, not equated with “DTD disabled”. The supplement scopes two internal implementation milestones before the public XML/SVG methods land.
Keep the direct API's syntax failure distinct from Browser's DOMParser parsererror document. Test all four XML MIME types, including XHTML routing, before deleting AngleSharp.Xml. SVG fragment handling and HTML foreign-content integration share node/namespace storage but not tokenizer error policy. Separate tasks implement SVG typed attributes/lists and existing interface brands where the binding inventory requires them. No rasterization dependency is introduced.
Implement a new XPathNavigator over these nodes and retain BCL XPath evaluation. Test namespace axes, attribute identity, document order and DOM XPath result conversions. Existing ignoreNamespaces: true behavior is a documented browser divergence, not a native DOM invariant. Preserve it during the backend swap or fix it in an independently tested change; do not make all standalone XPath queries ignore namespaces to match one browser workaround. Mutation stamps enable proper iterator invalidation later.
CSS and selectors
Implement a CSS Syntax tokenizer and component-value/rule parser before typed property grammars. Preserve custom property spelling/token content and distinguish unknown rules from syntactically invalid rules. The internal syntax representation can preserve opaque nodes even where CSSOM exposure omits unsupported constructs. Declaration parsing, stylesheet parsing, CSSOM insertRule, selector parsing and CSS.supports have different failure contracts and must select them deliberately.
Compile selectors into immutable engine-free programs. A program is reusable across documents; matching receives document mode and namespace rules, reads intrinsic control state from native nodes, and uses a host view only for environmental state such as focus/hover/active/target. It never retains a node or engine in a shared cache. Cover scope, relative selectors, escape processing, forgiving lists only where the grammar permits them, pseudo specificity and shadow boundaries. Use right-to-left matching and candidate indexes only after correct traversal baselines exist. Complex selectors and invalid input must be bounded; regex is not the parser.
The replacement cascade must cover the browser's present supported surface: selector specificity, source order, origins/importance, inheritance, initial/unset/revert behavior, shorthand expansion, custom properties and var() cycles/fallback, conditional rules, inline style, stylesheet enablement, imports, media/preferences and CSSOM writes. The inventory determines the exact property/rule set. Advanced features such as layers/nesting/container queries cannot be accepted as implemented merely because the syntax parser can retain their tokens. Mark their individual capability and tests.
CssCascade, ResolvedStyle and flat layout retain one authoritative path for computed/resolved values. Port UA defaults and current corrections; preserve CSS rule identity and source association for the protocol and rule-use coverage. Syntax objects, declaration blocks and stylesheets are usable without a Browser. Environment-dependent evaluation takes an explicit context, not global process state. Preserve the existing Browser-owned invalidation contract in PageLayout.Invalidation: mutation scopes clear on entry/exit and prohibit cache reuse during reentrant callbacks, with explicit fallbacks for unknown writers and asynchronous attachment. Native change stamps can later replace conservative invalidation only with proof that every write path is covered. Keep CSSOM invalidation distinct from DOM mutation records. See docs/design/layout-invalidation.md.
8. Binding and browser migration strategy
Keep one backend active per page. Do not mirror AngleSharp and native trees in a live page: wrapper identity, adoption, mutations and selectors would immediately have two possible answers. During development both implementations may exist in separate test fixtures or builds. No runtime backend switch is required in the final public API.
Replace AngleSharp reflection metadata with a checked-in WebIDL-derived manifest plus a typed native mapping. Pin upstream IDL/spec inputs and document generated versus curated fields. The manifest describes names, inheritance, mixins, overloads, nullability, conversions, enum strings, exposure, legacy indexed/named behavior and property attributes. Native mappings select semantic helper methods and interface brands without requiring one CLR interface per WebIDL interface.
Continue generating JsObjectShapes, registry/type dispatch and collection accessors in tooling; continue checking generated output for staleness. Never hand-edit .g.cs. Keep wrapper identity in the engine's weak table keyed on the new object and prototype/constructor state per realm. Cross-document adoption cannot rebrand already-created wrappers. Add generated surface diffs so lost members are reviewable rather than silently skipped as “unsupported conversion”.
Migration order is native algorithms → generated adapters → leaf consumers → page orchestration. Compile an alternate browser test build against the new backend while default shipping remains AngleSharp. This can be a temporary build property and separate output path; it must not add a public configuration matrix. If temporary adapters help leaf conversion, they wrap the new DOM and live in Browser, not in the new package as public AngleSharp-compatible contracts. Delete them when unused.
Port form-control state and element reflection into the native package (D7), then make Browser's bindings and event/input adapters call that owner (B2). Live collections and resource activation also need explicit migration; these are currently supplied in part by AngleSharp and do not appear by replacing INode. Likewise frame context creation must become Browser-owned. The existing thread-safe Page API and snapshot boundary remain. Remove ParserBaton only when the new coordinator owns all nested scripts, resource waits, frames and task budgets; not when one inline script happens to work.
9. Correctness gates
- Native parser corpus. Pin and vendor applicable html5lib tokenizer/tree-construction data with licensing/provenance. Assert trees including namespaces, attributes, comments, doctypes and template content, not only serialized text. Split each short fixture at every boundary, and use deterministic partition sampling for large ones. Run scripting-enabled/disabled and fragment contexts.
- Mutation semantics. Test direct .NET mutations and JS bindings over the same algorithms: adoption, replace-all, fragments, namespaced attributes, removed-subtree observation, repeated observe, disconnect/takeRecords, old values, ranges, iterators and collection liveness. Assert microtask order against promises and custom-element reactions, not just record counts.
- Browser parity. Run the complete existing
Jint.Tests.Browsersuite against each backend with identical fixture inputs. Preserve framework fixtures and actual Puppeteer/Playwright/CDP paths, not merely standalone tree snapshots. Exercise DOMParser, CSS supports/media, XPath and SVG separately. - WPT. Use the existing pinned corpus and upstream harness. No WPT-only parser setting, altered vendor files, lowered registration minima, widened globs or raised failure ceilings to absorb migration regressions. Record exact failing subtests and the responsible task. A newly passing test narrows its exclusion and refreshes the census; harness errors invalidate the run.
- Scheduling. Controlled local-server tests for parser blockers, delayed CSS/scripts, async/defer/ modules, nested writes, write-then-query, scripts at EOF, mutation during custom constructors, nested frames, source changes after insertion, navigation/close races and stale completions. Specify event/checkpoint traces and partial-DOM assertions. Timeouts are wedge ceilings, not proof of the required ordering.
- Robustness. Deterministic fuzzing and differential comparisons, with minimized repros for disagreements. AngleSharp is a useful comparator but standards/WPT decide discrepancies. Test deep malformed input, huge tokens/entities, repeated fragments, observer retention and cancellation. No stack recursion proportional to adversarial nesting; check for superlinear hot cases explicitly.
- Shipping. Release build/test both frameworks, public API snapshots, host-contract verification where applicable, generated bindings, native standalone and browser-tool smoke tests, dependency graph/packed NuGet checks, and all existing browser consumers. Parsing must not fetch or run script.
- Standalone completion. A no-friend consumer referencing only the packed
Jint.HtmlParserpackage must exercise every §4 construct/factory, inspect/query/mutate/serialize its result, and assert the named failure contracts. It must test intrinsic control selectors without a host, native observer records, XML fragment namespace inheritance, and CSS syntax-versus-property validity. No Browser assembly may enter the dependency graph. Unimplemented entry points, placeholder throws and deferred requested constructs block G1 even if the browser suite is green.
Do not claim full WPT or browser conformance from a selected corpus. Track implementation progress by the same case IDs and capabilities across default browser, proposed backend and standalone tests.
10. Comparable performance evidence
No speedup is claimed by this proposal. Create benchmarks before optimizing or switching the default. Use the repository's BenchmarkDotNet configuration, Release builds, production tiering/PGO, an idle machine, JINT_BENCH_MODE=gate for quoted comparisons, and paired alternating worktree runs. Never quote --job short. Use at least six paired rounds for small effects; keep control rows and report intervals, per-launch variance and inconclusive results according to Jint.Benchmark/AGENTS.md.
| Benchmark family | Equal work and output | Measures |
|---|---|---|
| HTML document/fragment | Same string, context, scripting flag, recovery and DOM retention | Time, throughput, bytes allocated, GC |
| Malformed/adversarial HTML | Equal semantic output, increasing sizes, same limits | Scaling, peak memory, cancellation responsiveness |
| Text-heavy and script-heavy | Large text and many < characters; tree read after parsing | Copy amplification and retained memory |
| XML/SVG | Same well-formedness/namespace/entity settings and equivalent DOM | Parser plus tree cost, no rendering |
| CSS | Same rules/declarations/property support and CSSOM access | Parse, validation and allocation separately |
| Selectors | Same DOM, compiled versus parse-and-match separately | Matching, query results, cache lifetime |
| Mutation | No observers; one observer; subtree/old-value observers; native and JS paths | Record cost, live views, detached-node retention |
| Browser navigation | Same local responses, scripts/CSS, viewport and JS backend | Total load, first-script/readiness latency, thread/memory footprint |
| Byte-stream phase | Same bytes, encoding, chunk schedule and semantic result | Peak input retention and time-to-first-script |
Pin the four current AngleSharp versions and record runtime/OS/CPU/corpus hashes. Parser-only comparison must exclude Jint on both arms. Browser comparison uses Jint on both arms and includes the full equivalent CSS/DOM behavior. Do not compare token-only parsing with tree construction, inert parsing with executing scripts, or omit styles/observers from only the new arm. Validate outputs outside measured intervals and consume a result from every measured invocation. Fresh parsing gets a fresh document; repeated queries may reuse an immutable fixture when both sides do. Separate cold construction from warm parser reuse.
Keep one engine/page per workload, and amortize cheap Page.EvaluateAsync rows with workload-specific in-script loops as existing browser benchmarks do. Peak retained memory requires a separate process/ heap measurement; BDN allocated bytes alone cannot show retention through detached nodes or source slices.
The switch gate is correctness plus no unresolved statistically supported regression on the agreed representative suite. Before measurement, identify throughput/allocation priorities and tolerances with the maintainer; do not invent a promised “2x” speedup. Improvements to one row do not excuse regressions hidden by a geometric mean. Investigate a confirmed regression or document a concrete accepted tradeoff.
11. Phased implementation and bounded tasks
Task IDs are intended for independent, reviewable feature changes, not permission to start additional agents. Each task owns a finite algorithm/input set, its tests and its benchmark rows. Public signature changes require API review before parallel implementation. The table names dependency completion, not just a branch containing unfinished scaffolding.
| ID | Deliverable and acceptance boundary | Depends on |
|---|---|---|
| A1 | Machine-readable native/binding usage inventory and current API/WPT baseline; every AngleSharp use assigned | None |
| A2 | Standalone project, shared limits/errors and package/native smoke consumer; public snapshots added with each implemented feature | A1 |
| A3 | Equivalent AngleSharp benchmark fixtures and semantic output comparers; no performance claims yet | A1 |
| D1 | Empty HTML/XML document and fragment creation, node links/identity/namespaces and insert/remove validity | A2 |
| D2 | Element/attribute/text/comment/PI/CDATA/doctype factories, ordered attributes and namespace setters | D1 |
| D3 | Adopt/import/clone/replace-all plus fragment/template ownership tests | D1, D2 |
| D4 | Live collection, static result and traversal primitives; mutation-version correctness | D3 |
| D5 | Observer registration/filtering/records, transient registrations and pull subscriptions | D3 |
| D6 | Range and iterator mutation fixups, shadow-root/slot tree primitives | D3, D5 |
| D7 | Native intrinsic HTML reflection, control state, form ownership and validity; no-host state/query fixtures, split by control family | D3, D4 |
| H1 | Resumable input cursor, chunk ownership, diagnostics, cancellation and tokenizer corpus runner | A2 |
| H2 | Data/tag/attribute/comment/doctype tokens and character references; every split boundary | H1 |
| H3 | RCDATA/rawtext/script-data/plaintext states and EOF transitions; linear text accumulation | H2 |
| H4 | Initial/before-html/head/body/text insertion modes and implied elements | H3, D3 |
| H5 | Table/caption/column/row/cell modes and foster parenting | H4 |
| H6 | Formatting reconstruction/adoption agency, select/template/frameset and after-body modes | H4, H5 |
| H7 | SVG/MathML foreign content, integration points, fragment contexts and quirks cases | H6 |
| H8 | Host pauses, insertion frames, nested write-then-query, secondary-document incremental writes and pending EOF fixtures | H6, D5 |
| X1 | XML frontend decision spike, XML document and contextual fragment entry points, entity/DTD policy and namespace-preserving trees | D3 |
| X2 | ParseSvg, SVG/HTML distinction, DOMParser error mapping fixtures and SVG native-state inventory | X1, H7 |
| X3 | HTML/XML serialization and round-trip invariants | H7, X1 |
| X4 | XPath navigator and node-result conversion tests | X1, D4 |
| C1 | Public CSS single rule/declaration/component-value and component-list syntax APIs, tokenizer/recovery corpus | A2 |
| C2 | Public selector programs, specificity, namespace/scope and tree-state matching | C1, D4 |
| C3 | Native control pseudo-class parity and internal browser environment adapter; no-host intrinsic selector tests | C2, D7 |
| C4 | Public stylesheet/declaration-block CSSOM, mutation/versioning, serialization and insert/delete contracts | C1, D5 |
| C5 | Public property-value result API, property grammar inventory and finite groups; shorthands/custom values/supports and C4 validation | C4 |
| C6 | Cascade origins/order/inheritance/variables plus UA sheets and media evaluation | C2, C5 |
| B1 | New binding manifest/emitter, surface-diff and staleness tests; Node/Element vertical slice | A1, D2 |
| B2 | Binding/input adapters over native control state, reflection and collections; no duplicate Browser-owned intrinsic state | B1, D7 |
| B3 | Event-path/activation, custom-element reaction hooks, mutation microtask and DevTools observers | B1, D5, D6 |
| B4 | Detached DOMParser/SVG/XMLSerializer/XPath bindings; cross-realm adoption/wrapper preservation | B1, X2, X3, X4 |
| B5 | CSS bindings/computed style/media/protocol coverage and flat-layout adapter | B1, C3, C6 |
| R1 | Page-loop coordinator, classic inline/external blockers and host resource-request protocol with controlled completions; no real stylesheet-blocking claim | H8, B3 |
| R2 | Real stylesheet-blocking integration, ordered defer/module/import-map and async scheduling, readiness/resource event traces | R1, B5 |
| R3 | Dynamic script/style/image/frame activation and nested frame document/realm ownership | R2, B2, B4 |
| R4 | Extraction/accessibility/layout/protocol leaf migration and complete alternate-backend browser suite | B4, B5, R3 |
| R5 | Displayed-document post-parse open/write/close lifecycle and listener-reset behavior | R3 |
| G0 | Standalone packed-consumer gate for every §4 requested entry point/factory, intrinsic state, mutations and serialization; no placeholders | H7, X2, X3, C3, C5, D5 |
| G1 | G0 standalone deliverables plus full browser corpus/framework/client parity, benchmark and dependency/package audit; switch default | G0, R4, A3 |
| G2 | Remove temporary backend build, AngleSharp production references, old metadata pin and baton; update docs/instructions | G1 |
| S1 | Bounded byte transport, encoding sniff/decode and chunk backpressure with no repeated script execution | G1, H8 |
| S2 | Optional public stream overloads after ownership/cancellation API review and streaming comparison | S1 |
D7, C5, B2, R3 and R4 are task families: before assigning them, split their A1 inventory into named, finite groups with independent regression sets. For example C5 has separate color/display/visibility, box lengths/shorthands, font/text, and custom-value groups; D7 separates input/textarea, select/option, button/output and shared form-owner/validation algorithms; R3 separates dynamic scripts, styles, images and frames. A task named only “implement CSS” or “replace DOM” is not bounded enough to start.
Phases and exit criteria:
- Inventory and contracts (A). Prove how completion will be measured before building the replacement.
- Standalone vertical slice (D/H beginnings, X/C in parallel where independent). Parse/query/serialize a real document with native mutation records. Package remains explicitly experimental.
- Standalone requested deliverables and native browser surface (D/H/X/C). Every §4 construct/factory and failure contract works without Browser; standard recovery corpus passes. Missing requested CSS/SVG/DOM capabilities are named blockers, not deferred to a later browser consumer.
- Binding and runtime integration (B/R). Keep the shipping backend until the alternate build passes its full parity gates. The longest dependencies are CSS semantics, element state and script scheduling, not scanning characters.
- Standalone gate, switch and cleanup (G). G0 passes before the default switch. Remove all production AngleSharp references and obsolete workarounds only after both standalone and browser tests demonstrate their replacement. Preserve benchmark comparators in development tooling.
- Extended conformance and streaming (R5/S if not landed earlier). Clearly label remaining behavior gaps; the dependency replacement can finish while independently tracked conformance work continues.
Expect a multi-month campaign for the present browser surface, not a one-task parser rewrite. An initial planning allowance is roughly 6–12 engineer-months across native algorithms, CSS/DOM parity, generator work and migration, with low confidence until A1 and the standalone vertical slice finish. Independent work can reduce elapsed time but not the integration dependencies. Full CSS/SVG/rendering conformance is outside that estimate. Re-estimate from completed task families; do not equate a packaged prototype with a browser ready to switch.
12. Decisions to revisit with evidence
The recommended defaults are decided above so implementation can proceed: managed package, modern framework floor, standard recovery, inert standalone APIs, one DOM, resumable string core and Browser-owned execution. Three choices need measured checkpoints, not open-ended up-front configuration: XML reader backend viability (X1), compact attributes/node representation (D/A3), and CSS property scope from the actual consumer inventory (A1/C5). Byte streaming is deliberately independent of the first browser switch.
A successful prototype should demonstrate nested document.write, native and JS observation of the same mutation, CSS query/cascade agreement, and equivalent AngleSharp comparison on both well-formed and malformed fixtures. Those tests determine whether the architecture can replace the dependency; parsing a static page alone does not.