Skip to main content

dioxus_core/diff/
template.rs

1use crate::{Attribute, DynamicNode, VNode};
2use dioxus_core_template::{
3    StaticTemplateElement, StaticTemplateNode, StaticTemplateNodeIter, StaticTemplateText,
4    TemplateAnchor, TemplatePath, TemplateSlotTarget,
5};
6
7/// A rendered child of a [`VNode`] or a static template element.
8#[derive(Clone, Copy)]
9pub enum VNodeChild<'a> {
10    /// A static template element.
11    Element(StaticElement<'a>),
12    /// A static template text node.
13    Text(StaticText<'a>),
14    /// One or more dynamic node values at the same insertion position.
15    Dynamic(DynamicAnchor<'a>),
16}
17
18/// A static template element viewed through a specific rendered [`VNode`].
19#[derive(Clone, Copy)]
20pub struct StaticElement<'a> {
21    vnode: &'a VNode,
22    op: usize,
23    path: TemplatePath,
24    anchor_idx: Option<usize>,
25}
26
27impl<'a> StaticElement<'a> {
28    fn new(vnode: &'a VNode, op: usize, path: TemplatePath, anchor_idx: Option<usize>) -> Self {
29        Self {
30            vnode,
31            op,
32            path,
33            anchor_idx,
34        }
35    }
36
37    /// The flat template op for this element.
38    pub fn op(self) -> usize {
39        self.op
40    }
41
42    fn template_element(self) -> StaticTemplateElement<'a> {
43        self.vnode
44            .template()
45            .static_element(self.op)
46            .expect("static element")
47    }
48
49    /// The element tag.
50    pub fn tag(self) -> &'static str {
51        self.template_element().tag()
52    }
53
54    /// The element namespace.
55    pub fn namespace(self) -> Option<&'static str> {
56        self.template_element().namespace()
57    }
58
59    /// The structural anchor for this static node when it has one.
60    pub fn anchor_index(self) -> Option<usize> {
61        self.anchor_idx
62    }
63
64    /// Iterate static template attributes for this element.
65    pub fn static_attributes(
66        self,
67    ) -> impl Iterator<Item = (&'static str, &'static str, Option<&'static str>)> + 'a {
68        self.template_element()
69            .attributes()
70            .map(|attr| (attr.name, attr.value, attr.namespace))
71    }
72
73    /// Iterate rendered children for this element.
74    pub fn children(self) -> impl ExactSizeIterator<Item = VNodeChild<'a>> + 'a {
75        VNodeChildren::element(self)
76    }
77
78    /// Return true if this element has any rendered child.
79    pub fn has_children(self) -> bool {
80        self.children().next().is_some()
81    }
82
83    /// Iterate dynamic anchors with attributes that target this element.
84    pub fn dynamic_anchors(self) -> impl Iterator<Item = DynamicAnchor<'a>> + 'a {
85        self.vnode
86            .dynamic_anchors()
87            .filter(move |anchor| anchor.parent_element_op_index() == Some(self.op))
88    }
89}
90
91/// A static template text node viewed through a specific rendered [`VNode`].
92#[derive(Clone, Copy)]
93pub struct StaticText<'a> {
94    vnode: &'a VNode,
95    op: usize,
96    anchor_idx: Option<usize>,
97}
98
99impl<'a> StaticText<'a> {
100    fn new(vnode: &'a VNode, op: usize, anchor_idx: Option<usize>) -> Self {
101        Self {
102            vnode,
103            op,
104            anchor_idx,
105        }
106    }
107
108    /// The flat template op for this text node.
109    pub fn op(self) -> usize {
110        self.op
111    }
112
113    fn template_text(self) -> StaticTemplateText<'a> {
114        self.vnode
115            .template()
116            .static_text(self.op)
117            .expect("static text")
118    }
119
120    /// The static text value.
121    pub fn text(self) -> &'static str {
122        self.template_text().text()
123    }
124
125    /// The structural anchor for this static node when it has one.
126    pub fn anchor_index(self) -> Option<usize> {
127        self.anchor_idx
128    }
129}
130
131/// Iterator over rendered children.
132pub(crate) struct VNodeChildren<'a> {
133    inner: VNodeChildrenInner<'a>,
134}
135
136#[derive(Clone, Copy)]
137pub(super) struct StaticAnchorTarget {
138    pub(super) anchor_index: usize,
139    pub(super) path: TemplatePath,
140}
141
142impl<'a> VNodeChildren<'a> {
143    fn roots(vnode: &'a VNode) -> Self {
144        Self {
145            inner: VNodeChildrenInner::Roots(RootChildCursor {
146                vnode,
147                anchor_index: 0,
148                pending_static: None,
149            }),
150        }
151    }
152
153    fn element(element: StaticElement<'a>) -> Self {
154        Self {
155            inner: VNodeChildrenInner::Element(ElementChildCursor::new(element)),
156        }
157    }
158
159    fn remaining_len(&self) -> usize {
160        self.inner.remaining_len()
161    }
162}
163
164impl<'a> Iterator for VNodeChildren<'a> {
165    type Item = VNodeChild<'a>;
166
167    fn next(&mut self) -> Option<Self::Item> {
168        self.inner.next()
169    }
170
171    fn size_hint(&self) -> (usize, Option<usize>) {
172        let len = self.remaining_len();
173        (len, Some(len))
174    }
175}
176
177impl ExactSizeIterator for VNodeChildren<'_> {}
178
179#[derive(Clone, Copy)]
180#[allow(clippy::large_enum_variant)]
181enum VNodeChildrenInner<'a> {
182    Roots(RootChildCursor<'a>),
183    Element(ElementChildCursor<'a>),
184}
185
186impl<'a> VNodeChildrenInner<'a> {
187    fn next(&mut self) -> Option<VNodeChild<'a>> {
188        match self {
189            Self::Roots(cursor) => cursor.next(),
190            Self::Element(cursor) => cursor.next(),
191        }
192    }
193
194    fn remaining_len(self) -> usize {
195        let mut remaining = 0;
196        let mut cursor = self;
197        while cursor.next().is_some() {
198            remaining += 1;
199        }
200        remaining
201    }
202}
203
204#[derive(Clone, Copy)]
205struct RootChildCursor<'a> {
206    vnode: &'a VNode,
207    anchor_index: usize,
208    pending_static: Option<(usize, StaticTemplateNode<'a>)>,
209}
210
211impl<'a> RootChildCursor<'a> {
212    fn next(&mut self) -> Option<VNodeChild<'a>> {
213        if let Some((anchor_index, node)) = self.pending_static.take() {
214            let path = self.vnode.template().anchors()[anchor_index].static_path();
215            return Some(static_child(self.vnode, node, path, Some(anchor_index)));
216        }
217
218        while self.anchor_index < self.vnode.template().anchors().len() {
219            let anchor_index = self.anchor_index;
220            self.anchor_index += 1;
221            let anchor = DynamicAnchor::new(self.vnode, anchor_index);
222            if !anchor.is_root_level() {
223                continue;
224            }
225
226            let static_root = (!anchor.is_last_static_node() && anchor.static_path().is_root())
227                .then(|| {
228                    self.vnode
229                        .template()
230                        .static_node_at_path(anchor.static_path())
231                        .expect("static root anchor")
232                });
233
234            if anchor.nodes().len() > 0 {
235                self.pending_static = static_root.map(|node| (anchor_index, node));
236                return Some(VNodeChild::Dynamic(anchor));
237            }
238
239            let node = static_root.expect("zero-node root anchor must own a static node");
240            let path = self.vnode.template().anchors()[anchor_index].static_path();
241            return Some(static_child(self.vnode, node, path, Some(anchor_index)));
242        }
243
244        None
245    }
246}
247
248#[derive(Clone, Copy)]
249struct ElementChildCursor<'a> {
250    static_children: StaticChildCursor<'a>,
251    dynamic_children: DynamicChildCursor<'a>,
252    next_static: Option<PositionedChild<'a>>,
253    next_dynamic: Option<PositionedChild<'a>>,
254}
255
256impl<'a> ElementChildCursor<'a> {
257    fn new(element: StaticElement<'a>) -> Self {
258        let mut static_children = StaticChildCursor::new(element);
259        let mut dynamic_children = DynamicChildCursor::new(element);
260        let next_static = static_children.next();
261        let next_dynamic = dynamic_children.next();
262        Self {
263            static_children,
264            dynamic_children,
265            next_static,
266            next_dynamic,
267        }
268    }
269
270    fn next(&mut self) -> Option<VNodeChild<'a>> {
271        let take_static = match (self.next_static, self.next_dynamic) {
272            (Some(static_child), Some(dynamic_child)) => static_child.key() <= dynamic_child.key(),
273            (Some(_), None) => true,
274            (None, Some(_)) => false,
275            (None, None) => return None,
276        };
277
278        if take_static {
279            let child = self.next_static.take().expect("static child checked");
280            self.next_static = self.static_children.next();
281            Some(child.child)
282        } else {
283            let child = self.next_dynamic.take().expect("dynamic child checked");
284            self.next_dynamic = self.dynamic_children.next();
285            Some(child.child)
286        }
287    }
288}
289
290#[derive(Clone, Copy)]
291struct PositionedChild<'a> {
292    position: usize,
293    order: usize,
294    child: VNodeChild<'a>,
295}
296
297impl<'a> PositionedChild<'a> {
298    fn key(self) -> (usize, usize) {
299        (self.position, self.order)
300    }
301}
302
303#[derive(Clone, Copy)]
304struct StaticChildCursor<'a> {
305    vnode: &'a VNode,
306    nodes: StaticTemplateNodeIter<'a>,
307    path: TemplatePath,
308    slot: usize,
309}
310
311impl<'a> StaticChildCursor<'a> {
312    fn new(element: StaticElement<'a>) -> Self {
313        let vnode = element.vnode;
314        Self {
315            vnode,
316            nodes: element.template_element().children(),
317            path: element.path.next_child(),
318            slot: 0,
319        }
320    }
321
322    fn next(&mut self) -> Option<PositionedChild<'a>> {
323        let node = self.nodes.next()?;
324        let path = self.path;
325        let current_slot = self.slot;
326        let anchor_idx = self.vnode.static_anchor_index_for_path(path);
327        self.path = self.path.next_sibling();
328        self.slot += 1;
329        Some(PositionedChild {
330            position: current_slot * 2 + 1,
331            order: current_slot,
332            child: static_child(self.vnode, node, path, anchor_idx),
333        })
334    }
335}
336
337#[derive(Clone, Copy)]
338struct DynamicChildCursor<'a> {
339    element: StaticElement<'a>,
340    anchor_index: usize,
341    order: usize,
342}
343
344impl<'a> DynamicChildCursor<'a> {
345    fn new(element: StaticElement<'a>) -> Self {
346        Self {
347            element,
348            anchor_index: 0,
349            order: 0,
350        }
351    }
352
353    fn next(&mut self) -> Option<PositionedChild<'a>> {
354        let element_op = self.element.op;
355        next_dynamic_child(
356            self.element.vnode,
357            &mut self.anchor_index,
358            &mut self.order,
359            |anchor| {
360                (anchor.parent_element_op_index() == Some(element_op))
361                    .then(|| child_position(anchor.slot_target()))
362            },
363        )
364    }
365}
366
367fn next_dynamic_child<'a>(
368    vnode: &'a VNode,
369    anchor_index: &mut usize,
370    order: &mut usize,
371    mut position: impl FnMut(DynamicAnchor<'a>) -> Option<usize>,
372) -> Option<PositionedChild<'a>> {
373    while *anchor_index < vnode.template().anchors().len() {
374        let current_anchor_index = *anchor_index;
375        *anchor_index += 1;
376        let anchor = DynamicAnchor::new(vnode, current_anchor_index);
377        if anchor.nodes().len() == 0 {
378            continue;
379        }
380
381        let Some(position) = position(anchor) else {
382            continue;
383        };
384
385        let current_order = *order;
386        *order += 1;
387        return Some(PositionedChild {
388            position,
389            order: current_order,
390            child: VNodeChild::Dynamic(anchor),
391        });
392    }
393
394    None
395}
396
397impl VNode {
398    /// Iterate rendered root children in document order.
399    pub fn children(&self) -> impl ExactSizeIterator<Item = VNodeChild<'_>> + '_ {
400        VNodeChildren::roots(self)
401    }
402
403    /// Return the number of root child positions.
404    pub fn root_child_count(&self) -> usize {
405        self.children().len()
406    }
407
408    /// Iterate dynamic anchors in template document order.
409    pub fn dynamic_anchors(&self) -> impl DoubleEndedIterator<Item = DynamicAnchor<'_>> + '_ {
410        (0..self.template().anchors().len())
411            .map(|anchor_index| DynamicAnchor::new(self, anchor_index))
412    }
413
414    pub(super) fn dynamic_node_slots(
415        &self,
416    ) -> impl DoubleEndedIterator<Item = DynamicNodeSlot<'_>> + '_ {
417        self.dynamic_anchors().flat_map(|anchor| anchor.nodes())
418    }
419
420    pub(crate) fn dynamic_anchor(&self, anchor_index: usize) -> DynamicAnchor<'_> {
421        DynamicAnchor::new(self, anchor_index)
422    }
423
424    pub(crate) fn dynamic_node_slot(
425        &self,
426        anchor_index: usize,
427        slot_index: usize,
428    ) -> Option<DynamicNodeSlot<'_>> {
429        let anchor = DynamicAnchor::new(self, anchor_index);
430        anchor
431            .template_anchor()
432            .nodes()
433            .contains(&slot_index)
434            .then_some(DynamicNodeSlot {
435                anchor,
436                index: slot_index,
437            })
438    }
439
440    fn static_anchor_index_for_path(&self, path: TemplatePath) -> Option<usize> {
441        self.template()
442            .anchors()
443            .iter()
444            .position(|anchor| anchor.static_path() == path)
445    }
446
447    pub(super) fn dynamic_node_slots_after<'a>(
448        &'a self,
449        slot: DynamicNodeSlot<'a>,
450    ) -> impl Iterator<Item = DynamicNodeSlot<'a>> + 'a {
451        let anchor = slot.anchor();
452        let anchor_index = anchor.anchor_index();
453        let current_anchor_slots = ((slot.index() + 1)..anchor.template_anchor().nodes().end)
454            .map(move |index| DynamicNodeSlot { anchor, index });
455        let later_anchor_slots = ((anchor_index + 1)..self.template().anchors().len())
456            .flat_map(move |anchor_index| DynamicAnchor::new(self, anchor_index).nodes());
457
458        current_anchor_slots.chain(later_anchor_slots)
459    }
460
461    pub(super) fn dynamic_node_slots_after_sharing_insertion_position<'a>(
462        &'a self,
463        slot: DynamicNodeSlot<'a>,
464    ) -> impl Iterator<Item = DynamicNodeSlot<'a>> + 'a {
465        self.dynamic_node_slots_after(slot)
466            .map_while(move |sibling| {
467                if !sibling.has_same_insertion_parent(slot) {
468                    Some(None)
469                } else if sibling.shares_insertion_position(slot) {
470                    Some(Some(sibling))
471                } else {
472                    None
473                }
474            })
475            .flatten()
476    }
477
478    pub(super) fn static_anchor_targets_under(
479        &self,
480        root_anchor_index: usize,
481    ) -> impl Iterator<Item = StaticAnchorTarget> + '_ {
482        let root_path = self.template().anchors()[root_anchor_index].static_path();
483        self.template()
484            .anchors()
485            .iter()
486            .enumerate()
487            .filter_map(move |(anchor_index, anchor)| {
488                let path = anchor.static_path();
489                (!path.is_empty() && path.starts_with(root_path))
490                    .then_some(StaticAnchorTarget { anchor_index, path })
491            })
492    }
493}
494
495/// A dynamic template anchor viewed through a rendered [`VNode`].
496#[derive(Clone, Copy)]
497pub struct DynamicAnchor<'a> {
498    vnode: &'a VNode,
499    anchor_index: usize,
500}
501
502impl<'a> DynamicAnchor<'a> {
503    pub(super) fn new(vnode: &'a VNode, anchor_index: usize) -> Self {
504        Self {
505            vnode,
506            anchor_index,
507        }
508    }
509
510    fn template_anchor(self) -> &'a TemplateAnchor {
511        &self.vnode.template().anchors()[self.anchor_index]
512    }
513
514    /// Iterate the dynamic node slots owned by this anchor.
515    pub fn nodes(
516        self,
517    ) -> impl ExactSizeIterator<Item = DynamicNodeSlot<'a>> + DoubleEndedIterator + 'a {
518        self.template_anchor()
519            .nodes()
520            .map(move |index| DynamicNodeSlot {
521                anchor: self,
522                index,
523            })
524    }
525
526    /// Iterate the dynamic attribute slots owned by this anchor.
527    pub fn attrs(
528        self,
529    ) -> impl ExactSizeIterator<Item = DynamicAttrSlot<'a>> + DoubleEndedIterator + 'a {
530        self.template_anchor()
531            .attributes()
532            .map(move |index| DynamicAttrSlot {
533                anchor: self,
534                index,
535            })
536    }
537
538    /// The static template position where this anchor is inserted.
539    pub fn slot_target(self) -> TemplateSlotTarget {
540        self.template_anchor().slot_target()
541    }
542
543    pub(crate) fn anchor_index(self) -> usize {
544        self.anchor_index
545    }
546
547    /// Whether this anchor points at the last static node at its sibling level.
548    pub fn is_last_static_node(self) -> bool {
549        self.slot_target().is_last_static_node()
550    }
551
552    pub(crate) fn is_parent_append_target(self) -> bool {
553        self.template_anchor().is_parent_append_target()
554    }
555
556    /// Return true when this dynamic anchor is inserted at the vnode root level, with no enclosing
557    /// static element.
558    pub fn is_root_level(self) -> bool {
559        self.parent_element_op_index().is_none()
560    }
561
562    /// The static element op that owns this anchor, or `None` for root-level anchors.
563    pub fn parent_element_op_index(self) -> Option<usize> {
564        self.template_anchor().parent_element_op_index()
565    }
566
567    /// The static template path for this anchor.
568    pub fn static_path(self) -> TemplatePath {
569        self.template_anchor().static_path()
570    }
571
572    fn has_same_insertion_parent(self, other: Self) -> bool {
573        self.parent_element_op_index() == other.parent_element_op_index()
574    }
575
576    pub(super) fn shares_insertion_position(self, other: Self) -> bool {
577        self.has_same_insertion_parent(other) && self.slot_target() == other.slot_target()
578    }
579
580    pub(super) fn static_attr_value_for_key(
581        self,
582        key: (&'static str, Option<&'static str>),
583    ) -> Option<&'static str> {
584        let element_op = self
585            .template_anchor()
586            .parent_element_op_index()
587            .expect("bad attr anchor");
588        self.vnode
589            .template()
590            .static_element(element_op)
591            .expect("dynamic attr anchor must reference a static element op")
592            .attribute_value(key)
593    }
594}
595
596#[derive(Clone, Copy)]
597/// A dynamic node slot owned by a [`DynamicAnchor`].
598pub struct DynamicNodeSlot<'a> {
599    anchor: DynamicAnchor<'a>,
600    index: usize,
601}
602
603impl<'a> DynamicNodeSlot<'a> {
604    /// The dynamic anchor that owns this node slot.
605    pub fn anchor(self) -> DynamicAnchor<'a> {
606        self.anchor
607    }
608
609    pub(crate) fn index(self) -> usize {
610        self.index
611    }
612
613    pub(super) fn has_same_insertion_parent(self, other: Self) -> bool {
614        self.anchor.has_same_insertion_parent(other.anchor)
615    }
616
617    pub(super) fn shares_insertion_position(self, other: Self) -> bool {
618        self.anchor.shares_insertion_position(other.anchor)
619    }
620}
621
622impl std::ops::Deref for DynamicNodeSlot<'_> {
623    type Target = DynamicNode;
624
625    fn deref(&self) -> &Self::Target {
626        &self.anchor.vnode.dynamic_node_values()[self.index]
627    }
628}
629
630#[derive(Clone, Copy)]
631/// A dynamic attribute slot owned by a [`DynamicAnchor`].
632pub struct DynamicAttrSlot<'a> {
633    anchor: DynamicAnchor<'a>,
634    index: usize,
635}
636
637impl<'a> DynamicAttrSlot<'a> {
638    /// The dynamic attributes for this slot.
639    pub fn attrs(self) -> &'a [Attribute] {
640        self.anchor.vnode.dynamic_attr_values()[self.index].as_ref()
641    }
642
643    /// The dynamic anchor that owns this attribute slot.
644    pub fn anchor(self) -> DynamicAnchor<'a> {
645        self.anchor
646    }
647}
648
649fn static_child<'a>(
650    vnode: &'a VNode,
651    node: StaticTemplateNode<'_>,
652    path: TemplatePath,
653    anchor_idx: Option<usize>,
654) -> VNodeChild<'a> {
655    match node {
656        StaticTemplateNode::Element(element) => {
657            VNodeChild::Element(StaticElement::new(vnode, element.op(), path, anchor_idx))
658        }
659        StaticTemplateNode::Text(text) => {
660            VNodeChild::Text(StaticText::new(vnode, text.op(), anchor_idx))
661        }
662    }
663}
664
665fn child_position(target: TemplateSlotTarget) -> usize {
666    if target.is_last_static_node() {
667        usize::MAX
668    } else {
669        target.static_path().split_insertion().1 * 2
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::{Attribute, DynamicNode, DynamicValues};
677    use dioxus_core_template::RuntimeTemplateBuilder;
678
679    fn vnode_from_builder(
680        builder: RuntimeTemplateBuilder,
681        dynamic_nodes: usize,
682        dynamic_attrs: usize,
683    ) -> VNode {
684        VNode::new(
685            builder.finish(),
686            DynamicValues::from_parts(
687                None,
688                (0..dynamic_nodes)
689                    .map(|_| DynamicNode::Fragment(Vec::new()))
690                    .collect::<Vec<_>>()
691                    .into_boxed_slice(),
692                (0..dynamic_attrs)
693                    .map(|_| vec![Attribute::new("class", "value", None, false)].into_boxed_slice())
694                    .collect::<Vec<_>>()
695                    .into_boxed_slice(),
696            ),
697        )
698    }
699
700    #[test]
701    fn nested_trailing_dynamic_before_root_dynamic_is_parent_append_target() {
702        let mut builder = RuntimeTemplateBuilder::default();
703        builder.open_element("tag0", None);
704        builder.dynamic_node(false);
705        builder.close_element();
706        builder.dynamic_node(false);
707        let template = builder.finish();
708
709        let vnode = VNode::new(
710            template,
711            DynamicValues::from_parts(
712                None,
713                Box::new([
714                    DynamicNode::Fragment(Vec::new()),
715                    DynamicNode::Fragment(Vec::new()),
716                ]),
717                Box::new([]),
718            ),
719        );
720
721        let anchor = vnode
722            .dynamic_anchors()
723            .find(|anchor| anchor.parent_element_op_index().is_some() && anchor.nodes().len() > 0)
724            .expect("nested dynamic anchor");
725
726        assert!(anchor.is_parent_append_target());
727    }
728
729    #[test]
730    fn nested_static_child_after_dynamic_keeps_anchor_index() {
731        let mut builder = RuntimeTemplateBuilder::default();
732        builder.open_element("root", None);
733        builder.dynamic_node(true);
734        builder.open_element("child", None);
735        builder.close_element();
736        builder.close_element();
737
738        let vnode = vnode_from_builder(builder, 1, 0);
739        let root = vnode
740            .children()
741            .find_map(|child| match child {
742                VNodeChild::Element(element) => Some(element),
743                _ => None,
744            })
745            .expect("root element");
746
747        let mut children = root.children();
748        let dynamic_anchor_index = match children.next().expect("dynamic child") {
749            VNodeChild::Dynamic(anchor) => anchor.anchor_index(),
750            _ => panic!("expected dynamic child before static child"),
751        };
752        let static_anchor_index = match children.next().expect("static child") {
753            VNodeChild::Element(element) => element.anchor_index(),
754            _ => panic!("expected static child"),
755        };
756
757        assert_eq!(static_anchor_index, Some(dynamic_anchor_index));
758    }
759
760    #[test]
761    fn nested_trailing_dynamic_targets_parent_append() {
762        let mut builder = RuntimeTemplateBuilder::default();
763        builder.open_element("root", None);
764        builder.open_element("child", None);
765        builder.close_element();
766        builder.dynamic_node(false);
767        builder.close_element();
768
769        let vnode = vnode_from_builder(builder, 1, 0);
770        let root = vnode
771            .children()
772            .find_map(|child| match child {
773                VNodeChild::Element(element) => Some(element),
774                _ => None,
775            })
776            .expect("root element");
777
778        let mut children = root.children();
779        let static_anchor_index = match children.next().expect("static child") {
780            VNodeChild::Element(element) => element.anchor_index(),
781            _ => panic!("expected static child"),
782        };
783        let dynamic_anchor = match children.next().expect("dynamic child") {
784            VNodeChild::Dynamic(anchor) => anchor,
785            _ => panic!("expected trailing dynamic child"),
786        };
787
788        assert_eq!(static_anchor_index, None);
789        assert!(dynamic_anchor.is_parent_append_target());
790    }
791
792    #[test]
793    fn root_static_before_last_static_emits_directly() {
794        // A root region beginning with a static element that is not the last
795        // static sibling produces a zero-node `push_static_anchor`.
796        // `RootChildCursor` must emit that static node directly through the
797        // `nodes().len() == 0` + `static_root` branch, rather than as a dynamic
798        // anchor's stashed `pending_static` sibling.
799        let mut builder = RuntimeTemplateBuilder::default();
800        builder.open_element("first", None);
801        builder.close_element();
802        builder.open_element("second", None);
803        builder.close_element();
804        builder.dynamic_node(false);
805
806        let vnode = vnode_from_builder(builder, 1, 0);
807
808        let children: Vec<_> = vnode.children().collect();
809        assert!(
810            matches!(children.first(), Some(VNodeChild::Element(_))),
811            "leading static element must be emitted as the first root child",
812        );
813    }
814
815    #[test]
816    fn root_child_count_matches_emitted_children() {
817        let mut builder = RuntimeTemplateBuilder::default();
818        builder.open_element("only", None);
819        builder.close_element();
820        builder.dynamic_node(false);
821
822        let vnode = vnode_from_builder(builder, 1, 0);
823
824        assert_eq!(vnode.root_child_count(), vnode.children().count());
825    }
826
827    #[test]
828    fn dynamic_attr_slot_exposes_anchor_and_index() {
829        let mut builder = RuntimeTemplateBuilder::default();
830        builder.open_element("el", None);
831        builder.dynamic_attr();
832        builder.close_element();
833
834        let vnode = vnode_from_builder(builder, 0, 1);
835
836        let anchor = vnode
837            .dynamic_anchors()
838            .find(|anchor| anchor.attrs().len() > 0)
839            .expect("anchor with a dynamic attribute slot");
840        let slot = anchor.attrs().next().expect("dynamic attr slot");
841
842        assert_eq!(slot.anchor().anchor_index(), anchor.anchor_index());
843    }
844}