1use 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#[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 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#[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 pub slot_projection_bindings: Vec<String>,
60}
61
62#[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#[must_use]
99pub fn generate_ordinary_instance_html(model: &ApplicationSemanticModel) -> String {
100 generate_ordinary_instance_html_for_roots(model, None)
101}
102
103#[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#[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#[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#[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('&', "&")
1402 .replace('"', """)
1403 .replace('<', "<")
1404 .replace('>', ">")
1405}
1406
1407fn escape_comment(value: &str) -> String {
1408 value.replace("--", "--")
1409}
1410fn escape_text(value: &str) -> String {
1411 value
1412 .replace('&', "&")
1413 .replace('<', "<")
1414 .replace('>', ">")
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, ®ion);
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, ®ion);
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, ®ion))
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}