Keep character-range text styles when publishing and placing components

Fixes #463

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-15 16:31:53 +02:00
co-authored by Claude Haiku 5.5
parent 102404e8c3
commit 7689af50d6
3 changed files with 73 additions and 2 deletions
+2
View File
@@ -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;
+5 -2
View File
@@ -134,9 +134,12 @@ pub fn closure(doc: &Document, roots: &[NodeId]) -> Vec<NodeId> {
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<Item = &String> {
[&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.
@@ -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, &params).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());
}