vize_l3 0.484.0

L3 - the Davinci reactivity IR (codename Impeto): flat scheduled ops with explicit state and effect edges
Documentation
//! Vapor's bounded eligibility joins the existing shared event stream.

use super::{VaporFacts, VaporPart, VaporRejection, VaporRoot, VaporUnsupported};
use alloc::vec::Vec;
use vize_l0::{Span, id::NodeId};
use vize_l2::{
    lang::js::NativeSelectedSetup,
    op::{BindingOp, ElementOp, Namespace, Op},
    walk::NodeRef,
};

pub(in crate::decision) struct VaporBuilder<'facts, 'owner, 'arena> {
    setup: Option<&'facts NativeSelectedSetup<'owner, 'arena>>,
    facts: VaporFacts<'owner, 'arena>,
    frames: Vec<(NodeId, Option<&'owner ElementOp<'arena>>, bool)>,
    non_comments: usize,
    root_element: Option<NodeId>,
}

impl<'facts, 'owner, 'arena> VaporBuilder<'facts, 'owner, 'arena> {
    pub fn new(setup: Option<&'facts NativeSelectedSetup<'owner, 'arena>>) -> Self {
        Self {
            setup,
            facts: VaporFacts {
                parts: Vec::new(),
                roots: Vec::new(),
                unsupported: Vec::new(),
                inherit_attrs: None,
            },
            frames: Vec::new(),
            non_comments: 0,
            root_element: None,
        }
    }

    pub fn enter(
        &mut self,
        node: NodeId,
        op: &'owner Op<'arena>,
        root: bool,
        original_values: &mut Option<
            crate::decision::attribute_value::OriginalAttributeCursor<'owner, 'arena>,
        >,
    ) {
        let span = NodeRef::Op(op).span();
        if root {
            self.facts.roots.push(VaporRoot {
                ordinal: self.facts.roots.len(),
                node,
                span,
                parts: self.facts.parts.len()..self.facts.parts.len(),
            });
            self.non_comments += usize::from(!matches!(op, Op::Comment(_)));
            if matches!(op, Op::Element(_)) {
                self.root_element = Some(node);
            }
        }
        let (element, void) = match op {
            Op::Element(element) => {
                let void = matches!(element.tag, "br" | "hr" | "img");
                self.element(node, element, void, original_values);
                self.facts.parts.push(VaporPart::Open {
                    node,
                    element,
                    void,
                });
                (Some(element.as_ref()), void)
            }
            Op::Text(text) => {
                if root && text.content.starts_with('<') {
                    self.reject(node, span, VaporUnsupported::RootTextMarkup);
                }
                // These decoded bytes are stable under both Vue's default
                // condense policy and the current native preserve policy.
                if text.content.is_empty()
                    || text.content.starts_with(' ')
                    || text.content.ends_with(' ')
                    || text.content.contains("  ")
                    || text
                        .content
                        .chars()
                        .any(|ch| matches!(ch, '\0' | '\t' | '\n' | '\r' | '\u{c}'))
                {
                    self.reject(node, span, VaporUnsupported::TextNormalization);
                }
                self.facts.parts.push(VaporPart::Text { node, text });
                (None, false)
            }
            Op::Interpolation(interpolation) if self.setup.is_some() => {
                if !root {
                    self.reject(node, span, VaporUnsupported::NestedInterpolation);
                } else if let Some(setup) = self.setup {
                    match super::expression::admit(setup, node, interpolation) {
                        Ok(expression) => self.facts.parts.push(VaporPart::Interpolation {
                            node,
                            interpolation,
                            expression,
                        }),
                        Err(reason) => self.reject(node, span, reason),
                    }
                }
                (None, false)
            }
            Op::Comment(comment) => {
                let content = comment.content;
                if content.contains(['\0', '\r'])
                    || content.starts_with('>')
                    || content.starts_with("->")
                    || content.ends_with("<!-")
                    || ["<!--", "-->", "--!>"]
                        .iter()
                        .any(|token| content.contains(token))
                {
                    self.reject(node, span, VaporUnsupported::UnsafeComment);
                }
                self.facts.parts.push(VaporPart::Comment { node, comment });
                (None, false)
            }
            _ => {
                self.reject(node, span, VaporUnsupported::Operation);
                (None, false)
            }
        };
        self.frames.push((node, element, void));
    }

    fn element(
        &mut self,
        node: NodeId,
        element: &'owner ElementOp<'arena>,
        void: bool,
        original_values: &mut Option<
            crate::decision::attribute_value::OriginalAttributeCursor<'owner, 'arena>,
        >,
    ) {
        if element.namespace != Namespace::Html {
            self.reject(node, element.span, VaporUnsupported::Namespace);
        }
        // No raw-text, RCDATA, table/select/p paragraph
        // recovery, custom-element, component or foreign-tree semantics.
        if !matches!(
            element.tag,
            "div"
                | "span"
                | "section"
                | "article"
                | "main"
                | "aside"
                | "header"
                | "footer"
                | "nav"
                | "small"
                | "br"
                | "hr"
                | "img"
        ) {
            self.reject(node, element.span, VaporUnsupported::ElementSemantics);
        }
        if void && !element.children.ops.is_empty() {
            self.reject(node, element.span, VaporUnsupported::VoidChildren);
        }
        let mut names = Vec::new();
        for (slot, attribute) in element.attributes.iter().enumerate() {
            crate::decision::attribute_value::observe(original_values, node, element, slot);
            let name = attribute.name;
            let generic = matches!(name, "class" | "id" | "title" | "role" | "dir" | "lang")
                || (name.starts_with("data-") || name.starts_with("aria-")) && name.len() > 5;
            if !generic
                || !name
                    .bytes()
                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
                || attribute.value.is_some_and(|value| {
                    if name == "class" {
                        value.contains('\0')
                    } else {
                        value.contains(['\0', '\r'])
                    }
                })
            {
                self.reject(node, attribute.span, VaporUnsupported::AttributeSemantics);
            }
            if names.contains(&name) {
                self.reject(node, attribute.span, VaporUnsupported::DuplicateAttribute);
            }
            names.push(name);
        }
    }

    pub fn binding(&mut self, node: NodeId, binding: &BindingOp<'arena>) {
        self.reject(
            node,
            NodeRef::Binding(binding).span(),
            VaporUnsupported::Binding,
        );
    }

    pub fn leave(&mut self, _node: NodeId) {
        if let Some((node, Some(element), false)) = self.frames.pop() {
            self.facts.parts.push(VaporPart::Close { node, element });
        }
        if self.frames.is_empty()
            && let Some(root) = self.facts.roots.last_mut()
        {
            root.parts.end = self.facts.parts.len();
        }
    }

    fn reject(&mut self, node: NodeId, span: Span, reason: VaporUnsupported) {
        self.facts
            .unsupported
            .push(VaporRejection { node, span, reason });
    }

    pub fn finish(mut self) -> VaporFacts<'owner, 'arena> {
        if self.non_comments == 1 {
            self.facts.inherit_attrs = self.root_element;
        }
        self.facts
    }
}