vize_l3 0.594.0

L3 - the Davinci reactivity IR (codename Impeto): flat scheduled ops with explicit state and effect edges
Documentation
//! DOM scratch attached to the existing native enter/leave frames.

use super::control::build::ConditionalFrame;
use super::{
    DomBinding, DomChanges, DomChild, DomChildren, DomDependency, DomExpressionFacts, DomFacts,
    DomNode, DomRejection, DomRoot, DomRootKind, DomText, DomUnsupported, PropertyRole, ValueKind,
};
use alloc::vec::Vec;
use vize_l0::{Span, id::NodeId, side_table::SideTable};
use vize_l2::{
    expr::ExprRef,
    file::FileArtifact,
    op::{Namespace, Op},
    walk::NodeRef,
};

mod binding;
mod control;
mod dependencies;
mod file;
mod for_head;
mod handler;
use super::vue::policy::FileReads;

pub(in crate::decision) struct DomBuilder<'facts, 'owner, 'arena, F, R> {
    expressions: &'facts F,
    reads: &'facts R,
    file: Option<&'owner FileArtifact<'arena>>,
    frames: Vec<(NodeId, DomFrame<'owner, 'arena>)>,
    facts: DomFacts<'owner, 'arena>,
    root_count: usize,
}

struct DomFrame<'owner, 'arena> {
    node: DomNode<'owner, 'arena>,
    children: Vec<DomChild>,
    binding_names: Vec<&'arena str>,
    conditional: Option<ConditionalFrame<'owner, 'arena>>,
    normalize_class: bool,
    normalize_style: bool,
    has_text: bool,
}

impl<'facts, 'owner, 'arena, F: DomExpressionFacts, R: FileReads<'owner, 'arena>>
    DomBuilder<'facts, 'owner, 'arena, F, R>
{
    pub(in crate::decision) fn new(
        expressions: &'facts F,
        root_count: usize,
        file: Option<&'owner FileArtifact<'arena>>,
        reads: &'facts R,
    ) -> Self {
        Self {
            expressions,
            reads,
            file,
            frames: Vec::new(),
            root_count,
            facts: DomFacts {
                root: DomRoot {
                    kind: DomRootKind::Empty,
                    children: Vec::new(),
                },
                nodes: SideTable::new(),
                bindings: SideTable::new(),
                controls: SideTable::new(),
                dependencies: Vec::new(),
                file_expressions: SideTable::new(),
                file_handlers: SideTable::new(),
                file_for_heads: SideTable::new(),
                vue_expressions: SideTable::new(),
                unsupported: Vec::new(),
            },
        }
    }

    pub(in crate::decision) fn enter(
        &mut self,
        id: NodeId,
        op: &'owner Op<'arena>,
        owner_span: Option<Span>,
        original_values: &mut Option<
            crate::decision::attribute_value::OriginalAttributeCursor<'owner, 'arena>,
        >,
    ) -> Result<(), super::super::DecisionBuildError> {
        let span = NodeRef::Op(op).span();
        let branch_root = if let Some((_, frame)) = self.frames.last_mut()
            && let Some(conditional) = &mut frame.conditional
        {
            conditional.child(id, op, owner_span)?
        } else {
            false
        };
        let mut block_eligible = branch_root
            || self.prepare_for_body(id, op)
            || (self.frames.is_empty() && self.root_count == 1 && matches!(op, Op::Element(_)));
        let conditional = self.prepare_conditional(id, op);
        let text = match op {
            Op::Text(_) => Some(false),
            Op::Interpolation(interpolation) => Some(
                self.value(id, interpolation.expression)
                    .is_some_and(ValueKind::is_dynamic),
            ),
            Op::Element(element) => {
                if element.namespace != Namespace::Html {
                    self.reject(id, span, DomUnsupported::Namespace);
                }
                // Neutral HTML custody does not grant a Vue component
                // role. The pinned Vue 3 DOM family resolves `search` as
                // a component; retain this actual node as a target hole.
                if element.tag == "search" {
                    self.reject(id, span, DomUnsupported::ElementRole);
                }
                // Observe every original slot during this same canonical
                // visit, including suffixes after a target-only refusal.
                if element.attributes.iter().enumerate().fold(
                    false,
                    |special, (slot, attribute)| {
                        crate::decision::attribute_value::observe(
                            original_values,
                            id,
                            element,
                            slot,
                        );
                        special || matches!(attribute.name, "style" | "key" | "ref")
                    },
                ) {
                    self.reject(id, span, DomUnsupported::SpecialAttribute);
                }
                None
            }
            Op::Comment(_) => None,
            Op::If(_) => None,
            Op::OriginalFor(original) => {
                block_eligible = self.record_for_head(id, original);
                if !block_eligible {
                    self.reject(id, span, DomUnsupported::Operation);
                }
                None
            }
            Op::Component(_) | Op::For(_) | Op::Slot(_) => {
                self.reject(id, span, DomUnsupported::Operation);
                None
            }
        };
        if !self
            .frames
            .last()
            .is_some_and(|(_, frame)| frame.conditional.is_some())
        {
            let groups =
                self.frames
                    .last_mut()
                    .map_or(&mut self.facts.root.children, |(_, frame)| {
                        frame.has_text |= text.is_some();
                        &mut frame.children
                    });
            if let Some(dynamic) = text {
                if let Some(DomChild::Text(group)) = groups.last_mut() {
                    group.nodes.push(id);
                    group.dynamic |= dynamic;
                } else {
                    let nodes = alloc::vec![id];
                    groups.push(DomChild::Text(DomText { nodes, dynamic }));
                }
            } else {
                groups.push(DomChild::Node(id));
            }
        }
        if matches!(op, Op::Interpolation(_)) {
            self.demand(DomDependency::DisplayValue);
        } else if matches!(op, Op::Comment(_)) {
            self.demand(DomDependency::CommentValue);
        }
        self.frames.push((
            id,
            DomFrame {
                node: DomNode {
                    op,
                    block_eligible,
                    children: DomChildren::Empty,
                    changes: DomChanges::default(),
                    dynamic_property_bindings: Vec::new(),
                },
                children: Vec::new(),
                binding_names: Vec::new(),
                conditional,
                normalize_class: false,
                normalize_style: false,
                has_text: false,
            },
        ));
        Ok(())
    }

    pub(in crate::decision) fn leave(
        &mut self,
        id: NodeId,
    ) -> Result<(), super::super::DecisionBuildError> {
        let Some((frame_id, mut frame)) = self.frames.pop() else {
            return Err(super::super::DecisionBuildError::InvalidTraversal { node: id });
        };
        if frame_id != id {
            return Err(super::super::DecisionBuildError::InvalidTraversal { node: id });
        }
        if let Some(conditional) = frame.conditional.take() {
            self.complete_conditional(id, NodeRef::Op(frame.node.op).span(), conditional)?;
        }
        self.complete_for_body(id, &mut frame);
        frame.node.children = if frame.children.is_empty() {
            DomChildren::Empty
        } else if frame.children.len() == 1
            && matches!(frame.children.first(), Some(DomChild::Text(_)))
        {
            match frame.children.pop() {
                Some(DomChild::Text(text)) => {
                    frame.node.changes.text = text.dynamic;
                    DomChildren::Text(text)
                }
                _ => DomChildren::Empty,
            }
        } else {
            DomChildren::Array(frame.children)
        };
        self.complete_dependencies(
            &frame.node,
            frame.has_text,
            frame.normalize_class,
            frame.normalize_style,
        );
        if self.frames.last().is_some_and(|(_, frame)| {
            frame
                .conditional
                .as_ref()
                .is_some_and(|conditional| conditional.closes_first_branch(id))
        }) {
            self.demand(DomDependency::ConditionalPlaceholder);
        }
        self.facts.nodes.insert(id, frame.node);
        Ok(())
    }

    pub(in crate::decision) fn finish(mut self) -> DomFacts<'owner, 'arena> {
        self.facts.root.kind = match self.facts.root.children.as_slice() {
            [] => DomRootKind::Empty,
            [DomChild::Node(id)] => {
                if let Some(node) = self.facts.nodes.get_mut(*id) {
                    node.block_eligible = matches!(node.op, Op::Element(_))
                        || (matches!(node.op, Op::OriginalFor(_)) && node.block_eligible);
                }
                DomRootKind::Direct
            }
            [_] => DomRootKind::Direct,
            children => {
                let mut non_comments = 0;
                let mut text_values = false;
                for child in children {
                    text_values |= matches!(child, DomChild::Text(_));
                    if !matches!(child, DomChild::Node(id) if self.facts.nodes.get(*id)
                        .is_some_and(|node| matches!(node.op, Op::Comment(_))))
                    {
                        non_comments += 1;
                    }
                }
                if text_values {
                    self.demand(DomDependency::TextValue);
                }
                self.demand(DomDependency::FragmentValue);
                self.demand(DomDependency::BlockBoundary);
                self.demand(DomDependency::NativeElementBlock);
                DomRootKind::Fragment {
                    single_non_comment: non_comments == 1,
                }
            }
        };
        self.facts
    }

    fn value(&mut self, id: NodeId, expression: ExprRef<'arena>) -> Option<ValueKind> {
        let span = expression.span();
        if let ExprRef::Js(js) = expression {
            if self.file.is_some() {
                return self.file_value(id, js);
            }
            if js.ast.is_literal() {
                return Some(ValueKind::LiteralConstant);
            }
            if self.expressions.is_context_only(js) {
                return Some(ValueKind::ContextDependent);
            }
        }
        self.reject(id, span, DomUnsupported::Expression);
        None
    }

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

    fn demand(&mut self, role: DomDependency) {
        if !self.facts.dependencies.contains(&role) {
            self.facts.dependencies.push(role);
        }
    }
}