From e8cd6c173a44ecb86927dd68da05b404772f335a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 11:29:14 +0700 Subject: [PATCH] =?UTF-8?q?Variables:=20Save=20Variable=20Library=E2=80=A6?= =?UTF-8?q?=20writes=20the=20variables=20and=20data=20sets=20as=20an=20XML?= =?UTF-8?q?=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed from #5147 onto main. --- crates/engine/src/cmd/variablecmds.rs | 14 +- crates/engine/src/cmd/variablecmds/library.rs | 209 +++++++++++++++- crates/engine/src/tests/variables.rs | 224 ++++++++++++++++++ crates/ui-egui/src/i18n/bg.tsv | 5 + crates/ui-egui/src/i18n/bs.tsv | 5 + crates/ui-egui/src/i18n/ca.tsv | 5 + crates/ui-egui/src/i18n/cs.tsv | 5 + crates/ui-egui/src/i18n/da.tsv | 5 + crates/ui-egui/src/i18n/de.tsv | 5 + crates/ui-egui/src/i18n/el.tsv | 5 + crates/ui-egui/src/i18n/es.tsv | 5 + crates/ui-egui/src/i18n/fi.tsv | 5 + crates/ui-egui/src/i18n/fr.tsv | 5 + crates/ui-egui/src/i18n/hu.tsv | 5 + crates/ui-egui/src/i18n/id.tsv | 5 + crates/ui-egui/src/i18n/it.tsv | 5 + crates/ui-egui/src/i18n/ja.tsv | 5 + crates/ui-egui/src/i18n/ka.tsv | 5 + crates/ui-egui/src/i18n/ko.tsv | 5 + crates/ui-egui/src/i18n/nl.tsv | 5 + crates/ui-egui/src/i18n/pl.tsv | 5 + crates/ui-egui/src/i18n/pt-br.tsv | 5 + crates/ui-egui/src/i18n/ru.tsv | 5 + crates/ui-egui/src/i18n/sv.tsv | 5 + crates/ui-egui/src/i18n/tr.tsv | 5 + crates/ui-egui/src/i18n/uk.tsv | 5 + crates/ui-egui/src/i18n/vi.tsv | 5 + crates/ui-egui/src/i18n/zh-hans.tsv | 5 + crates/ui-egui/src/i18n/zh-hant.tsv | 5 + crates/ui-egui/src/panels/variables.rs | 16 ++ crates/ui-egui/src/tests/picks.rs | 33 +++ docs/mcp.md | 12 + docs/target-app-parity.md | 1 + 33 files changed, 628 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/cmd/variablecmds.rs b/crates/engine/src/cmd/variablecmds.rs index 040d8c802..c3e706f7b 100644 --- a/crates/engine/src/cmd/variablecmds.rs +++ b/crates/engine/src/cmd/variablecmds.rs @@ -10,7 +10,8 @@ //! `name` column, and header cells that define new variables (`#` for visibility, `@` for //! image, no prefix for text). `variable.loadLibrary` replaces the variables and data sets with the //! text variables and data sets of a variable library, the XML file of the reference app's Save -//! Variable Library. Agents reach every command through `run_command`, like all engine commands. +//! Variable Library, and `variable.saveLibrary` writes the text variables and data sets as one. +//! Agents reach every command through `run_command`, like all engine commands. use std::collections::{BTreeMap, BTreeSet}; @@ -26,7 +27,7 @@ mod csv; mod library; use csv::{MAX_GRAPH_CSV, dataset_export, dataset_import}; -use library::load_library; +use library::{load_library, save_library}; /// The longest variable or dataset name, in characters (names arrive from MCP and files). const MAX_NAME: usize = 255; @@ -837,5 +838,14 @@ pub fn specs() -> Vec { has_doc, load_library ), + cmd!( + query "variable.saveLibrary", + "Save Variable Library…", + ["Window", "Variables"], + None, + "{path?} write the document's text variables and every data set as a variable library, the XML that variable.loadLibrary reads: a `variable` element with `varName` and `trait=\"textcontent\"` per text variable, and a `sampleDataSet` element with `dataSetName` per data set, holding an element named after each text variable it has a value for, with one `p` element per line of the value. Variables of other kinds are left out with their values and named in `skipped` and `warning`. Refused without a text variable, for a text variable whose name isn't an XML name (letters, digits, `-`, `_` and `.`, starting with a letter or `_`), for a data set name or value that holds a control character, for a data set name with white space at either end, for a name longer than 255 characters or used twice, and past variable.loadLibrary's caps (256 variables, 1,000 data sets, 8 MiB). Without path → {xml, variables, datasets, skipped, warning?}; with it → {path, variables, datasets, skipped, warning?}", + has_doc, + save_library + ), ] } diff --git a/crates/engine/src/cmd/variablecmds/library.rs b/crates/engine/src/cmd/variablecmds/library.rs index 7e57b2375..429a854f4 100644 --- a/crates/engine/src/cmd/variablecmds/library.rs +++ b/crates/engine/src/cmd/variablecmds/library.rs @@ -1,8 +1,16 @@ use super::{ BTreeMap, BTreeSet, DataSet, DataValue, EngineError, MAX_NAME, Result, Session, Value, Variable, VariableKind, absolute_path, bad, json, named, - read_file_head, str_param, variables_unlocked, + read_file_head, str_param, variables_unlocked, write_file, }; +/// The namespace of a variable library's elements, the default namespace of its data sets' values, +/// and the `category` of its text variables. +const NS_VARIABLES: &str = "http://ns.adobe.com/Variables/1.0/"; // brand-ok: the namespace variable libraries use + +const NS_CUSTOM: &str = "http://ns.adobe.com/GenericCustomNamespace/1.0/"; // brand-ok: the namespace variable libraries use + +const NS_FLOWS: &str = "http://ns.adobe.com/Flows/1.0/"; // brand-ok: the namespace variable libraries use + /// Caps on a variable library (paths and MCP payloads are untrusted): its size, its XML nodes, how /// deep its elements nest, and the entities, variables and data sets it declares. const MAX_LIBRARY_BYTES: usize = 8 << 20; @@ -285,17 +293,200 @@ pub(super) fn load_library(s: &mut Session, p: &Value) -> Result { let mut out = json!({ "variables": lib.text, "deleted": deleted, "deletedDataSets": deleted_sets, "names": names, "skipped": skipped, }); - // Each `trait` once, in the order the library first uses it, with how many variables have it. - let mut kinds: Vec<(&str, usize)> = Vec::new(); - for (_, t) in &lib.other { - match kinds.iter_mut().find(|(k, _)| k == t) { + if let Some(warning) = left_out_warning(lib.other.iter().map(|(_, t)| t.as_str())) { + out["warning"] = json!(warning); + } + Ok(out) +} + +/// The warning about variables that aren't text and were left out, given the kind of each: every +/// kind once, in the order it first comes, with how many variables have it. `None` without any. +fn left_out_warning<'a>(kinds: impl Iterator) -> Option { + let mut counted: Vec<(&str, usize)> = Vec::new(); + for t in kinds { + match counted.iter_mut().find(|(k, _)| *k == t) { Some((_, n)) => *n += 1, - None => kinds.push((t, 1)), + None => counted.push((t, 1)), + } + } + if counted.is_empty() { + return None; + } + let kinds = counted.iter().map(|(k, n)| format!("{k} ({n})")).collect::>().join(", "); + Some(format!("variables that aren't text were left out: {kinds}")) +} + +/// Whether `name` can name an element: an XML name without a colon (XML 1.0, productions 4, 4a and +/// 5). A data set holds a text variable's value in an element named after the variable. +fn element_name(name: &str) -> bool { + let start = |c: char| { + matches!(c, 'A'..='Z' | '_' | 'a'..='z' | '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}' | '\u{370}'..='\u{37D}' + | '\u{37F}'..='\u{1FFF}' | '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' | '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' + | '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' | '\u{10000}'..='\u{EFFFF}') + }; + let more = |c: char| start(c) || matches!(c, '-' | '.' | '0'..='9' | '\u{B7}' | '\u{300}'..='\u{36F}' | '\u{203F}'..='\u{2040}'); + let mut chars = name.chars(); + chars.next().is_some_and(start) && chars.all(more) +} + +/// Whether XML can hold `c` (XML 1.0, production 2): tab, line feed, carriage return, and the +/// characters from U+0020 on except U+FFFE and U+FFFF. +fn xml_char(c: char) -> bool { + matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}') +} + +/// `s` escaped for XML text, or with `attribute` for a quoted attribute value. `&`, `<` and `>` +/// become entity references, and so do quotes in an attribute value. A carriage return, and a tab +/// or line feed in an attribute value, become character references, which the parser reads back +/// as they are. +fn escaped(s: &str, attribute: bool) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + push_escaped(&mut out, c, attribute); + } + out +} + +/// `c` added to `out`, escaped as [`escaped`] says. +fn push_escaped(out: &mut String, c: char, attribute: bool) { + match c { + '&' => out.push_str("&"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '\r' => out.push_str(" "), + '"' if attribute => out.push_str("""), + '\'' if attribute => out.push_str("'"), + '\t' if attribute => out.push_str(" "), + '\n' if attribute => out.push_str(" "), + c => out.push(c), + } +} + +/// Save Variable Library: the document's text variables and its data sets as a variable library in +/// the layout [`parse_library`] reads, so Load Variable Library reads back the same text variables, +/// data set names and text values. A text value is written as one `p` element per line. Variables +/// of other kinds are left out with their values, named in `skipped` and in a warning. A text +/// variable whose name can't name an element ([`element_name`]), a data set name or text value that +/// holds a character XML can't hold, a name that Load Variable Library would read differently or +/// refuse (white space at either end of a data set name, a name longer than `MAX_NAME` characters, +/// a name used twice), and a library past the caps Load Variable Library reads are refused. With `path` the library is written to the file; without it the reply carries it as +/// `xml`. +pub(super) fn save_library(s: &mut Session, p: &Value) -> Result { + const C: &str = "variable.saveLibrary"; + let vars = &s.doc()?.doc.variables; + if vars.variables.is_empty() { + return Err(bad(C, "define a variable first")); + } + let (text, other): (Vec<&Variable>, Vec<&Variable>) = vars.variables.iter().partition(|v| v.kind == VariableKind::Text); + if text.is_empty() { + return Err(bad(C, "a variable library holds text variables, and the document has none")); + } + if let Some(name) = text.iter().map(|v| &v.name).find(|n| !element_name(n)) { + return Err(bad( + C, + format!( + "variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.`" + ), + )); + } + // Load Variable Library refuses a name longer than `MAX_NAME` characters and a name declared + // twice. + let mut declared = BTreeSet::new(); + for v in &text { + named(Some(&v.name), "varName", C)?; + if !declared.insert(v.name.as_str()) { + return Err(bad(C, format!("variable `{}` is already defined", v.name))); } } - if !kinds.is_empty() { - let kinds = kinds.iter().map(|(k, n)| format!("{k} ({n})")).collect::>().join(", "); - out["warning"] = json!(format!("variables that aren't text were left out: {kinds}")); + if text.len() > MAX_LIBRARY_VARIABLES { + return Err(bad(C, format!("the variable library declares more than {MAX_LIBRARY_VARIABLES} variables"))); + } + if vars.datasets.len() > MAX_LIBRARY_DATASETS { + return Err(bad(C, format!("the variable library declares more than {MAX_LIBRARY_DATASETS} data sets"))); + } + fn text_value<'a>(ds: &'a DataSet, var: &str) -> Option<&'a str> { + match ds.values.get(var) { + Some(DataValue::Text(t)) => Some(t.as_str()), + _ => None, + } + } + let mut xml = format!( + "\n\ + \n\ + \t\n\ + \t\n\ + ]>\n\ + \n\ + \n\ + \t\n\ + \t\t\n" + ); + for v in &text { + xml += &format!("\t\t\t\n", v.name); + } + xml += "\t\t\n\t\t\n"; + // Load Variable Library caps the file and the text its entity references expand to: one + // `&ns_flows;` per variable, two `&ns_vars;` and one `&ns_custom;`. The library stays within + // the cap less what the references add. + let grows = |value: &str, reference: &str| value.len().saturating_sub(reference.len()); + let growth = text.len() * grows(NS_FLOWS, "&ns_flows;") + 2 * grows(NS_VARIABLES, "&ns_vars;") + grows(NS_CUSTOM, "&ns_custom;"); + let room = MAX_LIBRARY_BYTES.saturating_sub(growth); + let too_large = || bad(C, format!("the variable library is larger than {MAX_LIBRARY_BYTES} bytes")); + let mut seen = BTreeSet::new(); + for ds in &vars.datasets { + let name = &ds.name; + // Load Variable Library reads a data set's name trimmed and at most `MAX_NAME` characters + // long ([`named`]), and refuses a name declared twice. + if named(Some(name), "dataSetName", C)? != *name { + return Err(bad(C, format!("data set `{name}` starts or ends with white space, which a variable library can't hold"))); + } + if !seen.insert(name.as_str()) { + return Err(bad(C, format!("dataset `{name}` already exists"))); + } + if !name.chars().all(xml_char) || text.iter().filter_map(|v| text_value(ds, &v.name)).any(|t| !t.chars().all(xml_char)) { + return Err(bad(C, format!("data set `{name}` holds a control character, which a variable library can't hold"))); + } + xml += &format!("\t\t\t\n", escaped(name, true)); + for v in &text { + let Some(t) = text_value(ds, &v.name) else { continue }; + xml += &format!("\t\t\t\t<{}>\n", v.name); + for line in t.split('\n') { + xml += "\t\t\t\t\t

"; + // A value can be larger than the cap, so the text stops growing once it passes it. + for c in line.chars() { + push_escaped(&mut xml, c, false); + if xml.len() > room { + return Err(too_large()); + } + } + xml += "

\n"; + } + xml += &format!("\t\t\t\t\n", v.name); + } + xml += "\t\t\t\n"; + if xml.len() > room { + return Err(too_large()); + } + } + xml += "\t\t\n\t\n\n\n"; + if xml.len() > room { + return Err(too_large()); + } + let skipped: Vec = other.iter().map(|v| json!({"name": v.name, "kind": v.kind.label()})).collect(); + let mut out = json!({ + "variables": text.iter().map(|v| v.name.as_str()).collect::>(), "datasets": vars.datasets.len(), "skipped": skipped, + }); + if let Some(warning) = left_out_warning(other.iter().map(|v| v.kind.label())) { + out["warning"] = json!(warning); + } + match str_param(p, "path") { + Some(path) => { + let path = absolute_path(path); + write_file(&path, xml.as_bytes())?; + out["path"] = json!(path); + } + None => out["xml"] = json!(xml), } Ok(out) } diff --git a/crates/engine/src/tests/variables.rs b/crates/engine/src/tests/variables.rs index c5fb4275b..8dd5a55e3 100644 --- a/crates/engine/src/tests/variables.rs +++ b/crates/engine/src/tests/variables.rs @@ -2096,6 +2096,230 @@ fn a_variable_library_expands_entities_of_plain_text() { assert_eq!(value, Some(vectorcraft_doc::DataValue::Text("x-y \"z\"-x&a;".into()))); } +/// Save Variable Library writes what Load Variable Library reads back: the text variables in their +/// order, every data set by name and in order, and each text value as it was, with several lines, +/// markup characters, quotes, a carriage return and an empty value. Variables of other kinds are +/// left out with their values, named in `skipped` and in the warning. The library is written while +/// the variables are locked too, to a file with `path`. +#[test] +fn a_saved_variable_library_loads_back_the_text_variables_and_data_sets() { + let mut s = session(); + let t = text(&mut s, "Alice"); + for (name, kind) in [("Name", "text"), ("Shown", "visibility"), ("Note", "text"), ("Photo", "image"), ("Badge", "visibility")] { + s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap(); + } + s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap(); + s.execute("dataset.new", &json!({"name": "First", "values": {"Name": "Bravo", "Note": "One\nTwo & \r\n'four' \"five\"", "Shown": true}})) + .unwrap(); + s.execute("dataset.new", &json!({"name": "Say \"hi\" & \tnow", "values": {"Name": "", "Photo": "a.png"}})).unwrap(); + s.execute("dataset.new", &json!({"name": "Empty"})).unwrap(); + s.execute("variable.lock", &json!({"locked": true})).unwrap(); + let before = s.doc().unwrap().doc.variables.clone(); + + let saved = s.execute("variable.saveLibrary", &json!({})).unwrap(); + assert_eq!(saved["variables"], json!(["Name", "Note"])); + assert_eq!(saved["datasets"], json!(3)); + assert_eq!( + saved["skipped"], + json!([{"name": "Shown", "kind": "visibility"}, {"name": "Photo", "kind": "image"}, {"name": "Badge", "kind": "visibility"}]) + ); + assert_eq!(saved["warning"], "variables that aren't text were left out: visibility (2), image (1)"); + assert_eq!(s.doc().unwrap().doc.variables, before, "saving changes nothing"); + + let mut other = session(); + let loaded = other.execute("variable.loadLibrary", &json!({"xml": saved["xml"]})).unwrap(); + assert_eq!((loaded["skipped"].clone(), loaded.get("warning")), (json!([]), None)); + let vars = &other.doc().unwrap().doc.variables; + assert_eq!(vars.variables.iter().map(|v| (v.name.as_str(), v.kind.label())).collect::>(), [("Name", "text"), ("Note", "text")]); + let text_only: Vec = before + .datasets + .iter() + .map(|d| { + let mut d = d.clone(); + d.values.retain(|k, _| k == "Name" || k == "Note"); + d + }) + .collect(); + assert_eq!(vars.datasets, text_only); + + let dir = Folder::new("vars-save-library"); + let path = dir.file("library.xml"); + let written = s.execute("variable.saveLibrary", &json!({"path": path})).unwrap(); + assert_eq!(written["path"], json!(path)); + assert!(written.get("xml").is_none()); + assert_eq!(std::fs::read_to_string(&path).unwrap(), saved["xml"].as_str().unwrap()); + let mut from_file = session(); + from_file.execute("variable.loadLibrary", &json!({"path": path})).unwrap(); + assert_eq!(from_file.doc().unwrap().doc.variables.datasets, text_only); +} + +/// The library is the XML of the public grammar: a document type declaration with the namespace +/// entities, `variableSets` › `variableSet` › `variables` with a `textcontent` variable per text +/// variable, and `sampleDataSets` with a `sampleDataSet` per data set, holding an element per text +/// variable it has a value for, with a `p` element per line. Markup characters, a carriage return, +/// and a tab or quote in a data set name are escaped. A document with text variables and no data +/// sets writes an empty `sampleDataSets`. +#[test] +fn a_saved_variable_library_has_the_library_layout() { + let mut s = session(); + for (name, kind) in [("Name", "text"), ("Shown", "visibility"), ("Note", "text")] { + s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap(); + } + let head = "\n\ + \n\ + \t\n\ + \t\n\ + ]>\n\ + \n\ + \n\ + \t\n\ + \t\t\n\ + \t\t\t\n\ + \t\t\t\n\ + \t\t\n\ + \t\t\n"; + let tail = "\t\t\n\t\n\n\n"; + let empty = s.execute("variable.saveLibrary", &json!({})).unwrap(); + assert_eq!(empty["xml"].as_str().unwrap(), format!("{head}{tail}")); + assert_eq!(empty["datasets"], json!(0)); + + s.execute("dataset.new", &json!({"name": "A &\t\"B\"", "values": {"Name": "x\r\ny < z", "Shown": false}})).unwrap(); + s.execute("dataset.new", &json!({"name": "C", "values": {"Note": ""}})).unwrap(); + let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].as_str().unwrap().to_string(); + let sets = "\t\t\t\n\ + \t\t\t\t\n\ + \t\t\t\t\t

x

\n\ + \t\t\t\t\t

y < z

\n\ + \t\t\t\t
\n\ + \t\t\t
\n\ + \t\t\t\n\ + \t\t\t\t\n\ + \t\t\t\t\t

\n\ + \t\t\t\t
\n\ + \t\t\t
\n"; + assert_eq!(xml, format!("{head}{sets}{tail}")); +} + +/// Save Variable Library refuses a document without variables or without text variables, a text +/// variable whose name can't name an element, a data set name or text value with a character XML +/// can't hold, a name that Load Variable Library would read differently or refuse, and a library +/// past the caps that Load Variable Library reads. A refused save writes no file. +#[test] +fn save_variable_library_refuses_what_a_library_cant_hold() { + let dir = Folder::new("vars-save-refused"); + let path = dir.file("library.xml"); + let refused = |s: &mut Session, why: &str| { + let e = s.execute("variable.saveLibrary", &json!({"path": path})).unwrap_err().to_string(); + assert!(e.contains(why), "{why}: {e}"); + assert!(!std::path::Path::new(&path).exists(), "{why}: a file was written"); + }; + + let mut s = session(); + refused(&mut s, "define a variable first"); + s.execute("variable.define", &json!({"name": "Shown", "kind": "visibility"})).unwrap(); + refused(&mut s, "holds text variables, and the document has none"); + + for name in ["First Name", "1st", "a:b", "x/y", "-dash"] { + let mut s = session(); + s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap(); + refused(&mut s, &format!("variable `{name}` can't be saved in a variable library")); + } + for name in ["Ünïcode", "_x", "a.b-c_d", "名前"] { + let mut s = session(); + s.execute("variable.define", &json!({"name": name, "kind": "text"})).unwrap(); + s.execute("dataset.new", &json!({"name": "A", "values": {name: "v"}})).unwrap(); + let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].clone(); + let mut other = session(); + other.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap(); + assert_eq!(other.doc().unwrap().doc.variables.datasets, s.doc().unwrap().doc.variables.datasets, "{name}"); + } + + let mut s = session(); + s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap(); + s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "bell \u{7}"}})).unwrap(); + refused(&mut s, "data set `A` holds a control character"); + let mut s = session(); + s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap(); + s.execute("dataset.new", &json!({"name": "B\u{1}"})).unwrap(); + refused(&mut s, "holds a control character"); + + // A document opened from a file can hold names that the commands don't make, and Load Variable + // Library would read them differently or refuse them. + let renamed = |dataset: Option<&str>, variable: Option<&str>| { + let mut s = session(); + s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap(); + s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap(); + s.execute("dataset.new", &json!({"name": "A"})).unwrap(); + s.execute("dataset.new", &json!({"name": "B"})).unwrap(); + let vars = &mut std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).variables; + if let Some(name) = dataset { + vars.datasets[1].name = name.to_string(); + } + if let Some(name) = variable { + vars.variables[1].name = name.to_string(); + } + s + }; + for name in [" B", "B\t", "B\n"] { + refused(&mut renamed(Some(name), None), "starts or ends with white space"); + } + refused(&mut renamed(Some(""), None), "missing `dataSetName`"); + refused(&mut renamed(Some(&"d".repeat(256)), None), "`dataSetName` is longer than 255 characters"); + refused(&mut renamed(Some("A"), None), "dataset `A` already exists"); + refused(&mut renamed(None, Some(&"v".repeat(256))), "`varName` is longer than 255 characters"); + refused(&mut renamed(None, Some("Name")), "variable `Name` is already defined"); + let mut s = renamed(Some(&"d".repeat(255)), Some(&"v".repeat(255))); + let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].clone(); + let mut other = session(); + other.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap(); + assert_eq!(other.doc().unwrap().doc.variables.datasets, s.doc().unwrap().doc.variables.datasets); + + let mut s = session(); + for i in 0..257 { + s.execute("variable.define", &json!({"name": format!("V{i}"), "kind": "text"})).unwrap(); + } + refused(&mut s, "more than 256 variables"); + let mut s = session(); + s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap(); + for i in 0..1001 { + s.execute("dataset.new", &json!({"name": format!("D{i}")})).unwrap(); + } + refused(&mut s, "more than 1000 data sets"); + let mut s = session(); + s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap(); + s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "x".repeat(9 << 20)}})).unwrap(); + refused(&mut s, "larger than 8388608 bytes"); +} + +/// A library near the size cap is saved only when Load Variable Library reads it back, after its +/// entity references expand: each text variable's `&ns_flows;`, two `&ns_vars;` and one +/// `&ns_custom;` grow by 20, 25 and 36 bytes. +#[test] +fn a_saved_variable_library_near_the_size_cap_loads_back() { + let mut s = session(); + for i in 0..200 { + s.execute("variable.define", &json!({"name": format!("V{i}"), "kind": "text"})).unwrap(); + } + s.execute("dataset.new", &json!({"name": "A", "values": {"V0": ""}})).unwrap(); + let base = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].as_str().unwrap().len(); + let room = (8 << 20) - (200 * 20 + 2 * 25 + 36); + s.execute("dataset.set", &json!({"name": "A", "values": {"V0": "x".repeat(room - base + 1)}})).unwrap(); + let e = s.execute("variable.saveLibrary", &json!({})).unwrap_err().to_string(); + assert!(e.contains("larger than 8388608 bytes"), "{e}"); + s.execute("dataset.set", &json!({"name": "A", "values": {"V0": "x".repeat(room - base)}})).unwrap(); + let xml = s.execute("variable.saveLibrary", &json!({})).unwrap()["xml"].clone(); + assert_eq!(xml.as_str().map(str::len), Some(room)); + let mut other = session(); + other.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap(); + assert_eq!(other.doc().unwrap().doc.variables.datasets, s.doc().unwrap().doc.variables.datasets); + // The same library with the value grown to a file of exactly 8 MiB doesn't load back. + let full = xml.as_str().unwrap().replacen("

x", &format!("

{}x", "x".repeat((8 << 20) - room)), 1); + assert_eq!(full.len(), 8 << 20); + let e = other.execute("variable.loadLibrary", &json!({"xml": full})).unwrap_err().to_string(); + assert!(e.contains("entities expand to more than"), "{e}"); +} + /// While the variables are locked, a library that would delete or define a variable is refused, as /// Delete Variable and New Variable are, and the document keeps its variables and data sets. That /// covers a library whose only variable is a text variable the document already has, and one diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index f6389ea22..1d11be5f2 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5367,6 +5367,11 @@ theme Appearance Външен вид Import Data Sets… Импортиране на набори от данни… Export Data Sets… Експортиране на набори от данни… Load Variable Library… Зареждане на библиотека с променливи… + Save Variable Library… Запазване на библиотека с променливи… +@msg a variable library holds text variables, and the document has none библиотеката с променливи съдържа текстови променливи, а документът няма такива +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` променливата `{name}` не може да бъде запазена в библиотека с променливи: преименувайте я така, че да започва с буква или `_`, без интервали и символи, различни от `-`, `_` и `.` +@msg data set `{name}` holds a control character, which a variable library can't hold наборът от данни `{name}` съдържа управляващ знак, който библиотеката с променливи не може да съдържа +@msg data set `{name}` starts or ends with white space, which a variable library can't hold наборът от данни `{name}` започва или завършва с интервал, който библиотеката с променливи не може да съдържа Make Graph Dynamic Динамична диаграма Batch… Пакет… Source Folder: Изходна папка: diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 2d0718693..c7fadc7f3 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5391,6 +5391,11 @@ mark-style Japanese Japanski Import Data Sets… Uvezi skupove podataka… Export Data Sets… Izvezi skupove podataka… Load Variable Library… Učitaj biblioteku varijabli… + Save Variable Library… Sačuvaj biblioteku varijabli… +@msg a variable library holds text variables, and the document has none biblioteka varijabli sadrži tekstualne varijable, a dokument ih nema +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` varijabla `{name}` ne može se sačuvati u biblioteci varijabli: preimenujte je tako da počinje slovom ili sa `_`, bez razmaka i simbola osim `-`, `_` i `.` +@msg data set `{name}` holds a control character, which a variable library can't hold skup podataka `{name}` sadrži kontrolni znak, koji biblioteka varijabli ne može sadržavati +@msg data set `{name}` starts or ends with white space, which a variable library can't hold skup podataka `{name}` počinje ili završava razmakom, koji biblioteka varijabli ne može sadržavati Make Graph Dynamic Učini grafikon dinamičkim Batch… Paketna obrada… Source Folder: Izvorni folder: diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index ea7002ef5..f4fb5a5f0 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2240,6 +2240,11 @@ digits Malayalam Malaiàlam Load Texture… Carrega una textura… Load Variable Library Carrega la biblioteca de variables Load Variable Library… Carrega la biblioteca de variables… + Save Variable Library… Desa la biblioteca de variables… +@msg a variable library holds text variables, and the document has none una biblioteca de variables conté variables de text, i el document no en té cap +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` la variable `{name}` no es pot desar en una biblioteca de variables: canvieu-li el nom perquè comenci amb una lletra o `_`, sense espais ni símbols que no siguin `-`, `_` i `.` +@msg data set `{name}` holds a control character, which a variable library can't hold el conjunt de dades `{name}` conté un caràcter de control, que una biblioteca de variables no pot contenir +@msg data set `{name}` starts or ends with white space, which a variable library can't hold el conjunt de dades `{name}` comença o acaba amb un espai en blanc, que una biblioteca de variables no pot contenir Loaded Texture Textura carregada Loads native code Carrega codi natiu Local Libraries Biblioteques locals diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 45f2cb1ef..15bcf5841 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5468,6 +5468,11 @@ mark-style Japanese Japonské Import Data Sets… Importovat sady dat… Export Data Sets… Exportovat sady dat… Load Variable Library… Načíst knihovnu proměnných… + Save Variable Library… Uložit knihovnu proměnných… +@msg a variable library holds text variables, and the document has none knihovna proměnných obsahuje textové proměnné a dokument žádné nemá +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` proměnnou `{name}` nelze uložit do knihovny proměnných: přejmenujte ji tak, aby začínala písmenem nebo `_` a neobsahovala mezery ani jiné symboly než `-`, `_` a `.` +@msg data set `{name}` holds a control character, which a variable library can't hold sada dat `{name}` obsahuje řídicí znak, který knihovna proměnných nemůže obsahovat +@msg data set `{name}` starts or ends with white space, which a variable library can't hold sada dat `{name}` začíná nebo končí prázdným znakem, který knihovna proměnných nemůže obsahovat Make Graph Dynamic Nastavit graf jako dynamický Batch… Dávka… Source Folder: Zdrojová složka: diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 0ada885a9..19edb61a2 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5129,6 +5129,11 @@ mark-style Japanese Japansk Import Data Sets… Importér datasæt… Export Data Sets… Eksportér datasæt… Load Variable Library… Indlæs variabelbibliotek… + Save Variable Library… Gem variabelbibliotek… +@msg a variable library holds text variables, and the document has none et variabelbibliotek indeholder tekstvariabler, og dokumentet har ingen +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` variablen `{name}` kan ikke gemmes i et variabelbibliotek: omdøb den, så den begynder med et bogstav eller `_`, uden mellemrum eller andre symboler end `-`, `_` og `.` +@msg data set `{name}` holds a control character, which a variable library can't hold datasættet `{name}` indeholder et kontroltegn, som et variabelbibliotek ikke kan indeholde +@msg data set `{name}` starts or ends with white space, which a variable library can't hold datasættet `{name}` begynder eller slutter med mellemrum, som et variabelbibliotek ikke kan indeholde @msg variables that aren't text were left out: {kinds} variabler, der ikke er tekst, blev udeladt: {kinds} @msg a new document won't read these variables back with their kind: {names} et nyt dokument læser ikke disse variabler tilbage med deres type: {names} @msg a new document refuses this file because of these variables: {names} et nyt dokument afviser denne fil på grund af disse variabler: {names} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index c6a8d9136..efcea52c7 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5045,6 +5045,11 @@ effect Soft Weich Import Data Sets… Datensätze importieren… Export Data Sets… Datensätze exportieren… Load Variable Library… Variablenbibliothek laden… + Save Variable Library… Variablenbibliothek speichern… +@msg a variable library holds text variables, and the document has none eine Variablenbibliothek enthält Textvariablen, und das Dokument hat keine +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` die Variable `{name}` kann nicht in einer Variablenbibliothek gespeichert werden: Benennen Sie sie so um, dass sie mit einem Buchstaben oder `_` beginnt, ohne Leerzeichen oder andere Symbole als `-`, `_` und `.` +@msg data set `{name}` holds a control character, which a variable library can't hold der Datensatz `{name}` enthält ein Steuerzeichen, das eine Variablenbibliothek nicht enthalten kann +@msg data set `{name}` starts or ends with white space, which a variable library can't hold der Datensatz `{name}` beginnt oder endet mit Leerraum, den eine Variablenbibliothek nicht enthalten kann Make Graph Dynamic Diagramm dynamisch machen Batch… Stapelverarbeitung… Source Folder: Quellordner: diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index c6245d792..a44a5fc92 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5326,6 +5326,11 @@ effect Points Σημεία Editable Επεξεργάσιμο Export Data Sets… Εξαγωγή συνόλων δεδομένων… Load Variable Library… Φόρτωση βιβλιοθήκης μεταβλητών… + Save Variable Library… Αποθήκευση βιβλιοθήκης μεταβλητών… +@msg a variable library holds text variables, and the document has none μια βιβλιοθήκη μεταβλητών περιέχει μεταβλητές κειμένου και το έγγραφο δεν έχει καμία +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` η μεταβλητή `{name}` δεν μπορεί να αποθηκευτεί σε βιβλιοθήκη μεταβλητών: μετονομάστε την ώστε να ξεκινά με γράμμα ή `_`, χωρίς κενά ή σύμβολα εκτός από `-`, `_` και `.` +@msg data set `{name}` holds a control character, which a variable library can't hold το σύνολο δεδομένων `{name}` περιέχει χαρακτήρα ελέγχου, τον οποίο δεν μπορεί να περιέχει μια βιβλιοθήκη μεταβλητών +@msg data set `{name}` starts or ends with white space, which a variable library can't hold το σύνολο δεδομένων `{name}` ξεκινά ή τελειώνει με κενό διάστημα, το οποίο δεν μπορεί να περιέχει μια βιβλιοθήκη μεταβλητών Filter Φίλτρο Filter Layers Panel Rows Φιλτράρισμα γραμμών πίνακα επιπέδων Flatten Ισοπέδωση diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ed4f728a8..7b5d4ec62 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5409,6 +5409,11 @@ mark-style Japanese Japonesas Import Data Sets… Importar conjuntos de datos… Export Data Sets… Exportar conjuntos de datos… Load Variable Library… Cargar biblioteca de variables… + Save Variable Library… Guardar biblioteca de variables… +@msg a variable library holds text variables, and the document has none una biblioteca de variables contiene variables de texto y el documento no tiene ninguna +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` la variable `{name}` no se puede guardar en una biblioteca de variables: cámbiele el nombre para que empiece por una letra o `_`, sin espacios ni símbolos distintos de `-`, `_` y `.` +@msg data set `{name}` holds a control character, which a variable library can't hold el conjunto de datos `{name}` contiene un carácter de control, que una biblioteca de variables no puede contener +@msg data set `{name}` starts or ends with white space, which a variable library can't hold el conjunto de datos `{name}` empieza o termina con espacio en blanco, que una biblioteca de variables no puede contener Make Graph Dynamic Hacer gráfico dinámico Batch… Lote… Source Folder: Carpeta de origen: diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 97a9a1c00..df44342dd 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5338,6 +5338,11 @@ mark-style Japanese Japanilainen Editable Muokattava Export Data Sets… Vie tietojoukot… Load Variable Library… Lataa muuttujakirjasto… + Save Variable Library… Tallenna muuttujakirjasto… +@msg a variable library holds text variables, and the document has none muuttujakirjasto sisältää tekstimuuttujia, eikä asiakirjassa ole yhtään +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` muuttujaa `{name}` ei voi tallentaa muuttujakirjastoon: nimeä se uudelleen niin, että se alkaa kirjaimella tai merkillä `_` eikä sisällä välilyöntejä tai muita symboleja kuin `-`, `_` ja `.` +@msg data set `{name}` holds a control character, which a variable library can't hold tietojoukko `{name}` sisältää ohjausmerkin, jota muuttujakirjasto ei voi sisältää +@msg data set `{name}` starts or ends with white space, which a variable library can't hold tietojoukko `{name}` alkaa tai päättyy tyhjeeseen, jota muuttujakirjasto ei voi sisältää Filter Suodata Filter Layers Panel Rows Suodata Tasot-paneelin rivit Flatten Litistä diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 71492222a..0f1b25b23 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5465,6 +5465,11 @@ theme Appearance Apparence Import Data Sets… Importer des jeux de données… Export Data Sets… Exporter des jeux de données… Load Variable Library… Charger une bibliothèque de variables… + Save Variable Library… Enregistrer une bibliothèque de variables… +@msg a variable library holds text variables, and the document has none une bibliothèque de variables contient des variables de texte, et le document n’en a aucune +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` la variable `{name}` ne peut pas être enregistrée dans une bibliothèque de variables : renommez-la pour qu’elle commence par une lettre ou `_`, sans espaces ni symboles autres que `-`, `_` et `.` +@msg data set `{name}` holds a control character, which a variable library can't hold le jeu de données `{name}` contient un caractère de contrôle, qu’une bibliothèque de variables ne peut pas contenir +@msg data set `{name}` starts or ends with white space, which a variable library can't hold le jeu de données `{name}` commence ou se termine par une espace, qu’une bibliothèque de variables ne peut pas contenir Make Graph Dynamic Rendre le graphe dynamique Batch… Traitement par lots… Source Folder: Dossier source : diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index c9dc2ccdd..daafaf168 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5453,6 +5453,11 @@ imageTrace Outline Körvonal Import Data Sets… Adatkészletek importálása… Export Data Sets… Adatkészletek exportálása… Load Variable Library… Változókönyvtár betöltése… + Save Variable Library… Változókönyvtár mentése… +@msg a variable library holds text variables, and the document has none a változókönyvtár szöveges változókat tartalmaz, a dokumentumban pedig nincs ilyen +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` a(z) `{name}` változó nem menthető változókönyvtárba: nevezze át úgy, hogy betűvel vagy `_` jellel kezdődjön, szóközök és a `-`, `_` és `.` jeltől eltérő szimbólumok nélkül +@msg data set `{name}` holds a control character, which a variable library can't hold a(z) `{name}` adatkészlet vezérlőkaraktert tartalmaz, amelyet a változókönyvtár nem tartalmazhat +@msg data set `{name}` starts or ends with white space, which a variable library can't hold a(z) `{name}` adatkészlet szóközzel kezdődik vagy végződik, amelyet a változókönyvtár nem tartalmazhat Make Graph Dynamic Diagram dinamikussá tétele Batch… Kötegelt feldolgozás… Source Folder: Forrásmappa: diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index f61266994..9f2e16d5b 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5129,6 +5129,11 @@ mark-style Japanese Jepang Import Data Sets… Impor himpunan data… Export Data Sets… Ekspor himpunan data… Load Variable Library… Muat pustaka variabel… + Save Variable Library… Simpan pustaka variabel… +@msg a variable library holds text variables, and the document has none pustaka variabel berisi variabel teks, dan dokumen tidak memilikinya +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` variabel `{name}` tidak dapat disimpan dalam pustaka variabel: ganti namanya agar diawali huruf atau `_`, tanpa spasi atau simbol selain `-`, `_`, dan `.` +@msg data set `{name}` holds a control character, which a variable library can't hold himpunan data `{name}` berisi karakter kontrol, yang tidak dapat dimuat dalam pustaka variabel +@msg data set `{name}` starts or ends with white space, which a variable library can't hold himpunan data `{name}` diawali atau diakhiri spasi, yang tidak dapat dimuat dalam pustaka variabel @msg variables that aren't text were left out: {kinds} variabel yang bukan teks dihilangkan: {kinds} @msg a new document won't read these variables back with their kind: {names} dokumen baru tidak akan membaca kembali variabel ini dengan jenisnya: {names} @msg a new document refuses this file because of these variables: {names} dokumen baru menolak file ini karena variabel berikut: {names} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 47802ca7f..8580ea7ba 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5080,6 +5080,11 @@ lighting Light Luce Import Data Sets… Importa set di dati… Export Data Sets… Esporta set di dati… Load Variable Library… Carica libreria di variabili… + Save Variable Library… Salva libreria di variabili… +@msg a variable library holds text variables, and the document has none una libreria di variabili contiene variabili di testo e il documento non ne ha +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` la variabile `{name}` non può essere salvata in una libreria di variabili: rinominala in modo che inizi con una lettera o `_`, senza spazi né simboli diversi da `-`, `_` e `.` +@msg data set `{name}` holds a control character, which a variable library can't hold il set di dati `{name}` contiene un carattere di controllo, che una libreria di variabili non può contenere +@msg data set `{name}` starts or ends with white space, which a variable library can't hold il set di dati `{name}` inizia o finisce con uno spazio, che una libreria di variabili non può contenere Make Graph Dynamic Rendi grafico dinamico Batch… Elaborazione batch… Source Folder: Cartella di origine: diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index e685841c9..dc4dd3827 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5125,6 +5125,11 @@ lighting Light ライト Import Data Sets… データセットを読み込み… Export Data Sets… データセットを書き出し… Load Variable Library… 変数ライブラリを読み込み… + Save Variable Library… 変数ライブラリを保存… +@msg a variable library holds text variables, and the document has none 変数ライブラリにはテキスト変数を保存しますが、このドキュメントにはテキスト変数がありません +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` 変数 `{name}` は変数ライブラリに保存できません: 英字または `_` で始まり、スペースや `-`、`_`、`.` 以外の記号を含まない名前に変更してください +@msg data set `{name}` holds a control character, which a variable library can't hold データセット `{name}` に変数ライブラリに保存できない制御文字が含まれています +@msg data set `{name}` starts or ends with white space, which a variable library can't hold データセット `{name}` の先頭または末尾に変数ライブラリに保存できない空白があります Make Graph Dynamic グラフを動的にする Batch… バッチ… Source Folder: ソースフォルダー: diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index eafdc314e..5e13eb084 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5112,6 +5112,11 @@ imageTrace Outline კონტური Import Data Sets… მონაცემთა ნაკრებების იმპორტი… Export Data Sets… მონაცემთა ნაკრებების ექსპორტი… Load Variable Library… ცვლადების ბიბლიოთეკის ჩატვირთვა… + Save Variable Library… ცვლადების ბიბლიოთეკის შენახვა… +@msg a variable library holds text variables, and the document has none ცვლადების ბიბლიოთეკა ტექსტურ ცვლადებს შეიცავს, დოკუმენტს კი არცერთი არ აქვს +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` ცვლადი `{name}` ვერ შეინახება ცვლადების ბიბლიოთეკაში: გადაარქვით სახელი ისე, რომ იწყებოდეს ასოთი ან `_`-ით, ჰარეების და `-`, `_` და `.`-ის გარდა სხვა სიმბოლოების გარეშე +@msg data set `{name}` holds a control character, which a variable library can't hold მონაცემთა ნაკრები `{name}` შეიცავს მმართველ სიმბოლოს, რომელსაც ცვლადების ბიბლიოთეკა ვერ შეინახავს +@msg data set `{name}` starts or ends with white space, which a variable library can't hold მონაცემთა ნაკრები `{name}` იწყება ან მთავრდება ჰარით, რომელსაც ცვლადების ბიბლიოთეკა ვერ შეინახავს Make Graph Dynamic დიაგრამის დინამიკურად ქცევა Batch… პაკეტური დამუშავება… Source Folder: წყარო საქაღალდე: diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 3482f3d37..ca5fa5c3a 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5043,6 +5043,11 @@ lighting Light 광원 Import Data Sets… 데이터 세트 가져오기… Export Data Sets… 데이터 세트 내보내기… Load Variable Library… 변수 라이브러리 불러오기… + Save Variable Library… 변수 라이브러리 저장… +@msg a variable library holds text variables, and the document has none 변수 라이브러리에는 텍스트 변수가 들어가는데 문서에 텍스트 변수가 없습니다 +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` 변수 `{name}`은(는) 변수 라이브러리에 저장할 수 없습니다: 문자나 `_`로 시작하고 공백이나 `-`, `_`, `.` 이외의 기호가 없도록 이름을 바꾸세요 +@msg data set `{name}` holds a control character, which a variable library can't hold 데이터 세트 `{name}`에 변수 라이브러리에 담을 수 없는 제어 문자가 있습니다 +@msg data set `{name}` starts or ends with white space, which a variable library can't hold 데이터 세트 `{name}`의 앞이나 뒤에 변수 라이브러리에 담을 수 없는 공백이 있습니다 Make Graph Dynamic 그래프를 동적으로 만들기 Batch… 일괄 처리… Source Folder: 소스 폴더: diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 8f8b538df..4ff69b6a1 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5141,6 +5141,11 @@ mark-style Japanese Japans Import Data Sets… Gegevenssets importeren… Export Data Sets… Gegevenssets exporteren… Load Variable Library… Variabelenbibliotheek laden… + Save Variable Library… Variabelenbibliotheek opslaan… +@msg a variable library holds text variables, and the document has none een variabelenbibliotheek bevat tekstvariabelen, en het document heeft er geen +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` variabele `{name}` kan niet in een variabelenbibliotheek worden opgeslagen: geef haar een naam die met een letter of `_` begint, zonder spaties of andere symbolen dan `-`, `_` en `.` +@msg data set `{name}` holds a control character, which a variable library can't hold gegevensset `{name}` bevat een stuurteken, dat een variabelenbibliotheek niet kan bevatten +@msg data set `{name}` starts or ends with white space, which a variable library can't hold gegevensset `{name}` begint of eindigt met witruimte, die een variabelenbibliotheek niet kan bevatten Make Graph Dynamic Grafiek dynamisch maken Batch… Batch… Source Folder: Bronmap: diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index d609be067..17db44fb6 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5320,6 +5320,11 @@ mark-style Japanese Japońskie Import Data Sets… Importuj zestawy danych… Export Data Sets… Eksportuj zestawy danych… Load Variable Library… Wczytaj bibliotekę zmiennych… + Save Variable Library… Zapisz bibliotekę zmiennych… +@msg a variable library holds text variables, and the document has none biblioteka zmiennych zawiera zmienne tekstowe, a dokument nie ma żadnej +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` nie można zapisać zmiennej `{name}` w bibliotece zmiennych: zmień jej nazwę tak, aby zaczynała się literą lub `_`, bez spacji i symboli innych niż `-`, `_` i `.` +@msg data set `{name}` holds a control character, which a variable library can't hold zestaw danych `{name}` zawiera znak sterujący, którego biblioteka zmiennych nie może zawierać +@msg data set `{name}` starts or ends with white space, which a variable library can't hold zestaw danych `{name}` zaczyna się lub kończy białym znakiem, którego biblioteka zmiennych nie może zawierać Make Graph Dynamic Uczyń wykres dynamicznym Batch… Przetwarzanie wsadowe… Source Folder: Folder źródłowy: diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 658823a2a..611bad136 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -951,6 +951,11 @@ Export CSS… Exportar CSS… Export Data Sets… Exportar Conjuntos de Dados… Load Variable Library… Carregar Biblioteca de Variáveis… + Save Variable Library… Salvar Biblioteca de Variáveis… +@msg a variable library holds text variables, and the document has none uma biblioteca de variáveis contém variáveis de texto, e o documento não tem nenhuma +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` a variável `{name}` não pode ser salva em uma biblioteca de variáveis: renomeie-a para começar com uma letra ou `_`, sem espaços nem símbolos além de `-`, `_` e `.` +@msg data set `{name}` holds a control character, which a variable library can't hold o conjunto de dados `{name}` contém um caractere de controle, que uma biblioteca de variáveis não pode conter +@msg data set `{name}` starts or ends with white space, which a variable library can't hold o conjunto de dados `{name}` começa ou termina com espaço em branco, que uma biblioteca de variáveis não pode conter Export DXF Exportar DXF Export Document Exportar Documento Export EPS Exportar EPS diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index bb35a427e..5afaa5167 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5075,6 +5075,11 @@ lighting Light Свет Import Data Sets… Импортировать наборы данных… Export Data Sets… Экспортировать наборы данных… Load Variable Library… Загрузить библиотеку переменных… + Save Variable Library… Сохранить библиотеку переменных… +@msg a variable library holds text variables, and the document has none библиотека переменных содержит текстовые переменные, а в документе их нет +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` переменную `{name}` нельзя сохранить в библиотеке переменных: переименуйте её так, чтобы имя начиналось с буквы или `_` и не содержало пробелов и символов, кроме `-`, `_` и `.` +@msg data set `{name}` holds a control character, which a variable library can't hold набор данных `{name}` содержит управляющий символ, который не может храниться в библиотеке переменных +@msg data set `{name}` starts or ends with white space, which a variable library can't hold набор данных `{name}` начинается или заканчивается пробелом, который не может храниться в библиотеке переменных Make Graph Dynamic Сделать диаграмму динамической Batch… Пакетная обработка… Source Folder: Исходная папка: diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 0e2091a05..90c5bd949 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5330,6 +5330,11 @@ theme Appearance Utseende Editable Redigerbar Export Data Sets… Exportera datauppsättningar… Load Variable Library… Läs in variabelbibliotek… + Save Variable Library… Spara variabelbibliotek… +@msg a variable library holds text variables, and the document has none ett variabelbibliotek innehåller textvariabler, och dokumentet har inga +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` variabeln `{name}` kan inte sparas i ett variabelbibliotek: byt namn på den så att den börjar med en bokstav eller `_`, utan mellanslag eller andra symboler än `-`, `_` och `.` +@msg data set `{name}` holds a control character, which a variable library can't hold datauppsättningen `{name}` innehåller ett kontrolltecken, som ett variabelbibliotek inte kan innehålla +@msg data set `{name}` starts or ends with white space, which a variable library can't hold datauppsättningen `{name}` börjar eller slutar med blanksteg, som ett variabelbibliotek inte kan innehålla Filter Filter Filter Layers Panel Rows Filtrera rader i panelen Lager Flatten Platta till diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index b21829143..5d3f51436 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5463,6 +5463,11 @@ theme Appearance Görünüm In Merge Mode, Clicking Stroke Splits the Path Birleştirme modunda kontura tıklamak yolu böler Insert Kashidas Keşide Ekle Load Variable Library… Değişken Kitaplığı Yükle… + Save Variable Library… Değişken Kitaplığını Kaydet… +@msg a variable library holds text variables, and the document has none değişken kitaplığı metin değişkenleri içerir ve belgede hiç metin değişkeni yok +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` `{name}` değişkeni değişken kitaplığına kaydedilemiyor: adını bir harf veya `_` ile başlayacak, boşluk ya da `-`, `_` ve `.` dışında simge içermeyecek şekilde değiştirin +@msg data set `{name}` holds a control character, which a variable library can't hold `{name}` veri kümesi, değişken kitaplığının tutamayacağı bir denetim karakteri içeriyor +@msg data set `{name}` starts or ends with white space, which a variable library can't hold `{name}` veri kümesi, değişken kitaplığının tutamayacağı bir boşlukla başlıyor veya bitiyor Long Uzun New Brush Yeni Fırça Pattern Brush Desen Fırçası diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 437cef11e..f14471d32 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5060,6 +5060,11 @@ lighting Light Світло Import Data Sets… Імпортувати набори даних… Export Data Sets… Експортувати набори даних… Load Variable Library… Завантажити бібліотеку змінних… + Save Variable Library… Зберегти бібліотеку змінних… +@msg a variable library holds text variables, and the document has none бібліотека змінних містить текстові змінні, а в документі їх немає +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` змінну `{name}` не можна зберегти в бібліотеці змінних: перейменуйте її так, щоб ім’я починалося з літери або `_` і не містило пробілів і символів, крім `-`, `_` і `.` +@msg data set `{name}` holds a control character, which a variable library can't hold набір даних `{name}` містить керівний символ, який бібліотека змінних не може містити +@msg data set `{name}` starts or ends with white space, which a variable library can't hold набір даних `{name}` починається або закінчується пробілом, який бібліотека змінних не може містити Make Graph Dynamic Зробити діаграму динамічною Batch… Пакетна обробка… Source Folder: Вихідна папка: diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index c63447073..f3246fb16 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5063,6 +5063,11 @@ theme Appearance Giao diện Import Data Sets… Nhập tập dữ liệu… Export Data Sets… Xuất tập dữ liệu… Load Variable Library… Tải thư viện biến số… + Save Variable Library… Lưu thư viện biến số… +@msg a variable library holds text variables, and the document has none thư viện biến số chứa các biến số văn bản, và tài liệu không có biến số văn bản nào +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` không thể lưu biến số `{name}` vào thư viện biến số: hãy đổi tên để bắt đầu bằng một chữ cái hoặc `_`, không có khoảng trắng hay ký hiệu nào ngoài `-`, `_` và `.` +@msg data set `{name}` holds a control character, which a variable library can't hold tập dữ liệu `{name}` chứa ký tự điều khiển mà thư viện biến số không thể chứa +@msg data set `{name}` starts or ends with white space, which a variable library can't hold tập dữ liệu `{name}` bắt đầu hoặc kết thúc bằng khoảng trắng mà thư viện biến số không thể chứa Make Graph Dynamic Tạo biểu đồ động Batch… Hàng loạt… Source Folder: Thư mục nguồn: diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index ecc3a3543..082cdc70b 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5310,6 +5310,11 @@ lighting Light 光源 Import Data Sets… 导入数据集… Export Data Sets… 导出数据集… Load Variable Library… 载入变量库… + Save Variable Library… 保存变量库… +@msg a variable library holds text variables, and the document has none 变量库包含文本变量,而文档中没有文本变量 +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` 无法将变量 `{name}` 保存到变量库中:请重命名,使其以字母或 `_` 开头,且不含空格或 `-`、`_`、`.` 以外的符号 +@msg data set `{name}` holds a control character, which a variable library can't hold 数据集 `{name}` 包含变量库无法保存的控制字符 +@msg data set `{name}` starts or ends with white space, which a variable library can't hold 数据集 `{name}` 的开头或结尾有变量库无法保存的空白字符 Make Graph Dynamic 使图表动态化 Batch… 批处理… Source Folder: 源文件夹: diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 9b9cd1b1c..ab4577e21 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5124,6 +5124,11 @@ lighting Light 光源 Import Data Sets… 匯入資料集… Export Data Sets… 匯出資料集… Load Variable Library… 載入變數資料庫… + Save Variable Library… 儲存變數資料庫… +@msg a variable library holds text variables, and the document has none 變數資料庫包含文字變數,但文件中沒有文字變數 +@msg variable `{name}` can't be saved in a variable library: rename it to start with a letter or `_`, without spaces or symbols other than `-`, `_` and `.` 無法將變數 `{name}` 儲存到變數資料庫:請重新命名,使其以字母或 `_` 開頭,且不含空格或 `-`、`_`、`.` 以外的符號 +@msg data set `{name}` holds a control character, which a variable library can't hold 資料集 `{name}` 包含變數資料庫無法儲存的控制字元 +@msg data set `{name}` starts or ends with white space, which a variable library can't hold 資料集 `{name}` 的開頭或結尾有變數資料庫無法儲存的空白字元 Make Graph Dynamic 使圖表動態化 Batch… 批次處理… Source Folder: 來源資料夾: diff --git a/crates/ui-egui/src/panels/variables.rs b/crates/ui-egui/src/panels/variables.rs index e45e618ac..882b0beac 100644 --- a/crates/ui-egui/src/panels/variables.rs +++ b/crates/ui-egui/src/panels/variables.rs @@ -263,6 +263,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let highlighted_bound: Vec = highlighted.as_ref().map(|h| bound_objects(st, h)).unwrap_or_default(); let Picked { selected, has_selection, selection_bound, single_type, single_image, selection_graph } = picked(st); let locked = st.doc.variables.locked; + // Save Variable Library writes text variables only. + let has_text = st.doc.variables.variables.iter().any(|v| v.kind == VariableKind::Text); if widgets::menu_item(ui, tl!("New Variable…"), !locked, false) { crate::menus::invoke(app, "variable.define", json!({})); @@ -326,6 +328,9 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if widgets::menu_item(ui, tl!("Load Variable Library…"), true, false) { crate::picks::button(app, load_library); } + if widgets::menu_item(ui, tl!("Save Variable Library…"), has_text, false) { + crate::picks::button(app, save_library); + } ui.separator(); // What the highlighted variable drives, selected — its bindings are left alone. if widgets::menu_item(ui, tl!("Select Bound Object"), !highlighted_bound.is_empty(), false) { @@ -364,6 +369,16 @@ fn load_library(app: &mut VectorcraftApp) { } } +/// Variables panel: Save Variable Library… opens a save dialog for an XML file and runs +/// `variable.saveLibrary` with the chosen path. +fn save_library(app: &mut VectorcraftApp) { + let pick = crate::FilePick { name: "variables.xml".into(), filters: vec![crate::FilePick::filter("XML", &["xml"])], ..Default::default() }; + let Some(path) = crate::picks::save(app, &pick) else { return }; + if let Err(e) = app.run("variable.saveLibrary", json!({"path": path})) { + app.status(e); + } +} + #[cfg(test)] mod tests { use super::*; @@ -451,6 +466,7 @@ mod tests { "Rename Data Set…", "Delete Data Set", "Load Variable Library…", + "Save Variable Library…", "Select Bound Object", "Select All Bound Objects", ] { diff --git a/crates/ui-egui/src/tests/picks.rs b/crates/ui-egui/src/tests/picks.rs index fc59fe485..8608b7813 100644 --- a/crates/ui-egui/src/tests/picks.rs +++ b/crates/ui-egui/src/tests/picks.rs @@ -334,6 +334,39 @@ fn load_variable_library_asks_off_the_ui_thread() { let _ = std::fs::remove_dir_all(dir); } +/// The Variables panel menu's Save Variable Library… is enabled once the document has a text +/// variable, and then asks off the UI thread: the library is written +/// to the path the dialog answers, Load Variable Library reads it back, and the warning about the +/// visibility variable it left out shows in the status bar. +#[test] +fn save_variable_library_asks_off_the_ui_thread() { + let (mut app, shown, _) = app(); + app.run("file.new", json!({"width": 100, "height": 80})).unwrap(); + app.run("variable.define", json!({"name": "Shown", "kind": "visibility"})).unwrap(); + // Without a text variable the item is disabled and asks for no file. + click(&mut app, "Save Variable Library…", crate::panels::variables::menu); + assert!(shown.requests().is_empty(), "{:?}", shown.requests()); + app.run("variable.define", json!({"name": "Name", "kind": "text"})).unwrap(); + app.run("dataset.new", json!({"name": "A", "values": {"Name": "x", "Shown": true}})).unwrap(); + let dir = vectorcraft_testkit::temp_dir("picks-save-variable-library"); + if let Err(e) = std::fs::remove_dir_all(&dir) { + assert_eq!(e.kind(), std::io::ErrorKind::NotFound, "cannot empty {}: {e}", dir.display()); + } + std::fs::create_dir_all(&dir).unwrap(); + let library = dir.join("library.xml"); + + click(&mut app, "Save Variable Library…", crate::panels::variables::menu); + assert!(matches!(shown.requests().as_slice(), [PickRequest::Save(p)] if p.name == "variables.xml")); + assert!(!library.exists(), "nothing is written before the dialog answers"); + shown.answer(&[&library.to_string_lossy()]); + poll(&mut app); + assert_eq!(app.ui.status, "variables that aren't text were left out: visibility (1)"); + let xml = std::fs::read_to_string(&library).unwrap_or_else(|e| panic!("{e}: {}", app.ui.status)); + let loaded = app.run("variable.loadLibrary", json!({"xml": xml})).unwrap(); + assert_eq!((loaded["variables"].clone(), loaded["names"].clone()), (json!(["Name"]), json!(["A"]))); + let _ = std::fs::remove_dir_all(dir); +} + /// The Keyboard Shortcuts dialog's Import… and Export… ask off the UI thread: the set imported /// replaces the dialog's working set (kept on OK), Export… writes it where the dialog answers, and /// an answer once the dialog is closed does nothing. diff --git a/docs/mcp.md b/docs/mcp.md index 430246c89..204690755 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1679,6 +1679,18 @@ already walks Image Trace. Short agent entry points: comments, processing instructions and up to 64 entity declarations whose values are plain text (no `<`, `&` or `%`). A declaration that holds anything else, and entity references that expand the library past 8 MiB, are refused. So are a variable or data set declared twice and a value for a variable the library doesn't declare. + `variable.saveLibrary {path?}` (the panel menu's Save Variable Library…) writes the document's text variables and + every data set as a variable library in the layout `variable.loadLibrary` reads: a `variable` element with + `varName` and `trait="textcontent"` for each text variable, and a `sampleDataSet` element with `dataSetName` for + each data set, holding an element named after each text variable it has a value for, with one `p` element per line + of the value. Loading the file gives back the same text variables, data set names and text values. Variables of + other kinds are left out with their values: the reply lists them in `skipped` (`name`, `kind`), and its `warning` + names each kind with its count. Saving is refused for a document without a text variable, for a text variable whose + name isn't an XML name (it starts with a letter or `_` and holds only letters, digits, `-`, `_` and `.`), for a + data set whose name or text value holds a control character, for a data set name with white space at either end, + for a name longer than 255 characters or used twice, and past `variable.loadLibrary`'s caps (256 variables, 1,000 + data sets, 8 MiB). Without `path` the reply is `{xml, variables, datasets, skipped, warning?}`; + with it, the file is written and the reply is `{path, variables, datasets, skipped, warning?}`. - Batch: `action.batch {commands, source?: folder|datasets, folder?, destination?: none|saveAndClose|folder, destinationFolder?, overrideOpen?, overrideSave?, overrideExport?, includeSubdirs?, stopOnError?}` plays an action on every file in a folder or every data set (the Actions panel menu's Batch…). Override Save and Override diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index de1311a17..bc03e36c4 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -565,6 +565,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Variables: Import Data Sets… (`dataset.import`) reads tab-separated files as well as comma-separated ones, and files without a `name` column, whose rows take the first free names Data Set 1, Data Set 2 and so on. An import error about a row's values or name gives the file line the row starts on. - Variables: Import Data Sets… (`dataset.import`) defines the variables a file's header names that the document lacks, unbound. `#Shown` defines a visibility variable, `@Photo` defines an image variable, and a header without a prefix defines a text variable. A header that is a variable's exact name keeps naming it, and any other `%` header (graph data files) is refused. - Variables: Load Variable Library… in the Variables panel menu (`variable.loadLibrary`) replaces the document's variables and data sets with a variable library's, read from its XML file, in one undo step: every variable is deleted with its bindings and values, and every data set is deleted; then the library's text variables are defined, unbound, and its data sets take the text of each value's `p` elements, one paragraph each. Variables of other kinds are left out with a warning that names their kinds. + - Variables: Save Variable Library… in the Variables panel menu (`variable.saveLibrary`) writes the document's text variables and data sets as a variable library's XML file, which Load Variable Library reads back with the same text variables, data set names and text values. Variables of other kinds are left out with a warning that names their kinds. - JSON-lines control channel with real egui pointer and keyboard injection. - Synthetic input validates complete gestures before queue mutation: at most 256 drag steps, 16 clicks and 1,024 pending events; up to 64 control requests per frame, with later requests retained for the next frame (#1709). - The control server writes each reply through a 64 KiB buffer. On Windows 11 it sends the 17 MB `document.inspect` reply of a document with 50,000 objects on one layer to a client over loopback in 0.09 s instead of 34 s.