#![forbid(unsafe_code)]
use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use html5ever::driver::{parse_document, ParseOpts, Parser};
use html5ever::interface::tree_builder::{
ElementFlags, NodeOrText, QuirksMode as H5QuirksMode, TreeSink,
};
use html5ever::tendril::{StrTendril, TendrilSink};
use html5ever::tokenizer::TokenizerOpts;
use html5ever::tree_builder::TreeBuilderOpts;
use html5ever::{Attribute as H5Attribute, QualName};
use markup5ever::interface::tree_builder::ElemName;
use markup5ever::{LocalName, Namespace as H5Namespace};
use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
use super::events::{HtmlAttrs, HtmlEvent, OwnedAttr, OwnedEvent};
use super::options::HtmlParseOptions;
pub(crate) trait EventEmit {
fn emit(&mut self, event: OwnedEvent);
}
pub(crate) struct BufferedEmit {
pub queue: VecDeque<OwnedEvent>,
}
impl BufferedEmit {
fn new() -> Self {
Self {
queue: VecDeque::with_capacity(32),
}
}
}
impl EventEmit for BufferedEmit {
fn emit(&mut self, event: OwnedEvent) {
if let OwnedEvent::Text(ref new_text) = event {
if let Some(OwnedEvent::Text(last)) = self.queue.back_mut() {
last.push_str(new_text);
return;
}
}
self.queue.push_back(event);
}
}
pub(crate) struct CallbackEmit<H: HtmlSaxHandler> {
pub handler: H,
pub pending_text: String,
}
impl<H: HtmlSaxHandler> CallbackEmit<H> {
fn new(handler: H) -> Self {
Self {
handler,
pending_text: String::new(),
}
}
fn flush_text(&mut self) {
if !self.pending_text.is_empty() {
let text = std::mem::take(&mut self.pending_text);
self.handler.text(&text);
}
}
}
impl<H: HtmlSaxHandler> EventEmit for CallbackEmit<H> {
fn emit(&mut self, event: OwnedEvent) {
match event {
OwnedEvent::Text(t) => {
self.pending_text.push_str(&t);
}
OwnedEvent::StartElement { name, attrs } => {
self.flush_text();
self.handler
.start_element(&name, HtmlAttrs { inner: &attrs });
}
OwnedEvent::EndElement { name } => {
self.flush_text();
self.handler.end_element(&name);
}
OwnedEvent::Comment(c) => {
self.flush_text();
self.handler.comment(&c);
}
OwnedEvent::Doctype {
name,
public_id,
system_id,
} => {
self.flush_text();
self.handler.doctype(&name, &public_id, &system_id);
}
OwnedEvent::ParseError(err) => {
self.handler.parse_error(&err);
}
OwnedEvent::Eof => {
self.flush_text();
self.handler.end_document();
}
}
}
}
enum StreamNode {
Document,
Element {
name: QualName,
attrs: Vec<H5Attribute>,
emitted: bool,
},
Comment(StrTendril),
}
fn is_void_element(name: &str) -> bool {
matches!(
name,
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
| "keygen"
| "menuitem"
)
}
pub(crate) struct OwnedElemName {
inner: QualName,
}
impl std::fmt::Debug for OwnedElemName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.inner, f)
}
}
impl ElemName for OwnedElemName {
fn ns(&self) -> &H5Namespace {
&self.inner.ns
}
fn local_name(&self) -> &LocalName {
&self.inner.local
}
}
struct StreamState<E: EventEmit> {
arena: Vec<StreamNode>,
emit: E,
open_stack: Vec<usize>,
text_bytes: u64,
fatal: Option<XmlError>,
}
pub(crate) struct StreamingSink<E: EventEmit> {
state: RefCell<StreamState<E>>,
quirks: Cell<H5QuirksMode>,
opts: HtmlParseOptions,
aborted: Cell<bool>,
}
impl<E: EventEmit> StreamingSink<E> {
fn new(opts: HtmlParseOptions, emit: E) -> Self {
let mut arena = Vec::with_capacity(32);
arena.push(StreamNode::Document);
Self {
state: RefCell::new(StreamState {
arena,
emit,
open_stack: Vec::with_capacity(32),
text_bytes: 0,
fatal: None,
}),
quirks: Cell::new(H5QuirksMode::NoQuirks),
opts,
aborted: Cell::new(false),
}
}
fn close_to_parent(s: &mut StreamState<E>, parent_id: usize) {
if parent_id == 0 {
return;
}
while let Some(&top) = s.open_stack.last() {
if top == parent_id {
return;
}
let popped = s.open_stack.pop().unwrap();
if let StreamNode::Element { name, .. } = &s.arena[popped] {
let local = name.local.to_string();
s.emit.emit(OwnedEvent::EndElement { name: local });
}
}
}
fn alloc(&self, node: StreamNode) -> usize {
let mut s = self.state.borrow_mut();
let id = s.arena.len();
s.arena.push(node);
id
}
fn record_error(&self, msg: impl Into<String>, level: ErrorLevel) {
let err = XmlError::new(ErrorDomain::Html, level, msg);
let mut s = self.state.borrow_mut();
if level == ErrorLevel::Fatal && s.fatal.is_none() {
s.fatal = Some(err.clone());
}
s.emit.emit(OwnedEvent::ParseError(err));
}
fn abort(&self, msg: impl Into<String>) {
if !self.aborted.get() {
self.aborted.set(true);
self.record_error(msg, ErrorLevel::Fatal);
}
}
}
impl<E: EventEmit + 'static> TreeSink for StreamingSink<E> {
type Handle = usize;
type Output = Self;
type ElemName<'a>
= OwnedElemName
where
Self: 'a;
fn finish(self) -> Self {
self
}
fn parse_error(&self, msg: Cow<'static, str>) {
let level = if self.opts.recovery_mode {
ErrorLevel::Error
} else if self.state.borrow().fatal.is_none() {
ErrorLevel::Fatal
} else {
ErrorLevel::Error
};
self.record_error(msg.into_owned(), level);
}
fn get_document(&self) -> usize {
0
}
fn elem_name<'a>(&'a self, target: &'a usize) -> OwnedElemName {
let s = self.state.borrow();
match &s.arena[*target] {
StreamNode::Element { name, .. } => OwnedElemName { inner: name.clone() },
_ => panic!("elem_name called on non-element node {target}"),
}
}
fn create_element(
&self,
name: QualName,
attrs: Vec<H5Attribute>,
_flags: ElementFlags,
) -> usize {
self.alloc(StreamNode::Element {
name,
attrs,
emitted: false,
})
}
fn create_comment(&self, text: StrTendril) -> usize {
self.alloc(StreamNode::Comment(text))
}
fn create_pi(&self, _target: StrTendril, _data: StrTendril) -> usize {
self.alloc(StreamNode::Comment(StrTendril::new()))
}
fn append(&self, parent: &usize, child: NodeOrText<usize>) {
if self.aborted.get() {
return;
}
let parent = *parent;
match child {
NodeOrText::AppendText(text) => {
let mut s = self.state.borrow_mut();
let added = text.len() as u64;
if s.text_bytes.saturating_add(added) > self.opts.max_text_bytes {
drop(s);
self.abort(format!(
"max_text_bytes ({}) exceeded",
self.opts.max_text_bytes
));
return;
}
s.text_bytes += added;
Self::close_to_parent(&mut s, parent);
s.emit.emit(OwnedEvent::Text(text.to_string()));
}
NodeOrText::AppendNode(child_id) => {
let mut s = self.state.borrow_mut();
Self::close_to_parent(&mut s, parent);
match &mut s.arena[child_id] {
StreamNode::Element {
name,
attrs,
emitted,
} => {
if *emitted {
s.open_stack.push(child_id);
return;
}
*emitted = true;
let local_name = name.local.to_string();
let owned_attrs: Vec<OwnedAttr> = attrs
.iter()
.map(|a| OwnedAttr {
name: a.name.local.to_string(),
value: a.value.to_string(),
})
.collect();
if !is_void_element(&local_name) {
s.open_stack.push(child_id);
}
s.emit.emit(OwnedEvent::StartElement {
name: local_name,
attrs: owned_attrs,
});
}
StreamNode::Comment(text) => {
let content = text.to_string();
s.emit.emit(OwnedEvent::Comment(content));
}
StreamNode::Document => {
}
}
}
}
}
fn append_based_on_parent_node(
&self,
element: &usize,
prev_element: &usize,
child: NodeOrText<usize>,
) {
let _ = prev_element;
self.append(element, child);
}
fn append_doctype_to_document(
&self,
name: StrTendril,
public_id: StrTendril,
system_id: StrTendril,
) {
if self.aborted.get() {
return;
}
let mut s = self.state.borrow_mut();
s.emit.emit(OwnedEvent::Doctype {
name: name.to_string(),
public_id: public_id.to_string(),
system_id: system_id.to_string(),
});
}
fn pop(&self, _node: &usize) {
}
fn get_template_contents(&self, target: &usize) -> usize {
*target
}
fn same_node(&self, x: &usize, y: &usize) -> bool {
x == y
}
fn set_quirks_mode(&self, mode: H5QuirksMode) {
self.quirks.set(mode);
}
fn append_before_sibling(&self, sibling: &usize, new_node: NodeOrText<usize>) {
self.append(sibling, new_node);
}
fn add_attrs_if_missing(&self, target: &usize, attrs: Vec<H5Attribute>) {
let mut s = self.state.borrow_mut();
if let StreamNode::Element { attrs: existing, .. } = &mut s.arena[*target] {
for new_attr in attrs {
if !existing.iter().any(|a| a.name == new_attr.name) {
existing.push(new_attr);
}
}
}
}
fn remove_from_parent(&self, _target: &usize) {
}
fn reparent_children(&self, _node: &usize, _new_parent: &usize) {
}
}
pub trait HtmlSaxHandler {
fn start_element(&mut self, _name: &str, _attrs: HtmlAttrs<'_>) {}
fn end_element(&mut self, _name: &str) {}
fn text(&mut self, _content: &str) {}
fn comment(&mut self, _content: &str) {}
fn doctype(&mut self, _name: &str, _public_id: &str, _system_id: &str) {}
fn parse_error(&mut self, _err: &XmlError) {}
fn end_document(&mut self) {}
}
pub struct HtmlSaxParser<H: HtmlSaxHandler + 'static> {
parser: Parser<StreamingSink<CallbackEmit<H>>>,
}
impl<H: HtmlSaxHandler + 'static> HtmlSaxParser<H> {
pub fn new(handler: H) -> Self {
Self::with_opts(handler, HtmlParseOptions::default())
}
pub fn with_opts(handler: H, opts: HtmlParseOptions) -> Self {
let sink = StreamingSink::new(opts.clone(), CallbackEmit::new(handler));
let parser = parse_document(sink, make_h5_opts(&opts));
Self { parser }
}
pub fn feed(&mut self, chunk: &str) -> Result<()> {
let tendril = StrTendril::from_slice(chunk);
self.parser.process(tendril);
let s = self.parser.tokenizer.sink.sink.state.borrow();
if let Some(err) = &s.fatal {
if self.parser.tokenizer.sink.sink.aborted.get() {
return Err(err.clone());
}
}
Ok(())
}
pub fn finish(self) -> Result<H> {
let fatal = self.parser.tokenizer.sink.sink.state.borrow().fatal.clone();
let aborted = self.parser.tokenizer.sink.sink.aborted.get();
let sink = self.parser.finish();
let mut state = sink.state.into_inner();
while let Some(open_id) = state.open_stack.pop() {
if let StreamNode::Element { name, .. } = &state.arena[open_id] {
let local = name.local.to_string();
state.emit.emit(OwnedEvent::EndElement { name: local });
}
}
state.emit.emit(OwnedEvent::Eof);
if aborted {
return Err(fatal.unwrap_or_else(|| {
XmlError::new(ErrorDomain::Html, ErrorLevel::Fatal, "aborted")
}));
}
Ok(state.emit.handler)
}
}
pub struct HtmlReader<'src> {
inner: ReaderInner<'src>,
}
pub struct HtmlBytesReader<'src> {
inner: ReaderInner<'src>,
}
struct ReaderInner<'src> {
parser: Parser<StreamingSink<BufferedEmit>>,
input: Cow<'src, [u8]>,
fed_pos: usize,
chunk_size: usize,
finished: bool,
current: Option<OwnedEvent>,
recovered: Vec<XmlError>,
strict: bool,
}
impl<'src> ReaderInner<'src> {
fn from_utf8_str(input: &'src str, opts: HtmlParseOptions) -> Self {
Self::with_cow(Cow::Borrowed(input.as_bytes()), opts)
}
fn from_bytes(input: &'src [u8], opts: HtmlParseOptions) -> Result<Self> {
let (decoded, _enc) = super::encoding::decode_html_input(input, &opts)?;
let cow: Cow<'src, [u8]> = match decoded {
Cow::Borrowed(b) => {
if std::ptr::eq(b.as_ptr(), input.as_ptr()) || input.starts_with(b) {
let offset = b.as_ptr() as usize - input.as_ptr() as usize;
Cow::Borrowed(&input[offset..offset + b.len()])
} else {
Cow::Owned(b.to_vec())
}
}
Cow::Owned(v) => Cow::Owned(v),
};
Ok(Self::with_cow(cow, opts))
}
fn with_cow(input: Cow<'src, [u8]>, opts: HtmlParseOptions) -> Self {
let strict = !opts.recovery_mode;
let sink = StreamingSink::new(opts.clone(), BufferedEmit::new());
let parser = parse_document(sink, make_h5_opts(&opts));
Self {
parser,
input,
fed_pos: 0,
chunk_size: 8 * 1024,
finished: false,
current: None,
recovered: Vec::new(),
strict,
}
}
fn pop_event(&mut self) -> Option<OwnedEvent> {
let mut s = self.parser.tokenizer.sink.sink.state.borrow_mut();
s.emit.queue.pop_front()
}
fn aborted(&self) -> bool {
self.parser.tokenizer.sink.sink.aborted.get()
}
fn fatal(&self) -> Option<XmlError> {
self.parser.tokenizer.sink.sink.state.borrow().fatal.clone()
}
fn feed_chunk(&mut self) -> bool {
if self.fed_pos >= self.input.len() {
return false;
}
let end = (self.fed_pos + self.chunk_size).min(self.input.len());
let chunk_bytes = trim_to_utf8_boundary(&self.input[self.fed_pos..end]);
let actual_end = self.fed_pos + chunk_bytes.len();
if chunk_bytes.is_empty() {
let lossy = String::from_utf8_lossy(&self.input[self.fed_pos..end]).into_owned();
self.parser.process(StrTendril::from_slice(&lossy));
self.fed_pos = end;
} else {
let lossy = String::from_utf8_lossy(chunk_bytes);
self.parser.process(StrTendril::from_slice(&lossy));
self.fed_pos = actual_end;
}
true
}
fn finish_parser(&mut self) {
if !self.finished {
self.parser.tokenizer.end();
self.finished = true;
let mut s = self.parser.tokenizer.sink.sink.state.borrow_mut();
while let Some(open_id) = s.open_stack.pop() {
if let StreamNode::Element { name, .. } = &s.arena[open_id] {
let local = name.local.to_string();
s.emit.emit(OwnedEvent::EndElement { name: local });
}
}
}
}
fn pull(&mut self) -> Result<OwnedEvent> {
loop {
if let Some(event) = self.pop_event() {
if let OwnedEvent::ParseError(err) = &event {
let err = err.clone();
if self.strict {
return Err(err);
} else {
self.recovered.push(err);
continue;
}
}
if self.aborted() {
if let Some(fatal) = self.fatal() {
return Err(fatal);
}
}
return Ok(event);
}
if !self.finished && self.feed_chunk() {
continue;
}
if !self.finished {
self.finish_parser();
continue;
}
return Ok(OwnedEvent::Eof);
}
}
fn recovered_errors(&self) -> &[XmlError] {
&self.recovered
}
}
fn trim_to_utf8_boundary(bytes: &[u8]) -> &[u8] {
if bytes.is_empty() {
return bytes;
}
let mut end = bytes.len();
while end > 0 {
let b = bytes[end - 1];
if b < 0x80 || b >= 0xC0 {
if b >= 0xC0 {
end -= 1;
}
break;
}
end -= 1;
if bytes.len() - end > 4 {
break;
}
}
&bytes[..end]
}
impl<'src> HtmlReader<'src> {
pub fn new(input: &'src str) -> Self {
Self::with_opts(input, HtmlParseOptions::default())
}
pub fn with_opts(input: &'src str, opts: HtmlParseOptions) -> Self {
Self {
inner: ReaderInner::from_utf8_str(input, opts),
}
}
pub fn next(&mut self) -> Result<HtmlEvent<'_>> {
let event = self.inner.pull()?;
self.inner.current = Some(event);
Ok(event_borrow(self.inner.current.as_ref().unwrap()))
}
pub fn recovered_errors(&self) -> &[XmlError] {
self.inner.recovered_errors()
}
}
impl<'src> HtmlBytesReader<'src> {
pub fn new(input: &'src [u8]) -> Result<Self> {
Self::with_opts(input, HtmlParseOptions::default())
}
pub fn with_opts(input: &'src [u8], opts: HtmlParseOptions) -> Result<Self> {
Ok(Self {
inner: ReaderInner::from_bytes(input, opts)?,
})
}
pub fn next(&mut self) -> Result<HtmlEvent<'_>> {
let event = self.inner.pull()?;
self.inner.current = Some(event);
Ok(event_borrow(self.inner.current.as_ref().unwrap()))
}
pub fn recovered_errors(&self) -> &[XmlError] {
self.inner.recovered_errors()
}
}
fn event_borrow(owned: &OwnedEvent) -> HtmlEvent<'_> {
match owned {
OwnedEvent::StartElement { name, attrs } => HtmlEvent::StartElement {
name: name.as_str(),
attributes: HtmlAttrs { inner: attrs.as_slice() },
},
OwnedEvent::EndElement { name } => HtmlEvent::EndElement {
name: name.as_str(),
},
OwnedEvent::Text(content) => HtmlEvent::Text(content.as_str()),
OwnedEvent::Comment(content) => HtmlEvent::Comment(content.as_str()),
OwnedEvent::Doctype {
name,
public_id,
system_id,
} => HtmlEvent::Doctype {
name: name.as_str(),
public_id: public_id.as_str(),
system_id: system_id.as_str(),
},
OwnedEvent::Eof => HtmlEvent::Eof,
OwnedEvent::ParseError(_) => {
HtmlEvent::Eof
}
}
}
fn make_h5_opts(opts: &HtmlParseOptions) -> ParseOpts {
ParseOpts {
tokenizer: TokenizerOpts {
discard_bom: opts.discard_bom,
..Default::default()
},
tree_builder: TreeBuilderOpts {
scripting_enabled: opts.scripting_enabled,
iframe_srcdoc: opts.iframe_srcdoc,
..Default::default()
},
}
}