Skip to main content

dioxus_core_template/
storage.rs

1use dioxus_const_vec::ConstVec;
2
3use super::anchor::ROOT_PARENT_OP_INDEX;
4use super::{Template, TemplateAnchor, TemplatePath, TemplateRawTree, TemplateSlotPath};
5use crate::op::TemplateOp;
6
7/// Maximum packed template storage capacity.
8pub const TEMPLATE_STORAGE_MAX_CAP: usize = TemplateOp::MAX_CAP;
9
10/// Default packed template operation storage capacity.
11pub const TEMPLATE_STORAGE_OPS_CAP: usize = 128;
12
13/// Default static string storage capacity.
14pub const TEMPLATE_STORAGE_STRING_CAP: usize = 128;
15
16/// Default dynamic anchor storage capacity.
17pub const TEMPLATE_STORAGE_DYNAMIC_CAP: usize = 16;
18
19/// Maximum element nesting depth handled by a single template chunk.
20///
21/// The rsx splitter wraps subtrees in synthetic boundaries once a path exceeds
22/// the [`crate::TEMPLATE_SLOT_PATH_MAX_PATH_BITS`] slot-path payload limit, and
23/// a path consumes at least one bit per nesting level (`TemplatePath::next_child`
24/// shifts left by one). This cap matches the `u128` path width so the bit-width
25/// splitter is the binding constraint and depths up to the slot-path payload
26/// limit lower directly instead of hitting a "stack capacity exceeded" panic.
27const TEMPLATE_PATH_STACK_CAP: usize = 128;
28
29/// Const storage for a template.
30#[derive(Clone, Copy)]
31pub struct TemplateStorage<
32    const OPS_CAP: usize = TEMPLATE_STORAGE_OPS_CAP,
33    const STRING_CAP: usize = TEMPLATE_STORAGE_STRING_CAP,
34    const DYNAMIC_CAP: usize = TEMPLATE_STORAGE_DYNAMIC_CAP,
35> {
36    ops: ConstVec<TemplateOp, OPS_CAP>,
37    strings: ConstVec<&'static str, STRING_CAP>,
38    anchors: ConstVec<TemplateAnchor, DYNAMIC_CAP>,
39}
40
41impl<const OPS_CAP: usize, const STRING_CAP: usize, const DYNAMIC_CAP: usize>
42    TemplateStorage<OPS_CAP, STRING_CAP, DYNAMIC_CAP>
43{
44    /// Build storage from a template tree.
45    pub const fn build_from_tree(tree: &'static TemplateRawTree) -> Self {
46        let mut storage = Self {
47            ops: ConstVec::new_with_max_size(),
48            strings: ConstVec::new_with_max_size(),
49            anchors: ConstVec::new_with_max_size(),
50        };
51        let mut cursor = TemplateLoweringCursor::new();
52
53        lower_raw_tree(tree, &mut storage, &mut cursor, false);
54        cursor.finish();
55        storage
56    }
57
58    /// Return this storage as a compact template.
59    pub const fn as_template(&'static self) -> Template {
60        Template::new(
61            self.ops.as_slice(),
62            self.strings.as_slice(),
63            self.anchors.as_slice(),
64        )
65    }
66
67    /// Leak this storage into a compact runtime template.
68    #[cfg(test)]
69    pub(crate) fn into_leaked_template(self) -> Template {
70        Template::new(
71            Box::leak(self.ops.as_slice().to_vec().into_boxed_slice()),
72            Box::leak(self.strings.as_slice().to_vec().into_boxed_slice()),
73            Box::leak(self.anchors.as_slice().to_vec().into_boxed_slice()),
74        )
75    }
76}
77
78/// Lower a raw template tree into a leaked [`Template`] at runtime.
79///
80/// Runs the same lowering (`build_from_tree`/`lower_raw_tree`) the const path uses, but at runtime
81/// instead of in const evaluation. The debug-only lazy template path uses this so dev builds skip
82/// const-evaluating the optimized template for every `rsx!` site. The leak is bounded because
83/// callers cache the result per template.
84#[cfg(debug_assertions)]
85pub fn build_runtime_template(tree: &'static TemplateRawTree) -> Template {
86    let mut builder = RuntimeTemplateBuilder::new();
87    lower_raw_tree_runtime(tree, &mut builder, false);
88    builder.finish()
89}
90
91/// Runtime mirror of [`lower_raw_tree`] that drives the non-generic [`RuntimeTemplateBuilder`].
92///
93/// Mirrors the const lowering arm-for-arm so it produces the identical op tape, strings, anchors,
94/// and value-kind hash, but without the capacity const generics - so the debug lazy path codegens
95/// the lowering once instead of monomorphizing it per `(ops, strings, anchors)` capacity combo.
96#[cfg(debug_assertions)]
97fn lower_raw_tree_runtime(
98    tree: &'static TemplateRawTree,
99    builder: &mut RuntimeTemplateBuilder,
100    following_static_at_parent: bool,
101) {
102    match tree {
103        TemplateRawTree::Empty => {}
104        TemplateRawTree::Sequence(children) => {
105            let mut index = 0;
106            while index < children.len() {
107                lower_raw_tree_runtime(
108                    children[index],
109                    builder,
110                    following_static_at_parent
111                        || children_have_static_root_node(children, index + 1),
112                );
113                index += 1;
114            }
115        }
116        TemplateRawTree::Element {
117            tag,
118            namespace,
119            attrs,
120            children,
121        } => {
122            builder.open_element(tag, *namespace);
123            lower_raw_tree_runtime(attrs, builder, false);
124            lower_raw_tree_runtime(children, builder, false);
125            builder.close_element();
126        }
127        TemplateRawTree::StaticAttr {
128            name,
129            value,
130            namespace,
131        } => builder.static_attr(name, value, *namespace),
132        TemplateRawTree::DynamicAttr => builder.dynamic_attr(),
133        TemplateRawTree::StaticText(value) => builder.static_text(value),
134        TemplateRawTree::DynamicNode => builder.dynamic_node(following_static_at_parent),
135    }
136}
137
138#[derive(Clone, Copy)]
139pub struct TemplateElementFrame {
140    pub(crate) enter_index: usize,
141    namespace: bool,
142    pub(crate) path: TemplatePath,
143}
144
145pub struct TemplateLoweringCursor {
146    enter_stack: [TemplateElementFrame; TEMPLATE_PATH_STACK_CAP],
147    pub(crate) next_paths: [TemplatePath; TEMPLATE_PATH_STACK_CAP],
148    last_static_paths: [TemplatePath; TEMPLATE_PATH_STACK_CAP],
149    pub(crate) stack_pointer: usize,
150}
151
152impl TemplateLoweringCursor {
153    pub const fn new() -> Self {
154        let mut next_paths = [TemplatePath::empty(); TEMPLATE_PATH_STACK_CAP];
155        next_paths[0] = TemplatePath::root(0);
156        Self {
157            enter_stack: [TemplateElementFrame {
158                enter_index: 0,
159                namespace: false,
160                path: TemplatePath::empty(),
161            }; TEMPLATE_PATH_STACK_CAP],
162            next_paths,
163            last_static_paths: [TemplatePath::empty(); TEMPLATE_PATH_STACK_CAP],
164            stack_pointer: 0,
165        }
166    }
167
168    pub const fn open_element(&mut self, enter_index: usize, namespace: bool) {
169        if self.stack_pointer + 1 >= TEMPLATE_PATH_STACK_CAP {
170            panic!("template path stack capacity exceeded");
171        }
172        let path = self.next_paths[self.stack_pointer];
173        self.next_paths[self.stack_pointer] = path.next_sibling();
174        self.enter_stack[self.stack_pointer] = TemplateElementFrame {
175            enter_index,
176            namespace,
177            path,
178        };
179        self.next_paths[self.stack_pointer + 1] = path.next_child();
180        self.last_static_paths[self.stack_pointer + 1] = TemplatePath::empty();
181        self.last_static_paths[self.stack_pointer] = path;
182        self.stack_pointer += 1;
183    }
184
185    pub const fn close_element(&mut self) -> TemplateElementFrame {
186        if self.stack_pointer == 0 {
187            panic!("template close op without matching open op");
188        }
189        self.stack_pointer -= 1;
190        self.enter_stack[self.stack_pointer]
191    }
192
193    const fn current_element_path(&self) -> TemplatePath {
194        if self.stack_pointer == 0 {
195            panic!("dynamic attr raw op without an open element");
196        }
197        self.current_element_frame().path
198    }
199
200    pub(crate) const fn node_anchor_parent_op_index(&self) -> u16 {
201        if self.stack_pointer == 0 {
202            ROOT_PARENT_OP_INDEX
203        } else {
204            self.current_element_frame().enter_index as u16
205        }
206    }
207
208    pub(crate) const fn current_element_frame(&self) -> TemplateElementFrame {
209        if self.stack_pointer == 0 {
210            panic!("template cursor is not inside an element");
211        }
212        let frame = self.enter_stack[self.stack_pointer - 1];
213        if frame.enter_index > TemplateOp::MAX_CAP {
214            panic!("template enter op exceeds packed op capacity");
215        }
216        frame
217    }
218
219    pub const fn next_node_path(&mut self) -> TemplatePath {
220        let path = self.next_paths[self.stack_pointer];
221        self.next_paths[self.stack_pointer] = path.next_sibling();
222        self.last_static_paths[self.stack_pointer] = path;
223        path
224    }
225
226    const fn next_slot_path_after_dynamic_node(
227        &self,
228        has_following_static_at_parent: bool,
229    ) -> TemplateSlotPath {
230        if has_following_static_at_parent {
231            return TemplateSlotPath::static_node(self.next_paths[self.stack_pointer]);
232        }
233
234        if self.stack_pointer > 0 {
235            return TemplateSlotPath::last_static_node(self.current_element_path());
236        }
237
238        let last_static_path = self.last_static_paths[self.stack_pointer];
239        TemplateSlotPath::last_static_node(last_static_path)
240    }
241
242    /// Return a structural root static anchor if the next root position needs one.
243    pub fn static_root_anchor(&self) -> Option<(u16, TemplateSlotPath)> {
244        let path = self.next_paths[self.stack_pointer];
245        (self.stack_pointer == 0 && !path.is_empty())
246            .then(|| (ROOT_PARENT_OP_INDEX, TemplateSlotPath::static_node(path)))
247    }
248
249    /// Return the current element's dynamic attribute anchor and whether its path overflowed.
250    pub fn dynamic_attr_anchor(&self) -> (u16, TemplateSlotPath, bool) {
251        let frame = self.current_element_frame();
252        (
253            frame.enter_index as u16,
254            TemplateSlotPath::static_node(frame.path),
255            frame.path.is_empty(),
256        )
257    }
258
259    /// Return the next dynamic node anchor and whether its path overflowed.
260    pub fn dynamic_node_anchor(
261        &self,
262        following_static_at_parent: bool,
263    ) -> (u16, TemplateSlotPath, bool) {
264        let parent_op_index = self.node_anchor_parent_op_index();
265        if following_static_at_parent {
266            let path = self.next_paths[self.stack_pointer];
267            if path.is_empty() {
268                return (
269                    parent_op_index,
270                    TemplateSlotPath::last_static_node(TemplatePath::empty()),
271                    true,
272                );
273            }
274            return (parent_op_index, TemplateSlotPath::static_node(path), false);
275        }
276
277        if self.stack_pointer > 0 {
278            let path = self.current_element_path();
279            if path.is_empty() {
280                return (
281                    parent_op_index,
282                    TemplateSlotPath::last_static_node(TemplatePath::empty()),
283                    true,
284                );
285            }
286            return (
287                parent_op_index,
288                TemplateSlotPath::last_static_node(path),
289                false,
290            );
291        }
292
293        (
294            parent_op_index,
295            TemplateSlotPath::last_static_node(self.last_static_paths[self.stack_pointer]),
296            false,
297        )
298    }
299
300    pub const fn finish(&self) {
301        if self.stack_pointer != 0 {
302            panic!("template ended with unclosed elements");
303        }
304    }
305}
306
307impl Default for TemplateLoweringCursor {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313const fn tree_has_static_root_node(tree: &'static TemplateRawTree) -> bool {
314    match tree {
315        TemplateRawTree::Empty
316        | TemplateRawTree::StaticAttr { .. }
317        | TemplateRawTree::DynamicAttr
318        | TemplateRawTree::DynamicNode => false,
319        TemplateRawTree::Element { .. } | TemplateRawTree::StaticText(_) => true,
320        TemplateRawTree::Sequence(children) => children_have_static_root_node(children, 0),
321    }
322}
323
324const fn children_have_static_root_node(
325    children: &'static [&'static TemplateRawTree],
326    start: usize,
327) -> bool {
328    let mut index = start;
329    while index < children.len() {
330        if tree_has_static_root_node(children[index]) {
331            return true;
332        }
333        index += 1;
334    }
335
336    false
337}
338
339// Replace this macro with a const trait once const trait methods are stable enough for this shared
340// lowering path.
341macro_rules! template_lowering {
342    (open_element($storage:expr, $cursor:expr, $tag:expr, $namespace:expr)) => {{
343        let namespace = $namespace;
344        let has_namespace = namespace.is_some();
345        if ($cursor).stack_pointer == 0 {
346            ($storage).push_static_anchor(
347                ROOT_PARENT_OP_INDEX,
348                TemplateSlotPath::static_node(($cursor).next_paths[0]),
349            );
350        }
351        ($cursor).open_element(($storage).ops_len(), has_namespace);
352        ($storage).push_op(TemplateOp::enter(0, has_namespace));
353        ($storage).push_static($tag);
354        if let Some(namespace) = namespace {
355            ($storage).push_static(namespace);
356        }
357    }};
358    (close_element($storage:expr, $cursor:expr)) => {{
359        let frame = ($cursor).close_element();
360        let enter_index = frame.enter_index;
361        let namespace = frame.namespace;
362        let skip = ($storage).ops_len() - enter_index;
363        if skip > TemplateOp::MAX_CAP {
364            panic!("template op skip exceeds packed op capacity");
365        }
366        ($storage).set_op(enter_index, TemplateOp::enter(skip as u16, namespace));
367    }};
368    (static_attr($storage:expr, $name:expr, $value:expr, $namespace:expr)) => {{
369        let namespace = $namespace;
370        ($storage).push_op(TemplateOp::attr(namespace.is_some()));
371        ($storage).push_static($name);
372        ($storage).push_static($value);
373        if let Some(namespace) = namespace {
374            ($storage).push_static(namespace);
375        }
376    }};
377    (static_text($storage:expr, $cursor:expr, $value:expr)) => {{
378        if ($cursor).stack_pointer == 0 {
379            ($storage).push_static_anchor(
380                ROOT_PARENT_OP_INDEX,
381                TemplateSlotPath::static_node(($cursor).next_paths[0]),
382            );
383        }
384        let _ = ($cursor).next_node_path();
385        ($storage).push_op(TemplateOp::text());
386        ($storage).push_static($value);
387    }};
388    (dynamic_node($storage:expr, $cursor:expr, $following_static_at_parent:expr)) => {{
389        let path = ($cursor).next_slot_path_after_dynamic_node($following_static_at_parent);
390        ($storage).push_anchor(($cursor).node_anchor_parent_op_index(), path, false);
391    }};
392}
393
394macro_rules! template_storage_methods {
395    ($($constness:tt)?) => {
396        $($constness)? fn push_static(&mut self, value: &'static str) {
397            let id = self.strings.len();
398            if id >= TemplateOp::MAX_CAP {
399                panic!("static op id exceeds packed op capacity");
400            }
401            self.strings.push(value);
402            self.push_op(TemplateOp::static_text(id as u16));
403        }
404
405        $($constness)? fn ops_len(&self) -> usize {
406            self.ops.len()
407        }
408
409        $($constness)? fn push_op(&mut self, op: TemplateOp) {
410            if self.ops.len() >= TemplateOp::MAX_CAP {
411                panic!("template ops exceed packed op capacity");
412            }
413            self.ops.push(op);
414        }
415
416        $($constness)? fn set_op(&mut self, index: usize, op: TemplateOp) {
417            self.ops.set(index, op);
418        }
419
420        $($constness)? fn push_anchor(
421            &mut self,
422            parent_op_index: u16,
423            path: TemplateSlotPath,
424            is_attr: bool,
425        ) {
426            let len = self.anchors.len();
427            if len > 0 {
428                let last = self.anchors.at(len - 1);
429                if last.same_anchor(parent_op_index, path) {
430                    if is_attr {
431                        if last.attr_end == u16::MAX {
432                            panic!("anchor overflow");
433                        }
434                        self.anchors.set(
435                            len - 1,
436                            TemplateAnchor {
437                                attr_end: last.attr_end + 1,
438                                ..last
439                            },
440                        );
441                    } else {
442                        if last.node_end == u16::MAX {
443                            panic!("anchor overflow");
444                        }
445                        self.anchors.set(
446                            len - 1,
447                            TemplateAnchor {
448                                node_end: last.node_end + 1,
449                                ..last
450                            },
451                        );
452                    }
453                    return;
454                }
455            }
456
457            let (node_start, attr_start) = if len == 0 {
458                (0, 0)
459            } else {
460                let last = self.anchors.at(len - 1);
461                (last.node_end, last.attr_end)
462            };
463            if (is_attr && attr_start == u16::MAX) || (!is_attr && node_start == u16::MAX) {
464                panic!("anchor overflow");
465            }
466            self.anchors.push(TemplateAnchor {
467                parent_op_index,
468                path,
469                node_start,
470                node_end: node_start + (!is_attr as u16),
471                attr_start,
472                attr_end: attr_start + (is_attr as u16),
473            });
474        }
475
476        $($constness)? fn push_static_anchor(
477            &mut self,
478            parent_op_index: u16,
479            path: TemplateSlotPath,
480        ) {
481            let len = self.anchors.len();
482            if len > 0 {
483                let last = self.anchors.at(len - 1);
484                if last.same_anchor(parent_op_index, path) {
485                    return;
486                }
487            }
488
489            let (node_start, attr_start) = if len == 0 {
490                (0, 0)
491            } else {
492                let last = self.anchors.at(len - 1);
493                (last.node_end, last.attr_end)
494            };
495            self.anchors.push(TemplateAnchor {
496                parent_op_index,
497                path,
498                node_start,
499                node_end: node_start,
500                attr_start,
501                attr_end: attr_start,
502            });
503        }
504
505    };
506}
507
508impl<const OPS_CAP: usize, const STRING_CAP: usize, const DYNAMIC_CAP: usize>
509    TemplateStorage<OPS_CAP, STRING_CAP, DYNAMIC_CAP>
510{
511    template_storage_methods!(const);
512}
513
514const fn push_element_start<
515    const OPS_CAP: usize,
516    const STRING_CAP: usize,
517    const DYNAMIC_CAP: usize,
518>(
519    storage: &mut TemplateStorage<OPS_CAP, STRING_CAP, DYNAMIC_CAP>,
520    cursor: &mut TemplateLoweringCursor,
521    tag: &'static str,
522    namespace: Option<&'static str>,
523) {
524    template_lowering!(open_element(storage, cursor, tag, namespace));
525}
526
527const fn push_element_end<
528    const OPS_CAP: usize,
529    const STRING_CAP: usize,
530    const DYNAMIC_CAP: usize,
531>(
532    storage: &mut TemplateStorage<OPS_CAP, STRING_CAP, DYNAMIC_CAP>,
533    cursor: &mut TemplateLoweringCursor,
534) {
535    template_lowering!(close_element(storage, cursor));
536}
537
538const fn push_static_attr<
539    const OPS_CAP: usize,
540    const STRING_CAP: usize,
541    const DYNAMIC_CAP: usize,
542>(
543    storage: &mut TemplateStorage<OPS_CAP, STRING_CAP, DYNAMIC_CAP>,
544    name: &'static str,
545    value: &'static str,
546    namespace: Option<&'static str>,
547) {
548    template_lowering!(static_attr(storage, name, value, namespace));
549}
550
551const fn lower_raw_tree<const OPS_CAP: usize, const STRING_CAP: usize, const DYNAMIC_CAP: usize>(
552    tree: &'static TemplateRawTree,
553    storage: &mut TemplateStorage<OPS_CAP, STRING_CAP, DYNAMIC_CAP>,
554    cursor: &mut TemplateLoweringCursor,
555    following_static_at_parent: bool,
556) {
557    match tree {
558        TemplateRawTree::Empty => {}
559        TemplateRawTree::Sequence(children) => {
560            let mut index = 0;
561            while index < children.len() {
562                lower_raw_tree(
563                    children[index],
564                    storage,
565                    cursor,
566                    following_static_at_parent
567                        || children_have_static_root_node(children, index + 1),
568                );
569                index += 1;
570            }
571        }
572        TemplateRawTree::Element {
573            tag,
574            namespace,
575            attrs,
576            children,
577        } => {
578            push_element_start(storage, cursor, tag, *namespace);
579            lower_raw_tree(attrs, storage, cursor, false);
580            lower_raw_tree(children, storage, cursor, false);
581            push_element_end(storage, cursor);
582        }
583        TemplateRawTree::StaticAttr {
584            name,
585            value,
586            namespace,
587        } => {
588            push_static_attr(storage, name, value, *namespace);
589        }
590        TemplateRawTree::DynamicAttr => {
591            let frame = cursor.current_element_frame();
592            let path = TemplateSlotPath::static_node(frame.path);
593            storage.push_anchor(frame.enter_index as u16, path, true);
594        }
595        TemplateRawTree::StaticText(value) => {
596            template_lowering!(static_text(storage, cursor, value));
597        }
598        TemplateRawTree::DynamicNode => {
599            template_lowering!(dynamic_node(storage, cursor, following_static_at_parent));
600        }
601    }
602}
603
604/// Builds a leaked runtime template directly from semantic template events.
605pub struct RuntimeTemplateBuilder {
606    storage: RuntimeTemplateStorage,
607    cursor: TemplateLoweringCursor,
608}
609
610#[derive(Default)]
611struct RuntimeTemplateStorage {
612    ops: RuntimeTemplateVec<TemplateOp>,
613    strings: RuntimeTemplateVec<&'static str>,
614    anchors: RuntimeTemplateVec<TemplateAnchor>,
615}
616
617struct RuntimeTemplateVec<T>(Vec<T>);
618
619impl<T> Default for RuntimeTemplateVec<T> {
620    fn default() -> Self {
621        Self(Vec::new())
622    }
623}
624
625impl<T: Copy> RuntimeTemplateVec<T> {
626    fn len(&self) -> usize {
627        self.0.len()
628    }
629
630    fn push(&mut self, value: T) {
631        self.0.push(value);
632    }
633
634    fn set(&mut self, index: usize, value: T) {
635        self.0[index] = value;
636    }
637
638    fn at(&self, index: usize) -> T {
639        self.0[index]
640    }
641
642    fn into_boxed_slice(self) -> Box<[T]> {
643        self.0.into_boxed_slice()
644    }
645}
646
647impl Default for RuntimeTemplateBuilder {
648    fn default() -> Self {
649        Self::new()
650    }
651}
652
653impl RuntimeTemplateBuilder {
654    /// Create a new runtime template builder.
655    fn new() -> Self {
656        Self {
657            storage: RuntimeTemplateStorage::default(),
658            cursor: TemplateLoweringCursor::new(),
659        }
660    }
661
662    /// Emit an element start.
663    pub fn open_element(&mut self, tag: &'static str, namespace: Option<&'static str>) {
664        template_lowering!(open_element(
665            &mut self.storage,
666            &mut self.cursor,
667            tag,
668            namespace
669        ));
670    }
671
672    /// Emit the end of the current element.
673    pub fn close_element(&mut self) {
674        template_lowering!(close_element(&mut self.storage, &mut self.cursor));
675    }
676
677    /// Emit a static attribute.
678    pub fn static_attr(
679        &mut self,
680        name: &'static str,
681        value: &'static str,
682        namespace: Option<&'static str>,
683    ) {
684        template_lowering!(static_attr(&mut self.storage, name, value, namespace));
685    }
686
687    /// Emit a dynamic attribute slot on the current element.
688    pub fn dynamic_attr(&mut self) {
689        let frame = self.cursor.current_element_frame();
690        let path = TemplateSlotPath::static_node(frame.path);
691        self.storage
692            .push_anchor(frame.enter_index as u16, path, true);
693    }
694
695    /// Emit a static text node.
696    pub fn static_text(&mut self, value: &'static str) {
697        template_lowering!(static_text(&mut self.storage, &mut self.cursor, value));
698    }
699
700    /// Emit a dynamic node slot.
701    pub fn dynamic_node(&mut self, following_static_at_parent: bool) {
702        template_lowering!(dynamic_node(
703            &mut self.storage,
704            &mut self.cursor,
705            following_static_at_parent
706        ));
707    }
708
709    /// Finish this builder and return a leaked template.
710    pub fn finish(self) -> Template {
711        self.cursor.finish();
712        self.storage.into_leaked_template()
713    }
714}
715
716impl RuntimeTemplateStorage {
717    fn into_leaked_template(self) -> Template {
718        Template::new(
719            Box::leak(self.ops.into_boxed_slice()),
720            Box::leak(self.strings.into_boxed_slice()),
721            Box::leak(self.anchors.into_boxed_slice()),
722        )
723    }
724
725    template_storage_methods!();
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731
732    fn template_from_tree(tree: &'static TemplateRawTree) -> Template {
733        TemplateStorage::<64, 64, 16>::build_from_tree(tree).into_leaked_template()
734    }
735
736    fn anchor_parts(template: Template) -> Vec<(u16, u128, u16, u16, u16, u16)> {
737        template
738            .anchors()
739            .iter()
740            .map(|anchor| {
741                (
742                    anchor.parent_op_index,
743                    anchor.path.bits(),
744                    anchor.node_start,
745                    anchor.node_end,
746                    anchor.attr_start,
747                    anchor.attr_end,
748                )
749            })
750            .collect()
751    }
752
753    fn assert_same_template(actual: Template, expected: Template) {
754        assert_eq!(
755            actual.decoded_ops().collect::<Vec<_>>(),
756            expected.decoded_ops().collect::<Vec<_>>()
757        );
758        assert_eq!(actual.strings(), expected.strings());
759        assert_eq!(anchor_parts(actual), anchor_parts(expected));
760    }
761
762    #[test]
763    fn runtime_builder_matches_tree_for_nested_namespaces_and_dynamic_attrs() {
764        static ATTR: TemplateRawTree = TemplateRawTree::StaticAttr {
765            name: "fill",
766            value: "red",
767            namespace: Some("style"),
768        };
769        static ATTRS: [&TemplateRawTree; 2] = [&ATTR, &TemplateRawTree::DynamicAttr];
770        static ATTRS_TREE: TemplateRawTree = TemplateRawTree::Sequence(&ATTRS);
771        static TEXT: TemplateRawTree = TemplateRawTree::StaticText("hello");
772        static INNER_CHILDREN: [&TemplateRawTree; 1] = [&TemplateRawTree::DynamicNode];
773        static INNER_CHILDREN_TREE: TemplateRawTree = TemplateRawTree::Sequence(&INNER_CHILDREN);
774        static INNER: TemplateRawTree = TemplateRawTree::Element {
775            tag: "span",
776            namespace: None,
777            attrs: &TemplateRawTree::Empty,
778            children: &INNER_CHILDREN_TREE,
779        };
780        static CHILDREN: [&TemplateRawTree; 2] = [&TEXT, &INNER];
781        static CHILDREN_TREE: TemplateRawTree = TemplateRawTree::Sequence(&CHILDREN);
782        static TREE: TemplateRawTree = TemplateRawTree::Element {
783            tag: "svg",
784            namespace: Some("svg"),
785            attrs: &ATTRS_TREE,
786            children: &CHILDREN_TREE,
787        };
788
789        let mut builder = RuntimeTemplateBuilder::new();
790        builder.open_element("svg", Some("svg"));
791        builder.static_attr("fill", "red", Some("style"));
792        builder.dynamic_attr();
793        builder.static_text("hello");
794        builder.open_element("span", None);
795        builder.dynamic_node(false);
796        builder.close_element();
797        builder.close_element();
798
799        assert_same_template(builder.finish(), template_from_tree(&TREE));
800    }
801
802    #[test]
803    fn runtime_builder_places_dynamic_nodes_before_static_siblings() {
804        static TEXT: TemplateRawTree = TemplateRawTree::StaticText("after");
805        static CHILDREN: [&TemplateRawTree; 2] = [&TemplateRawTree::DynamicNode, &TEXT];
806        static TREE: TemplateRawTree = TemplateRawTree::Sequence(&CHILDREN);
807
808        let mut builder = RuntimeTemplateBuilder::new();
809        builder.dynamic_node(true);
810        builder.static_text("after");
811
812        assert_same_template(builder.finish(), template_from_tree(&TREE));
813    }
814
815    #[test]
816    fn runtime_builder_groups_adjacent_trailing_dynamic_nodes() {
817        static CHILDREN: [&TemplateRawTree; 2] =
818            [&TemplateRawTree::DynamicNode, &TemplateRawTree::DynamicNode];
819        static TREE: TemplateRawTree = TemplateRawTree::Sequence(&CHILDREN);
820
821        let mut builder = RuntimeTemplateBuilder::new();
822        builder.dynamic_node(false);
823        builder.dynamic_node(false);
824
825        let template = builder.finish();
826        assert_same_template(template, template_from_tree(&TREE));
827        assert_eq!(template.anchors()[0].nodes(), 0..2);
828    }
829}