use alloc::borrow::Cow;
use alloc::vec::Vec;
use crate::entities::Problem;
use crate::error::{ErrorKind, ParseError};
use crate::node::{AttrRange, NodeId, RawKind, Span, SrcRange};
use crate::tags;
use crate::token::{AttrValue, Token};
use crate::tokenizer::Tokenizer;
use crate::tree::Dom;
pub(crate) fn parse_document(src: &str) -> Dom {
let mut tk = Tokenizer::new(src);
let mut builder = TreeBuilder::new(DomSink::new(src.len()), src);
while let Some((tok, pos)) = tk.next_token() {
let end = tk.offset();
builder.token(tok, tk.attrs(), pos, end);
}
builder.finish(src.len(), &mut tk.errors);
let mut dom = builder.sink.dom;
dom.errors = builder.errors;
dom.release_spare_capacity();
dom
}
pub(crate) trait Sink<'a> {
type Open;
fn name<'s>(&'s self, open: &'s Self::Open) -> &'s str;
fn attr(&mut self, name: &'a str, value: &'a str) -> Option<Problem>;
fn start(
&mut self,
parent: Option<&Self::Open>,
name: Cow<'a, str>,
attrs: usize,
start: usize,
end: usize,
) -> Self::Open;
fn end(&mut self, open: Self::Open, start: usize, end: usize, implied: bool);
fn text(
&mut self,
parent: Option<&Self::Open>,
text: &'a str,
decode: bool,
start: usize,
end: usize,
) -> Option<Problem>;
fn comment(&mut self, parent: Option<&Self::Open>, text: &'a str, start: usize, end: usize);
fn doctype(&mut self, parent: Option<&Self::Open>, text: &'a str, start: usize, end: usize);
}
pub(crate) struct TreeBuilder<'a, S: Sink<'a>> {
pub(crate) sink: S,
open: OpenElements<S::Open>,
pub(crate) errors: Vec<ParseError>,
src: &'a str,
}
impl<'a, S: Sink<'a>> TreeBuilder<'a, S> {
pub(crate) fn new(sink: S, src: &'a str) -> Self {
TreeBuilder {
sink,
open: OpenElements::default(),
errors: Vec::new(),
src,
}
}
fn source_offset(&self, s: &str) -> Option<usize> {
let offset = s.as_ptr().addr().checked_sub(self.src.as_ptr().addr())?;
(offset.checked_add(s.len())? <= self.src.len()).then_some(offset)
}
#[inline]
pub(crate) fn token(
&mut self,
tok: Token<'a>,
attrs: &[(&'a str, AttrValue<'a>)],
pos: usize,
end: usize,
) {
match tok {
Token::StartTag {
name, self_closing, ..
} => self.start_tag(name, attrs, self_closing, pos, end),
Token::EndTag { name } => self.end_tag(name, pos, end),
Token::Text(t) => self.text(t, pos, end),
Token::Cdata(c) => {
if !c.is_empty() {
self.sink.text(self.open.top(), c, false, pos, end);
}
}
Token::Comment(c) => self.sink.comment(self.open.top(), c, pos, end),
Token::Doctype(d) => self.sink.doctype(self.open.top(), d.trim(), pos, end),
}
}
pub(crate) fn finish(&mut self, len: usize, lexical: &mut Vec<ParseError>) {
if self.open.len() > 0 {
self.error(len, ErrorKind::ImplicitlyClosed);
}
while self.open.len() > 0 {
self.pop(len, len, true);
}
self.errors.append(lexical);
self.errors.sort_by_key(|e| e.position);
}
#[inline]
fn error(&mut self, pos: usize, kind: ErrorKind) {
self.errors.push(ParseError::new(pos, kind));
}
#[inline]
fn pop(&mut self, start: usize, end: usize, implied: bool) {
if let Some(open) = self.open.pop(&self.sink) {
self.sink.end(open, start, end, implied);
}
}
fn start_tag(
&mut self,
name: &'a str,
attrs: &[(&'a str, AttrValue<'a>)],
self_closing: bool,
pos: usize,
end: usize,
) {
let mut lname = lower(name);
if lname == "image"
&& self.open.find(&self.sink, "svg").is_none()
&& self.open.find(&self.sink, "math").is_none()
{
lname = Cow::Borrowed("img");
}
for p in [false, true] {
if let Some(idx) = self.open.closed_by(&self.sink, &lname, p) {
while self.open.len() > idx + 1 {
self.pop(pos, pos, true);
self.error(pos, ErrorKind::ImplicitlyClosed);
}
self.pop(pos, pos, true);
self.error(pos, ErrorKind::ImplicitlyClosed);
}
}
while self
.open
.top()
.is_some_and(|top| tags::implicitly_closes(&lname, self.sink.name(top)))
{
self.pop(pos, pos, true);
self.error(pos, ErrorKind::ImplicitlyClosed);
}
for &(k, v) in attrs {
let value = v.as_str();
if let Some((at, kind)) = self.sink.attr(k, value) {
let at = self.source_offset(value).map_or(pos, |offset| offset + at);
self.error(at, kind);
}
}
let void = self_closing || tags::is_void(&lname);
let open = self
.sink
.start(self.open.top(), lname, attrs.len(), pos, end);
if void {
self.sink.end(open, end, end, true);
} else {
self.open.push(&self.sink, open);
}
}
fn end_tag(&mut self, name: &'a str, pos: usize, end: usize) {
let lname = lower(name);
if lname == "br" {
self.start_tag("br", &[], false, pos, end);
return;
}
if tags::is_void(&lname) {
return; }
match self.open.find(&self.sink, &lname) {
Some(idx) => {
if idx + 1 != self.open.len() {
let kind = if self.open.only_optional_above(&self.sink, idx) {
ErrorKind::ImplicitlyClosed
} else {
ErrorKind::MismatchedEndTag
};
self.error(pos, kind);
}
while self.open.len() > idx + 1 {
self.pop(pos, pos, true);
}
self.pop(pos, end, false);
}
None => {
self.error(pos, ErrorKind::StrayEndTag);
if lname == "p" {
self.start_tag("p", &[], false, pos, pos);
self.pop(pos, end, false);
}
}
}
}
fn text(&mut self, t: &'a str, pos: usize, end: usize) {
if t.is_empty() {
return;
}
let (raw, closes) = {
let top = self.open.top().map_or("", |o| self.sink.name(o));
(tags::is_raw_text(top), tags::text_closes(top))
};
if closes && t.bytes().any(|b| !b.is_ascii_whitespace()) {
self.pop(pos, pos, true);
self.error(pos, ErrorKind::ImplicitlyClosed);
}
if let Some((at, kind)) = self.sink.text(self.open.top(), t, !raw, pos, end) {
self.error(pos + at, kind);
}
}
}
pub(crate) struct DomSink {
pub(crate) dom: Dom,
fits: bool,
}
impl DomSink {
pub(crate) fn new(src_len: usize) -> Self {
DomSink {
dom: Dom::with_capacity(src_len),
fits: src_len < u32::MAX as usize,
}
}
#[inline]
fn range(&self, start: usize, end: usize) -> SrcRange {
if self.fits {
SrcRange::new(start as u32, end as u32)
} else {
SrcRange::NONE
}
}
#[inline]
fn leaf(&mut self, parent: Option<&(NodeId, Span)>, raw: RawKind, start: usize, end: usize) {
let id = self.dom.alloc(raw, self.range(start, end));
self.dom.append(parent.map(|&(p, _)| p), id);
}
}
impl<'a> Sink<'a> for DomSink {
type Open = (NodeId, Span);
#[inline]
fn name<'s>(&'s self, open: &'s (NodeId, Span)) -> &'s str {
self.dom.span_str(open.1)
}
#[inline]
fn attr(&mut self, name: &'a str, value: &'a str) -> Option<Problem> {
let key = self.dom.intern_lower(name);
let (value, unknown) = self.dom.intern_decoded(value, true);
self.dom.attrs.push((key, value));
unknown
}
#[inline]
fn start(
&mut self,
parent: Option<&(NodeId, Span)>,
name: Cow<'a, str>,
attrs: usize,
start: usize,
end: usize,
) -> (NodeId, Span) {
let name = self.dom.intern(&name);
let first = self.dom.attrs.len().saturating_sub(attrs);
let attrs = AttrRange {
start: first as u32,
len: (self.dom.attrs.len() - first) as u32,
};
let id = self
.dom
.alloc(RawKind::Element { name, attrs }, self.range(start, end));
self.dom.append(parent.map(|&(p, _)| p), id);
(id, name)
}
#[inline]
fn end(&mut self, (id, _): (NodeId, Span), _start: usize, end: usize, _implied: bool) {
if let Some(n) = self.dom.node_mut(id) {
n.src.set_end(end as u32);
}
}
#[inline]
fn text(
&mut self,
parent: Option<&(NodeId, Span)>,
text: &'a str,
decode: bool,
start: usize,
end: usize,
) -> Option<Problem> {
let (span, unknown) = if decode {
self.dom.intern_decoded(text, false)
} else {
(self.dom.intern(text), None)
};
self.leaf(parent, RawKind::Text(span), start, end);
unknown
}
fn comment(
&mut self,
parent: Option<&(NodeId, Span)>,
text: &'a str,
start: usize,
end: usize,
) {
let span = self.dom.intern(text);
self.leaf(parent, RawKind::Comment(span), start, end);
}
fn doctype(
&mut self,
parent: Option<&(NodeId, Span)>,
text: &'a str,
start: usize,
end: usize,
) {
let span = self.dom.intern(text);
self.leaf(parent, RawKind::Doctype(span), start, end);
}
}
const MAX_END_TAG_SEARCH: usize = 256;
const OUTER_END_TAG_SEARCH: usize = 8;
struct OpenElements<O> {
stack: Vec<O>,
open_p: usize,
open_items: usize,
}
impl<O> Default for OpenElements<O> {
fn default() -> Self {
OpenElements {
stack: Vec::new(),
open_p: 0,
open_items: 0,
}
}
}
impl<O> OpenElements<O> {
#[inline]
fn top(&self) -> Option<&O> {
self.stack.last()
}
#[inline]
fn len(&self) -> usize {
self.stack.len()
}
#[inline]
fn push<'a, S: Sink<'a, Open = O>>(&mut self, sink: &S, open: O) {
self.count(sink.name(&open), true);
self.stack.push(open);
}
#[inline]
fn pop<'a, S: Sink<'a, Open = O>>(&mut self, sink: &S) -> Option<O> {
let open = self.stack.pop()?;
self.count(sink.name(&open), false);
Some(open)
}
#[inline]
fn count(&mut self, name: &str, pushed: bool) {
let counter = match name {
"p" => &mut self.open_p,
"li" | "dd" | "dt" | "td" | "th" | "tr" | "thead" | "tbody" | "tfoot" | "caption" => {
&mut self.open_items
}
_ => return,
};
*counter = if pushed {
counter.saturating_add(1)
} else {
counter.saturating_sub(1)
};
}
fn only_optional_above<'a, S: Sink<'a, Open = O>>(&self, sink: &S, idx: usize) -> bool {
self.stack
.get(idx + 1..)
.unwrap_or(&[])
.iter()
.all(|open| tags::end_tag_optional(sink.name(open)))
}
fn closed_by<'a, S: Sink<'a, Open = O>>(
&self,
sink: &S,
opening: &str,
p: bool,
) -> Option<usize> {
let (search, open): (tags::SearchFn, usize) = if p {
(tags::p_search, self.open_p)
} else {
(tags::search, self.open_items)
};
if open == 0 {
return None;
}
let inner = self.stack.len().saturating_sub(MAX_END_TAG_SEARCH);
for i in (inner..self.stack.len()).rev() {
match search(opening, sink.name(self.stack.get(i)?))? {
tags::Search::Closes => return Some(i),
tags::Search::Stops => return None,
tags::Search::Continue => {}
}
}
None
}
fn find<'a, S: Sink<'a, Open = O>>(&self, sink: &S, name: &str) -> Option<usize> {
let named = |open: &O| sink.name(open) == name;
let inner = self.stack.len().saturating_sub(MAX_END_TAG_SEARCH);
if let Some(i) = self
.stack
.get(inner..)
.and_then(|s| s.iter().rposition(named))
{
return Some(inner + i);
}
let outer = inner.min(OUTER_END_TAG_SEARCH);
self.stack.get(..outer)?.iter().rposition(named)
}
}
pub(crate) fn lower(name: &str) -> Cow<'_, str> {
if name.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(name.to_ascii_lowercase())
} else {
Cow::Borrowed(name)
}
}