Skip to main content

presolve_compiler/
template_graph.rs

1use crate::component_graph::{
2    ComponentGraph, MethodLocalVariable, RenderAttribute, RenderAttributeValue, RenderChild,
3    RenderConditional, RenderElement, RenderEventHandler, RenderFragment, RenderList, RenderModel,
4    SerializableValue, StateField,
5};
6use presolve_parser::SourceSpan;
7
8use crate::semantic_id::{SemanticId, SemanticOwner};
9use crate::semantic_provenance::SourceProvenance;
10
11#[derive(Debug, Default)]
12struct TemplateIdAllocator {
13    next: usize,
14}
15
16impl TemplateIdAllocator {
17    fn alloc(&mut self) -> TemplateNodeId {
18        let id = TemplateNodeId(format!("n{}", self.next));
19        self.next += 1;
20        id
21    }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct TemplateGraph {
26    pub templates: Vec<TemplateNode>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct TemplateNode {
31    pub id: SemanticId,
32    pub owner: SemanticOwner,
33    pub provenance: SourceProvenance,
34    pub component_name: String,
35    pub root: Option<ElementNode>,
36    pub root_fragment: Option<FragmentNode>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ElementNode {
41    pub id: TemplateNodeId,
42    pub tag_name: String,
43    pub tag_name_span: SourceSpan,
44    pub span: SourceSpan,
45    pub attributes: Vec<TemplateAttribute>,
46    /// Complete normalized authored attributes retained for compiler analyses.
47    /// Backend-facing `attributes` intentionally contains only executable
48    /// template attributes.
49    pub authored_attributes: Vec<RenderAttribute>,
50    pub children: Vec<TemplateChild>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct FragmentNode {
55    pub id: TemplateNodeId,
56    pub span: SourceSpan,
57    pub children: Vec<TemplateChild>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct TemplateAttribute {
62    pub name: String,
63    pub value: AttributeValue,
64    pub span: Option<SourceSpan>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum AttributeValue {
69    Boolean,
70    Static(String),
71    Binding {
72        id: TemplateNodeId,
73        expression: String,
74        initial_value: Option<SerializableValue>,
75    },
76    EventHandler {
77        event: String,
78        handler: String,
79        arguments: Vec<SerializableValue>,
80    },
81    BindingList(Vec<String>),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum TemplateChild {
86    Text {
87        value: String,
88        span: SourceSpan,
89    },
90    Binding {
91        id: TemplateNodeId,
92        expression: String,
93        initial_value: Option<SerializableValue>,
94        span: SourceSpan,
95    },
96    Element(ElementNode),
97    Fragment(FragmentNode),
98    Conditional(ConditionalNode),
99    List(ListNode),
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct ConditionalNode {
104    pub id: TemplateNodeId,
105    pub start_id: TemplateNodeId,
106    pub end_id: TemplateNodeId,
107    pub condition: String,
108    pub initial_value: Option<SerializableValue>,
109    pub span: SourceSpan,
110    pub when_true: Vec<TemplateChild>,
111    pub when_false: Vec<TemplateChild>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct ListNode {
116    pub id: TemplateNodeId,
117    pub start_id: TemplateNodeId,
118    pub end_id: TemplateNodeId,
119    pub iterable: String,
120    pub initial_value: Option<SerializableValue>,
121    pub item_variable: String,
122    pub index_variable: Option<String>,
123    pub key_expression: String,
124    pub span: SourceSpan,
125    pub item_template: Vec<TemplateChild>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct TemplateNodeId(pub String);
130
131/// Maps the small JSX spelling subset that differs from HTML to the canonical
132/// attribute name carried by every compiler product.
133///
134/// This runs while lowering authored attributes, before template semantics,
135/// manifests, static HTML, and runtime bindings are constructed. No runtime
136/// JSX alias table or framework transform participates in the behavior.
137#[must_use]
138pub fn canonical_html_attribute_name(name: &str) -> &str {
139    match name {
140        "className" => "class",
141        "htmlFor" => "for",
142        _ => name,
143    }
144}
145
146#[derive(Clone, Copy)]
147struct ListItemTemplateScope<'a> {
148    item_variable: &'a str,
149    index_variable: Option<&'a str>,
150}
151
152#[must_use]
153///
154/// # Panics
155///
156/// Panics when a component has no semantic provenance entry. Component-graph
157/// construction establishes this invariant before template construction.
158pub fn build_template_graph(component_graph: &ComponentGraph) -> TemplateGraph {
159    let mut ids = TemplateIdAllocator::default();
160
161    let templates = component_graph
162        .components
163        .iter()
164        .map(|component| {
165            let local_variables = component
166                .methods
167                .iter()
168                .find(|method| method.name == "render")
169                .map_or(&[][..], |method| method.local_variables.as_slice());
170
171            TemplateNode {
172                id: component.id.template(),
173                owner: SemanticOwner::entity(component.id.clone()),
174                provenance: component_graph
175                    .provenance
176                    .get(&component.id.template())
177                    .or_else(|| component_graph.provenance.get(&component.id))
178                    .cloned()
179                    .expect("component semantic provenance should exist"),
180                component_name: component.class_name.clone(),
181                root: component.render.as_ref().and_then(|render| {
182                    element_from_render(render, &component.state_fields, local_variables, &mut ids)
183                }),
184                root_fragment: component.render.as_ref().and_then(|render| {
185                    render.root_fragment.as_ref().map(|fragment| {
186                        fragment_from_render_fragment(
187                            fragment,
188                            &component.state_fields,
189                            local_variables,
190                            &mut ids,
191                            None,
192                        )
193                    })
194                }),
195            }
196        })
197        .collect::<Vec<_>>();
198
199    TemplateGraph { templates }
200}
201
202fn element_from_render(
203    render: &RenderModel,
204    state_fields: &[StateField],
205    local_variables: &[MethodLocalVariable],
206    ids: &mut TemplateIdAllocator,
207) -> Option<ElementNode> {
208    let tag_name = render.root_element.clone()?;
209    let tag_name_span = render.root_element_name_span?;
210    let span = render.root_span?;
211    let id = ids.alloc();
212
213    let direct_bindings = collect_direct_bindings_from_children(&render.children);
214
215    let attributes = template_attributes(
216        &tag_name,
217        &render.attributes,
218        &render.event_handlers,
219        &direct_bindings,
220        state_fields,
221        local_variables,
222        ids,
223        None,
224    );
225
226    let children = render
227        .children
228        .iter()
229        .map(|child| template_child_from_render(child, state_fields, local_variables, ids, None))
230        .collect::<Vec<_>>();
231
232    Some(ElementNode {
233        id,
234        tag_name,
235        tag_name_span,
236        span,
237        attributes,
238        authored_attributes: render.attributes.clone(),
239        children,
240    })
241}
242
243fn element_from_render_element(
244    element: &RenderElement,
245    state_fields: &[StateField],
246    local_variables: &[MethodLocalVariable],
247    ids: &mut TemplateIdAllocator,
248    list_scope: Option<ListItemTemplateScope<'_>>,
249) -> ElementNode {
250    let id = ids.alloc();
251
252    ElementNode {
253        id,
254        tag_name: element.tag_name.clone(),
255        tag_name_span: element.tag_name_span,
256        span: element.span,
257        attributes: template_attributes(
258            &element.tag_name,
259            &element.attributes,
260            &element.event_handlers,
261            &collect_direct_bindings_from_children(&element.children),
262            state_fields,
263            local_variables,
264            ids,
265            list_scope,
266        ),
267        authored_attributes: element.attributes.clone(),
268        children: element
269            .children
270            .iter()
271            .map(|child| {
272                template_child_from_render(child, state_fields, local_variables, ids, list_scope)
273            })
274            .collect::<Vec<_>>(),
275    }
276}
277
278fn fragment_from_render_fragment(
279    fragment: &RenderFragment,
280    state_fields: &[StateField],
281    local_variables: &[MethodLocalVariable],
282    ids: &mut TemplateIdAllocator,
283    list_scope: Option<ListItemTemplateScope<'_>>,
284) -> FragmentNode {
285    FragmentNode {
286        id: ids.alloc(),
287        span: fragment.span,
288        children: fragment
289            .children
290            .iter()
291            .map(|child| {
292                template_child_from_render(child, state_fields, local_variables, ids, list_scope)
293            })
294            .collect(),
295    }
296}
297
298#[allow(clippy::too_many_arguments)]
299fn template_attributes(
300    tag_name: &str,
301    static_attributes: &[RenderAttribute],
302    event_handlers: &[RenderEventHandler],
303    bindings: &[String],
304    state_fields: &[StateField],
305    local_variables: &[MethodLocalVariable],
306    ids: &mut TemplateIdAllocator,
307    list_scope: Option<ListItemTemplateScope<'_>>,
308) -> Vec<TemplateAttribute> {
309    let mut attributes = Vec::new();
310
311    for attribute in static_attributes {
312        // `field` and the explicit intrinsic-form host marker are compiler
313        // facts, not ordinary HTML attributes.
314        if attribute.name == "field" || (tag_name == "form" && attribute.name == "form") {
315            continue;
316        }
317        let name = canonical_html_attribute_name(&attribute.name);
318        match &attribute.value {
319            RenderAttributeValue::Boolean if !is_event_attribute(name) => {
320                attributes.push(TemplateAttribute {
321                    name: name.to_string(),
322                    value: AttributeValue::Boolean,
323                    span: Some(attribute.span),
324                });
325            }
326            RenderAttributeValue::Static(value) if !is_event_attribute(name) => {
327                attributes.push(TemplateAttribute {
328                    name: name.to_string(),
329                    value: AttributeValue::Static(value.clone()),
330                    span: Some(attribute.span),
331                });
332            }
333            RenderAttributeValue::Expression(Some(expression))
334                if !is_event_attribute(name)
335                    && name != "key"
336                    && (expression.strip_prefix("this.").is_some()
337                        || (list_scope.is_none()
338                            && unique_local_variable(local_variables, expression).is_some())
339                        || list_scope
340                            .is_some_and(|scope| list_item_expression(expression, scope))) =>
341            {
342                attributes.push(TemplateAttribute {
343                    name: name.to_string(),
344                    value: AttributeValue::Binding {
345                        id: ids.alloc(),
346                        expression: expression.clone(),
347                        initial_value: binding_initial_value(
348                            expression,
349                            state_fields,
350                            local_variables,
351                            list_scope.is_none(),
352                        ),
353                    },
354                    span: Some(attribute.span),
355                });
356            }
357            _ => {}
358        }
359    }
360
361    for event_handler in event_handlers {
362        attributes.push(TemplateAttribute {
363            name: format!("data-presolve-on-{}", event_handler.event),
364            value: AttributeValue::EventHandler {
365                event: event_handler.event.clone(),
366                handler: event_handler.handler.clone(),
367                arguments: event_handler.arguments.clone(),
368            },
369            span: Some(event_handler.span),
370        });
371    }
372
373    if !bindings.is_empty() {
374        attributes.push(TemplateAttribute {
375            name: "data-presolve-bindings".to_string(),
376            value: AttributeValue::BindingList(bindings.to_vec()),
377            span: None,
378        });
379    }
380
381    attributes
382}
383
384fn is_event_attribute(name: &str) -> bool {
385    name.strip_prefix("on")
386        .and_then(|event| event.chars().next())
387        .is_some_and(char::is_uppercase)
388}
389
390fn list_item_expression(expression: &str, scope: ListItemTemplateScope<'_>) -> bool {
391    expression == scope.item_variable
392        || scope.index_variable == Some(expression)
393        || expression
394            .strip_prefix(scope.item_variable)
395            .and_then(|suffix| suffix.strip_prefix('.'))
396            .is_some_and(|path| !path.is_empty() && !path.split('.').any(str::is_empty))
397}
398
399fn collect_direct_bindings_from_children(children: &[RenderChild]) -> Vec<String> {
400    let mut bindings = Vec::new();
401
402    for child in children {
403        match child {
404            RenderChild::Binding { expression, .. } => bindings.push(expression.clone()),
405            RenderChild::Fragment(fragment) => {
406                bindings.extend(collect_direct_bindings_from_children(&fragment.children));
407            }
408            RenderChild::Conditional(conditional) => {
409                bindings.push(conditional.condition.clone());
410            }
411            RenderChild::List(list) => bindings.push(list.iterable.clone()),
412            RenderChild::Element(_) | RenderChild::Text { .. } => {}
413        }
414    }
415
416    bindings
417}
418
419fn template_child_from_render(
420    child: &RenderChild,
421    state_fields: &[StateField],
422    local_variables: &[MethodLocalVariable],
423    ids: &mut TemplateIdAllocator,
424    list_scope: Option<ListItemTemplateScope<'_>>,
425) -> TemplateChild {
426    match child {
427        RenderChild::Text { value, span } => TemplateChild::Text {
428            value: value.clone(),
429            span: *span,
430        },
431
432        RenderChild::Binding { expression, span } => TemplateChild::Binding {
433            id: ids.alloc(),
434            expression: expression.clone(),
435            initial_value: binding_initial_value(
436                expression,
437                state_fields,
438                local_variables,
439                list_scope.is_none(),
440            ),
441            span: *span,
442        },
443
444        RenderChild::Element(element) => TemplateChild::Element(element_from_render_element(
445            element,
446            state_fields,
447            local_variables,
448            ids,
449            list_scope,
450        )),
451        RenderChild::Fragment(fragment) => TemplateChild::Fragment(fragment_from_render_fragment(
452            fragment,
453            state_fields,
454            local_variables,
455            ids,
456            list_scope,
457        )),
458        RenderChild::Conditional(conditional) => {
459            TemplateChild::Conditional(conditional_from_render_conditional(
460                conditional,
461                state_fields,
462                local_variables,
463                ids,
464                list_scope,
465            ))
466        }
467        RenderChild::List(list) => TemplateChild::List(list_from_render_list(
468            list,
469            state_fields,
470            local_variables,
471            ids,
472        )),
473    }
474}
475
476fn list_from_render_list(
477    list: &RenderList,
478    state_fields: &[StateField],
479    local_variables: &[MethodLocalVariable],
480    ids: &mut TemplateIdAllocator,
481) -> ListNode {
482    ListNode {
483        id: ids.alloc(),
484        start_id: ids.alloc(),
485        end_id: ids.alloc(),
486        iterable: list.iterable.clone(),
487        initial_value: binding_initial_value(&list.iterable, state_fields, local_variables, false),
488        item_variable: list.item_variable.clone(),
489        index_variable: list.index_variable.clone(),
490        key_expression: list.key_expression.clone(),
491        span: list.span,
492        item_template: list
493            .item_template
494            .iter()
495            .map(|child| {
496                template_child_from_render(
497                    child,
498                    state_fields,
499                    local_variables,
500                    ids,
501                    Some(ListItemTemplateScope {
502                        item_variable: &list.item_variable,
503                        index_variable: list.index_variable.as_deref(),
504                    }),
505                )
506            })
507            .collect(),
508    }
509}
510
511fn conditional_from_render_conditional(
512    conditional: &RenderConditional,
513    state_fields: &[StateField],
514    local_variables: &[MethodLocalVariable],
515    ids: &mut TemplateIdAllocator,
516    list_scope: Option<ListItemTemplateScope<'_>>,
517) -> ConditionalNode {
518    ConditionalNode {
519        id: ids.alloc(),
520        start_id: ids.alloc(),
521        end_id: ids.alloc(),
522        condition: conditional.condition.clone(),
523        initial_value: binding_initial_value(
524            &conditional.condition,
525            state_fields,
526            local_variables,
527            false,
528        ),
529        span: conditional.span,
530        when_true: conditional
531            .when_true
532            .iter()
533            .map(|child| {
534                template_child_from_render(child, state_fields, local_variables, ids, list_scope)
535            })
536            .collect(),
537        when_false: conditional
538            .when_false
539            .iter()
540            .map(|child| {
541                template_child_from_render(child, state_fields, local_variables, ids, list_scope)
542            })
543            .collect(),
544    }
545}
546
547fn binding_initial_value(
548    expression: &str,
549    state_fields: &[StateField],
550    local_variables: &[MethodLocalVariable],
551    allow_local: bool,
552) -> Option<SerializableValue> {
553    if let Some(field_name) = expression.strip_prefix("this.") {
554        return state_fields
555            .iter()
556            .find(|field| field.name == field_name)
557            .and_then(|field| field.initial_value.clone());
558    }
559
560    if allow_local {
561        return unique_local_variable(local_variables, expression).map(|local| local.value.clone());
562    }
563
564    None
565}
566
567fn unique_local_variable<'a>(
568    local_variables: &'a [MethodLocalVariable],
569    name: &str,
570) -> Option<&'a MethodLocalVariable> {
571    let mut locals = local_variables.iter().filter(|local| local.name == name);
572    let local = locals.next()?;
573    locals.next().is_none().then_some(local)
574}
575
576#[cfg(test)]
577mod tests {
578    use super::canonical_html_attribute_name;
579
580    #[test]
581    fn normalizes_jsx_html_attribute_aliases() {
582        assert_eq!(canonical_html_attribute_name("className"), "class");
583        assert_eq!(canonical_html_attribute_name("htmlFor"), "for");
584        assert_eq!(canonical_html_attribute_name("aria-label"), "aria-label");
585    }
586}