use vize_l0::{Box, Vec, is_html_tag, is_math_ml_tag, is_svg_tag, is_void_tag};
use vize_relief::Namespace;
use super::{Builder, Frame, ImplicitlyClosedTag};
use crate::event::{Event, EventKind};
use crate::surface::{Attribute, CloseTag, Element, ElementClose, OpenTag, SurfaceChild, Token};
impl<'a> Builder<'a, '_> {
pub(super) fn element(&mut self, ev: Event) {
let (name_s, name_e) = (ev.start as usize, ev.end as usize);
self.i += 1;
let lt = name_s.saturating_sub(1);
self.flush_gap(lt);
let lt_name = self.token_at(lt, name_e);
let tag = crate::slice::range(self.src, name_s, name_e);
let mut attrs: Vec<'a, Attribute<'a>> = Vec::new_in(&self.allocator);
let mut slash = None;
let gt;
loop {
match self.events.get(self.i).map(|next| next.kind) {
Some(EventKind::AttrName) => {
let attr = self.attribute();
attrs.push(attr);
}
Some(EventKind::OpenTagEnd) => {
let idx = self
.events
.get(self.i)
.map_or(self.cursor, |ev| ev.start as usize);
self.i += 1;
gt = self.open_gt(idx);
break;
}
Some(EventKind::SelfClosingTag) => {
let idx = self
.events
.get(self.i)
.map_or(self.cursor, |ev| ev.start as usize);
self.i += 1;
let (found_slash, found_gt) = self.self_closing(idx);
slash = found_slash;
gt = found_gt;
break;
}
Some(EventKind::AttrNameEnd | EventKind::AttrData | EventKind::AttrEnd) => {
self.i += 1;
}
_ => {
gt = self.missing_to(self.cursor);
break;
}
}
}
let self_closing = slash.is_some();
let ns = self.enter_ns(tag);
let open = OpenTag {
lt_name,
attrs,
slash,
gt,
};
if !self_closing {
self.handle_nested_interactive_start_tag(tag, ns);
}
if self_closing || is_void_tag(tag) {
let element = Element {
open,
children: Vec::new_in(&self.allocator),
close: ElementClose::NotExpected,
};
self.attach(element);
} else {
self.stack.push(Frame {
open,
children: Vec::new_in(&self.allocator),
ns,
});
}
}
fn open_gt(&mut self, idx: usize) -> Token<'a> {
let bytes = self.src.as_bytes();
if idx >= self.cursor && bytes.get(idx) == Some(&b'>') {
self.token_at(idx, idx + 1)
} else {
self.missing_to(self.src.len())
}
}
fn self_closing(&mut self, idx: usize) -> (Option<Token<'a>>, Token<'a>) {
let slash_pos = self.find_byte(b'/', self.cursor, idx);
let slash = slash_pos.map(|p| self.token_at(p, p + 1));
let gt = self.token_at(idx, idx + 1);
(slash, gt)
}
pub(super) fn close_tag(&mut self, ev: Event) {
let (s, e) = (ev.start as usize, ev.end as usize);
self.i += 1;
let lt_start = self.close_lt_start(s);
let gt_pos = self.find_byte(b'>', e, self.src.len());
let name = crate::slice::range(self.src, s, e);
self.flush_gap(lt_start);
let matched = self
.stack
.iter()
.rposition(|frame| frame.tag().eq_ignore_ascii_case(name));
if self.should_consume_implicitly_closed_tag(name, matched) {
self.implicitly_closed_tags.pop();
self.unexpected_close_tag(lt_start, gt_pos);
return;
}
let Some(depth) = matched else {
self.unexpected_close_tag(lt_start, gt_pos);
return;
};
while self.stack.len() > depth + 1 {
let Some(frame) = self.stack.pop() else {
break;
};
let element = Element {
open: frame.open,
children: frame.children,
close: ElementClose::Missing,
};
self.attach(element);
}
let lt_slash_name = self.token_at(lt_start, e);
let gt = match gt_pos {
Some(g) => self.token_at(g, g + 1),
None => self.missing_to(self.src.len()),
};
let Some(frame) = self.stack.pop() else {
return;
};
let element = Element {
open: frame.open,
children: frame.children,
close: ElementClose::Present(CloseTag { lt_slash_name, gt }),
};
self.attach(element);
}
fn close_lt_start(&self, s: usize) -> usize {
let bytes = self.src.as_bytes();
let before = |p: usize| {
(p > self.cursor)
.then(|| bytes.get(p - 1).copied())
.flatten()
};
let mut p = s;
while before(p).is_some_and(|byte| byte.is_ascii_whitespace()) {
p -= 1;
}
if before(p) == Some(b'/') {
p -= 1;
}
if before(p) == Some(b'<') {
p - 1
} else {
self.cursor
}
}
fn unexpected_close_tag(&mut self, lt_start: usize, gt_pos: Option<usize>) {
let end = gt_pos.map_or(self.src.len(), |g| g + 1);
let token = self.token_at(lt_start, end);
self.push_child(SurfaceChild::Unexpected(token));
}
fn enter_ns(&self, tag: &str) -> Namespace {
if is_svg_tag(tag) {
Namespace::Svg
} else if is_math_ml_tag(tag) {
Namespace::MathMl
} else {
self.stack
.last()
.map_or(Namespace::Html, |frame| children_ns(frame.ns, frame.tag()))
}
}
fn handle_nested_interactive_start_tag(&mut self, tag: &'a str, ns: Namespace) {
if !self.repair_interactive || ns != Namespace::Html {
return;
}
if !is_interactive_html_tree_tag(tag) {
return;
}
let Some(depth) = self.stack.iter().rposition(|frame| {
frame.ns == Namespace::Html
&& is_interactive_html_tree_tag(frame.tag())
&& frame.tag().eq_ignore_ascii_case(tag)
}) else {
return;
};
self.repaired = true;
self.note_implicitly_closed_stack_entries_from(depth);
self.implicitly_close_stack_element_at(depth);
}
fn note_implicitly_closed_stack_entries_from(&mut self, start_depth: usize) {
for (depth, frame) in self.stack.iter().enumerate().skip(start_depth) {
self.implicitly_closed_tags.push(ImplicitlyClosedTag {
tag: frame.tag(),
depth,
});
}
}
fn should_consume_implicitly_closed_tag(&self, tag: &str, live_depth: Option<usize>) -> bool {
let Some(closed) = self.implicitly_closed_tags.last() else {
return false;
};
if !closed.tag.eq_ignore_ascii_case(tag) {
return false;
}
match live_depth {
Some(depth) => depth < closed.depth,
None => true,
}
}
fn implicitly_close_stack_element_at(&mut self, depth: usize) {
let mut child = None;
while self.stack.len() > depth {
let Some(frame) = self.stack.pop() else {
break;
};
let mut children = frame.children;
if let Some(element) = child.take() {
children.push(SurfaceChild::Element(Box::new_in(element, &self.allocator)));
}
child = Some(Element {
open: frame.open,
children,
close: ElementClose::Implicit,
});
}
if let Some(element) = child {
self.attach(element);
}
}
}
fn is_interactive_html_tree_tag(tag: &str) -> bool {
is_html_tag(tag) && (tag.eq_ignore_ascii_case("a") || tag.eq_ignore_ascii_case("button"))
}
fn children_ns(ns: Namespace, tag: &str) -> Namespace {
match ns {
Namespace::Svg if matches!(tag, "foreignObject" | "desc" | "title") => Namespace::Html,
Namespace::MathMl if matches!(tag, "mi" | "mo" | "mn" | "ms" | "mtext") => Namespace::Html,
other => other,
}
}