Skip to main content

presolve_compiler/
ordinary_html_codegen.rs

1//! Compiler-owned J1-P materialization of ordinary template instance markers.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::template_graph::{
6    AttributeValue, ConditionalNode, ElementNode, FragmentNode, ListNode, TemplateAttribute,
7};
8use crate::{
9    build_ordinary_template_instance_registry, build_resume_anchor_plan, ApplicationSemanticModel,
10    ComponentInstanceId, ComponentInstanceStatus, ResumeAnchorPlacement, SerializableValue,
11    SlotBindingStatus, TemplateChild, TemplateNode, TemplateSemanticKind,
12};
13
14#[derive(Debug, Default)]
15struct ResumeHtmlMarkers {
16    element_anchors: BTreeMap<String, String>,
17    text_anchors: BTreeMap<String, String>,
18    structural_starts: BTreeMap<String, String>,
19    structural_ends: BTreeMap<String, String>,
20    events: BTreeMap<String, String>,
21}
22
23/// Compiler-rendered branches for one compiler-addressed conditional host.
24///
25/// These fragments deliberately carry the same target, binding, and exact
26/// structural-invocation markers as the ordinary initial renderer. They are
27/// artifact data only: runtime materialization remains separately gated.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct StructuralConditionalHostFragments {
30    pub host_scope: StructuralConditionalHostScope,
31    pub host_instance: ComponentInstanceId,
32    pub when_true_html: String,
33    pub when_false_html: String,
34    pub when_true_invocations: Vec<String>,
35    pub when_false_invocations: Vec<String>,
36    /// Exact canonical Slot bindings consumed while rendering this host.
37    pub slot_projection_bindings: Vec<String>,
38}
39
40#[must_use]
41pub fn structural_invocations_in_compiler_html(html: &str) -> Vec<String> {
42    const MARKER: &str = "data-presolve-structural-invocation=\"";
43    html.match_indices(MARKER)
44        .filter_map(|(start, _)| {
45            let value = &html[start + MARKER.len()..];
46            value.find('"').map(|end| value[..end].to_string())
47        })
48        .collect()
49}
50
51/// Compiler-rendered keyed item fragment for one exact component host scope.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct StructuralKeyedHostFragment {
54    pub host_scope: StructuralConditionalHostScope,
55    pub host_instance: ComponentInstanceId,
56    pub item_template_html: String,
57    pub item_invocations: Vec<String>,
58    /// Exact canonical Slot bindings consumed while rendering this host.
59    pub slot_projection_bindings: Vec<String>,
60}
61
62/// The only host scopes the compiler currently renders exactly.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum StructuralConditionalHostScope {
65    StaticInstance,
66    StructuralOccurrence,
67}
68
69impl StructuralConditionalHostScope {
70    #[must_use]
71    pub const fn artifact_text(self) -> &'static str {
72        match self {
73            Self::StaticInstance => "static-instance",
74            Self::StructuralOccurrence => "structural-occurrence",
75        }
76    }
77}
78
79#[derive(Debug, Clone)]
80enum SlotProjection<'a> {
81    Authored {
82        outlet_entity: crate::SemanticId,
83        caller_instance: ComponentInstanceId,
84        caller_template: &'a TemplateNode,
85        invocation_element: &'a ElementNode,
86        invocation_path: String,
87        content_entities: BTreeSet<crate::SemanticId>,
88        caller_slot_projections: &'a [SlotProjection<'a>],
89    },
90    DirectLayoutChild {
91        outlet_entity: crate::SemanticId,
92        child_instance: ComponentInstanceId,
93    },
94}
95
96/// Render the planned initial component topology with compiler-precomputed
97/// ordinary instance and J10 resume markers.
98#[must_use]
99pub fn generate_ordinary_instance_html(model: &ApplicationSemanticModel) -> String {
100    generate_ordinary_instance_html_for_roots(model, None)
101}
102
103/// Renders only the planned ordinary instance tree rooted at one compiler
104/// selected component. Application publication uses this entry-scoped form so
105/// unrelated top-level components cannot leak into an entry page.
106#[must_use]
107pub fn generate_ordinary_instance_html_for_component(
108    model: &ApplicationSemanticModel,
109    root_component: &crate::SemanticId,
110) -> String {
111    generate_ordinary_instance_html_for_roots(model, Some(root_component))
112}
113
114/// Renders one compiler-planned structural template with the same exact
115/// ordinary target and binding markers as an initial instance. The static
116/// template-instance prefix is replaced by an opaque runtime-occurrence token;
117/// only the later structural materializer may substitute that token.
118#[must_use]
119pub fn generate_structural_template_instance_html(
120    model: &ApplicationSemanticModel,
121    template_instance: &ComponentInstanceId,
122) -> Option<String> {
123    let instance = model
124        .component_instance_plan
125        .instances
126        .get(template_instance)?;
127    if instance.status != ComponentInstanceStatus::StructuralTemplate {
128        return None;
129    }
130    let registry = build_ordinary_template_instance_registry(model);
131    let targets = registry
132        .targets
133        .iter()
134        .map(|record| {
135            (
136                (
137                    record.component_instance_id.clone(),
138                    record.template_entity_id.clone(),
139                ),
140                record.target_id.to_string(),
141            )
142        })
143        .collect::<BTreeMap<_, _>>();
144    let bindings = registry
145        .bindings
146        .iter()
147        .map(|record| {
148            (
149                (
150                    record.component_instance_id.clone(),
151                    record.declaration_binding_id.clone(),
152                ),
153                record.instance_binding_id.to_string(),
154            )
155        })
156        .collect::<BTreeMap<_, _>>();
157    let children = model
158        .component_instance_plan
159        .instances
160        .values()
161        .filter_map(|child| {
162            child.parent_instance.as_ref().and_then(|parent| {
163                child
164                    .invocation
165                    .as_ref()
166                    .map(|invocation| ((parent.clone(), invocation.clone()), child))
167            })
168        })
169        .collect::<BTreeMap<_, _>>();
170    let templates = model
171        .templates
172        .iter()
173        .map(|template| (template.id.clone(), template))
174        .collect::<BTreeMap<_, _>>();
175    let slot_projections = structural_instance_slot_projections(model, &templates, instance)?;
176    Some(
177        render_instance(
178            model,
179            &templates,
180            &children,
181            &targets,
182            &bindings,
183            &ResumeHtmlMarkers::default(),
184            &instance.id,
185            &instance.component,
186            &slot_projections,
187        )
188        .replace(instance.id.as_str(), "__PRESOLVE_STRUCTURAL_OCCURRENCE__"),
189    )
190}
191
192/// Render both branches for a compiler-issued conditional structural region.
193///
194/// Instances with caller-owned Slot projection remain absent: publishing an
195/// incomplete fragment would create a second renderer authority. A structural
196/// host that has no such projection uses the already-authorized opaque
197/// occurrence placeholder rather than a fabricated static instance ID.
198#[must_use]
199pub fn generate_structural_conditional_host_fragments(
200    model: &ApplicationSemanticModel,
201    region: &crate::ComponentStructuralRegionId,
202) -> Vec<StructuralConditionalHostFragments> {
203    let Some(entity) =
204        crate::structural_template_entity_for_region(region, &model.template_entities)
205    else {
206        return Vec::new();
207    };
208    if entity.kind != TemplateSemanticKind::Conditional {
209        return Vec::new();
210    }
211    let Some(template_id) = entity.owner.entity_id() else {
212        return Vec::new();
213    };
214    let Some(template) = model
215        .templates
216        .iter()
217        .find(|template| template.id == *template_id)
218    else {
219        return Vec::new();
220    };
221    let Some(component) = template.owner.entity_id() else {
222        return Vec::new();
223    };
224    let Some((conditional, path)) = conditional_at_span(template, entity.provenance.span) else {
225        return Vec::new();
226    };
227
228    let registry = build_ordinary_template_instance_registry(model);
229    let targets = registry
230        .targets
231        .iter()
232        .map(|record| {
233            (
234                (
235                    record.component_instance_id.clone(),
236                    record.template_entity_id.clone(),
237                ),
238                record.target_id.to_string(),
239            )
240        })
241        .collect::<BTreeMap<_, _>>();
242    let bindings = registry
243        .bindings
244        .iter()
245        .map(|record| {
246            (
247                (
248                    record.component_instance_id.clone(),
249                    record.declaration_binding_id.clone(),
250                ),
251                record.instance_binding_id.to_string(),
252            )
253        })
254        .collect::<BTreeMap<_, _>>();
255    let children = model
256        .component_instance_plan
257        .instances
258        .values()
259        .filter_map(|child| {
260            child.parent_instance.as_ref().and_then(|parent| {
261                child
262                    .invocation
263                    .as_ref()
264                    .map(|invocation| ((parent.clone(), invocation.clone()), child))
265            })
266        })
267        .collect::<BTreeMap<_, _>>();
268    let templates = model
269        .templates
270        .iter()
271        .map(|candidate| (candidate.id.clone(), candidate))
272        .collect::<BTreeMap<_, _>>();
273
274    model
275        .component_instance_plan
276        .instances
277        .values()
278        .filter(|instance| {
279            matches!(
280                instance.status,
281                ComponentInstanceStatus::Planned | ComponentInstanceStatus::StructuralTemplate
282            ) && instance.component == *component
283        })
284        .filter_map(|instance| {
285            let host_scope = match instance.status {
286                ComponentInstanceStatus::Planned => StructuralConditionalHostScope::StaticInstance,
287                ComponentInstanceStatus::StructuralTemplate => {
288                    StructuralConditionalHostScope::StructuralOccurrence
289                }
290            };
291            let slot_projections =
292                structural_instance_slot_projections(model, &templates, instance);
293            if !model.slot_bindings.for_callee(&instance.id).is_empty()
294                && slot_projections.is_none()
295            {
296                return None;
297            }
298            let slot_projections = slot_projections.unwrap_or_default();
299            let when_true_html = render_children(
300                model,
301                &templates,
302                &children,
303                &targets,
304                &bindings,
305                &ResumeHtmlMarkers::default(),
306                &instance.id,
307                template,
308                &conditional.when_true,
309                &format!("{path}.true"),
310                &slot_projections,
311            );
312            let when_false_html = render_children(
313                model,
314                &templates,
315                &children,
316                &targets,
317                &bindings,
318                &ResumeHtmlMarkers::default(),
319                &instance.id,
320                template,
321                &conditional.when_false,
322                &format!("{path}.false"),
323                &slot_projections,
324            );
325            let (when_true_html, when_false_html) = match host_scope {
326                StructuralConditionalHostScope::StaticInstance => (when_true_html, when_false_html),
327                StructuralConditionalHostScope::StructuralOccurrence => (
328                    when_true_html
329                        .replace(instance.id.as_str(), "__PRESOLVE_STRUCTURAL_OCCURRENCE__"),
330                    when_false_html
331                        .replace(instance.id.as_str(), "__PRESOLVE_STRUCTURAL_OCCURRENCE__"),
332                ),
333            };
334            Some(StructuralConditionalHostFragments {
335                host_scope,
336                host_instance: instance.id.clone(),
337                when_true_invocations: structural_invocations_in_compiler_html(&when_true_html),
338                when_false_invocations: structural_invocations_in_compiler_html(&when_false_html),
339                slot_projection_bindings: model
340                    .slot_bindings
341                    .for_callee(&instance.id)
342                    .into_iter()
343                    .filter(|binding| binding.status == SlotBindingStatus::Bound)
344                    .map(|binding| binding.id.to_string())
345                    .collect(),
346                when_true_html,
347                when_false_html,
348            })
349        })
350        .collect()
351}
352
353/// Render keyed item templates with compiler-issued structural invocation
354/// anchors. The generic keyed tokens remain owned by `html_codegen`; this
355/// function only joins exact template nodes to planned component invocations.
356#[must_use]
357pub fn generate_structural_keyed_host_fragments(
358    model: &ApplicationSemanticModel,
359    region: &crate::ComponentStructuralRegionId,
360) -> Vec<StructuralKeyedHostFragment> {
361    let Some(entity) =
362        crate::structural_template_entity_for_region(region, &model.template_entities)
363    else {
364        return Vec::new();
365    };
366    if entity.kind != TemplateSemanticKind::List {
367        return Vec::new();
368    }
369    let Some(template_id) = entity.owner.entity_id() else {
370        return Vec::new();
371    };
372    let Some(template) = model
373        .templates
374        .iter()
375        .find(|template| template.id == *template_id)
376    else {
377        return Vec::new();
378    };
379    let Some(component) = template.owner.entity_id() else {
380        return Vec::new();
381    };
382    let Some((list, path)) = list_at_span(template, entity.provenance.span) else {
383        return Vec::new();
384    };
385    let registry = build_ordinary_template_instance_registry(model);
386    let targets = registry
387        .targets
388        .iter()
389        .map(|record| {
390            (
391                (
392                    record.component_instance_id.clone(),
393                    record.template_entity_id.clone(),
394                ),
395                record.target_id.to_string(),
396            )
397        })
398        .collect::<BTreeMap<_, _>>();
399    let bindings = registry
400        .bindings
401        .iter()
402        .map(|record| {
403            (
404                (
405                    record.component_instance_id.clone(),
406                    record.declaration_binding_id.clone(),
407                ),
408                record.instance_binding_id.to_string(),
409            )
410        })
411        .collect::<BTreeMap<_, _>>();
412    let children = model
413        .component_instance_plan
414        .instances
415        .values()
416        .filter_map(|child| {
417            child.parent_instance.as_ref().and_then(|parent| {
418                child
419                    .invocation
420                    .as_ref()
421                    .map(|invocation| ((parent.clone(), invocation.clone()), child))
422            })
423        })
424        .collect::<BTreeMap<_, _>>();
425    let templates = model
426        .templates
427        .iter()
428        .map(|candidate| (candidate.id.clone(), candidate))
429        .collect::<BTreeMap<_, _>>();
430
431    model
432        .component_instance_plan
433        .instances
434        .values()
435        .filter(|instance| {
436            matches!(
437                instance.status,
438                ComponentInstanceStatus::Planned | ComponentInstanceStatus::StructuralTemplate
439            ) && instance.component == *component
440        })
441        .filter_map(|instance| {
442            let host_scope = if instance.status == ComponentInstanceStatus::Planned {
443                StructuralConditionalHostScope::StaticInstance
444            } else {
445                StructuralConditionalHostScope::StructuralOccurrence
446            };
447            let slot_projections =
448                structural_instance_slot_projections(model, &templates, instance);
449            if !model.slot_bindings.for_callee(&instance.id).is_empty()
450                && slot_projections.is_none()
451            {
452                return None;
453            }
454            let slot_projections = slot_projections.unwrap_or_default();
455            let mut html = qualify_keyed_item_node_ids(&render_children(
456                model,
457                &templates,
458                &children,
459                &targets,
460                &bindings,
461                &ResumeHtmlMarkers::default(),
462                &instance.id,
463                template,
464                &list.item_template,
465                &format!("{path}.item"),
466                &slot_projections,
467            ));
468            if instance.status == ComponentInstanceStatus::StructuralTemplate {
469                html = html.replace(instance.id.as_str(), "__PRESOLVE_STRUCTURAL_OCCURRENCE__");
470            }
471            Some(StructuralKeyedHostFragment {
472                host_scope,
473                host_instance: instance.id.clone(),
474                item_invocations: structural_invocations_in_compiler_html(&html),
475                slot_projection_bindings: model
476                    .slot_bindings
477                    .for_callee(&instance.id)
478                    .into_iter()
479                    .filter(|binding| binding.status == SlotBindingStatus::Bound)
480                    .map(|binding| binding.id.to_string())
481                    .collect(),
482                item_template_html: html,
483            })
484        })
485        .collect()
486}
487
488fn qualify_keyed_item_node_ids(html: &str) -> String {
489    const MARKER: &str = "data-presolve-node=\"";
490    let mut output = String::new();
491    let mut remaining = html;
492    while let Some(index) = remaining.find(MARKER) {
493        output.push_str(&remaining[..index + MARKER.len()]);
494        let value = &remaining[index + MARKER.len()..];
495        let Some(end) = value.find('"') else {
496            return html.to_string();
497        };
498        output.push_str(&value[..end]);
499        output.push_str(":__ez_list_key__");
500        output.push('"');
501        remaining = &value[end + 1..];
502    }
503    output.push_str(remaining);
504    output
505}
506
507fn structural_instance_slot_projections<'a>(
508    model: &'a ApplicationSemanticModel,
509    templates: &'a BTreeMap<crate::SemanticId, &'a TemplateNode>,
510    instance: &'a crate::ComponentInstance,
511) -> Option<Vec<SlotProjection<'a>>> {
512    if model.slot_bindings.for_callee(&instance.id).is_empty() {
513        return Some(Vec::new());
514    }
515    let parent = instance.parent_instance.as_ref()?;
516    let invocation = instance.invocation.as_ref()?;
517    let parent_instance = model.component_instance_plan.instances.get(parent)?;
518    let parent_template = templates.get(&parent_instance.component.template())?;
519    let invocation_entity = model
520        .component_invocations
521        .get(invocation)?
522        .template_entity
523        .clone();
524    let (element, path) = element_for_template_entity(parent_template, &invocation_entity)?;
525    Some(slot_projections_for_invocation(
526        model,
527        &instance.id,
528        parent,
529        parent_template,
530        element,
531        &path,
532        &[],
533    ))
534}
535
536fn element_for_template_entity<'a>(
537    template: &'a TemplateNode,
538    target: &crate::SemanticId,
539) -> Option<(&'a ElementNode, String)> {
540    fn visit<'a>(
541        template: &'a TemplateNode,
542        target: &crate::SemanticId,
543        nodes: &'a [TemplateChild],
544        parent_path: &str,
545    ) -> Option<(&'a ElementNode, String)> {
546        for (index, node) in nodes.iter().enumerate() {
547            let path = format!("{parent_path}.{index}");
548            match node {
549                TemplateChild::Element(element) => {
550                    if template.id.template_entity("element", &path) == *target {
551                        return Some((element, path));
552                    }
553                    if let Some(found) = visit(template, target, &element.children, &path) {
554                        return Some(found);
555                    }
556                }
557                TemplateChild::Fragment(fragment) => {
558                    if let Some(found) = visit(template, target, &fragment.children, &path) {
559                        return Some(found);
560                    }
561                }
562                TemplateChild::Conditional(conditional) => {
563                    if let Some(found) = visit(
564                        template,
565                        target,
566                        &conditional.when_true,
567                        &format!("{path}.true"),
568                    )
569                    .or_else(|| {
570                        visit(
571                            template,
572                            target,
573                            &conditional.when_false,
574                            &format!("{path}.false"),
575                        )
576                    }) {
577                        return Some(found);
578                    }
579                }
580                TemplateChild::List(list) => {
581                    if let Some(found) = visit(
582                        template,
583                        target,
584                        &list.item_template,
585                        &format!("{path}.item"),
586                    ) {
587                        return Some(found);
588                    }
589                }
590                TemplateChild::Text { .. } | TemplateChild::Binding { .. } => {}
591            }
592        }
593        None
594    }
595    if let Some(root) = &template.root {
596        if template.id.template_entity("element", "root") == *target {
597            return Some((root, "root".to_string()));
598        }
599        return visit(template, target, &root.children, "root");
600    }
601    template
602        .root_fragment
603        .as_ref()
604        .and_then(|fragment| visit(template, target, &fragment.children, "root"))
605}
606
607fn conditional_at_span(
608    template: &TemplateNode,
609    span: presolve_parser::SourceSpan,
610) -> Option<(&ConditionalNode, String)> {
611    fn visit<'a>(
612        children: &'a [TemplateChild],
613        parent_path: &str,
614        span: presolve_parser::SourceSpan,
615    ) -> Option<(&'a ConditionalNode, String)> {
616        for (index, child) in children.iter().enumerate() {
617            let path = format!("{parent_path}.{index}");
618            match child {
619                TemplateChild::Conditional(conditional) => {
620                    if conditional.span == span {
621                        return Some((conditional, path));
622                    }
623                    if let Some(found) =
624                        visit(&conditional.when_true, &format!("{path}.true"), span).or_else(|| {
625                            visit(&conditional.when_false, &format!("{path}.false"), span)
626                        })
627                    {
628                        return Some(found);
629                    }
630                }
631                TemplateChild::Element(element) => {
632                    if let Some(found) = visit(&element.children, &path, span) {
633                        return Some(found);
634                    }
635                }
636                TemplateChild::Fragment(fragment) => {
637                    if let Some(found) = visit(&fragment.children, &path, span) {
638                        return Some(found);
639                    }
640                }
641                TemplateChild::List(list) => {
642                    if let Some(found) = visit(&list.item_template, &format!("{path}.item"), span) {
643                        return Some(found);
644                    }
645                }
646                TemplateChild::Text { .. } | TemplateChild::Binding { .. } => {}
647            }
648        }
649        None
650    }
651
652    template
653        .root
654        .as_ref()
655        .and_then(|root| visit(&root.children, "root", span))
656        .or_else(|| {
657            template
658                .root_fragment
659                .as_ref()
660                .and_then(|fragment| visit(&fragment.children, "root", span))
661        })
662}
663
664fn list_at_span(
665    template: &TemplateNode,
666    span: presolve_parser::SourceSpan,
667) -> Option<(&ListNode, String)> {
668    fn visit<'a>(
669        children: &'a [TemplateChild],
670        parent_path: &str,
671        span: presolve_parser::SourceSpan,
672    ) -> Option<(&'a ListNode, String)> {
673        for (index, child) in children.iter().enumerate() {
674            let path = format!("{parent_path}.{index}");
675            match child {
676                TemplateChild::List(list) => {
677                    if list.span == span {
678                        return Some((list, path));
679                    }
680                    if let Some(found) = visit(&list.item_template, &format!("{path}.item"), span) {
681                        return Some(found);
682                    }
683                }
684                TemplateChild::Element(element) => {
685                    if let Some(found) = visit(&element.children, &path, span) {
686                        return Some(found);
687                    }
688                }
689                TemplateChild::Fragment(fragment) => {
690                    if let Some(found) = visit(&fragment.children, &path, span) {
691                        return Some(found);
692                    }
693                }
694                TemplateChild::Conditional(conditional) => {
695                    if let Some(found) =
696                        visit(&conditional.when_true, &format!("{path}.true"), span).or_else(|| {
697                            visit(&conditional.when_false, &format!("{path}.false"), span)
698                        })
699                    {
700                        return Some(found);
701                    }
702                }
703                TemplateChild::Text { .. } | TemplateChild::Binding { .. } => {}
704            }
705        }
706        None
707    }
708
709    template
710        .root
711        .as_ref()
712        .and_then(|root| visit(&root.children, "root", span))
713        .or_else(|| {
714            template
715                .root_fragment
716                .as_ref()
717                .and_then(|fragment| visit(&fragment.children, "root", span))
718        })
719}
720
721fn generate_ordinary_instance_html_for_roots(
722    model: &ApplicationSemanticModel,
723    root_component: Option<&crate::SemanticId>,
724) -> String {
725    let registry = build_ordinary_template_instance_registry(model);
726    let resume = build_resume_anchor_plan(model);
727    let mut resume_markers = ResumeHtmlMarkers::default();
728    for anchor in &resume.anchors {
729        let target = anchor.marker_target_id.to_string();
730        let index = match anchor.placement {
731            ResumeAnchorPlacement::ElementAttribute => &mut resume_markers.element_anchors,
732            ResumeAnchorPlacement::TextTemplate => &mut resume_markers.text_anchors,
733            ResumeAnchorPlacement::StructuralStartComment => &mut resume_markers.structural_starts,
734            ResumeAnchorPlacement::StructuralEndComment => &mut resume_markers.structural_ends,
735        };
736        index.insert(target, anchor.anchor_id.to_string());
737    }
738    for event in &resume.events {
739        resume_markers.events.insert(
740            event.target_id.to_string(),
741            event.resume_event_id.to_string(),
742        );
743    }
744    let targets = registry
745        .targets
746        .iter()
747        .map(|record| {
748            (
749                (
750                    record.component_instance_id.clone(),
751                    record.template_entity_id.clone(),
752                ),
753                record.target_id.to_string(),
754            )
755        })
756        .collect::<BTreeMap<_, _>>();
757    let bindings = registry
758        .bindings
759        .iter()
760        .map(|record| {
761            (
762                (
763                    record.component_instance_id.clone(),
764                    record.declaration_binding_id.clone(),
765                ),
766                record.instance_binding_id.to_string(),
767            )
768        })
769        .collect::<BTreeMap<_, _>>();
770    let children = model
771        .component_instance_plan
772        .instances
773        .values()
774        .filter_map(|instance| {
775            instance.parent_instance.as_ref().and_then(|parent| {
776                instance
777                    .invocation
778                    .as_ref()
779                    .map(|invocation| ((parent.clone(), invocation.clone()), instance))
780            })
781        })
782        .collect::<BTreeMap<_, _>>();
783    let templates = model
784        .templates
785        .iter()
786        .map(|template| (template.id.clone(), template))
787        .collect::<BTreeMap<_, _>>();
788    model
789        .component_instance_plan
790        .instances
791        .values()
792        .filter(|instance| {
793            instance.parent_instance.is_none()
794                && instance.status == ComponentInstanceStatus::Planned
795                && root_component.is_none_or(|component| instance.component == *component)
796        })
797        .map(|instance| {
798            render_instance(
799                model,
800                &templates,
801                &children,
802                &targets,
803                &bindings,
804                &resume_markers,
805                &instance.id,
806                &instance.component,
807                &[],
808            )
809        })
810        .collect::<Vec<_>>()
811        .join("\n")
812}
813
814#[allow(clippy::too_many_arguments)]
815fn render_instance(
816    model: &ApplicationSemanticModel,
817    templates: &BTreeMap<crate::SemanticId, &TemplateNode>,
818    children: &BTreeMap<
819        (ComponentInstanceId, crate::ComponentInvocationId),
820        &crate::ComponentInstance,
821    >,
822    targets: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
823    bindings: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
824    resume_markers: &ResumeHtmlMarkers,
825    instance: &ComponentInstanceId,
826    component: &crate::SemanticId,
827    slot_projections: &[SlotProjection<'_>],
828) -> String {
829    let mut instance_slot_projections = slot_projections.to_vec();
830    instance_slot_projections.extend(direct_layout_slot_projections(model, instance));
831    let Some(template) = templates.get(&component.template()) else {
832        return String::new();
833    };
834    if let Some(root) = &template.root {
835        return render_element(
836            model,
837            templates,
838            children,
839            targets,
840            bindings,
841            resume_markers,
842            instance,
843            template,
844            root,
845            "root",
846            &instance_slot_projections,
847        );
848    }
849    template
850        .root_fragment
851        .as_ref()
852        .map_or_else(String::new, |fragment| {
853            render_fragment(
854                model,
855                templates,
856                children,
857                targets,
858                bindings,
859                resume_markers,
860                instance,
861                template,
862                fragment,
863                "root",
864                &instance_slot_projections,
865            )
866        })
867}
868
869#[allow(clippy::too_many_arguments)]
870fn render_fragment(
871    model: &ApplicationSemanticModel,
872    templates: &BTreeMap<crate::SemanticId, &TemplateNode>,
873    children: &BTreeMap<
874        (ComponentInstanceId, crate::ComponentInvocationId),
875        &crate::ComponentInstance,
876    >,
877    targets: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
878    bindings: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
879    resume_markers: &ResumeHtmlMarkers,
880    instance: &ComponentInstanceId,
881    template: &TemplateNode,
882    fragment: &FragmentNode,
883    path: &str,
884    slot_projections: &[SlotProjection<'_>],
885) -> String {
886    render_children(
887        model,
888        templates,
889        children,
890        targets,
891        bindings,
892        resume_markers,
893        instance,
894        template,
895        &fragment.children,
896        path,
897        slot_projections,
898    )
899}
900
901#[allow(clippy::too_many_arguments)]
902fn render_children(
903    model: &ApplicationSemanticModel,
904    templates: &BTreeMap<crate::SemanticId, &TemplateNode>,
905    children: &BTreeMap<
906        (ComponentInstanceId, crate::ComponentInvocationId),
907        &crate::ComponentInstance,
908    >,
909    targets: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
910    bindings: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
911    resume_markers: &ResumeHtmlMarkers,
912    instance: &ComponentInstanceId,
913    template: &TemplateNode,
914    nodes: &[TemplateChild],
915    parent_path: &str,
916    slot_projections: &[SlotProjection<'_>],
917) -> String {
918    nodes
919        .iter()
920        .enumerate()
921        .map(|(index, node)| {
922            render_child(
923                model,
924                templates,
925                children,
926                targets,
927                bindings,
928                resume_markers,
929                instance,
930                template,
931                node,
932                &format!("{parent_path}.{index}"),
933                slot_projections,
934            )
935        })
936        .collect()
937}
938
939#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
940fn render_child(
941    model: &ApplicationSemanticModel,
942    templates: &BTreeMap<crate::SemanticId, &TemplateNode>,
943    children: &BTreeMap<
944        (ComponentInstanceId, crate::ComponentInvocationId),
945        &crate::ComponentInstance,
946    >,
947    targets: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
948    bindings: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
949    resume_markers: &ResumeHtmlMarkers,
950    instance: &ComponentInstanceId,
951    template: &TemplateNode,
952    node: &TemplateChild,
953    path: &str,
954    slot_projections: &[SlotProjection<'_>],
955) -> String {
956    match node {
957        TemplateChild::Text { value, .. } => escape_text(value),
958        TemplateChild::Binding {
959            expression,
960            initial_value,
961            ..
962        } => {
963            let entity = template.id.template_entity("binding", path);
964            let value = initial_value
965                .as_ref()
966                .map_or_else(String::new, SerializableValue::render_text);
967            let marker_target = targets.get(&(instance.clone(), entity.clone()));
968            let resume_marker = marker_target
969                .and_then(|target| resume_markers.text_anchors.get(target))
970                .map_or_else(String::new, |anchor| {
971                    format!(
972                        "<template data-presolve-r=\"{}\"></template>",
973                        escape_attr(anchor)
974                    )
975                });
976            bindings.get(&(instance.clone(), entity)).map_or_else(
977                || format!("{resume_marker}<!-- presolve-binding:{expression} -->{value}"),
978                |id| format!("{resume_marker}<!--presolve-ti-binding-start:{id}-->{value}<!--presolve-ti-binding-end:{id}-->"),
979            )
980        }
981        TemplateChild::Element(element) => render_element(
982            model,
983            templates,
984            children,
985            targets,
986            bindings,
987            resume_markers,
988            instance,
989            template,
990            element,
991            path,
992            slot_projections,
993        ),
994        TemplateChild::Fragment(fragment) => render_fragment(
995            model,
996            templates,
997            children,
998            targets,
999            bindings,
1000            resume_markers,
1001            instance,
1002            template,
1003            fragment,
1004            path,
1005            slot_projections,
1006        ),
1007        TemplateChild::Conditional(conditional) => {
1008            let entity = template.id.template_entity("conditional", path);
1009            let marker = targets
1010                .get(&(instance.clone(), entity))
1011                .map_or("", String::as_str);
1012            let resume_start = resume_markers
1013                .structural_starts
1014                .get(marker)
1015                .map_or("", String::as_str);
1016            let resume_end = resume_markers
1017                .structural_ends
1018                .get(marker)
1019                .map_or("", String::as_str);
1020            let selected_true = matches!(
1021                conditional.initial_value,
1022                Some(SerializableValue::Boolean(true))
1023            );
1024            let selected = if selected_true {
1025                &conditional.when_true
1026            } else {
1027                &conditional.when_false
1028            };
1029            let selected_path = format!("{path}.{}", if selected_true { "true" } else { "false" });
1030            format!(
1031                "<!--presolve-r-start:{}--><!--presolve-conditional-start:{}:ti:{}-->{}<!--presolve-conditional-end:{}:ti:{}--><!--presolve-r-end:{}-->",
1032                escape_comment(resume_start),
1033                conditional.start_id.0,
1034                marker,
1035                render_children(
1036                    model,
1037                    templates,
1038                    children,
1039                    targets,
1040                    bindings,
1041                    resume_markers,
1042                    instance,
1043                    template,
1044                    selected,
1045                    &selected_path,
1046                    slot_projections,
1047                ),
1048                conditional.end_id.0,
1049                marker,
1050                escape_comment(resume_end)
1051            )
1052        }
1053        TemplateChild::List(list) => {
1054            let entity = template.id.template_entity("list", path);
1055            let marker = targets
1056                .get(&(instance.clone(), entity))
1057                .map_or("", String::as_str);
1058            let resume_start = resume_markers
1059                .structural_starts
1060                .get(marker)
1061                .map_or("", String::as_str);
1062            let resume_end = resume_markers
1063                .structural_ends
1064                .get(marker)
1065                .map_or("", String::as_str);
1066            format!(
1067                "<!--presolve-r-start:{}--><!--presolve-ti-target-start:{marker}-->{}<!--presolve-ti-target-end:{marker}--><!--presolve-r-end:{}-->",
1068                escape_comment(resume_start),
1069                crate::html_codegen::generate_list_html(list),
1070                escape_comment(resume_end),
1071            )
1072        }
1073    }
1074}
1075
1076#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
1077fn render_element(
1078    model: &ApplicationSemanticModel,
1079    templates: &BTreeMap<crate::SemanticId, &TemplateNode>,
1080    children: &BTreeMap<
1081        (ComponentInstanceId, crate::ComponentInvocationId),
1082        &crate::ComponentInstance,
1083    >,
1084    targets: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
1085    bindings: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
1086    resume_markers: &ResumeHtmlMarkers,
1087    instance: &ComponentInstanceId,
1088    template: &TemplateNode,
1089    element: &ElementNode,
1090    path: &str,
1091    slot_projections: &[SlotProjection<'_>],
1092) -> String {
1093    let entity = template.id.template_entity("element", path);
1094    if element.tag_name == "slot" {
1095        if let Some(projection) = slot_projections
1096            .iter()
1097            .find(|projection| projection.outlet_entity() == &entity)
1098        {
1099            return render_slot_projection(
1100                model,
1101                templates,
1102                children,
1103                targets,
1104                bindings,
1105                resume_markers,
1106                projection,
1107            );
1108        }
1109        if let Some(outlet) = model
1110            .slot_outlets
1111            .values()
1112            .find(|outlet| outlet.template_entity == entity)
1113        {
1114            let empty = model
1115                .slot_bindings
1116                .for_callee(instance)
1117                .into_iter()
1118                .any(|binding| {
1119                    binding.outlet.as_ref() == Some(&outlet.id)
1120                        && binding.status == SlotBindingStatus::Empty
1121                });
1122            if empty {
1123                return String::new();
1124            }
1125        }
1126    }
1127    if let Some(invocation) = model
1128        .component_invocations
1129        .values()
1130        .find(|candidate| candidate.template_entity == entity)
1131    {
1132        if let Some(child) = children.get(&(instance.clone(), invocation.id.clone())) {
1133            if child.status == ComponentInstanceStatus::Planned {
1134                let projections = slot_projections_for_invocation(
1135                    model,
1136                    &child.id,
1137                    instance,
1138                    template,
1139                    element,
1140                    path,
1141                    slot_projections,
1142                );
1143                return render_instance(
1144                    model,
1145                    templates,
1146                    children,
1147                    targets,
1148                    bindings,
1149                    resume_markers,
1150                    &child.id,
1151                    &child.component,
1152                    &projections,
1153                );
1154            }
1155        }
1156    }
1157    let structural_invocation = model
1158        .component_invocations
1159        .values()
1160        .find(|candidate| candidate.template_entity == entity)
1161        .and_then(|invocation| {
1162            children
1163                .get(&(instance.clone(), invocation.id.clone()))
1164                .filter(|child| child.status == ComponentInstanceStatus::StructuralTemplate)
1165                .map(|_| invocation.id.as_str())
1166        });
1167    let mut html = format!(
1168        "<{} data-presolve-node=\"{}\"",
1169        element.tag_name,
1170        escape_attr(&element.id.0)
1171    );
1172    if let Some(target) = targets.get(&(instance.clone(), entity)) {
1173        html.push_str(" data-presolve-ti=\"");
1174        html.push_str(&escape_attr(target));
1175        html.push('"');
1176        if let Some(anchor) = resume_markers.element_anchors.get(target) {
1177            html.push_str(" data-presolve-r=\"");
1178            html.push_str(&escape_attr(anchor));
1179            html.push('"');
1180        }
1181        if let Some(event) = resume_markers.events.get(target) {
1182            html.push_str(" data-presolve-e=\"");
1183            html.push_str(&escape_attr(event));
1184            html.push('"');
1185        }
1186    }
1187    if let Some(invocation) = structural_invocation {
1188        html.push_str(" data-presolve-structural-invocation=\"");
1189        html.push_str(&escape_attr(invocation));
1190        html.push('"');
1191    }
1192    for attribute in &element.attributes {
1193        html.push(' ');
1194        html.push_str(&attribute_html(attribute));
1195    }
1196    html.push('>');
1197    html.push_str(&render_children(
1198        model,
1199        templates,
1200        children,
1201        targets,
1202        bindings,
1203        resume_markers,
1204        instance,
1205        template,
1206        &element.children,
1207        path,
1208        slot_projections,
1209    ));
1210    html.push_str("</");
1211    html.push_str(&element.tag_name);
1212    html.push('>');
1213    html
1214}
1215
1216#[allow(clippy::similar_names, clippy::too_many_arguments)]
1217fn slot_projections_for_invocation<'a>(
1218    model: &'a ApplicationSemanticModel,
1219    callee_instance: &ComponentInstanceId,
1220    caller_instance: &ComponentInstanceId,
1221    caller_template: &'a TemplateNode,
1222    invocation_element: &'a ElementNode,
1223    invocation_path: &str,
1224    caller_slot_projections: &'a [SlotProjection<'a>],
1225) -> Vec<SlotProjection<'a>> {
1226    model
1227        .slot_bindings
1228        .for_callee(callee_instance)
1229        .into_iter()
1230        .filter(|binding| binding.status == SlotBindingStatus::Bound)
1231        .filter_map(|binding| {
1232            let outlet = model.slot_outlets.get(binding.outlet.as_ref()?)?;
1233            let fragment = model
1234                .slot_content_fragments
1235                .get(binding.content_fragment.as_ref()?)?;
1236            Some(SlotProjection::Authored {
1237                outlet_entity: outlet.template_entity.clone(),
1238                caller_instance: caller_instance.clone(),
1239                caller_template,
1240                invocation_element,
1241                invocation_path: invocation_path.to_string(),
1242                content_entities: fragment.content_template_entities.iter().cloned().collect(),
1243                caller_slot_projections,
1244            })
1245        })
1246        .collect()
1247}
1248
1249impl SlotProjection<'_> {
1250    fn outlet_entity(&self) -> &crate::SemanticId {
1251        match self {
1252            Self::Authored { outlet_entity, .. }
1253            | Self::DirectLayoutChild { outlet_entity, .. } => outlet_entity,
1254        }
1255    }
1256}
1257
1258fn direct_layout_slot_projections(
1259    model: &ApplicationSemanticModel,
1260    instance: &ComponentInstanceId,
1261) -> Vec<SlotProjection<'static>> {
1262    model
1263        .slot_bindings
1264        .for_callee(instance)
1265        .into_iter()
1266        .filter(|binding| binding.status == SlotBindingStatus::Bound)
1267        .filter_map(|binding| {
1268            let child_instance = binding.direct_child_instance.clone()?;
1269            let outlet = model.slot_outlets.get(binding.outlet.as_ref()?)?;
1270            Some(SlotProjection::DirectLayoutChild {
1271                outlet_entity: outlet.template_entity.clone(),
1272                child_instance,
1273            })
1274        })
1275        .collect()
1276}
1277
1278#[allow(clippy::too_many_arguments)]
1279fn render_slot_projection(
1280    model: &ApplicationSemanticModel,
1281    templates: &BTreeMap<crate::SemanticId, &TemplateNode>,
1282    children: &BTreeMap<
1283        (ComponentInstanceId, crate::ComponentInvocationId),
1284        &crate::ComponentInstance,
1285    >,
1286    targets: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
1287    bindings: &BTreeMap<(ComponentInstanceId, crate::SemanticId), String>,
1288    resume_markers: &ResumeHtmlMarkers,
1289    projection: &SlotProjection<'_>,
1290) -> String {
1291    if let SlotProjection::DirectLayoutChild { child_instance, .. } = projection {
1292        let Some(child) = model.component_instance_plan.instances.get(child_instance) else {
1293            return String::new();
1294        };
1295        return render_instance(
1296            model,
1297            templates,
1298            children,
1299            targets,
1300            bindings,
1301            resume_markers,
1302            &child.id,
1303            &child.component,
1304            &[],
1305        );
1306    }
1307    let SlotProjection::Authored {
1308        caller_instance,
1309        caller_template,
1310        invocation_element,
1311        invocation_path,
1312        content_entities,
1313        caller_slot_projections,
1314        ..
1315    } = projection
1316    else {
1317        unreachable!("direct layout Slot projection returns above");
1318    };
1319    let mut html = String::new();
1320    for (index, child) in invocation_element.children.iter().enumerate() {
1321        let path = format!("{invocation_path}.{index}");
1322        let entity = template_child_entity(&caller_template.id, child, &path);
1323        if content_entities.contains(&entity) {
1324            html.push_str(&render_child(
1325                model,
1326                templates,
1327                children,
1328                targets,
1329                bindings,
1330                resume_markers,
1331                caller_instance,
1332                caller_template,
1333                child,
1334                &path,
1335                caller_slot_projections,
1336            ));
1337            continue;
1338        }
1339        let TemplateChild::Element(wrapper) = child else {
1340            continue;
1341        };
1342        if wrapper.tag_name != "template" {
1343            continue;
1344        }
1345        for (child_index, wrapped) in wrapper.children.iter().enumerate() {
1346            let wrapped_path = format!("{path}.{child_index}");
1347            let wrapped_entity = template_child_entity(&caller_template.id, wrapped, &wrapped_path);
1348            if content_entities.contains(&wrapped_entity) {
1349                html.push_str(&render_child(
1350                    model,
1351                    templates,
1352                    children,
1353                    targets,
1354                    bindings,
1355                    resume_markers,
1356                    caller_instance,
1357                    caller_template,
1358                    wrapped,
1359                    &wrapped_path,
1360                    caller_slot_projections,
1361                ));
1362            }
1363        }
1364    }
1365    html
1366}
1367
1368fn template_child_entity(
1369    template: &crate::SemanticId,
1370    child: &TemplateChild,
1371    path: &str,
1372) -> crate::SemanticId {
1373    template.template_entity(
1374        match child {
1375            TemplateChild::Text { .. } => "text",
1376            TemplateChild::Binding { .. } => "binding",
1377            TemplateChild::Element(_) => "element",
1378            TemplateChild::Fragment(_) => "fragment",
1379            TemplateChild::Conditional(_) => "conditional",
1380            TemplateChild::List(_) => "list",
1381        },
1382        path,
1383    )
1384}
1385
1386fn attribute_html(attribute: &TemplateAttribute) -> String {
1387    let value = match &attribute.value {
1388        AttributeValue::Boolean => return attribute.name.clone(),
1389        AttributeValue::Static(value) => value.clone(),
1390        AttributeValue::Binding { initial_value, .. } => initial_value
1391            .as_ref()
1392            .map_or_else(String::new, SerializableValue::render_text),
1393        AttributeValue::EventHandler { handler, .. } => handler.clone(),
1394        AttributeValue::BindingList(values) => values.join(","),
1395    };
1396    format!("{}=\"{}\"", attribute.name, escape_attr(&value))
1397}
1398
1399fn escape_attr(value: &str) -> String {
1400    value
1401        .replace('&', "&amp;")
1402        .replace('"', "&quot;")
1403        .replace('<', "&lt;")
1404        .replace('>', "&gt;")
1405}
1406
1407fn escape_comment(value: &str) -> String {
1408    value.replace("--", "&#45;&#45;")
1409}
1410fn escape_text(value: &str) -> String {
1411    value
1412        .replace('&', "&amp;")
1413        .replace('<', "&lt;")
1414        .replace('>', "&gt;")
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419    use super::{
1420        generate_ordinary_instance_html, generate_ordinary_instance_html_for_component,
1421        generate_structural_conditional_host_fragments, generate_structural_keyed_host_fragments,
1422        generate_structural_template_instance_html, StructuralConditionalHostScope,
1423    };
1424    use crate::{
1425        build_application_semantic_model, build_resume_anchor_plan, validate_resume_marker_html,
1426    };
1427
1428    #[test]
1429    fn emits_precomputed_repeated_instance_and_exact_resume_markers() {
1430        let model = build_application_semantic_model(&presolve_parser::parse_file(
1431            "src/Markers.tsx",
1432            r#"
1433@component("x-child") class Child {
1434  count = state(0);
1435  @action() increment() { this.count++; }
1436  render() { return <button onClick={() => this.increment()}>{this.count}</button>; }
1437}
1438@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }
1439"#,
1440        ));
1441        let html = generate_ordinary_instance_html(&model);
1442        assert_eq!(html.matches("data-presolve-ti=").count(), 2);
1443        assert_eq!(html.matches("presolve-ti-binding-start:").count(), 2);
1444        assert_eq!(html.matches("data-presolve-r=").count(), 4);
1445        assert_eq!(html.matches("data-presolve-e=").count(), 2);
1446        assert_eq!(html, generate_ordinary_instance_html(&model));
1447    }
1448
1449    #[test]
1450    fn renders_structural_conditional_slot_projection_from_the_canonical_binding() {
1451        let model = build_application_semantic_model(&presolve_parser::parse_file(
1452            "src/StructuralSlot.tsx",
1453            r#"
1454@component("x-panel") class Panel extends Component {
1455  @slot() children!: SlotContent;
1456  open = state(true);
1457  render() { return <section>{this.open ? <><slot /><Leaf /></> : <aside>Closed</aside>}</section>; }
1458}
1459@component("x-leaf") class Leaf extends Component { render() { return <small>Leaf</small>; } }
1460@component("x-page") class Page extends Component {
1461  visible = state(true);
1462  render() { return <main>{this.visible ? <Panel><button>Projected</button></Panel> : <aside>Hidden</aside>}</main>; }
1463}
1464"#,
1465        ));
1466        let region = model
1467            .component_instance_plan
1468            .instances
1469            .values()
1470            .find(|instance| instance.component.as_str().contains("component:x-leaf"))
1471            .and_then(|instance| instance.structural_region.clone())
1472            .expect("Panel conditional has a structural region");
1473        let fragments = generate_structural_conditional_host_fragments(&model, &region);
1474        let projected = fragments
1475            .iter()
1476            .find(|fragment| {
1477                fragment.host_scope == StructuralConditionalHostScope::StructuralOccurrence
1478            })
1479            .expect("structural Panel host fragment is emitted");
1480        assert!(projected.when_true_html.contains("Projected"));
1481        let panel = model
1482            .component_instance_plan
1483            .instances
1484            .values()
1485            .find(|instance| instance.component.as_str().contains("component:x-panel"))
1486            .expect("structural Panel instance");
1487        let panel_html = generate_structural_template_instance_html(&model, &panel.id)
1488            .expect("structural Panel template HTML");
1489        assert!(panel_html.contains("Projected"));
1490    }
1491
1492    #[test]
1493    fn renders_structural_keyed_slot_projection_from_the_canonical_binding() {
1494        let model = build_application_semantic_model(&presolve_parser::parse_file(
1495            "src/StructuralKeyedSlot.tsx",
1496            r#"
1497@component("x-panel") class Panel extends Component {
1498  @slot() children!: SlotContent;
1499  items = state([{ id: "a" }]);
1500  render() { return <ul>{this.items.map(item => <li key={item.id}><slot /><Leaf /></li>)}</ul>; }
1501}
1502@component("x-leaf") class Leaf extends Component { render() { return <small>Leaf</small>; } }
1503@component("x-page") class Page extends Component {
1504  visible = state(true);
1505  render() { return <main>{this.visible ? <Panel><button>Projected</button></Panel> : <aside>Hidden</aside>}</main>; }
1506}
1507"#,
1508        ));
1509        let region = model
1510            .component_instance_plan
1511            .instances
1512            .values()
1513            .find(|instance| instance.component.as_str().contains("component:x-leaf"))
1514            .and_then(|instance| instance.structural_region.clone())
1515            .expect("Panel keyed host has a structural region");
1516        let fragments = generate_structural_keyed_host_fragments(&model, &region);
1517        let projected = fragments
1518            .iter()
1519            .find(|fragment| {
1520                fragment.host_scope == StructuralConditionalHostScope::StructuralOccurrence
1521            })
1522            .expect("structural Panel keyed host fragment is emitted");
1523        assert!(projected.item_template_html.contains("Projected"));
1524        assert!(projected
1525            .item_template_html
1526            .contains("data-presolve-node=\""));
1527        assert!(projected.item_template_html.contains(":__ez_list_key__"));
1528    }
1529
1530    #[test]
1531    fn places_caller_owned_slot_content_at_the_exact_compiler_outlet() {
1532        let model = build_application_semantic_model(&presolve_parser::parse_file(
1533            "src/Slots.tsx",
1534            r#"
1535@component("x-leaf") class Leaf {
1536  count = state(0);
1537  @action() increment() { this.count++; }
1538  render() { return <button onClick={() => this.increment()}>{this.count}</button>; }
1539}
1540@component("x-card") class Card {
1541  @slot() children!: SlotContent;
1542  render() { return <article><slot /><Leaf /></article>; }
1543}
1544@component("x-page") class Page { render() { return <main><Card><Leaf /></Card></main>; } }
1545"#,
1546        ));
1547        let html = generate_ordinary_instance_html(&model);
1548        let resume = build_resume_anchor_plan(&model);
1549
1550        assert_eq!(html.matches("<button").count(), 2);
1551        assert!(!html.contains("<slot"));
1552        assert_eq!(html.matches("presolve-ti-binding-start:").count(), 2);
1553        assert!(
1554            validate_resume_marker_html(&resume, &html).is_empty(),
1555            "caller-owned Slot content must carry every required resume marker"
1556        );
1557    }
1558
1559    #[test]
1560    fn renders_only_the_selected_entry_component_tree() {
1561        let model = build_application_semantic_model(&presolve_parser::parse_file(
1562            "src/Entries.tsx",
1563            r#"
1564@component("x-home") class Home { render() { return <main>Home</main>; } }
1565@component("x-about") class About { render() { return <main>About</main>; } }
1566"#,
1567        ));
1568        let home = model
1569            .components
1570            .iter()
1571            .find(|component| component.class_name == "Home")
1572            .unwrap();
1573
1574        let html = generate_ordinary_instance_html_for_component(&model, &home.id);
1575
1576        assert!(html.contains("Home"));
1577        assert!(!html.contains("About"));
1578    }
1579
1580    #[test]
1581    fn preserves_conditional_template_paths_for_structural_invocations() {
1582        let model = build_application_semantic_model(&presolve_parser::parse_file(
1583            "src/StructuralMarker.tsx",
1584            r#"
1585@component("x-leaf") class Leaf { count = state(0); render() { return <strong>{this.count}</strong>; } }
1586@component("x-page") class Page {
1587  visible = state(true);
1588  render() { return <main>{this.visible ? <Leaf /> : <span>Hidden</span>}</main>; }
1589}
1590"#,
1591        ));
1592        let html = generate_ordinary_instance_html(&model);
1593        assert!(html.contains("data-presolve-node"));
1594        assert!(!html.contains("componentByTag"));
1595        assert!(html.contains("data-presolve-structural-invocation="));
1596    }
1597
1598    #[test]
1599    fn renders_both_compiler_addressed_conditional_host_fragments() {
1600        let model = build_application_semantic_model(&presolve_parser::parse_file(
1601            "src/StructuralHostFragments.tsx",
1602            r#"
1603@component("x-leaf") class Leaf { count = state(0); render() { return <strong>{this.count}</strong>; } }
1604@component("x-page") class Page {
1605  visible = state(true);
1606  render() { return <main>{this.visible ? <Leaf /> : <span>Hidden</span>}</main>; }
1607}
1608"#,
1609        ));
1610        let region = model
1611            .component_instance_plan
1612            .instances
1613            .values()
1614            .find_map(|instance| instance.structural_region.as_ref())
1615            .expect("conditional leaf is a structural template");
1616
1617        let fragments = generate_structural_conditional_host_fragments(&model, region);
1618
1619        assert_eq!(fragments.len(), 1);
1620        assert!(fragments[0]
1621            .when_true_html
1622            .contains("data-presolve-structural-invocation="));
1623        assert!(fragments[0].when_false_html.contains("Hidden"));
1624        assert!(fragments[0].when_true_html.contains("data-presolve-node"));
1625    }
1626
1627    #[test]
1628    fn renders_nested_conditional_hosts_against_the_opaque_parent_occurrence() {
1629        let model = build_application_semantic_model(&presolve_parser::parse_file(
1630            "src/NestedStructuralHostFragments.tsx",
1631            r#"
1632@component("x-leaf") class Leaf { render() { return <small />; } }
1633@component("x-card") class Card {
1634  expanded = state(true);
1635  render() { return <article>{this.expanded ? <section><Leaf />{this.expanded}</section> : <em>Collapsed</em>}</article>; }
1636}
1637@component("x-page") class Page {
1638  shown = state(true);
1639  render() { return <main>{this.shown ? <Card /> : <span>Hidden</span>}</main>; }
1640}
1641"#,
1642        ));
1643
1644        let fragments = model
1645            .component_instance_plan
1646            .instances
1647            .values()
1648            .filter_map(|instance| instance.structural_region.clone())
1649            .flat_map(|region| generate_structural_conditional_host_fragments(&model, &region))
1650            .find(|fragments| {
1651                fragments.host_scope == StructuralConditionalHostScope::StructuralOccurrence
1652            })
1653            .expect("nested structural host fragments");
1654
1655        assert!(fragments
1656            .when_true_html
1657            .contains("__PRESOLVE_STRUCTURAL_OCCURRENCE__"));
1658        assert!(fragments
1659            .when_true_html
1660            .contains("data-presolve-structural-invocation="));
1661        assert!(fragments.when_false_html.contains("Collapsed"));
1662    }
1663
1664    #[test]
1665    fn renders_keyed_component_invocations_with_compiler_anchors() {
1666        let model = build_application_semantic_model(&presolve_parser::parse_file(
1667            "src/KeyedStructuralHostFragments.tsx",
1668            r#"
1669@component("x-leaf") class Leaf { render() { return <small />; } }
1670@component("x-page") class Page {
1671  items = state([{ id: "a" }]);
1672  render() { return <ul>{this.items.map(item => <li key={item.id}><Leaf /></li>)}</ul>; }
1673}
1674"#,
1675        ));
1676        let region = model
1677            .component_instance_plan
1678            .instances
1679            .values()
1680            .find_map(|instance| instance.structural_region.as_ref())
1681            .expect("keyed leaf is a structural template");
1682        let fragments = generate_structural_keyed_host_fragments(&model, region);
1683
1684        assert_eq!(fragments.len(), 1);
1685        assert_eq!(
1686            fragments[0].host_scope,
1687            StructuralConditionalHostScope::StaticInstance
1688        );
1689        assert!(fragments[0].item_template_html.contains("__ez_list_key__"));
1690        assert!(fragments[0]
1691            .item_template_html
1692            .contains("data-presolve-structural-invocation="));
1693    }
1694}