diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 42423dbc..5bcb0802 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1500,6 +1500,8 @@ mod invite_links_tests; #[cfg(test)] mod keymap_tests; #[cfg(test)] +mod library_range_styles_tests; +#[cfg(test)] mod library_variables_tests; #[cfg(test)] mod live_access_tests; diff --git a/crates/engine/src/library.rs b/crates/engine/src/library.rs index 936d798a..6b98196f 100644 --- a/crates/engine/src/library.rs +++ b/crates/engine/src/library.rs @@ -134,9 +134,12 @@ pub fn closure(doc: &Document, roots: &[NodeId]) -> Vec { out } -/// The style ids a layer links to (fill, stroke, effect, grid, text). +/// The style ids a layer links to (fill, stroke, effect, grid, text, and text styles on +/// character ranges). pub(crate) fn style_links(n: &Node) -> impl Iterator { - [&n.styles.fill, &n.styles.stroke, &n.styles.effect, &n.styles.grid, &n.styles.text].into_iter().flatten() + let chars = + n.kind.text().into_iter().flat_map(|t| std::iter::once(&t.style).chain(t.runs.iter().map(|r| &r.style))).filter_map(|s| s.style_id.as_ref()); + [&n.styles.fill, &n.styles.stroke, &n.styles.effect, &n.styles.grid, &n.styles.text].into_iter().flatten().chain(chars) } /// Style ids the layers under `roots` link to. diff --git a/crates/engine/src/library_range_styles_tests.rs b/crates/engine/src/library_range_styles_tests.rs new file mode 100644 index 00000000..077470e7 --- /dev/null +++ b/crates/engine/src/library_range_styles_tests.rs @@ -0,0 +1,66 @@ +//! Text styles linked on character ranges travel with components (publish and place). + +use crate::Session; +use figcraft_doc::NodeId; +use serde_json::{Value, json}; + +fn run(s: &mut Session, command: &str, params: Value) -> Value { + s.execute(command, ¶ms).unwrap_or_else(|e| panic!("{command}: {e}")) +} + +fn node(value: Value) -> NodeId { + NodeId(value["id"].as_u64().unwrap()) +} + +/// A component with "Hello" (chars 0..2 linked to a range style) and "World" (whole-layer style). +fn fixture() -> (Session, NodeId, String, String) { + let mut s = Session::new(); + let frame = node(run(&mut s, "frame.create", json!({"name":"Card", "width":100, "height":40}))); + let text = node(run(&mut s, "text.create", json!({"parent":frame, "text":"Hello"}))); + let range = run(&mut s, "style.create", json!({"type":"text", "name":"Emphasis", "value":{"fontSize":32}}))["id"].as_str().unwrap().to_owned(); + run(&mut s, "style.apply", json!({"id":text, "style":range, "start":0, "end":2})); + let whole_text = node(run(&mut s, "text.create", json!({"parent":frame, "text":"World"}))); + let whole = run(&mut s, "style.create", json!({"type":"text", "name":"Whole", "value":{"fontSize":24}}))["id"].as_str().unwrap().to_owned(); + run(&mut s, "style.apply", json!({"id":whole_text, "style":whole})); + let component = node(run(&mut s, "component.create", json!({"ids":[frame]}))); + let source = s.doc().unwrap().node(text).unwrap(); + let chars = source.kind.text().unwrap(); + assert!(source.styles.text.is_none()); + assert_eq!(chars.style_at(0).style_id.as_deref(), Some(range.as_str())); + assert_eq!(chars.style_at(2).style_id, None); + (s, component, range, whole) +} + +#[test] +fn component_publish_keeps_range_text_style() { + let (mut s, component, range, whole) = fixture(); + run(&mut s, "library.publish", json!({"include":[component]})); + let snapshot = &s.doc().unwrap().published.as_ref().unwrap().doc; + assert!(snapshot.style(&whole).is_some()); + assert!(snapshot.style(&range).is_some(), "character-range style missing from snapshot"); +} + +#[test] +fn component_place_imports_range_text_style() { + let (mut source, component, range, whole) = fixture(); + let publication = run(&mut source, "library.publish", json!({})); + let library = source.doc().unwrap().published.as_ref().unwrap().clone(); + assert!(library.doc.style(&range).is_some()); + let mut target = Session::new(); + let path = "figcraft://library.test/range-styles"; + target.libraries.insert(path, Ok(library)); + run(&mut target, "library.add", json!({"path":path})); + let instance = node(run(&mut target, "library.place", json!({"library":publication["id"], "component":component}))); + let doc = target.doc().unwrap(); + let children = &doc.node(instance).unwrap().children; + let ranged = doc.node(children[0]).unwrap().kind.text().unwrap(); + let full = doc.node(children[1]).unwrap().kind.text().unwrap(); + let whole_record = doc.libraries.styles.iter().find(|r| r.key == whole).unwrap(); + assert_eq!(full.style_at(0).style_id.as_deref(), Some(whole_record.local.as_str())); + assert_eq!(ranged.style_at(0).font_size, 32.0); + assert!(ranged.style_at(2).style_id.is_none()); + let record = doc.libraries.styles.iter().find(|r| r.key == range); + assert!(record.is_some(), "character-range style missing after placement"); + assert_eq!(ranged.style_at(0).style_id.as_deref(), record.map(|r| r.local.as_str())); + assert!(doc.style(ranged.style_at(0).style_id.as_deref().unwrap()).is_some()); +}