1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use indexmap::IndexMap;
10use serde_json::Value;
11
12use crate::basic_map::map_basic_component;
13use crate::limits;
14use crate::catalog::{CatalogView, A2UI_BASIC_CATALOG_ID, UNKNOWN_COMPONENT};
15use crate::macros::expand_macros_with_templates;
16use crate::types::{FlatChildren, FlatComponent, NestedNode, Props, ReduceIssue, UiNode};
17use crate::validate::validate_props;
18
19pub const RESERVED_KEYS: &[&str] = &["id", "component", "children", "slots", "on", "style", "visible", "accessibility", "template"];
21const RESERVED: &[&str] = RESERVED_KEYS;
22
23pub fn valid_visible(value: &Value) -> bool {
25 value.is_boolean() || crate::expr::is_dynamic(value)
26}
27
28pub fn slot_allowed(slots: Option<&Vec<String>>, name: &str) -> bool {
30 slots.is_some_and(|s| s.iter().any(|x| x == "*" || x == name))
31}
32
33const VISIBLE_ISSUE: &str = "visible: expected a boolean, a binding or a function call";
34
35#[derive(Debug, Clone)]
36pub struct ReduceOptions {
37 pub catalog_id: String,
39 pub view: Arc<CatalogView>,
41 pub root_id: Option<String>,
43 pub expand: bool,
45 pub validate: bool,
47}
48
49impl ReduceOptions {
50 pub fn new(catalog_id: &str) -> Self {
51 ReduceOptions { catalog_id: catalog_id.to_string(), view: CatalogView::core(), root_id: None, expand: true, validate: true }
52 }
53
54 pub fn with_view(self, view: Arc<CatalogView>) -> Self {
55 ReduceOptions { view, ..self }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
60pub struct ReduceResult {
61 pub root: UiNode,
62 pub issues: Vec<ReduceIssue>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub templates: Option<IndexMap<String, UiNode>>,
70}
71
72type BuildMissing<'a> = dyn FnMut(&str) -> (Option<UiNode>, Vec<ReduceIssue>) + 'a;
75
76fn lift_templates(root: &mut UiNode, issues: &mut Vec<ReduceIssue>, build_missing: &mut BuildMissing) -> IndexMap<String, UiNode> {
87 let original = root.clone();
88 let mut in_place: Vec<String> = Vec::new();
91 let mut dropped: Vec<String> = Vec::new();
92 let mut cycles: Vec<ReduceIssue> = Vec::new();
93 loop {
94 *root = original.clone();
95 let mut pass_issues = Vec::new();
96 let pass = lift_pass(root, &mut pass_issues, build_missing, &in_place);
97 let lifted: Vec<&String> = pass.order.iter().filter(|id| pass.found.contains_key(*id) && !in_place.contains(id) && !dropped.contains(id)).collect();
98 fn walk(n: &UiNode, lifted: &[&String], out: &mut Vec<String>) {
100 if let Some(t) = &n.template {
101 if lifted.iter().any(|l| **l == t.component) && !out.contains(&t.component) {
102 out.push(t.component.clone());
103 }
104 }
105 for slot in n.slots.iter().flat_map(|s| s.values()) {
106 walk(slot, lifted, out);
107 }
108 for c in &n.children {
109 walk(c, lifted, out);
110 }
111 }
112 let deps: HashMap<&str, Vec<String>> = lifted
113 .iter()
114 .map(|id| {
115 let mut out = Vec::new();
116 walk(&pass.found[id.as_str()], &lifted, &mut out);
117 (id.as_str(), out)
118 })
119 .collect();
120 let reaches_itself = |from: &str| {
121 let mut seen: HashSet<&str> = HashSet::new();
122 let mut stack: Vec<&str> = deps.get(from).map(|d| d.iter().map(String::as_str).collect()).unwrap_or_default();
123 while let Some(id) = stack.pop() {
124 if id == from {
125 return true;
126 }
127 if seen.insert(id) {
128 stack.extend(deps.get(id).into_iter().flatten().map(String::as_str));
129 }
130 }
131 false
132 };
133 let cyclic: Vec<String> = lifted.iter().filter(|id| reaches_itself(id)).map(|id| id.to_string()).collect();
134 if cyclic.is_empty() {
135 issues.extend(pass_issues);
136 if pass.root_named {
137 issues.push(ReduceIssue { id: original.id.clone(), message: "template: cycle through this id".into() });
138 }
139 issues.extend(cycles);
140 let mut found = pass.found;
141 return pass.order.iter().filter(|id| lifted.contains(id)).filter_map(|id| found.shift_remove(id).map(|n| (id.clone(), n))).collect();
142 }
143 for id in cyclic {
144 if pass.built.contains(&id) {
145 dropped.push(id.clone());
146 } else {
147 in_place.push(id.clone());
148 }
149 cycles.push(ReduceIssue { id, message: "template: cycle through this id".into() });
150 }
151 }
152}
153
154struct LiftPass {
157 found: IndexMap<String, UiNode>,
159 order: Vec<String>,
161 built: Vec<String>,
163 root_named: bool,
165}
166
167fn lift_pass(root: &mut UiNode, issues: &mut Vec<ReduceIssue>, build_missing: &mut BuildMissing, in_place: &[String]) -> LiftPass {
168 fn collect(n: &UiNode, root_id: &str, want: &mut Vec<String>, root_named: &mut bool) {
169 if let Some(t) = &n.template {
170 if t.component == root_id {
171 *root_named = true;
172 } else if !want.contains(&t.component) {
173 want.push(t.component.clone());
174 }
175 }
176 for slot in n.slots.iter().flat_map(|s| s.values()) {
177 collect(slot, root_id, want, root_named);
178 }
179 for c in &n.children {
180 collect(c, root_id, want, root_named);
181 }
182 }
183 fn strip(n: &mut UiNode, want: &[String], found: &mut IndexMap<String, UiNode>) {
184 let children = std::mem::take(&mut n.children);
185 for c in children {
186 if want.contains(&c.id) {
187 if !found.contains_key(&c.id) {
188 found.insert(c.id.clone(), c);
189 }
190 } else {
191 n.children.push(c);
192 }
193 }
194 if let Some(slots) = &mut n.slots {
195 let names: Vec<String> = slots.iter().filter(|(_, v)| want.contains(&v.id)).map(|(k, _)| k.clone()).collect();
196 for name in names {
197 if let Some(slot) = slots.shift_remove(&name) {
198 if !found.contains_key(&slot.id) {
199 found.insert(slot.id.clone(), slot);
200 }
201 }
202 }
203 if slots.is_empty() {
204 n.slots = None;
205 }
206 }
207 for slot in n.slots.iter_mut().flat_map(|s| s.values_mut()) {
208 strip(slot, want, found);
209 }
210 for c in &mut n.children {
211 strip(c, want, found);
212 }
213 }
214 let root_id = root.id.clone();
215 let mut want: Vec<String> = Vec::new();
216 let mut found: IndexMap<String, UiNode> = IndexMap::new();
217 let mut missing: Vec<String> = Vec::new();
218 let mut built: Vec<String> = Vec::new();
219 let mut root_named = false;
220 collect(root, &root_id, &mut want, &mut root_named);
221 let mut before = (usize::MAX, 0, 0);
222 while before != (want.len(), found.len(), missing.len()) {
223 before = (want.len(), found.len(), missing.len());
224 let strippable: Vec<String> = want.iter().filter(|id| !in_place.contains(id)).cloned().collect();
225 strip(root, &strippable, &mut found);
226 loop {
227 let n = found.len();
228 for i in 0..n {
229 let mut node = std::mem::replace(&mut found[i], UiNode::new("", ""));
230 strip(&mut node, &strippable, &mut found);
231 found[i] = node;
232 }
233 if found.len() == n {
234 break;
235 }
236 }
237 for id in want.clone() {
238 if found.contains_key(&id) || missing.contains(&id) || in_place.contains(&id) {
239 continue;
240 }
241 let (node, raised) = build_missing(&id);
242 issues.extend(raised);
243 match node {
244 Some(node) => {
245 built.push(id.clone());
246 found.insert(id, node);
247 }
248 None => {
249 missing.push(id.clone());
250 issues.push(ReduceIssue { id, message: "template: no component with this id".into() });
251 }
252 }
253 }
254 for node in found.values() {
255 collect(node, &root_id, &mut want, &mut root_named);
256 }
257 }
258 LiftPass { found, order: want, built, root_named }
259}
260
261fn unknown(id: &str, component: &str, catalog_id: &str) -> UiNode {
262 let mut node = UiNode::new(id, UNKNOWN_COMPONENT);
263 node.props.insert("component".into(), Value::String(component.to_string()));
264 node.props.insert("catalogId".into(), Value::String(catalog_id.to_string()));
265 node
266}
267
268fn own_props(flat: &FlatComponent) -> Props {
269 flat.rest.iter().filter(|(k, _)| !RESERVED.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect()
270}
271
272fn js_truthy(value: &Value) -> bool {
274 match value {
275 Value::Null => false,
276 Value::Bool(b) => *b,
277 Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
278 Value::String(s) => !s.is_empty(),
279 _ => true,
280 }
281}
282
283#[inline(never)]
287fn validate_node(node: &UiNode, id: &str, options: &ReduceOptions, skip_unknown: bool, issues: &mut Vec<ReduceIssue>) {
288 if !options.validate || (skip_unknown && node.component == UNKNOWN_COMPONENT) {
289 return;
290 }
291 let Some(def) = options.view.components.get(&node.component) else { return };
292 for issue in validate_props(def, &node.props, "props", &options.view) {
293 issues.push(ReduceIssue { id: id.to_string(), message: format!("{}: {}", issue.path, issue.message) });
294 }
295 if def.children == "none" && !node.children.is_empty() {
296 issues.push(ReduceIssue { id: id.to_string(), message: format!("{} takes no children", node.component) });
297 }
298 for slot in node.slots.iter().flat_map(|s| s.keys()) {
299 if !slot_allowed(def.slots.as_ref(), slot) {
300 issues.push(ReduceIssue { id: id.to_string(), message: format!("slots.{slot}: {} has no such slot", node.component) });
301 }
302 }
303 for issue in crate::validate::validate_node_in(node, &options.view) {
304 issues.push(ReduceIssue { id: id.to_string(), message: format!("{}: {}", issue.path, issue.message) });
305 }
306}
307
308fn expand(node: UiNode, issues: &mut Vec<ReduceIssue>, built: &mut Vec<UiNode>, options: &ReduceOptions) -> UiNode {
309 match expand_macros_with_templates(&node, &options.view) {
310 Ok((expanded, expansion_issues, templates)) => {
311 issues.extend(expansion_issues);
312 built.extend(templates);
313 expanded
314 }
315 Err(message) => {
316 issues.push(ReduceIssue { id: node.id.clone(), message });
317 node
318 }
319 }
320}
321
322fn finish(root: UiNode, mut issues: Vec<ReduceIssue>, lifted: IndexMap<String, UiNode>, options: &ReduceOptions) -> ReduceResult {
327 let mut built: Vec<UiNode> = Vec::new();
330 let mut root = if options.expand { expand(root, &mut issues, &mut built, options) } else { root };
331 cap_depth(&mut root, 1, &mut issues, &options.catalog_id);
332 let mut budget = Budget { left: limits::MAX_COMPONENTS, reported: issues.iter().any(|i| i.message == limits::components_issue()) };
335 budget.spend(&root.id, &mut issues);
336 cap_components(&mut root, &mut budget, &mut issues);
337 let mut templates: IndexMap<String, UiNode> = lifted
338 .into_iter()
339 .map(|(id, n)| {
340 let mut n = if options.expand { expand(n, &mut issues, &mut built, options) } else { n };
341 cap_depth(&mut n, 1, &mut issues, &options.catalog_id);
342 budget.spend(&n.id, &mut issues);
343 cap_components(&mut n, &mut budget, &mut issues);
344 (id, n)
345 })
346 .collect();
347 for mut n in built {
348 if templates.contains_key(&n.id) {
349 continue;
350 }
351 cap_depth(&mut n, 1, &mut issues, &options.catalog_id);
352 budget.spend(&n.id, &mut issues);
353 cap_components(&mut n, &mut budget, &mut issues);
354 templates.insert(n.id.clone(), n);
355 }
356 let templates = (!templates.is_empty()).then_some(templates);
357 ReduceResult { root, issues, templates }
358}
359
360fn cap_components(node: &mut UiNode, budget: &mut Budget, issues: &mut Vec<ReduceIssue>) {
366 crate::deep(|| {
367 node.children.retain_mut(|child| {
368 if !budget.spend(&child.id, issues) {
369 return false;
370 }
371 cap_components(child, budget, issues);
372 true
373 });
374 if let Some(slots) = &mut node.slots {
375 slots.retain(|_, child| {
376 if !budget.spend(&child.id, issues) {
377 return false;
378 }
379 cap_components(child, budget, issues);
380 true
381 });
382 }
383 })
384}
385
386fn cap_depth(node: &mut UiNode, depth: usize, issues: &mut Vec<ReduceIssue>, catalog_id: &str) {
390 if depth > limits::MAX_DEPTH {
391 if node.component != UNKNOWN_COMPONENT || !node.children.is_empty() || node.slots.is_some() {
392 let issue = ReduceIssue { id: node.id.clone(), message: limits::depth_issue() };
393 if !issues.contains(&issue) {
394 issues.push(issue);
395 }
396 *node = unknown(&node.id, &node.component, catalog_id);
397 }
398 return;
399 }
400 crate::deep(|| {
401 for child in &mut node.children {
402 cap_depth(child, depth + 1, issues, catalog_id);
403 }
404 for child in node.slots.iter_mut().flat_map(|s| s.values_mut()) {
405 cap_depth(child, depth + 1, issues, catalog_id);
406 }
407 })
408}
409
410struct Builder<'a> {
411 by_id: HashMap<&'a str, &'a FlatComponent>,
412 options: &'a ReduceOptions,
413 basic: bool,
414 visiting: HashSet<String>,
415 placed: HashSet<String>,
417 twice_reported: HashSet<String>,
420 budget: Budget,
421 issues: Vec<ReduceIssue>,
422}
423
424struct Budget {
426 left: usize,
427 reported: bool,
428}
429
430impl Budget {
431 fn new() -> Self {
432 Budget { left: limits::MAX_COMPONENTS, reported: false }
433 }
434
435 fn spend(&mut self, id: &str, issues: &mut Vec<ReduceIssue>) -> bool {
437 if self.left > 0 {
438 self.left -= 1;
439 return true;
440 }
441 if !self.reported {
442 issues.push(ReduceIssue { id: id.to_string(), message: limits::components_issue() });
443 }
444 self.reported = true;
445 false
446 }
447}
448
449impl Builder<'_> {
450 fn build(&mut self, id: &str, depth: usize) -> Option<UiNode> {
451 crate::deep(|| self.build_node(id, depth))
452 }
453
454 fn build_node(&mut self, id: &str, depth: usize) -> Option<UiNode> {
460 let catalog_id = self.options.catalog_id.as_str();
461 let flat = self.by_id.get(id).copied();
462 if let Some(flat) = flat {
463 if self.visiting.contains(id) {
466 if !self.budget.spend(id, &mut self.issues) {
467 return None;
468 }
469 self.issues.push(ReduceIssue { id: id.to_string(), message: "cycle through this id".into() });
470 return Some(unknown(id, &flat.component, catalog_id));
471 }
472 if self.placed.contains(id) {
473 if self.twice_reported.insert(id.to_string()) {
474 self.issues.push(ReduceIssue { id: id.to_string(), message: limits::USED_TWICE_ISSUE.into() });
475 }
476 return None;
477 }
478 }
479 if !self.budget.spend(id, &mut self.issues) {
480 return None;
481 }
482 let Some(flat) = flat else {
483 self.issues.push(ReduceIssue { id: id.to_string(), message: "no component with this id".into() });
484 return Some(unknown(id, &format!("#{id}"), catalog_id));
485 };
486 self.placed.insert(id.to_string());
487 if depth > limits::MAX_DEPTH {
488 self.issues.push(ReduceIssue { id: id.to_string(), message: limits::depth_issue() });
489 return Some(unknown(id, &flat.component, catalog_id));
490 }
491 self.visiting.insert(id.to_string());
492 let mut node;
493 if self.basic {
494 let by_id = &self.by_id;
495 let lookup = |id: &str| by_id.get(id).map(|f| (*f).clone());
496 match map_basic_component(flat, &lookup) {
497 None => {
498 self.issues.push(ReduceIssue { id: id.to_string(), message: format!("basic component {} has no mapping", flat.component) });
499 node = unknown(id, &flat.component, catalog_id);
500 }
501 Some(mapped) => {
502 node = UiNode::new(id, mapped.component);
503 node.props = mapped.props;
504 node.children = mapped.children_ids.iter().filter_map(|c| self.build(c, depth + 1)).collect();
505 node.template = mapped.template;
506 node.style = mapped.style;
507 node.on = mapped.on;
508 for (slot, child_id) in &mapped.slots {
509 if let Some(child) = self.build(child_id, depth + 1) {
510 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), child);
511 }
512 }
513 }
514 }
515 } else if !self.options.view.components.contains_key(&flat.component) {
516 self.issues.push(ReduceIssue { id: id.to_string(), message: format!("unknown component {}", flat.component) });
517 node = unknown(id, &flat.component, catalog_id);
518 } else {
519 node = UiNode::new(id, flat.component.clone());
520 node.props = own_props(flat);
521 match &flat.children {
522 Some(FlatChildren::Ids(ids)) => node.children = ids.iter().filter_map(|c| self.build(c, depth + 1)).collect(),
523 Some(t @ FlatChildren::Template { .. }) => node.template = t.template(),
524 None => {}
525 }
526 node.style = flat.style.clone();
527 if let Some(visible) = &flat.visible {
528 if valid_visible(visible) {
529 node.visible = Some(visible.clone());
530 } else {
531 self.issues.push(ReduceIssue { id: id.to_string(), message: VISIBLE_ISSUE.into() });
532 }
533 }
534 node.on = flat.on.clone();
535 for (slot, child_id) in flat.slots.iter().flatten() {
536 if let Some(child) = self.build(child_id, depth + 1) {
537 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), child);
538 }
539 }
540 }
541 if let Some(accessibility) = flat.accessibility.as_ref().filter(|a| js_truthy(a)) {
542 node.accessibility = Some(accessibility.clone());
543 }
544 self.visiting.remove(id);
545 validate_node(&node, id, self.options, true, &mut self.issues);
546 Some(node)
547 }
548}
549
550pub fn component_issues(flat: &FlatComponent, options: &ReduceOptions, lookup: &dyn Fn(&str) -> Option<FlatComponent>) -> Vec<ReduceIssue> {
557 let mut issues = Vec::new();
558 let id = flat.id.as_str();
559 let stub = |cid: &str| UiNode::new(cid, UNKNOWN_COMPONENT);
560 let mut node;
561 if options.catalog_id == A2UI_BASIC_CATALOG_ID {
562 match map_basic_component(flat, lookup) {
563 None => {
564 issues.push(ReduceIssue { id: id.to_string(), message: format!("basic component {} has no mapping", flat.component) });
565 return issues;
566 }
567 Some(mapped) => {
568 node = UiNode::new(id, mapped.component);
569 node.props = mapped.props;
570 node.children = mapped.children_ids.iter().map(|c| stub(c)).collect();
571 node.template = mapped.template;
572 node.style = mapped.style;
573 node.on = mapped.on;
574 for (slot, child_id) in &mapped.slots {
575 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), stub(child_id));
576 }
577 }
578 }
579 } else if !options.view.components.contains_key(&flat.component) {
580 issues.push(ReduceIssue { id: id.to_string(), message: format!("unknown component {}", flat.component) });
581 return issues;
582 } else {
583 node = UiNode::new(id, flat.component.clone());
584 node.props = own_props(flat);
585 match &flat.children {
586 Some(FlatChildren::Ids(ids)) => node.children = ids.iter().map(|c| stub(c)).collect(),
587 Some(t @ FlatChildren::Template { .. }) => node.template = t.template(),
588 None => {}
589 }
590 node.style = flat.style.clone();
591 if let Some(visible) = &flat.visible {
592 if valid_visible(visible) {
593 node.visible = Some(visible.clone());
594 } else {
595 issues.push(ReduceIssue { id: id.to_string(), message: VISIBLE_ISSUE.into() });
596 }
597 }
598 node.on = flat.on.clone();
599 for (slot, child_id) in flat.slots.iter().flatten() {
600 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), stub(child_id));
601 }
602 }
603 if let Some(accessibility) = flat.accessibility.as_ref().filter(|a| js_truthy(a)) {
604 node.accessibility = Some(accessibility.clone());
605 }
606 validate_node(&node, id, options, true, &mut issues);
607 issues
608}
609
610pub fn reduce_surface(components: &[FlatComponent], options: &ReduceOptions) -> ReduceResult {
618 crate::roomy(|| reduce_flat(components, options))
619}
620
621fn reduce_flat(components: &[FlatComponent], options: &ReduceOptions) -> ReduceResult {
622 let mut issues = Vec::new();
623 let mut by_id: HashMap<&str, &FlatComponent> = HashMap::new();
624 for flat in components {
625 if by_id.contains_key(flat.id.as_str()) {
626 issues.push(ReduceIssue { id: flat.id.clone(), message: "duplicate id; the last definition wins".into() });
627 }
628 by_id.insert(flat.id.as_str(), flat);
629 }
630 let root_id = options.root_id.clone().unwrap_or_else(|| "root".to_string());
631 if !options.view.knows_catalog(&options.catalog_id) {
632 issues.push(ReduceIssue { id: root_id.clone(), message: format!("unsupported catalog {}", options.catalog_id) });
633 }
634 let mut builder = Builder {
635 by_id,
636 options,
637 basic: options.catalog_id == A2UI_BASIC_CATALOG_ID,
638 visiting: HashSet::new(),
639 placed: HashSet::new(),
640 twice_reported: HashSet::new(),
641 budget: Budget::new(),
642 issues,
643 };
644 let mut root = builder.build(&root_id, 1).unwrap_or_else(|| unknown(&root_id, &format!("#{root_id}"), &options.catalog_id));
645 let mut issues = std::mem::take(&mut builder.issues);
646 let mut built: HashMap<String, (Option<UiNode>, Vec<ReduceIssue>)> = HashMap::new();
649 let lifted = lift_templates(&mut root, &mut issues, &mut |id| {
650 if let Some(done) = built.get(id) {
651 return done.clone();
652 }
653 let node = if builder.by_id.contains_key(id) && !builder.placed.contains(id) { builder.build(id, 1) } else { None };
654 let out = (node, std::mem::take(&mut builder.issues));
655 built.insert(id.to_string(), out.clone());
656 out
657 });
658 finish(root, issues, lifted, options)
659}
660
661pub fn reduce_nested(tree: &NestedNode, options: &ReduceOptions) -> ReduceResult {
664 crate::roomy(|| reduce_tree(tree, options))
665}
666
667fn reduce_tree(tree: &NestedNode, options: &ReduceOptions) -> ReduceResult {
668 let mut issues = Vec::new();
669 let mut budget = Budget::new();
670 let mut root = walk_nested(tree, options, &mut issues, &mut budget, 1).unwrap_or_else(|| unknown(&tree.id, &tree.component, &options.catalog_id));
671 let lifted = lift_templates(&mut root, &mut issues, &mut |_| (None, Vec::new()));
672 finish(root, issues, lifted, options)
673}
674
675fn walk_nested(n: &NestedNode, options: &ReduceOptions, issues: &mut Vec<ReduceIssue>, budget: &mut Budget, depth: usize) -> Option<UiNode> {
676 crate::deep(|| walk_nested_node(n, options, issues, budget, depth))
677}
678
679fn walk_nested_node(n: &NestedNode, options: &ReduceOptions, issues: &mut Vec<ReduceIssue>, budget: &mut Budget, depth: usize) -> Option<UiNode> {
680 if !budget.spend(&n.id, issues) {
681 return None;
682 }
683 if depth > limits::MAX_DEPTH {
684 issues.push(ReduceIssue { id: n.id.clone(), message: limits::depth_issue() });
685 return Some(unknown(&n.id, &n.component, &options.catalog_id));
686 }
687 if !options.view.components.contains_key(&n.component) {
688 issues.push(ReduceIssue { id: n.id.clone(), message: format!("unknown component {}", n.component) });
689 return Some(unknown(&n.id, &n.component, &options.catalog_id));
690 }
691 let mut node = UiNode::new(n.id.clone(), n.component.clone());
692 node.props = n.props.clone().unwrap_or_default();
693 node.children = n.children.iter().flatten().filter_map(|c| walk_nested(c, options, issues, budget, depth + 1)).collect();
694 node.style = n.style.clone();
695 if let Some(visible) = &n.visible {
696 if valid_visible(visible) {
697 node.visible = Some(visible.clone());
698 } else {
699 issues.push(ReduceIssue { id: n.id.clone(), message: VISIBLE_ISSUE.into() });
700 }
701 }
702 node.on = n.on.clone();
703 node.accessibility = n.accessibility.clone().filter(js_truthy);
704 node.template = n.template.clone();
705 if let Some(slots) = &n.slots {
706 let mut out = IndexMap::new();
707 for (slot, child) in slots {
708 if let Some(built) = walk_nested(child, options, issues, budget, depth + 1) {
709 out.insert(slot.clone(), built);
710 }
711 }
712 node.slots = Some(out);
713 }
714 validate_node(&node, &n.id, options, false, issues);
715 Some(node)
716}
717
718pub fn preorder(root: &UiNode) -> Vec<String> {
721 let mut out = Vec::new();
722 root.walk(&mut |n| out.push(n.id.clone()));
723 out
724}