1use crate::semantic_id::{SemanticId, SemanticOwner};
2use crate::semantic_provenance::SourceProvenance;
3use crate::template_graph::{
4 AttributeValue, ConditionalNode, ElementNode, FragmentNode, ListNode, TemplateChild,
5 TemplateNode,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct TemplateSemanticEntity {
11 pub id: SemanticId,
12 pub owner: SemanticOwner,
13 pub kind: TemplateSemanticKind,
14 pub scope: TemplateSemanticScope,
15 pub tag_name: Option<String>,
16 pub tag_name_provenance: Option<SourceProvenance>,
17 pub attribute_name: Option<String>,
18 pub list_item_variable: Option<String>,
19 pub list_index_variable: Option<String>,
20 pub expression: Option<String>,
21 pub provenance: SourceProvenance,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TemplateSemanticKind {
26 Element,
27 Fragment,
28 Text,
29 Binding,
30 Attribute,
31 AttributeBinding,
32 EventAttribute,
33 Conditional,
34 List,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum TemplateSemanticScope {
40 Render,
41 ListItem,
42}
43
44#[must_use]
45pub fn build_template_semantic_entities(templates: &[TemplateNode]) -> Vec<TemplateSemanticEntity> {
46 let mut entities = Vec::new();
47
48 for template in templates {
49 if let Some(root) = &template.root {
50 collect_element(
51 root,
52 template,
53 "root",
54 TemplateSemanticScope::Render,
55 &mut entities,
56 );
57 }
58 if let Some(root) = &template.root_fragment {
59 collect_fragment(
60 root,
61 template,
62 "root",
63 TemplateSemanticScope::Render,
64 &mut entities,
65 );
66 }
67 }
68
69 entities
70}
71
72fn collect_element(
73 element: &ElementNode,
74 template: &TemplateNode,
75 path: &str,
76 scope: TemplateSemanticScope,
77 entities: &mut Vec<TemplateSemanticEntity>,
78) {
79 push_entity(
80 entities,
81 template,
82 TemplateSemanticKind::Element,
83 scope,
84 "element",
85 path,
86 None,
87 element.span,
88 );
89 entities
90 .last_mut()
91 .expect("element semantic entity was just inserted")
92 .tag_name = Some(element.tag_name.clone());
93 entities
94 .last_mut()
95 .expect("element semantic entity was just inserted")
96 .tag_name_provenance = Some(SourceProvenance::new(
97 &template.provenance.path,
98 element.tag_name_span,
99 ));
100
101 for attribute in &element.attributes {
102 let Some(span) = attribute.span else {
103 continue;
104 };
105 let attribute_path = format!("{path}.{}", attribute.name);
106 let (kind, expression) = match &attribute.value {
107 AttributeValue::Binding { expression, .. } => (
108 TemplateSemanticKind::AttributeBinding,
109 Some(expression.clone()),
110 ),
111 AttributeValue::EventHandler { handler, .. } => {
112 (TemplateSemanticKind::EventAttribute, Some(handler.clone()))
113 }
114 AttributeValue::BindingList(_) => continue,
115 AttributeValue::Boolean | AttributeValue::Static(_) => {
116 (TemplateSemanticKind::Attribute, None)
117 }
118 };
119 push_entity(
120 entities,
121 template,
122 kind,
123 scope,
124 match kind {
125 TemplateSemanticKind::AttributeBinding => "attribute-binding",
126 TemplateSemanticKind::EventAttribute => "event-attribute",
127 _ => "attribute",
128 },
129 &attribute_path,
130 expression,
131 span,
132 );
133 entities
134 .last_mut()
135 .expect("attribute semantic entity was just inserted")
136 .attribute_name = Some(attribute.name.clone());
137 }
138
139 collect_children(&element.children, template, path, scope, entities);
140}
141
142fn collect_fragment(
143 fragment: &FragmentNode,
144 template: &TemplateNode,
145 path: &str,
146 scope: TemplateSemanticScope,
147 entities: &mut Vec<TemplateSemanticEntity>,
148) {
149 push_entity(
150 entities,
151 template,
152 TemplateSemanticKind::Fragment,
153 scope,
154 "fragment",
155 path,
156 None,
157 fragment.span,
158 );
159 collect_children(&fragment.children, template, path, scope, entities);
160}
161
162fn collect_children(
163 children: &[TemplateChild],
164 template: &TemplateNode,
165 parent_path: &str,
166 scope: TemplateSemanticScope,
167 entities: &mut Vec<TemplateSemanticEntity>,
168) {
169 for (index, child) in children.iter().enumerate() {
170 let path = format!("{parent_path}.{index}");
171 match child {
172 TemplateChild::Text { span, .. } => push_entity(
173 entities,
174 template,
175 TemplateSemanticKind::Text,
176 scope,
177 "text",
178 &path,
179 None,
180 *span,
181 ),
182 TemplateChild::Binding {
183 expression, span, ..
184 } => push_entity(
185 entities,
186 template,
187 TemplateSemanticKind::Binding,
188 scope,
189 "binding",
190 &path,
191 Some(expression.clone()),
192 *span,
193 ),
194 TemplateChild::Element(element) => {
195 collect_element(element, template, &path, scope, entities);
196 }
197 TemplateChild::Fragment(fragment) => {
198 collect_fragment(fragment, template, &path, scope, entities);
199 }
200 TemplateChild::Conditional(conditional) => {
201 collect_conditional(conditional, template, &path, scope, entities);
202 }
203 TemplateChild::List(list) => collect_list(list, template, &path, scope, entities),
204 }
205 }
206}
207
208fn collect_conditional(
209 conditional: &ConditionalNode,
210 template: &TemplateNode,
211 path: &str,
212 scope: TemplateSemanticScope,
213 entities: &mut Vec<TemplateSemanticEntity>,
214) {
215 push_entity(
216 entities,
217 template,
218 TemplateSemanticKind::Conditional,
219 scope,
220 "conditional",
221 path,
222 Some(conditional.condition.clone()),
223 conditional.span,
224 );
225 collect_children(
226 &conditional.when_true,
227 template,
228 &format!("{path}.true"),
229 scope,
230 entities,
231 );
232 collect_children(
233 &conditional.when_false,
234 template,
235 &format!("{path}.false"),
236 scope,
237 entities,
238 );
239}
240
241fn collect_list(
242 list: &ListNode,
243 template: &TemplateNode,
244 path: &str,
245 scope: TemplateSemanticScope,
246 entities: &mut Vec<TemplateSemanticEntity>,
247) {
248 push_entity(
249 entities,
250 template,
251 TemplateSemanticKind::List,
252 scope,
253 "list",
254 path,
255 Some(list.iterable.clone()),
256 list.span,
257 );
258 let entity = entities
259 .last_mut()
260 .expect("list semantic entity was just inserted");
261 entity.list_item_variable = Some(list.item_variable.clone());
262 entity.list_index_variable.clone_from(&list.index_variable);
263 collect_children(
264 &list.item_template,
265 template,
266 &format!("{path}.item"),
267 TemplateSemanticScope::ListItem,
268 entities,
269 );
270}
271
272#[allow(clippy::too_many_arguments)]
273fn push_entity(
274 entities: &mut Vec<TemplateSemanticEntity>,
275 template: &TemplateNode,
276 kind: TemplateSemanticKind,
277 scope: TemplateSemanticScope,
278 id_kind: &str,
279 path: &str,
280 expression: Option<String>,
281 span: presolve_parser::SourceSpan,
282) {
283 entities.push(TemplateSemanticEntity {
284 id: template.id.template_entity(id_kind, path),
285 owner: SemanticOwner::entity(template.id.clone()),
286 kind,
287 scope,
288 tag_name: None,
289 tag_name_provenance: None,
290 attribute_name: None,
291 list_item_variable: None,
292 list_index_variable: None,
293 expression,
294 provenance: SourceProvenance::new(&template.provenance.path, span),
295 });
296}
297
298#[cfg(test)]
299mod tests {
300 use super::{build_template_semantic_entities, TemplateSemanticKind};
301 use crate::{
302 build_component_graph_for_module, build_template_graph, SemanticId, SemanticOwner,
303 };
304
305 #[test]
306 fn lowers_authored_template_descendants_and_bindings() {
307 let parsed = presolve_parser::parse_file(
308 "src/Panel.tsx",
309 r#"
310@component("x-panel")
311class Panel extends Component {
312 enabled = state(true);
313
314 render() {
315 return <section title="Panel" hidden={this.enabled}>{this.enabled}</section>;
316 }
317}
318"#,
319 );
320 let graph = build_component_graph_for_module(&parsed);
321 let templates = build_template_graph(&graph);
322 let entities = build_template_semantic_entities(&templates.templates);
323 let template =
324 SemanticId::component_in_module("src/Panel.tsx", Some("x-panel"), "Panel").template();
325
326 let attribute_binding = entities
327 .iter()
328 .find(|entity| {
329 entity
330 .id
331 .as_str()
332 .ends_with("attribute-binding:root.hidden")
333 })
334 .expect("attribute binding entity");
335 let text_binding = entities
336 .iter()
337 .find(|entity| entity.id.as_str().ends_with("binding:root.0"))
338 .expect("text binding entity");
339
340 assert!(entities.iter().any(|entity| {
341 entity.kind == TemplateSemanticKind::Element
342 && entity.id.as_str().ends_with("element:root")
343 }));
344 assert_eq!(
345 attribute_binding.kind,
346 TemplateSemanticKind::AttributeBinding
347 );
348 assert_eq!(
349 attribute_binding.expression.as_deref(),
350 Some("this.enabled")
351 );
352 assert_eq!(attribute_binding.provenance.span.line, 7);
353 assert_eq!(text_binding.kind, TemplateSemanticKind::Binding);
354 assert_eq!(text_binding.owner, SemanticOwner::entity(template));
355 }
356}