Merge main into #2304

This commit is contained in:
bflatastic committed 2026-11-13 22:43:54 +07:00
commit 0263ec8a3a
42 files changed
+816 -72

No files matched your search

+20 -2
View File
@@ -64,7 +64,7 @@ fn numbered_lists_keep_their_start() {
assert_eq!(starts(&doc, "From 1"), [(N, 0)]);
assert_eq!(starts(&doc, "From 2"), [(N, 1)], "shows 2.");
assert_eq!(starts(&doc, "First only"), [(N, 3), (N, 0), (N, 0)]);
assert_eq!(starts(&doc, "Every line"), [(N, 3), (N, 3), (N, 3)]);
assert_eq!(starts(&doc, "Every line"), [(N, 3), (N, 0), (N, 0)], "Figma writes the start on every line; only the first counts");
assert_eq!(starts(&doc, "Mixed"), [(P, 0), (N, 4), (B, 0)], "bullets have no number, so no start");
assert_eq!(starts(&doc, "Junk"), [(N, 0)], "a negative offset counts from 1");
}
@@ -89,5 +89,23 @@ fn the_export_writes_where_a_list_starts() {
// line or per list counts the same); bullets and plain lines carry none.
assert_eq!(offsets, [1.0, 1.0, 0.0, 5.0, 0.0]);
let (back, _) = import(&bytes, "x").unwrap();
assert_eq!(starts(&back, "List"), [(N, 1), (N, 1), (P, 0), (N, 5), (B, 0)]);
assert_eq!(starts(&back, "List"), [(N, 1), (N, 0), (P, 0), (N, 5), (B, 0)], "reads back as it went in");
}
#[test]
fn a_start_on_every_line_counts_only_where_the_list_starts() {
use ListStyle::{None as P, Numbered as N};
let nodes = vec![
node(0, None, "DOCUMENT", "Document", vec![]),
node(1, Some((0, "!")), "CANVAS", "Page", vec![]),
text_node(2, "Three", "a\nb\nc", vec![line("ORDERED_LIST", 4, true), line("ORDERED_LIST", 4, false), line("ORDERED_LIST", 4, false)]),
text_node(3, "Split", "a\nb\nc", vec![line("ORDERED_LIST", 4, true), line("PLAIN", 4, false), line("ORDERED_LIST", 0, true)]),
text_node(4, "Two lists", "a\nb\nc", vec![line("ORDERED_LIST", 4, true), line("PLAIN", 4, false), line("ORDERED_LIST", 6, true)]),
];
let sc = Schema::parse(LIST_SCHEMA).unwrap();
let (doc, warnings) = import(&canvas(&sc, &o(vec![("nodeChanges", arr(nodes))])), "x").unwrap();
assert!(warnings.is_empty(), "{warnings:?}");
assert_eq!(starts(&doc, "Three"), [(N, 4), (N, 0), (N, 0)]);
assert_eq!(starts(&doc, "Split"), [(N, 4), (P, 0), (N, 0)]);
assert_eq!(starts(&doc, "Two lists"), [(N, 4), (P, 0), (N, 6)], "a second list keeps its own start");
}
+20 -1
View File
@@ -675,7 +675,11 @@ fn kitchen_sink() -> Value {
]},
{"id": "k9", "name": "Sec", "type": "SECTION", "sectionContentsHidden": true, "relativeTransform": at(0.0, 0.0), "size": {"x": 50, "y": 50},
"children": [{"id": "k10", "name": "Use", "type": "INSTANCE", "componentId": "3:1", "relativeTransform": at(0.0, 0.0), "size": {"x": 24, "y": 24},
"componentProperties": {"Show#4:1": {"type": "BOOLEAN", "value": false}}}]}
"componentProperties": {"Show#4:1": {"type": "BOOLEAN", "value": false}}}]},
{"id": "k11", "name": "Listed", "type": "TEXT", "relativeTransform": at(0.0, 0.0), "size": {"x": 10, "y": 10}, "characters": "a\nb\nc\nd\ne\n",
"style": {"fontFamily": "Inter", "fontSize": 12},
"lineTypes": ["ORDERED", "ORDERED", "ORDERED", "UNORDERED", "ORDERED", "ORDERED"], "lineIndentations": [0, 0, 1, 0, 300, 0],
"listStartOffsets": [4, 4, 3, 2, 7, 9]}
]);
if let (Some(kids), Some(more)) = (v.pointer_mut("/document/children/0/children").and_then(Value::as_array_mut), extra.as_array()) {
kids.extend(more.iter().cloned());
@@ -692,6 +696,9 @@ fn kitchen_sink_imports_cleanly() {
assert!(matches!(named(&doc, "Sec").kind, NodeKind::Section { contents_hidden: true }));
assert!(matches!(&named(&doc, "Use").kind, NodeKind::Instance { props, .. } if props.get("Show") == Some(&json!(false))));
assert!(matches!(named(&doc, "Txt").kind, NodeKind::Text(ref t) if t.list == ListStyle::Numbered && t.runs.len() == 3));
let NodeKind::Text(l) = &named(&doc, "Listed").kind else { panic!("not text") };
let got: Vec<(u8, u32)> = (0..l.paragraph_count()).map(|p| (l.para_list(p).indent, l.para_list(p).start_offset)).collect();
assert_eq!(got, [(0, 4), (0, 0), (1, 3), (0, 0), (8, 7), (0, 9)], "starts stay where a count starts; the level is capped");
assert!(warnings.iter().all(|w| !w.contains("panick")), "{warnings:?}");
}
@@ -801,3 +808,15 @@ fn an_arc_without_an_end_is_whole_wherever_it_starts() {
assert_eq!(sweep, std::f64::consts::TAU, "start {start}");
}
}
#[test]
fn a_start_on_every_line_counts_only_where_the_list_starts() {
let (doc, _) = one(json!([
{"id": "t1", "name": "Every", "type": "TEXT", "relativeTransform": at(0.0, 0.0), "size": {"x": 10, "y": 10}, "characters": "a\nb\nc",
"style": {"fontFamily": "Inter", "fontSize": 12},
"lineTypes": ["ORDERED", "ORDERED", "ORDERED"], "listStartOffsets": [4, 4, 4]}
]));
let figcraft_doc::NodeKind::Text(t) = &named(&doc, "Every").kind else { panic!("not text") };
let offsets: Vec<u32> = (0..t.paragraph_count()).map(|p| t.para_list(p).start_offset).collect();
assert_eq!(offsets, [4, 0, 0]);
}
+3
View File
@@ -283,6 +283,9 @@ fn lists(v: &Value, td: &mut TextData) {
.collect();
// Clamp the levels and fold one style for all back into the layer's list style.
td.set_para_lists(0, td.char_count(), |_| {});
// Only the item where a numbered list starts counting keeps its start; Figma writes the start on
// every line of the list, and a list split later must restart at 1.
td.keep_list_starts_where_counting_starts();
}
/// The decoration's style, thickness, offset, skip ink and colour (`{value, unit}` lengths with