vize_l1 0.429.0

L1 - the lossless Vue-template surface tree (codename Sinopia, Davinci P2-7)
Documentation
//! Open and close tags: the element half of the builder.

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;
                }
                // Stray value plumbing without a name: covered by leading.
                Some(EventKind::AttrNameEnd | EventKind::AttrData | EventKind::AttrEnd) => {
                    self.i += 1;
                }
                // The tokenizer always terminates a tag (EOF recovery
                // included); reaching here means it did not — close with
                // a zero-width hole and reprocess the event outside.
                _ => {
                    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,
            });
        }
    }

    /// The open tag's `>`. A non-`>` index only ever comes from the
    /// tokenizer's EOF-in-tag recovery (the inferred index), so the hole
    /// absorbs the remaining tag bytes as leading.
    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())
        }
    }

    /// `/` then `>`, whitespace between them kept as the `>`'s leading.
    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 {
            // Stray end tag: a typed `Unexpected` hole, whole extent.
            self.unexpected_close_tag(lt_start, gt_pos);
            return;
        };
        // Elements left open above the match get node-level holes.
        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 {
            // `AfterClosingTagName` only exits at `>`, so junk between
            // the name and the `>` rides in the `>`'s leading.
            Some(g) => self.token_at(g, g + 1),
            None => self.missing_to(self.src.len()),
        };
        // `matched` found the frame at `depth`, the top after the pops above.
        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);
    }

    /// Back-scan from an end-tag name to its `</`, tolerating the
    /// whitespace forms the tokenizer accepts (`</ div`, `</div `).
    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,
    }
}