use memchr::memmem;
use crate::Tag;
use crate::dom::{Data, Span};
use crate::entity::decode_into;
use super::{Builder, Mode, Ns, table_context};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Next {
Data,
RawText,
RcData,
PlainText,
}
pub(super) fn is_ws(c: u8) -> bool {
matches!(c, b'\t' | b'\n' | b'\x0c' | b' ' | b'\r')
}
pub(super) type RawAttr = ((usize, usize), (usize, usize));
struct TagToken {
name: (usize, usize),
self_closing: bool,
end: usize,
}
pub(super) fn tokenize(src: &str, tb: &mut Builder<'_>) {
let b = src.as_bytes();
let mut pos = 0;
let mut text_start = 0;
while let Some(off) = memchr::memchr(b'<', &b[pos..]) {
let lt = pos + off;
match b.get(lt + 1) {
Some(c) if c.is_ascii_alphabetic() => {
tb.text_range(text_start, lt, true);
let Some(t) = scan_tag(b, lt + 1, &mut tb.scratch) else {
return;
};
let next = tb.start_tag(t.name, t.self_closing);
pos = t.end;
if next != Next::Data {
let name = src[t.name.0..t.name.1].to_ascii_lowercase();
pos = raw_text(src, pos, &name, next, tb);
}
text_start = pos;
}
Some(b'/') => match b.get(lt + 2) {
Some(c) if c.is_ascii_alphabetic() => {
tb.text_range(text_start, lt, true);
let Some(t) = scan_tag(b, lt + 2, &mut tb.scratch) else {
return;
};
tb.end_tag(t.name);
pos = t.end;
text_start = pos;
}
Some(b'>') => {
tb.text_range(text_start, lt, true);
pos = lt + 3;
text_start = pos;
}
None => break,
Some(_) => {
tb.text_range(text_start, lt, true);
pos = bogus_comment(b, lt + 2, tb);
text_start = pos;
}
},
Some(b'!') => {
tb.text_range(text_start, lt, true);
pos = markup_declaration(b, lt + 2, tb);
text_start = pos;
}
Some(b'?') => {
tb.text_range(text_start, lt, true);
pos = bogus_comment(b, lt + 1, tb);
text_start = pos;
}
_ => pos = lt + 1,
}
}
tb.text_range(text_start, b.len(), true);
tb.finish();
}
fn scan_tag(b: &[u8], mut i: usize, attrs: &mut Vec<RawAttr>) -> Option<TagToken> {
let name_start = i;
while i < b.len() && !is_ws(b[i]) && b[i] != b'/' && b[i] != b'>' {
i += 1;
}
let name = (name_start, i);
attrs.clear();
loop {
while i < b.len() && is_ws(b[i]) {
i += 1;
}
match b.get(i)? {
b'>' => {
return Some(TagToken {
name,
self_closing: false,
end: i + 1,
});
}
b'/' => {
i += 1;
if b.get(i) == Some(&b'>') {
return Some(TagToken {
name,
self_closing: true,
end: i + 1,
});
}
continue;
}
_ => {}
}
let an_start = i;
i += 1;
while i < b.len() && !is_ws(b[i]) && !matches!(b[i], b'/' | b'>' | b'=') {
i += 1;
}
let an = (an_start, i);
while i < b.len() && is_ws(b[i]) {
i += 1;
}
let mut value = (i, i);
if b.get(i) == Some(&b'=') {
i += 1;
while i < b.len() && is_ws(b[i]) {
i += 1;
}
match *b.get(i)? {
q @ (b'"' | b'\'') => {
let vs = i + 1;
let ve = vs + memchr::memchr(q, &b[vs..])?;
value = (vs, ve);
i = ve + 1;
}
b'>' => value = (i, i),
_ => {
let vs = i;
while i < b.len() && !is_ws(b[i]) && b[i] != b'>' {
i += 1;
}
value = (vs, i);
}
}
}
attrs.push((an, value));
}
}
fn raw_text(src: &str, start: usize, name: &str, next: Next, tb: &mut Builder<'_>) -> usize {
let b = src.as_bytes();
if next == Next::PlainText {
tb.raw_text(start, b.len(), false);
return b.len();
}
let finder = memmem::Finder::new(b"</");
let mut i = start;
while let Some(off) = finder.find(&b[i..]) {
let lt = i + off;
let ns = lt + 2;
let ne = ns + name.len();
if ne < b.len()
&& b[ns..ne].eq_ignore_ascii_case(name.as_bytes())
&& (is_ws(b[ne]) || b[ne] == b'/' || b[ne] == b'>')
{
tb.raw_text(start, lt, next == Next::RcData);
let Some(t) = scan_tag(b, ns, &mut tb.scratch) else {
return b.len();
};
tb.end_tag(t.name);
return t.end;
}
i = lt + 2;
}
tb.raw_text(start, b.len(), next == Next::RcData);
b.len()
}
fn markup_declaration(b: &[u8], i: usize, tb: &mut Builder<'_>) -> usize {
let rest = &b[i..];
if rest.starts_with(b"--") {
let start = i + 2;
if b.get(start) == Some(&b'>') {
tb.comment(start, start);
return start + 1;
}
if b[start..].starts_with(b"->") {
tb.comment(start, start);
return start + 2;
}
return match memmem::find(&b[start..], b"-->") {
Some(off) => {
tb.comment(start, start + off);
start + off + 3
}
None => {
tb.comment(start, b.len());
b.len()
}
};
}
if rest.len() >= 7 && rest[..7].eq_ignore_ascii_case(b"doctype") {
let end = memchr::memchr(b'>', rest).map_or(b.len(), |o| i + o + 1);
tb.doctype();
return end;
}
if rest.starts_with(b"[CDATA[") && tb.in_foreign() {
let start = i + 7;
return match memmem::find(&b[start..], b"]]>") {
Some(off) => {
tb.raw_text(start, start + off, false);
start + off + 3
}
None => {
tb.raw_text(start, b.len(), false);
b.len()
}
};
}
bogus_comment(b, i, tb)
}
fn bogus_comment(b: &[u8], start: usize, tb: &mut Builder<'_>) -> usize {
match memchr::memchr(b'>', &b[start..]) {
Some(off) => {
tb.comment(start, start + off);
start + off + 1
}
None => {
tb.comment(start, b.len());
b.len()
}
}
}
impl<'a> Builder<'a> {
pub(super) fn doctype(&mut self) {
if self.mode == Mode::Initial {
let id = self.new_node(Data::Doctype);
self.append(0, id);
}
}
pub(super) fn comment(&mut self, s: usize, e: usize) {
self.skip_lf = false;
let id = self.new_node(Data::Comment(Span::source(s, e)));
if self.mode == Mode::Initial || self.stack.is_empty() {
self.append(0, id);
} else {
self.place(id, false);
}
}
pub(super) fn text_range(&mut self, s: usize, e: usize, decode: bool) {
if s >= e {
return;
}
let span = self.text_span(s, e, decode);
let Some(span) = self.drop_leading_lf(span) else {
return;
};
let mut span = span;
if self.mode < Mode::InBody && !self.in_template() {
let lead = self.str(span).bytes().take_while(|&c| is_ws(c)).count();
if lead == span.end() - span.offset() {
return;
}
span = span.skip(lead);
self.ensure_body();
}
let table_ws = self
.current()
.is_some_and(|o| o.ns == Ns::Html && table_context(o.tag))
&& self.str(span).bytes().all(is_ws);
if !self.in_foreign() && !table_ws {
self.reconstruct();
}
self.add_text(span, true);
}
pub(super) fn raw_text(&mut self, s: usize, e: usize, decode: bool) {
if s >= e {
self.skip_lf = false;
return;
}
let span = self.text_span(s, e, decode);
let Some(span) = self.drop_leading_lf(span) else {
return;
};
self.add_text(span, false);
}
pub(super) fn text_span(&mut self, s: usize, e: usize, decode: bool) -> Span {
let raw = &self.src[s..e];
if decode && memchr::memchr(b'&', raw.as_bytes()).is_some() {
let start = self.buf.len();
decode_into(&mut self.buf, raw, false);
Span::owned(start, self.buf.len())
} else {
Span::source(s, e)
}
}
pub(super) fn drop_leading_lf(&mut self, span: Span) -> Option<Span> {
let span = if std::mem::take(&mut self.skip_lf) && self.str(span).starts_with('\n') {
span.skip(1)
} else {
span
};
(!span.is_empty()).then_some(span)
}
pub(super) fn tag_of(&mut self, (s, e): (usize, usize)) -> Tag {
let raw = &self.src.as_bytes()[s..e];
if raw.len() > 16 {
return Tag::Other;
}
if raw.iter().any(u8::is_ascii_uppercase) {
self.lower.clear();
self.lower
.extend(raw.iter().map(|c| c.to_ascii_lowercase() as char));
Tag::from_name(&self.lower)
} else {
Tag::from_name(&self.src[s..e])
}
}
}