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 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#[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]
153pub 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 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}