← Back to Documentation

HTML Parsing

Fetch a page, then ask it a question. Parse turns HTML into links, text, Markdown or any CSS selection — including attributes.

One verb, routed by the qualifier

Parse decides what to do from the result qualifier, never by sniffing the input. That makes the statement readable on its own:

(indexPage: Crawler) {
    Request the <page> from the <http-client> with "https://example.com".
    Parse the <links: links> from the <page: body>.
    Return an <OK: status> with <links>.
}
QualifierGives you
linksEvery href on the page, as a list of strings
textThe body's text, as a list of strings
content{ title, content } — the title and cleaned body text
markdown{ title, markdown } — the body converted to Markdown
page / html{ title, markdown, links } — all three at once
selectA CSS selection, text or attributes (below)
jsonStrict JSON — a structured value, or a descriptive error
link-headerAn RFC 8288 Link header, keyed by rel

An unqualified Parse is an alias of Extract — reading a field off an already-structured value is Extract's job, not a parse format. Any other qualifier is an error naming the valid ones, rather than a guess.

Reading attributes with select

The fixed qualifiers answer the four questions their names ask. select answers the rest. The with clause is an ordinary CSS selector, optionally followed by @attribute:

Parse the <hrefs: select> from the <page> with "a@href".
Parse the <srcs: select>  from the <page> with "img@src".
Parse the <desc: select>  from the <page> with "meta[name=description]@content".
Parse the <headings: select> from the <page> with "h2".

One qualifier covers both, so nobody has to remember two names for "the link texts" and "the link targets".

An element without the attribute contributes nothing

Given <a href="/one">One</a> <a>Not a link</a> <a href="/two">Two</a>:

Parse the <texts: select> from the <page> with "a".        (* 3 items *)
Parse the <hrefs: select> from the <page> with "a@href".   (* 2 items *)

The result is the attributes that exist, not a list with holes where they do not. A list with holes is only useful if you are correlating it against another list by index — and for that, CSS already says what you meant:

Parse the <texts: select> from the <page> with "a[href]".
Parse the <hrefs: select> from the <page> with "a[href]@href".
(* same length, same order, aligned by construction *)

An attribute that is present but empty — <input value=""> — is reported as the empty string rather than dropped. Dropping it would conflate "no value" with "an empty value", and only the first is a missing attribute.

The attribute comes from the last @

An attribute name cannot contain one and a CSS selector can, so [data-x="a@b"]@href selects on data-x and reads href. A trailing @, a leading @, or something after it that is not an attribute name leaves the whole string as the selector — so a malformed one is reported by the parser rather than silently selecting something else.

Where the content comes from

content, markdown and page take the document's main body in priority order — <main>, then <article>, then <body>. Whitespace is normalised, empty segments are dropped, and script and style elements are ignored.

markdown preserves document structure: headings become #…######, links become [text](url), images ![alt](src), <strong> becomes **bold**, <pre><code> becomes a fenced block, and lists, tables, blockquotes and rules all carry over.

A crawler, end to end

(crawl: Crawler takes <url>) {
    Extract the <start> from the <parameter: url>.
    Request the <response> from the <http-client> with <start>.

    (* title and body in one parse *)
    Parse the <page: page> from the <response: body>.
    Extract the <title> from the <page: title>.

    (* relative hrefs are resolved against the page, then normalised *)
    Parse the <hrefs: select> from the <response: body> with "a[href]@href".
    for each <href> in <hrefs> {
        Compute the <absolute: url-resolve> from <href> with { base: <start> }.
        Compute the <canonical: url-normalize> from <absolute>.
        Store the <canonical> into the <visited-repository>.
    }

    Return an <OK: status> with { title: <title>, found: <hrefs> }.
}

url-resolve and url-normalize are what make a crawler's "have I seen this?" answerable — two spellings of one address compare equal after normalising. See Computations.

XML is not implemented

There is no ParseXml and no XPath. The proposal was once titled "HTML/XML" and listed XML as something that could be added, which read as a promise of a parser that does not exist; it was retitled rather than left to mislead. Parse handles HTML, JSON and the Link header.

The full specification is ARO-0011: HTML Parsing. The historical ParseHtml spelling still works as a legacy alias, but Parse with a qualifier is the current form.