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_issues;
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>, options: &ReduceOptions) -> UiNode {
309 match expand_macros_with_issues(&node, &options.view) {
310 Ok((expanded, expansion_issues)) => {
311 issues.extend(expansion_issues);
312 expanded
313 }
314 Err(message) => {
315 issues.push(ReduceIssue { id: node.id.clone(), message });
316 node
317 }
318 }
319}
320
321fn finish(root: UiNode, mut issues: Vec<ReduceIssue>, lifted: IndexMap<String, UiNode>, options: &ReduceOptions) -> ReduceResult {
326 let mut root = if options.expand { expand(root, &mut issues, options) } else { root };
327 cap_depth(&mut root, 1, &mut issues, &options.catalog_id);
328 let mut budget = Budget { left: limits::MAX_COMPONENTS, reported: issues.iter().any(|i| i.message == limits::components_issue()) };
331 budget.spend(&root.id, &mut issues);
332 cap_components(&mut root, &mut budget, &mut issues);
333 let templates = (!lifted.is_empty()).then(|| {
334 lifted
335 .into_iter()
336 .map(|(id, n)| {
337 let mut n = if options.expand { expand(n, &mut issues, options) } else { n };
338 cap_depth(&mut n, 1, &mut issues, &options.catalog_id);
339 budget.spend(&n.id, &mut issues);
340 cap_components(&mut n, &mut budget, &mut issues);
341 (id, n)
342 })
343 .collect()
344 });
345 ReduceResult { root, issues, templates }
346}
347
348fn cap_components(node: &mut UiNode, budget: &mut Budget, issues: &mut Vec<ReduceIssue>) {
354 crate::deep(|| {
355 node.children.retain_mut(|child| {
356 if !budget.spend(&child.id, issues) {
357 return false;
358 }
359 cap_components(child, budget, issues);
360 true
361 });
362 if let Some(slots) = &mut node.slots {
363 slots.retain(|_, child| {
364 if !budget.spend(&child.id, issues) {
365 return false;
366 }
367 cap_components(child, budget, issues);
368 true
369 });
370 }
371 })
372}
373
374fn cap_depth(node: &mut UiNode, depth: usize, issues: &mut Vec<ReduceIssue>, catalog_id: &str) {
378 if depth > limits::MAX_DEPTH {
379 if node.component != UNKNOWN_COMPONENT || !node.children.is_empty() || node.slots.is_some() {
380 let issue = ReduceIssue { id: node.id.clone(), message: limits::depth_issue() };
381 if !issues.contains(&issue) {
382 issues.push(issue);
383 }
384 *node = unknown(&node.id, &node.component, catalog_id);
385 }
386 return;
387 }
388 crate::deep(|| {
389 for child in &mut node.children {
390 cap_depth(child, depth + 1, issues, catalog_id);
391 }
392 for child in node.slots.iter_mut().flat_map(|s| s.values_mut()) {
393 cap_depth(child, depth + 1, issues, catalog_id);
394 }
395 })
396}
397
398struct Builder<'a> {
399 by_id: HashMap<&'a str, &'a FlatComponent>,
400 options: &'a ReduceOptions,
401 basic: bool,
402 visiting: HashSet<String>,
403 placed: HashSet<String>,
405 twice_reported: HashSet<String>,
408 budget: Budget,
409 issues: Vec<ReduceIssue>,
410}
411
412struct Budget {
414 left: usize,
415 reported: bool,
416}
417
418impl Budget {
419 fn new() -> Self {
420 Budget { left: limits::MAX_COMPONENTS, reported: false }
421 }
422
423 fn spend(&mut self, id: &str, issues: &mut Vec<ReduceIssue>) -> bool {
425 if self.left > 0 {
426 self.left -= 1;
427 return true;
428 }
429 if !self.reported {
430 issues.push(ReduceIssue { id: id.to_string(), message: limits::components_issue() });
431 }
432 self.reported = true;
433 false
434 }
435}
436
437impl Builder<'_> {
438 fn build(&mut self, id: &str, depth: usize) -> Option<UiNode> {
439 crate::deep(|| self.build_node(id, depth))
440 }
441
442 fn build_node(&mut self, id: &str, depth: usize) -> Option<UiNode> {
448 let catalog_id = self.options.catalog_id.as_str();
449 let flat = self.by_id.get(id).copied();
450 if let Some(flat) = flat {
451 if self.visiting.contains(id) {
454 if !self.budget.spend(id, &mut self.issues) {
455 return None;
456 }
457 self.issues.push(ReduceIssue { id: id.to_string(), message: "cycle through this id".into() });
458 return Some(unknown(id, &flat.component, catalog_id));
459 }
460 if self.placed.contains(id) {
461 if self.twice_reported.insert(id.to_string()) {
462 self.issues.push(ReduceIssue { id: id.to_string(), message: limits::USED_TWICE_ISSUE.into() });
463 }
464 return None;
465 }
466 }
467 if !self.budget.spend(id, &mut self.issues) {
468 return None;
469 }
470 let Some(flat) = flat else {
471 self.issues.push(ReduceIssue { id: id.to_string(), message: "no component with this id".into() });
472 return Some(unknown(id, &format!("#{id}"), catalog_id));
473 };
474 self.placed.insert(id.to_string());
475 if depth > limits::MAX_DEPTH {
476 self.issues.push(ReduceIssue { id: id.to_string(), message: limits::depth_issue() });
477 return Some(unknown(id, &flat.component, catalog_id));
478 }
479 self.visiting.insert(id.to_string());
480 let mut node;
481 if self.basic {
482 let by_id = &self.by_id;
483 let lookup = |id: &str| by_id.get(id).map(|f| (*f).clone());
484 match map_basic_component(flat, &lookup) {
485 None => {
486 self.issues.push(ReduceIssue { id: id.to_string(), message: format!("basic component {} has no mapping", flat.component) });
487 node = unknown(id, &flat.component, catalog_id);
488 }
489 Some(mapped) => {
490 node = UiNode::new(id, mapped.component);
491 node.props = mapped.props;
492 node.children = mapped.children_ids.iter().filter_map(|c| self.build(c, depth + 1)).collect();
493 node.template = mapped.template;
494 node.style = mapped.style;
495 node.on = mapped.on;
496 for (slot, child_id) in &mapped.slots {
497 if let Some(child) = self.build(child_id, depth + 1) {
498 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), child);
499 }
500 }
501 }
502 }
503 } else if !self.options.view.components.contains_key(&flat.component) {
504 self.issues.push(ReduceIssue { id: id.to_string(), message: format!("unknown component {}", flat.component) });
505 node = unknown(id, &flat.component, catalog_id);
506 } else {
507 node = UiNode::new(id, flat.component.clone());
508 node.props = own_props(flat);
509 match &flat.children {
510 Some(FlatChildren::Ids(ids)) => node.children = ids.iter().filter_map(|c| self.build(c, depth + 1)).collect(),
511 Some(t @ FlatChildren::Template { .. }) => node.template = t.template(),
512 None => {}
513 }
514 node.style = flat.style.clone();
515 if let Some(visible) = &flat.visible {
516 if valid_visible(visible) {
517 node.visible = Some(visible.clone());
518 } else {
519 self.issues.push(ReduceIssue { id: id.to_string(), message: VISIBLE_ISSUE.into() });
520 }
521 }
522 node.on = flat.on.clone();
523 for (slot, child_id) in flat.slots.iter().flatten() {
524 if let Some(child) = self.build(child_id, depth + 1) {
525 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), child);
526 }
527 }
528 }
529 if let Some(accessibility) = flat.accessibility.as_ref().filter(|a| js_truthy(a)) {
530 node.accessibility = Some(accessibility.clone());
531 }
532 self.visiting.remove(id);
533 validate_node(&node, id, self.options, true, &mut self.issues);
534 Some(node)
535 }
536}
537
538pub fn component_issues(flat: &FlatComponent, options: &ReduceOptions, lookup: &dyn Fn(&str) -> Option<FlatComponent>) -> Vec<ReduceIssue> {
545 let mut issues = Vec::new();
546 let id = flat.id.as_str();
547 let stub = |cid: &str| UiNode::new(cid, UNKNOWN_COMPONENT);
548 let mut node;
549 if options.catalog_id == A2UI_BASIC_CATALOG_ID {
550 match map_basic_component(flat, lookup) {
551 None => {
552 issues.push(ReduceIssue { id: id.to_string(), message: format!("basic component {} has no mapping", flat.component) });
553 return issues;
554 }
555 Some(mapped) => {
556 node = UiNode::new(id, mapped.component);
557 node.props = mapped.props;
558 node.children = mapped.children_ids.iter().map(|c| stub(c)).collect();
559 node.template = mapped.template;
560 node.style = mapped.style;
561 node.on = mapped.on;
562 for (slot, child_id) in &mapped.slots {
563 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), stub(child_id));
564 }
565 }
566 }
567 } else if !options.view.components.contains_key(&flat.component) {
568 issues.push(ReduceIssue { id: id.to_string(), message: format!("unknown component {}", flat.component) });
569 return issues;
570 } else {
571 node = UiNode::new(id, flat.component.clone());
572 node.props = own_props(flat);
573 match &flat.children {
574 Some(FlatChildren::Ids(ids)) => node.children = ids.iter().map(|c| stub(c)).collect(),
575 Some(t @ FlatChildren::Template { .. }) => node.template = t.template(),
576 None => {}
577 }
578 node.style = flat.style.clone();
579 if let Some(visible) = &flat.visible {
580 if valid_visible(visible) {
581 node.visible = Some(visible.clone());
582 } else {
583 issues.push(ReduceIssue { id: id.to_string(), message: VISIBLE_ISSUE.into() });
584 }
585 }
586 node.on = flat.on.clone();
587 for (slot, child_id) in flat.slots.iter().flatten() {
588 node.slots.get_or_insert_with(IndexMap::new).insert(slot.clone(), stub(child_id));
589 }
590 }
591 if let Some(accessibility) = flat.accessibility.as_ref().filter(|a| js_truthy(a)) {
592 node.accessibility = Some(accessibility.clone());
593 }
594 validate_node(&node, id, options, true, &mut issues);
595 issues
596}
597
598pub fn reduce_surface(components: &[FlatComponent], options: &ReduceOptions) -> ReduceResult {
606 crate::roomy(|| reduce_flat(components, options))
607}
608
609fn reduce_flat(components: &[FlatComponent], options: &ReduceOptions) -> ReduceResult {
610 let mut issues = Vec::new();
611 let mut by_id: HashMap<&str, &FlatComponent> = HashMap::new();
612 for flat in components {
613 if by_id.contains_key(flat.id.as_str()) {
614 issues.push(ReduceIssue { id: flat.id.clone(), message: "duplicate id; the last definition wins".into() });
615 }
616 by_id.insert(flat.id.as_str(), flat);
617 }
618 let root_id = options.root_id.clone().unwrap_or_else(|| "root".to_string());
619 if !options.view.knows_catalog(&options.catalog_id) {
620 issues.push(ReduceIssue { id: root_id.clone(), message: format!("unsupported catalog {}", options.catalog_id) });
621 }
622 let mut builder = Builder {
623 by_id,
624 options,
625 basic: options.catalog_id == A2UI_BASIC_CATALOG_ID,
626 visiting: HashSet::new(),
627 placed: HashSet::new(),
628 twice_reported: HashSet::new(),
629 budget: Budget::new(),
630 issues,
631 };
632 let mut root = builder.build(&root_id, 1).unwrap_or_else(|| unknown(&root_id, &format!("#{root_id}"), &options.catalog_id));
633 let mut issues = std::mem::take(&mut builder.issues);
634 let mut built: HashMap<String, (Option<UiNode>, Vec<ReduceIssue>)> = HashMap::new();
637 let lifted = lift_templates(&mut root, &mut issues, &mut |id| {
638 if let Some(done) = built.get(id) {
639 return done.clone();
640 }
641 let node = if builder.by_id.contains_key(id) && !builder.placed.contains(id) { builder.build(id, 1) } else { None };
642 let out = (node, std::mem::take(&mut builder.issues));
643 built.insert(id.to_string(), out.clone());
644 out
645 });
646 finish(root, issues, lifted, options)
647}
648
649pub fn reduce_nested(tree: &NestedNode, options: &ReduceOptions) -> ReduceResult {
652 crate::roomy(|| reduce_tree(tree, options))
653}
654
655fn reduce_tree(tree: &NestedNode, options: &ReduceOptions) -> ReduceResult {
656 let mut issues = Vec::new();
657 let mut budget = Budget::new();
658 let mut root = walk_nested(tree, options, &mut issues, &mut budget, 1).unwrap_or_else(|| unknown(&tree.id, &tree.component, &options.catalog_id));
659 let lifted = lift_templates(&mut root, &mut issues, &mut |_| (None, Vec::new()));
660 finish(root, issues, lifted, options)
661}
662
663fn walk_nested(n: &NestedNode, options: &ReduceOptions, issues: &mut Vec<ReduceIssue>, budget: &mut Budget, depth: usize) -> Option<UiNode> {
664 crate::deep(|| walk_nested_node(n, options, issues, budget, depth))
665}
666
667fn walk_nested_node(n: &NestedNode, options: &ReduceOptions, issues: &mut Vec<ReduceIssue>, budget: &mut Budget, depth: usize) -> Option<UiNode> {
668 if !budget.spend(&n.id, issues) {
669 return None;
670 }
671 if depth > limits::MAX_DEPTH {
672 issues.push(ReduceIssue { id: n.id.clone(), message: limits::depth_issue() });
673 return Some(unknown(&n.id, &n.component, &options.catalog_id));
674 }
675 if !options.view.components.contains_key(&n.component) {
676 issues.push(ReduceIssue { id: n.id.clone(), message: format!("unknown component {}", n.component) });
677 return Some(unknown(&n.id, &n.component, &options.catalog_id));
678 }
679 let mut node = UiNode::new(n.id.clone(), n.component.clone());
680 node.props = n.props.clone().unwrap_or_default();
681 node.children = n.children.iter().flatten().filter_map(|c| walk_nested(c, options, issues, budget, depth + 1)).collect();
682 node.style = n.style.clone();
683 if let Some(visible) = &n.visible {
684 if valid_visible(visible) {
685 node.visible = Some(visible.clone());
686 } else {
687 issues.push(ReduceIssue { id: n.id.clone(), message: VISIBLE_ISSUE.into() });
688 }
689 }
690 node.on = n.on.clone();
691 node.accessibility = n.accessibility.clone().filter(js_truthy);
692 node.template = n.template.clone();
693 if let Some(slots) = &n.slots {
694 let mut out = IndexMap::new();
695 for (slot, child) in slots {
696 if let Some(built) = walk_nested(child, options, issues, budget, depth + 1) {
697 out.insert(slot.clone(), built);
698 }
699 }
700 node.slots = Some(out);
701 }
702 validate_node(&node, &n.id, options, false, issues);
703 Some(node)
704}
705
706pub fn preorder(root: &UiNode) -> Vec<String> {
709 let mut out = Vec::new();
710 root.walk(&mut |n| out.push(n.id.clone()));
711 out
712}