jsoup
HTML parsing — the DOM as a GADT
Released —
v1.0.0(full feature parity).goforge.dev/gpsoupis a Go+ rewrite ofjhy/jsoup— the HTML parser, DOM, sanitizer, and HTTP client — that models the DOM as a GADT and consumesstd/parsecfor its CSS-selector grammar. Every jsoup subsystem is ported and differentially tested against jsoup’s own JUnit oracle cases; the parity ledger is the null set. Install withgo get goforge.dev/gpsoup@v1.0.1.
GoForge jsoup targets jhy/jsoup jsoup-1.22.2 (pinned). Requires the goplus
v0.140.1 toolchain to regenerate. As of v1.0.1 the buffered rune scanner
behind the tokenizer is std/text/scan and byte-level charset
detection/decoding is std/charset (stdlib v0.208.0) — both extracted from
this port for the wider portfolio, consumed with no functional change.
The DOM is a GADT
jsoup’s DOM is a mutable, cyclic node graph — the canonical case for goplus’s
//goplus:box pointer enums. Node is a pointer-boxed enum whose variants
(Document, Element, Text, Comment, Data, CData, Doctype,
XmlDecl) each carry a shared *nodeCore (parent / children / sibling index),
and every shared operation — NodeName, AppendChild, serialization, text
extraction — is an exhaustive match, so adding a node kind without handling
it everywhere is a compile error. The tokenizer’s Token is a pointer-boxed enum
too. gpsoup is a real, DOM-shaped consumer of the pointer-boxed-enum feature it
helped motivate. Deep traversals use Go+ tail recursion (tail func +
recur), so serialization, text, and selection are stack-safe on arbitrarily
deep documents.
std/parsec
The tokenizer’s tag/attribute-name grammar and the whole CSS-selector grammar
(including the :nth-child an+b expression) are std/parsec combinators.
Full parity — every subsystem
- HTML5 tokenizer — jsoup’s faithful ~69-state machine (
CharacterReader+Token+Tokeniser+TokeniserState, ~3000 LOC), verified against jsoup’sCharacterReaderTest. Malformed markup never fails. - HTML5 tree construction — the full algorithm (
TreeBuilder+HtmlTreeBuilder+HtmlTreeBuilderState): the 24 insertion modes, document normalization, implicit<tbody>/<tr>,<p>/<li>/heading auto-closing, table foster parenting, active-formatting reconstruction, and the adoption agency algorithm (<b>bold<i>both</b>italic→<b>bold<i>both</i></b><i>italic</i>). - Serializer — jsoup’s pretty-printer: block-boundary whitespace normalization, indentation, void/self-close handling, boolean-attr collapse, and the base/extended/xhtml escape modes with charset-aware entity encoding.
- Text —
TextOfwith block-boundary spacing, readable-boundary tag separators, /invisible normalization,<pre>/<code>preservation, and the complete 2125-entry entity table plus numeric references. - CSS selectors — the full
Evaluator/QueryParser: attribute operators, the combinators>/+/~/ descendant, structural pseudo-classes,:has,:not,:matchText(serialization-transparent), the::text/::comment/::data/::cdata/::nodenode selectors, and namespaces. - Sanitizer —
safety:Cleaner+Safelist(none / basic / basicWithImages / relaxed), base-URI resolution, external-onlyrel=nofollow, andIsValid/IsValidBodyHtml. - HTTP —
Connection/HttpConnection, consumingstd/http(HTTP/3 → HTTP/2 → HTTP/1.1): sessions, proxies, TLS options, request-body streams, andDataUtilcharset detection (BOM / meta / WHATWG index) with gzip. - XML —
XmlTreeBuilderwith namespace scoping, case preservation, a configurableTagSet, DOCTYPE/CDATA/decl nodes, and a dedicated serializer. - Traversal, mutation & streaming —
NodeVisitor/NodeTraversor/NodeFilter, deepClone,FormElementassociation, source ranges, the DOM mutators, and a genuinely incrementalStreamParserthat steps the tree builder token-by-token and emits each element as it completes.
Differential parity
Each subsystem carries a *_diff_test.go that ports jsoup’s own JUnit cases
(exact inputs and expected outputs) and enforces them with t.Errorf — HTML tree
construction (HtmlParserTest), serialization, .text(), selectors
(SelectorTest), the sanitizer (CleanerTest / SafelistTest), XML, and the
stream parser (StreamParserTest). The parity ledger is empty: no t.Skips and
no logged-but-unenforced divergences. Gen-check, go vet, and -race are clean.
Provenance
Upstream: jhy/jsoup, MIT (© Jonathan Hedley). Independent Go+ implementation, not vendored source.