diff --git a/crates/engine/src/fonts.rs b/crates/engine/src/fonts.rs index 672d4f2c..bdda411c 100644 --- a/crates/engine/src/fonts.rs +++ b/crates/engine/src/fonts.rs @@ -64,16 +64,20 @@ pub fn missing(doc: &Document) -> Vec { used.into_values().filter(|(family, _)| !figcraft_text::has_family(family)).map(|(family, layers)| Missing { family, layers }).collect() } -/// Every string value of variable `id` in any mode, following aliases (at most 32 deep). +/// Every string value of variable `id` in any mode, following aliases (each variable once). fn string_values(vars: &Variables, id: &str) -> Vec { let mut out = Vec::new(); - let mut todo = vec![(id.to_string(), 0)]; - while let Some((id, depth)) = todo.pop() { + let mut seen = BTreeSet::new(); + let mut todo = vec![id.to_string()]; + while let Some(id) = todo.pop() { + if !seen.insert(id.clone()) { + continue; + } let Some(var) = vars.get(&id) else { continue }; for v in var.values.values() { match v { VariableValue::String(s) => out.push(s.clone()), - VariableValue::Alias(a) if depth < 32 => todo.push((a.clone(), depth + 1)), + VariableValue::Alias(a) => todo.push(a.clone()), _ => {} } } diff --git a/crates/engine/src/fonts_tests.rs b/crates/engine/src/fonts_tests.rs index 0fe6dcb3..5df9a854 100644 --- a/crates/engine/src/fonts_tests.rs +++ b/crates/engine/src/fonts_tests.rs @@ -119,6 +119,25 @@ fn blank_families_and_aliases_are_handled() { assert_eq!(families(&mut s), vec![("FigCraft Missing F".to_string(), 1), ("FigCraft Missing G".to_string(), 1)]); } +/// Aliases in several modes are followed once per variable: a chain that branches in every mode +/// would otherwise grow to billions of steps (a file can hold one). +#[test] +fn branching_alias_chains_are_walked_once_per_variable() { + let mut s = Session::new(); + s.execute("variable.createCollection", &json!({"name": "Type"})).unwrap(); + for i in 0..32 { + s.execute("variable.create", &json!({"collection": "Type", "name": format!("V{i}"), "type": "string", "value": "x"})).unwrap(); + } + let doc = s.doc_mut().unwrap(); + let ids: Vec = doc.variables.variables.iter().map(|v| v.id.clone()).collect(); + for (i, var) in doc.variables.variables.iter_mut().enumerate() { + let next = ids.get(i + 1).cloned().unwrap_or_else(|| "FigCraft Missing H".into()); + let leaf = |v: String| if i + 1 < ids.len() { VariableValue::Alias(v) } else { VariableValue::String(v) }; + var.values = [("m1".to_string(), leaf(next.clone())), ("m2".to_string(), leaf(next))].into(); + } + assert_eq!(string_values(&doc.variables, &ids[0]), vec!["FigCraft Missing H", "FigCraft Missing H"]); +} + #[test] fn needs_an_open_document() { let mut s = Session::empty();