///|
/// Markup text to a tree.
///
/// A stack machine over the token stream, and short, because the hard part of
/// HTML parsing is the part this deliberately does not do. There are no
/// insertion modes here, no adoption agency, no foster parenting, and no
/// invented `html`/`head`/`body`: what comes out is what was written.
///
/// What it does take from tree construction is the part the specification
/// states as tables rather than as algorithms -- implied end tags, void
/// elements, raw text, and the namespace of a name -- and all four of those
/// live in `html/names` as data. The line is drawn there on purpose. Every step
/// past it buys a few more files that nest the way a browser would nest them,
/// and costs the property that makes this library worth having: that the tree
/// can be printed back.
///
/// Tolerant by construction. There is no path here that returns without a
/// document: an end tag with nothing to close is dropped with a warning, an
/// element that runs to the end of the input is closed with one, and anything
/// the tokenizer could not read arrives as a `Bogus` token and leaves as a
/// `Bogus` node carrying its source. `strict=true` raises the first error
/// instead, for the caller whose input is their own.
///|
/// How deep the element stack may go.
///
/// A bound rather than a stack overflow. Ten thousand `
`s is not a
/// document, it is a fuzzer's output or an attack, and a library that
/// segfaulted on one would be unusable in exactly the place a tolerant parser
/// is wanted. Past the bound a start tag becomes a `Bogus` node holding its own
/// source, so nothing is lost and nothing recurses.
const MAX_DEPTH : Int = 400
///|
/// A document, and everything noticed while reading it.
pub struct Parsed {
document : @ast.Document
diagnostics : Array[@error.Diagnostic]
}
///|
pub fn Parsed::document(self : Parsed) -> @ast.Document {
self.document
}
///|
pub fn Parsed::diagnostics(self : Parsed) -> Array[@error.Diagnostic] {
self.diagnostics
}
///|
pub fn Parsed::has_errors(self : Parsed) -> Bool {
for d in self.diagnostics {
if d.is_error() {
return true
}
}
false
}
///|
/// An element that has been opened and not yet closed.
priv struct Frame {
name : @ast.TagName
attrs : Array[@ast.Attribute]
children : Array[@ast.Node]
start : Int
name_span : @span.Span
}
///|
priv struct Parser {
toks : Array[@token.Token]
mut pos : Int
stack : Array[Frame]
roots : Array[@ast.Node]
diagnostics : Array[@error.Diagnostic]
strict : Bool
}
///|
/// Parse markup text.
///
/// Tolerant: malformed input produces diagnostics and a tree that still covers
/// the whole file, so this returns a document for any input at all. Pass
/// `strict=true` to have the first error raised instead.
pub fn parse(
src : String,
strict? : Bool = false,
) -> Parsed raise @error.HtmlError {
let scanned = @token.tokenize(src)
let p = {
toks: scanned.tokens,
pos: 0,
stack: [],
roots: [],
diagnostics: [],
strict,
}
for problem in scanned.problems {
let (kind, span) = problem
p.report(kind, span)
}
p.run()
let span = @span.Span::new(0, src.length())
{ document: { children: p.roots, span, }, diagnostics: p.diagnostics, }
}
///|
/// Parse markup text, raising on the first error.
pub fn parse_strict(src : String) -> @ast.Document raise @error.HtmlError {
parse(src, strict=true).document
}
// ---------------------------------------------------------------- reporting
///|
fn Parser::report(
self : Parser,
kind : @kind.ErrorKind,
span : @span.Span,
) -> Unit raise @error.HtmlError {
let d = @error.Diagnostic::of_kind(kind, span)
if self.strict && d.is_error() {
d.raise_()
}
self.diagnostics.push(d)
}
// ------------------------------------------------------------------- stack
///|
/// Where a new child goes: into the innermost open element, or into the
/// document.
fn Parser::sink(self : Parser) -> Array[@ast.Node] {
let n = self.stack.length()
if n == 0 {
self.roots
} else {
self.stack[n - 1].children
}
}
///|
/// The namespace a child of the innermost open element belongs to.
///
/// Foreign content is entered by `