mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2853 from dotCooCoo/feat/load-variable-library
Variables: Load Variable Library… reads a variable library's text variables and data sets
This commit is contained in:
@@ -165,7 +165,7 @@ fn parse<'a>(xml: &'a str, name: &str, cmd: &str) -> Result<roxmltree::Document<
|
||||
|
||||
/// How deep the elements of `xml` nest: start tags less end tags, at most, outside comments,
|
||||
/// CDATA sections, processing instructions, declarations and quoted attribute values.
|
||||
fn nesting(xml: &str) -> usize {
|
||||
pub(crate) fn nesting(xml: &str) -> usize {
|
||||
let b = xml.as_bytes();
|
||||
let (mut depth, mut most, mut i) = (0usize, 0usize, 0usize);
|
||||
// The index just past the first `end` at or after `from` (the end of the text if none).
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
pub(crate) mod adopt;
|
||||
pub(crate) mod document;
|
||||
mod docx;
|
||||
pub(crate) mod docx;
|
||||
mod rtf;
|
||||
mod text;
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
//! step. `dataset.import` / `dataset.export` move those rows as CSV (header: `name`
|
||||
//! then variable names); import also reads tab-separated files, files without the
|
||||
//! `name` column, and header cells that define new variables (`#` for visibility, `@` for
|
||||
//! image, no prefix for text). Agents reach every command through `run_command`, like all engine
|
||||
//! commands.
|
||||
//! 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.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@@ -30,9 +31,18 @@ const MAX_CSV_BYTES: usize = 1_048_576;
|
||||
const MAX_CSV_ROWS: usize = 1_000;
|
||||
const MAX_CSV_COLS: usize = 256;
|
||||
|
||||
/// 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;
|
||||
const MAX_LIBRARY_ENTITIES: usize = 64;
|
||||
const MAX_LIBRARY_NODES: u32 = 2_000_000;
|
||||
const MAX_LIBRARY_NESTING: usize = 256;
|
||||
const MAX_LIBRARY_VARIABLES: usize = 256;
|
||||
const MAX_LIBRARY_DATASETS: usize = 1_000;
|
||||
|
||||
/// Lock Variables refuses New Variable, Delete Variable, Rename Variable (Variable Options),
|
||||
/// Make … Dynamic and an import whose header would define a variable; binding, unbinding and the
|
||||
/// other dataset commands still run.
|
||||
/// Make … Dynamic, an import whose header would define a variable and a Load Variable Library
|
||||
/// that would delete or define one; binding, unbinding and the other dataset commands still run.
|
||||
fn variables_unlocked(s: &Session) -> std::result::Result<(), String> {
|
||||
has_doc(s)?;
|
||||
if s.active().is_some_and(|st| st.doc.variables.locked) { Err("variables are locked".into()) } else { Ok(()) }
|
||||
@@ -891,6 +901,289 @@ fn dataset_step(s: &mut Session, cmd: &str, forward: bool) -> Result<Value> {
|
||||
apply_dataset(s, &name)
|
||||
}
|
||||
|
||||
/// Bytes of a variable library from `path`, `xml` or `dataBase64`.
|
||||
fn library_bytes(p: &Value, cmd: &str) -> Result<Vec<u8>> {
|
||||
let bytes = match (str_param(p, "path"), str_param(p, "xml"), str_param(p, "dataBase64")) {
|
||||
// One byte past the cap is enough to refuse the file below.
|
||||
(Some(path), ..) => read_file_head(&absolute_path(path), MAX_LIBRARY_BYTES + 1)?,
|
||||
(None, Some(xml), _) => xml.as_bytes().to_vec(),
|
||||
(None, None, Some(b64)) => vectorcraft_format::base64_decode(b64).ok_or_else(|| bad(cmd, "bad dataBase64"))?,
|
||||
_ => return Err(bad(cmd, "give `path`, `xml` or `dataBase64`")),
|
||||
};
|
||||
if bytes.len() > MAX_LIBRARY_BYTES {
|
||||
return Err(bad(cmd, format!("the variable library is larger than {MAX_LIBRARY_BYTES} bytes")));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// `xml` without the document type declaration of its prolog, with each reference to a general
|
||||
/// entity that the declaration defines replaced by the entity's value, its quotes and `>` escaped,
|
||||
/// outside CDATA sections, comments and processing instructions. The declaration may hold an
|
||||
/// external identifier and an internal subset of white space, comments, processing instructions and
|
||||
/// up to [`MAX_LIBRARY_ENTITIES`] entity declarations, each with a name of at most [`MAX_NAME`]
|
||||
/// bytes and a quoted value that holds no `<`, `&` or `%`. Anything else in it is refused, and so is
|
||||
/// a text that the replacements make longer than [`MAX_LIBRARY_BYTES`]. The XML parser then refuses
|
||||
/// any other document type declaration, so [`nesting`](super::place::docx::nesting) counts the
|
||||
/// elements the parser reads.
|
||||
fn without_doctype(xml: &str, cmd: &str) -> Result<String> {
|
||||
let b = xml.as_bytes();
|
||||
let past = |from: usize, end: &str| xml.get(from..).and_then(|rest| rest.find(end)).map(|at| from + at + end.len());
|
||||
let spaces = |from: usize| from + b.get(from..).map_or(0, |rest| rest.iter().take_while(|c| c.is_ascii_whitespace()).count());
|
||||
let at = |i: usize, s: &str| xml.get(i..).is_some_and(|rest| rest.starts_with(s));
|
||||
let refused = || bad(cmd, "the variable library's document type declaration holds something other than entities of plain text");
|
||||
// The prolog: an XML declaration, white space, comments and processing instructions.
|
||||
let mut i = 0;
|
||||
loop {
|
||||
i = spaces(i);
|
||||
if at(i, "<?") {
|
||||
i = past(i + 2, "?>").ok_or_else(refused)?;
|
||||
} else if at(i, "<!--") {
|
||||
i = past(i + 4, "-->").ok_or_else(refused)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !at(i, "<!DOCTYPE") {
|
||||
return Ok(xml.to_string());
|
||||
}
|
||||
let start = i;
|
||||
// A quoted literal starting at `i` → the index past it and its text.
|
||||
let literal = |i: usize| -> Option<(usize, &str)> {
|
||||
let quote = *b.get(i).filter(|c| matches!(c, b'"' | b'\''))?;
|
||||
let len = b.get(i + 1..)?.iter().position(|c| *c == quote)?;
|
||||
Some((i + len + 2, xml.get(i + 1..i + 1 + len)?))
|
||||
};
|
||||
// `<!DOCTYPE` S Name (S ExternalID)? S? ('[' intSubset ']' S?)? '>'
|
||||
i = spaces(i + "<!DOCTYPE".len());
|
||||
i += b.get(i..).map_or(0, |rest| rest.iter().take_while(|c| !c.is_ascii_whitespace() && !matches!(c, b'[' | b'>')).count());
|
||||
i = spaces(i);
|
||||
for (id, literals) in [("SYSTEM", 1), ("PUBLIC", 2)] {
|
||||
if at(i, id) {
|
||||
i += id.len();
|
||||
for _ in 0..literals {
|
||||
i = literal(spaces(i)).ok_or_else(refused)?.0;
|
||||
}
|
||||
i = spaces(i);
|
||||
}
|
||||
}
|
||||
let mut entities: Vec<(&str, String)> = Vec::new();
|
||||
if at(i, "[") {
|
||||
i += 1;
|
||||
loop {
|
||||
i = spaces(i);
|
||||
if at(i, "]") {
|
||||
i = spaces(i + 1);
|
||||
break;
|
||||
} else if at(i, "<!--") {
|
||||
i = past(i + 4, "-->").ok_or_else(refused)?;
|
||||
} else if at(i, "<?") {
|
||||
i = past(i + 2, "?>").ok_or_else(refused)?;
|
||||
} else if at(i, "<!ENTITY") {
|
||||
i = spaces(i + "<!ENTITY".len());
|
||||
let len =
|
||||
b.get(i..).map_or(0, |rest| rest.iter().take_while(|c| !c.is_ascii_whitespace() && !matches!(c, b'"' | b'\'' | b'>')).count());
|
||||
let name = xml.get(i..i + len).filter(|n| !n.is_empty() && n.len() <= MAX_NAME && !n.starts_with('%')).ok_or_else(refused)?;
|
||||
let (end, value) = literal(spaces(i + len)).ok_or_else(refused)?;
|
||||
if value.contains(['<', '&', '%']) {
|
||||
return Err(refused());
|
||||
}
|
||||
i = spaces(end);
|
||||
if !at(i, ">") {
|
||||
return Err(refused());
|
||||
}
|
||||
i += 1;
|
||||
if entities.len() == MAX_LIBRARY_ENTITIES {
|
||||
return Err(bad(cmd, format!("the variable library declares more than {MAX_LIBRARY_ENTITIES} entities")));
|
||||
}
|
||||
entities.push((name, value.replace('"', """).replace('\'', "'").replace('>', ">")));
|
||||
} else {
|
||||
return Err(refused());
|
||||
}
|
||||
}
|
||||
}
|
||||
if !at(i, ">") {
|
||||
return Err(refused());
|
||||
}
|
||||
let mut out = String::with_capacity(xml.len());
|
||||
out.push_str(xml.get(..start).unwrap_or_default());
|
||||
// The rest, copied with the references replaced outside CDATA sections, comments and
|
||||
// processing instructions, where the parser reads none.
|
||||
let mut i = i + 1;
|
||||
while let Some(c) = xml.get(i..).and_then(|rest| rest.chars().next()) {
|
||||
let verbatim = [("<![CDATA[", "]]>"), ("<!--", "-->"), ("<?", "?>")].into_iter().find(|(open, _)| at(i, open));
|
||||
if let Some((open, close)) = verbatim {
|
||||
let end = past(i + open.len(), close).unwrap_or(xml.len());
|
||||
out.push_str(xml.get(i..end).unwrap_or_default());
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
// Entity names are at most `MAX_NAME` bytes long, so the `;` is looked for that far.
|
||||
let entity = if c == '&' {
|
||||
let name = xml.get(i + 1..).and_then(|rest| rest.get(..rest.bytes().take(MAX_NAME + 1).position(|b| b == b';')?));
|
||||
name.and_then(|name| entities.iter().find(|(n, _)| *n == name))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match entity {
|
||||
Some((name, value)) => {
|
||||
out.push_str(value);
|
||||
i += name.len() + 2;
|
||||
if out.len() > MAX_LIBRARY_BYTES {
|
||||
return Err(bad(cmd, format!("the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes")));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
out.push(c);
|
||||
i += c.len_utf8();
|
||||
}
|
||||
}
|
||||
}
|
||||
if out.len() > MAX_LIBRARY_BYTES {
|
||||
return Err(bad(cmd, format!("the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes")));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A variable library as Load Variable Library reads it: the names of its text variables in the
|
||||
/// order it declares them, the name and `trait` of each variable of another kind, and its data
|
||||
/// sets with their text values.
|
||||
struct Library {
|
||||
text: Vec<String>,
|
||||
other: Vec<(String, String)>,
|
||||
datasets: Vec<DataSet>,
|
||||
}
|
||||
|
||||
/// Read a variable library: XML whose `variableSets` element holds `variableSet` elements, each
|
||||
/// with a `variables` element of `variable` elements (`varName`, `trait`) and a `sampleDataSets`
|
||||
/// element of `sampleDataSet` elements (`dataSetName`). Elements are matched by their local names.
|
||||
/// A variable whose `trait` is `textcontent` is a text variable. In a data set, an element named
|
||||
/// after a text variable gives its value: the text of its `p` elements, one paragraph each, and
|
||||
/// none when it has no `p` element. Values for variables of other kinds are left out. A variable
|
||||
/// or data set declared twice, a value for a variable the library doesn't declare, and a library
|
||||
/// past the caps are refused. Entities that its document type declaration defines are read as
|
||||
/// [`without_doctype`] says.
|
||||
fn parse_library(xml: &str, cmd: &str) -> Result<Library> {
|
||||
let xml = without_doctype(xml, cmd)?;
|
||||
// roxmltree parses nested elements recursively.
|
||||
if super::place::docx::nesting(&xml) > MAX_LIBRARY_NESTING {
|
||||
return Err(bad(cmd, format!("the variable library nests elements more than {MAX_LIBRARY_NESTING} deep")));
|
||||
}
|
||||
let opts = roxmltree::ParsingOptions { nodes_limit: MAX_LIBRARY_NODES, ..roxmltree::ParsingOptions::default() };
|
||||
let doc =
|
||||
roxmltree::Document::parse_with_options(&xml, opts).map_err(|e| bad(cmd, format!("the variable library is not well-formed XML: {e}")))?;
|
||||
let is = |n: &roxmltree::Node, name: &str| n.is_element() && n.tag_name().name() == name;
|
||||
let Some(root) = doc.descendants().find(|n| is(n, "variableSets")) else {
|
||||
return Err(bad(cmd, "the file holds no `variableSets` element"));
|
||||
};
|
||||
let sets: Vec<roxmltree::Node> = root.children().filter(|n| is(n, "variableSet")).collect();
|
||||
let (mut text, mut other, mut declared) = (Vec::new(), Vec::new(), BTreeSet::new());
|
||||
for var in sets.iter().flat_map(|s| s.children()).filter(|n| is(n, "variables")).flat_map(|v| v.children()).filter(|n| is(n, "variable")) {
|
||||
let name = named(var.attribute("varName"), "varName", cmd)?;
|
||||
if !declared.insert(name.clone()) {
|
||||
return Err(bad(cmd, format!("variable `{name}` is declared twice")));
|
||||
}
|
||||
if declared.len() > MAX_LIBRARY_VARIABLES {
|
||||
return Err(bad(cmd, format!("the variable library declares more than {MAX_LIBRARY_VARIABLES} variables")));
|
||||
}
|
||||
match var.attribute("trait") {
|
||||
Some("textcontent") => text.push(name),
|
||||
Some(t) => other.push((name, t.chars().take(MAX_NAME).collect())),
|
||||
None => return Err(bad(cmd, format!("variable `{name}` has no `trait`"))),
|
||||
}
|
||||
}
|
||||
if declared.is_empty() {
|
||||
return Err(bad(cmd, "the variable library declares no variables"));
|
||||
}
|
||||
let text_names: BTreeSet<&str> = text.iter().map(String::as_str).collect();
|
||||
let (mut datasets, mut seen) = (Vec::new(), BTreeSet::new());
|
||||
for row in
|
||||
sets.iter().flat_map(|s| s.children()).filter(|n| is(n, "sampleDataSets")).flat_map(|d| d.children()).filter(|n| is(n, "sampleDataSet"))
|
||||
{
|
||||
let name = named(row.attribute("dataSetName"), "dataSetName", cmd)?;
|
||||
if !seen.insert(name.clone()) {
|
||||
return Err(bad(cmd, format!("data set `{name}` is declared twice")));
|
||||
}
|
||||
if seen.len() > MAX_LIBRARY_DATASETS {
|
||||
return Err(bad(cmd, format!("the variable library declares more than {MAX_LIBRARY_DATASETS} data sets")));
|
||||
}
|
||||
let (mut values, mut held) = (BTreeMap::new(), BTreeSet::new());
|
||||
for el in row.children().filter(roxmltree::Node::is_element) {
|
||||
let var = el.tag_name().name();
|
||||
if !declared.contains(var) {
|
||||
return Err(bad(cmd, format!("data set `{name}` holds a value for `{var}`, which the library doesn't declare")));
|
||||
}
|
||||
if !held.insert(var) {
|
||||
return Err(bad(cmd, format!("data set `{name}` holds two values for `{var}`")));
|
||||
}
|
||||
if !text_names.contains(var) {
|
||||
continue;
|
||||
}
|
||||
let paras: Vec<String> = el
|
||||
.children()
|
||||
.filter(|n| is(n, "p"))
|
||||
.map(|p| p.descendants().filter(roxmltree::Node::is_text).filter_map(|t| t.text()).collect())
|
||||
.collect();
|
||||
if !paras.is_empty() {
|
||||
values.insert(var.to_string(), DataValue::Text(paras.join("\n")));
|
||||
}
|
||||
}
|
||||
datasets.push(DataSet { name, values });
|
||||
}
|
||||
Ok(Library { text, other, datasets })
|
||||
}
|
||||
|
||||
/// Load Variable Library: replace the document's variables and data sets with a variable library's
|
||||
/// ([`parse_library`]) in one undo step. Every variable of the document is deleted with its bindings
|
||||
/// and values, and every data set is deleted. The library's text variables are then defined,
|
||||
/// unbound, and its data sets are added. The library's variables of other kinds are left out, named
|
||||
/// in `skipped` and in a warning.
|
||||
fn load_library(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "variable.loadLibrary";
|
||||
let bytes = library_bytes(p, C)?;
|
||||
let xml = std::str::from_utf8(bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(&bytes)).map_err(|_| bad(C, "the variable library is not UTF-8"))?;
|
||||
let lib = parse_library(xml, C)?;
|
||||
let vars = &s.doc()?.doc.variables;
|
||||
let deleted: Vec<String> = vars.variables.iter().map(|v| v.name.clone()).collect();
|
||||
let deleted_sets: Vec<String> = vars.datasets.iter().map(|d| d.name.clone()).collect();
|
||||
// Lock Variables refuses New Variable and Delete Variable, so the load is refused when it would
|
||||
// delete or define a variable.
|
||||
if !deleted.is_empty() || !lib.text.is_empty() {
|
||||
variables_unlocked(s).map_err(|why| EngineError::Disabled(C.into(), why))?;
|
||||
}
|
||||
let added: Vec<Variable> = lib.text.iter().map(|n| Variable { name: n.clone(), kind: VariableKind::Text }).collect();
|
||||
let names: Vec<String> = lib.datasets.iter().map(|d| d.name.clone()).collect();
|
||||
s.edit("Load Variable Library", |d, _| {
|
||||
d.variables.prune(&deleted);
|
||||
d.variables.variables.extend(added);
|
||||
d.variables.datasets = lib.datasets;
|
||||
d.variables.active_dataset = None;
|
||||
Ok(())
|
||||
})?;
|
||||
// A highlight of a variable that is gone highlights nothing.
|
||||
let st = s.doc_mut()?;
|
||||
if st.variables_highlight.as_ref().is_some_and(|h| st.doc.variables.variable(h).is_none()) {
|
||||
st.variables_highlight = None;
|
||||
}
|
||||
st.revision += 1;
|
||||
let skipped: Vec<Value> = lib.other.iter().map(|(name, t)| json!({"name": name, "trait": t})).collect();
|
||||
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) {
|
||||
Some((_, n)) => *n += 1,
|
||||
None => kinds.push((t, 1)),
|
||||
}
|
||||
}
|
||||
if !kinds.is_empty() {
|
||||
let kinds = kinds.iter().map(|(k, n)| format!("{k} ({n})")).collect::<Vec<_>>().join(", ");
|
||||
out["warning"] = json!(format!("variables that aren't text were left out: {kinds}"));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
@@ -980,7 +1273,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Lock Variables",
|
||||
["Window", "Variables"],
|
||||
None,
|
||||
"{locked?: bool} lock or unlock the variables (default: toggle); saved with the document; a change is one undo step. While locked, variable.define, variable.delete, variable.rename and variable.make…Dynamic are refused; binding, unbinding and the dataset commands still run → {locked}",
|
||||
"{locked?: bool} lock or unlock the variables (default: toggle); saved with the document; a change is one undo step. While locked, variable.define, variable.delete, variable.rename, variable.make…Dynamic and a variable.loadLibrary that would delete or define a variable are refused; binding, unbinding and the dataset commands still run → {locked}",
|
||||
has_doc,
|
||||
lock
|
||||
),
|
||||
@@ -1075,5 +1368,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
dataset_export
|
||||
),
|
||||
cmd!(
|
||||
"variable.loadLibrary",
|
||||
"Load Variable Library…",
|
||||
["Window", "Variables"],
|
||||
None,
|
||||
"{path? | xml? | dataBase64?} replace the document's variables and data sets with a variable library's (XML: `variableSets` › `variableSet` › `variables` of `variable` elements with `varName` and `trait`, and `sampleDataSets` of `sampleDataSet` elements with `dataSetName`) in one undo step. Every variable of the document is deleted with its bindings and values, and every data set is deleted; then variables whose `trait` is `textcontent` are defined as text variables, unbound, and each data set takes a value for a variable from the `p` elements of the element named after it, one paragraph each. Variables of other kinds are left out and named in `skipped` and `warning`. Refused while the variables are locked when it would delete or define a variable → {variables, deleted, deletedDataSets, names, skipped, warning?}",
|
||||
has_doc,
|
||||
load_library
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1346,6 +1346,263 @@ fn datasets_csv_round_trips_and_rejects_bad_input() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
|
||||
/// A data set of [`library`]: its name, and each variable's name with its paragraphs.
|
||||
type LibrarySet<'a> = (&'a str, &'a [(&'a str, &'a [&'a str])]);
|
||||
|
||||
/// A variable library written from its public grammar: a document type declaration with the
|
||||
/// namespace entities, `variableSets` › `variableSet` › `variables` with one `variable` element per
|
||||
/// `(varName, trait)`, then `sampleDataSets` with one `sampleDataSet` per data set, holding an
|
||||
/// element per variable with a `p` element per paragraph.
|
||||
fn library(vars: &[(&str, &str)], sets: &[LibrarySet]) -> String {
|
||||
let mut x = String::from(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
|
||||
<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\" [\n\
|
||||
\t<!ENTITY ns_vars \"http://ns.adobe.com/Variables/1.0/\">\n\
|
||||
\t<!ENTITY ns_custom \"http://ns.adobe.com/GenericCustomNamespace/1.0/\">\n\
|
||||
\t<!ENTITY ns_flows \"http://ns.adobe.com/Flows/1.0/\">\n\
|
||||
]>\n<svg>\n<variableSets xmlns=\"&ns_vars;\">\n<variableSet varSetName=\"binding1\" locked=\"none\">\n<variables>\n",
|
||||
);
|
||||
for (name, kind) in vars {
|
||||
x += &format!("<variable varName=\"{name}\" trait=\"{kind}\" category=\"&ns_flows;\"></variable>\n");
|
||||
}
|
||||
x += "</variables>\n<v:sampleDataSets xmlns:v=\"&ns_vars;\" xmlns=\"&ns_custom;\">\n";
|
||||
for (set, values) in sets {
|
||||
x += &format!("<v:sampleDataSet dataSetName=\"{set}\">\n");
|
||||
for (var, paras) in *values {
|
||||
let paras: String = paras.iter().map(|p| format!("<p>{p}</p>")).collect();
|
||||
x += &format!("<{var}>{paras}</{var}>\n");
|
||||
}
|
||||
x += "</v:sampleDataSet>\n";
|
||||
}
|
||||
x + "</v:sampleDataSets>\n</variableSet>\n</variableSets>\n</svg>\n"
|
||||
}
|
||||
|
||||
/// Load Variable Library defines the library's text variables, unbound, and adds its data sets,
|
||||
/// in one undo step. A value is the text of the `p` elements, one paragraph each; an element
|
||||
/// without a `p` gives no value. A file, and base64 with a UTF-8 byte-order mark, load the same.
|
||||
#[test]
|
||||
fn a_variable_library_defines_text_variables_and_data_sets() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Alice");
|
||||
let xml = library(
|
||||
&[("Name", "textcontent"), ("Note", "textcontent")],
|
||||
&[("Row 1", &[("Name", &["Bravo"]), ("Note", &["One", "Two & three"])]), ("Row 2", &[("Name", &["Charlie"]), ("Note", &[])])],
|
||||
);
|
||||
let r = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
|
||||
assert_eq!(r["variables"], json!(["Name", "Note"]));
|
||||
assert_eq!((r["deleted"].clone(), r["deletedDataSets"].clone()), (json!([]), json!([])));
|
||||
assert_eq!(r["names"], json!(["Row 1", "Row 2"]));
|
||||
assert_eq!((r["skipped"].clone(), r.get("warning")), (json!([]), None));
|
||||
let listed = s.execute("variable.list", &json!({})).unwrap();
|
||||
assert_eq!(listed["variables"], json!([{"name": "Name", "kind": "text", "bindings": 0}, {"name": "Note", "kind": "text", "bindings": 0}]));
|
||||
let sets = s.execute("dataset.list", &json!({})).unwrap();
|
||||
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "Bravo"}, "Note": {"text": "One\nTwo & three"}}));
|
||||
assert_eq!(sets["datasets"][1]["values"], json!({"Name": {"text": "Charlie"}}));
|
||||
assert_eq!(sets["active"], Value::Null);
|
||||
assert_eq!(plain(&s, t), "Alice", "loading binds nothing and applies nothing");
|
||||
|
||||
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
|
||||
s.execute("dataset.select", &json!({"name": "Row 2"})).unwrap();
|
||||
assert_eq!(plain(&s, t), "Charlie");
|
||||
|
||||
// One undo step each: the select, the bind, then the load.
|
||||
for _ in 0..3 {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert!(vars.variables.is_empty() && vars.datasets.is_empty());
|
||||
|
||||
let dir = Folder::new("vars-library");
|
||||
let path = dir.file("library.xml");
|
||||
write(&path, xml.as_bytes());
|
||||
let from_file = s.execute("variable.loadLibrary", &json!({"path": path})).unwrap();
|
||||
assert_eq!(from_file["names"], json!(["Row 1", "Row 2"]));
|
||||
let bom = [b"\xEF\xBB\xBF".as_slice(), xml.as_bytes()].concat();
|
||||
let from_base64 = s.execute("variable.loadLibrary", &json!({"dataBase64": vectorcraft_format::base64_encode(&bom)})).unwrap();
|
||||
assert_eq!((from_base64["deleted"].clone(), from_base64["deletedDataSets"].clone()), (json!(["Name", "Note"]), json!(["Row 1", "Row 2"])));
|
||||
assert_eq!(s.doc().unwrap().doc.variables.datasets.len(), 2);
|
||||
}
|
||||
|
||||
/// Load Variable Library replaces the document's variables and data sets: every variable goes with
|
||||
/// its bindings and values, a text variable of the same name included, and every data set goes,
|
||||
/// the active one included. The highlight of a variable that went highlights nothing. Undo brings
|
||||
/// back the variables, bindings, data sets and active data set as they were.
|
||||
#[test]
|
||||
fn a_variable_library_replaces_every_variable_and_data_set() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Alice");
|
||||
let r = rect(&mut s);
|
||||
for (name, kind, id) in [("Name", "text", t), ("Title", "visibility", r), ("Kept", "visibility", r)] {
|
||||
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": name, "ids": [id.0]})).unwrap();
|
||||
}
|
||||
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Old", "Title": true}})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "Other", "values": {"Name": "Stays", "Title": false, "Kept": true}})).unwrap();
|
||||
s.execute("dataset.select", &json!({"name": "Other"})).unwrap();
|
||||
s.execute("variable.highlight", &json!({"variable": "Kept"})).unwrap();
|
||||
let before = s.doc().unwrap().doc.variables.clone();
|
||||
|
||||
let xml = library(&[("Name", "textcontent"), ("Title", "textcontent")], &[("A", &[("Name", &["New"]), ("Title", &["Heading"])])]);
|
||||
let loaded = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
|
||||
assert_eq!(loaded["deleted"], json!(["Name", "Title", "Kept"]));
|
||||
assert_eq!((loaded["deletedDataSets"].clone(), loaded["names"].clone()), (json!(["A", "Other"]), json!(["A"])));
|
||||
let listed = s.execute("variable.list", &json!({})).unwrap();
|
||||
assert_eq!(listed["variables"], json!([{"name": "Name", "kind": "text", "bindings": 0}, {"name": "Title", "kind": "text", "bindings": 0}]));
|
||||
let sets = s.execute("dataset.list", &json!({})).unwrap();
|
||||
assert_eq!(sets["datasets"].as_array().map(Vec::len), Some(1));
|
||||
assert_eq!(sets["datasets"][0]["values"], json!({"Name": {"text": "New"}, "Title": {"text": "Heading"}}));
|
||||
assert_eq!(sets["active"], Value::Null);
|
||||
assert!(s.doc().unwrap().doc.variables.bindings.is_empty(), "the bindings went with the variables");
|
||||
assert!(s.doc().unwrap().variables_highlight.is_none(), "the highlighted variable went");
|
||||
assert_eq!(plain(&s, t), "Stays", "loading applies nothing");
|
||||
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.variables, before, "one undo step brings everything back");
|
||||
}
|
||||
|
||||
/// A library that is refused leaves the document's variables and data sets as they were.
|
||||
#[test]
|
||||
fn a_refused_variable_library_deletes_nothing() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Alice");
|
||||
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Old"}})).unwrap();
|
||||
let before = s.doc().unwrap().doc.variables.clone();
|
||||
let dup = library(&[("Name", "textcontent")], &[("B", &[]), ("B", &[])]);
|
||||
assert!(s.execute("variable.loadLibrary", &json!({"xml": dup})).is_err());
|
||||
assert_eq!(s.doc().unwrap().doc.variables, before);
|
||||
}
|
||||
|
||||
/// A library's variables of other kinds are left out with their values, named in `skipped` and
|
||||
/// in a warning that gives each kind once with its count; its text variables load.
|
||||
#[test]
|
||||
fn a_variable_library_leaves_out_variables_that_are_not_text() {
|
||||
let mut s = session();
|
||||
let xml = library(
|
||||
&[("Shown", "kindB"), ("Name", "textcontent"), ("Photo", "kindC"), ("Other", "kindB")],
|
||||
&[("A", &[("Shown", &["true"]), ("Name", &["Bravo"]), ("Photo", &["a.png"])])],
|
||||
);
|
||||
let r = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
|
||||
assert_eq!(r["variables"], json!(["Name"]));
|
||||
assert_eq!(r["skipped"], json!([{"name": "Shown", "trait": "kindB"}, {"name": "Photo", "trait": "kindC"}, {"name": "Other", "trait": "kindB"}]));
|
||||
assert_eq!(r["warning"], "variables that aren't text were left out: kindB (2), kindC (1)");
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert_eq!(vars.variables.len(), 1);
|
||||
assert_eq!(vars.dataset("A").map(|d| d.values.len()), Some(1));
|
||||
}
|
||||
|
||||
/// A library that isn't one, or that breaks its grammar or the caps, is refused, and the document
|
||||
/// is left as it was.
|
||||
#[test]
|
||||
fn malformed_variable_libraries_are_refused() {
|
||||
let mut s = session();
|
||||
let ok = library(&[("Name", "textcontent")], &[("A", &[("Name", &["x"])])]);
|
||||
let deep = format!("{}<variableSets/>{}", "<a>".repeat(300), "</a>".repeat(300));
|
||||
let markup = "<!DOCTYPE svg [<!ENTITY deep \"<a><a></a></a>\">]><svg>&deep;<variableSets/></svg>".to_string();
|
||||
// A document type declaration in a comment before the real one, and markup reached through
|
||||
// another entity.
|
||||
let hidden = format!("<!-- <!DOCTYPE svg> -->{markup}");
|
||||
let nested = "<!DOCTYPE svg [<!ENTITY a \"&b;&b;\"><!ENTITY b 'x'>]><svg>&a;<variableSets/></svg>".to_string();
|
||||
let parameter = "<!DOCTYPE svg [<!ENTITY % p \"x\">]><svg><variableSets/></svg>".to_string();
|
||||
let element = "<!DOCTYPE svg [<!ELEMENT svg ANY>]><svg><variableSets/></svg>".to_string();
|
||||
// A `>` and a comment opener inside a quoted system identifier, before elements nested too deep.
|
||||
let quoted = format!("<!DOCTYPE svg SYSTEM \"x><!--\"><svg>{}<variableSets/>{}</svg>", "<a>".repeat(300), "</a>".repeat(300));
|
||||
let entities: String = (0..65).map(|i| format!("<!ENTITY e{i} \"x\">")).collect();
|
||||
let many_entities = format!("<!DOCTYPE svg [{entities}]><svg><variableSets/></svg>");
|
||||
// 1 KiB of text referenced 9,000 times.
|
||||
let expanding = format!("<!DOCTYPE svg [<!ENTITY big \"{}\">]><svg>{}<variableSets/></svg>", "x".repeat(1024), "&big;".repeat(9000));
|
||||
// References that expand to just under the cap, then a comment that takes the text past it.
|
||||
let then_literal = format!(
|
||||
"<!DOCTYPE svg [<!ENTITY big \"{}\">]><svg>{}<!--{}--><variableSets/></svg>",
|
||||
"x".repeat(1024),
|
||||
"&big;".repeat(8000),
|
||||
" ".repeat(300_000)
|
||||
);
|
||||
let cases = [
|
||||
("not well-formed XML", "<variableSets><variableSet>".to_string()),
|
||||
("no `variableSets` element", "<svg><variables/></svg>".to_string()),
|
||||
("declares no variables", "<variableSets><variableSet><variables/></variableSet></variableSets>".to_string()),
|
||||
("is declared twice", library(&[("Name", "textcontent"), ("Name", "textcontent")], &[])),
|
||||
("has no `trait`", ok.replace(" trait=\"textcontent\"", "")),
|
||||
("missing `varName`", ok.replace("varName=\"Name\"", "varName=\" \"")),
|
||||
("missing `dataSetName`", ok.replace(" dataSetName=\"A\"", "")),
|
||||
("data set `A` is declared twice", library(&[("Name", "textcontent")], &[("A", &[]), ("A", &[])])),
|
||||
("which the library doesn't declare", library(&[("Name", "textcontent")], &[("A", &[("Nmae", &["x"])])])),
|
||||
("holds two values for `Name`", library(&[("Name", "textcontent")], &[("A", &[("Name", &["x"]), ("Name", &["y"])])])),
|
||||
("nests elements more than 256 deep", deep),
|
||||
("something other than entities of plain text", markup),
|
||||
("something other than entities of plain text", hidden),
|
||||
("something other than entities of plain text", nested),
|
||||
("something other than entities of plain text", parameter),
|
||||
("something other than entities of plain text", element),
|
||||
("nests elements more than 256 deep", quoted),
|
||||
("more than 64 entities", many_entities),
|
||||
("entities expand to more than", expanding),
|
||||
("entities expand to more than", then_literal),
|
||||
("larger than", format!("{ok}<!--{}-->", " ".repeat(8 << 20))),
|
||||
];
|
||||
for (why, xml) in cases {
|
||||
let e = s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap_err().to_string();
|
||||
assert!(e.contains(why), "{why}: {e}");
|
||||
}
|
||||
let names: Vec<String> = (0..257).map(|i| format!("V{i}")).collect();
|
||||
let many: Vec<(&str, &str)> = names.iter().map(|n| (n.as_str(), "textcontent")).collect();
|
||||
let e = s.execute("variable.loadLibrary", &json!({"xml": library(&many, &[])})).unwrap_err().to_string();
|
||||
assert!(e.contains("more than 256 variables"), "{e}");
|
||||
// A Latin-1 é in a comment.
|
||||
let latin1 = [ok.as_bytes(), b"<!-- \xE9 -->"].concat();
|
||||
let e = s.execute("variable.loadLibrary", &json!({"dataBase64": vectorcraft_format::base64_encode(&latin1)})).unwrap_err().to_string();
|
||||
assert!(e.contains("not UTF-8"), "{e}");
|
||||
let e = s.execute("variable.loadLibrary", &json!({})).unwrap_err().to_string();
|
||||
assert!(e.contains("give `path`, `xml` or `dataBase64`"), "{e}");
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert!(vars.variables.is_empty() && vars.datasets.is_empty(), "a refused library loads nothing");
|
||||
}
|
||||
|
||||
/// Entities whose values hold plain text expand to it, in attribute values too, and a reference in
|
||||
/// a CDATA section stays as written.
|
||||
#[test]
|
||||
fn a_variable_library_expands_entities_of_plain_text() {
|
||||
let mut s = session();
|
||||
let xml = "<!DOCTYPE svg [<!ENTITY a \"x\"><!ENTITY b 'y \"z\"'>]><svg><variableSets><variableSet><variables>\
|
||||
<variable varName=\"Name\" trait=\"textcontent\"/></variables><sampleDataSets><sampleDataSet dataSetName=\"&b;\">\
|
||||
<Name><p>&a;-&b;-&a;<![CDATA[&a;]]></p></Name></sampleDataSet></sampleDataSets></variableSet></variableSets></svg>";
|
||||
s.execute("variable.loadLibrary", &json!({"xml": xml})).unwrap();
|
||||
let value = s.doc().unwrap().doc.variables.dataset("y \"z\"").and_then(|d| d.values.get("Name")).cloned();
|
||||
assert_eq!(value, Some(vectorcraft_doc::DataValue::Text("x-y \"z\"-x&a;".into())));
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// without text variables loaded into a document with variables. A library without text variables
|
||||
/// loads into a document without variables, replacing its data sets.
|
||||
#[test]
|
||||
fn locked_variables_refuse_a_library_that_deletes_or_defines_variables() {
|
||||
let mut s = session();
|
||||
s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "A", "values": {"Name": "Old"}})).unwrap();
|
||||
s.execute("variable.lock", &json!({"locked": true})).unwrap();
|
||||
let before = s.doc().unwrap().doc.variables.clone();
|
||||
for vars in [&[("Name", "textcontent")][..], &[("Shown", "other")][..]] {
|
||||
let e = s.execute("variable.loadLibrary", &json!({"xml": library(vars, &[("B", &[])])})).unwrap_err();
|
||||
assert!(matches!(&e, EngineError::Disabled(cmd, why) if cmd == "variable.loadLibrary" && why == "variables are locked"), "{e}");
|
||||
assert_eq!(s.doc().unwrap().doc.variables, before);
|
||||
}
|
||||
|
||||
let mut s = session();
|
||||
s.execute("dataset.new", &json!({"name": "A"})).unwrap();
|
||||
s.execute("variable.lock", &json!({"locked": true})).unwrap();
|
||||
let e = s.execute("variable.loadLibrary", &json!({"xml": library(&[("Name", "textcontent")], &[])})).unwrap_err();
|
||||
assert!(matches!(&e, EngineError::Disabled(_, why) if why == "variables are locked"), "{e}");
|
||||
let r = s.execute("variable.loadLibrary", &json!({"xml": library(&[("Shown", "other")], &[("B", &[])])})).unwrap();
|
||||
assert_eq!((r["deletedDataSets"].clone(), r["names"].clone()), (json!(["A"]), json!(["B"])));
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert!(vars.variables.is_empty() && vars.locked);
|
||||
assert_eq!(vars.datasets.iter().map(|d| d.name.as_str()).collect::<Vec<_>>(), ["B"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_variable_applies_csv_and_captures() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1252,6 +1252,68 @@ proptest! {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- variable library XML files ----------
|
||||
|
||||
/// A variable library written from its public grammar: two text variables (`Title` is a
|
||||
/// variable of [`merge_session`]'s document), a variable of another kind and two data sets.
|
||||
const VARIABLE_LIBRARY: &str = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
|
||||
<!DOCTYPE svg [\n<!ENTITY ns_vars \"http://ns.adobe.com/Variables/1.0/\">\n<!ENTITY ns_custom \"http://ns.adobe.com/GenericCustomNamespace/1.0/\">\n]>\n\
|
||||
<svg>\n<variableSets xmlns=\"&ns_vars;\">\n<variableSet varSetName=\"binding1\" locked=\"none\">\n<variables>\n\
|
||||
<variable varName=\"Title\" trait=\"textcontent\"></variable>\n<variable varName=\"Note\" trait=\"textcontent\"></variable>\n\
|
||||
<variable varName=\"Show\" trait=\"other\"></variable>\n</variables>\n\
|
||||
<v:sampleDataSets xmlns:v=\"&ns_vars;\" xmlns=\"&ns_custom;\">\n\
|
||||
<v:sampleDataSet dataSetName=\"A\">\n<Title><p>One</p><p>Two & three</p></Title>\n<Note><p>x</p></Note>\n<Show><p>true</p></Show>\n</v:sampleDataSet>\n\
|
||||
<v:sampleDataSet dataSetName=\"C\">\n<Title><p>Four</p></Title>\n<Note></Note>\n</v:sampleDataSet>\n\
|
||||
</v:sampleDataSets>\n</variableSet>\n</variableSets>\n</svg>\n";
|
||||
|
||||
/// `variable.loadLibrary` with `params` on [`merge_session`]'s document, the first three data sets
|
||||
/// it read selected in turn and every data set exported; the document then renders and exports.
|
||||
fn variable_library(what: &str, params: Value) -> Result<(), TestCaseError> {
|
||||
survive(what, || {
|
||||
let mut s = merge_session();
|
||||
if let Ok(r) = no_panic(s.execute("variable.loadLibrary", ¶ms)) {
|
||||
for name in r["names"].as_array().into_iter().flatten().take(3) {
|
||||
let _ = no_panic(s.execute("dataset.select", &json!({"name": name})));
|
||||
}
|
||||
}
|
||||
let _ = no_panic(s.execute("dataset.export", &json!({})));
|
||||
Some((*s.doc().ok()?.doc).clone())
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
#[test]
|
||||
fn variable_library_garbage_never_panics(s in ".{0,300}", head in prop::sample::select(vec![
|
||||
"",
|
||||
"<variableSets>",
|
||||
"<variableSets><variableSet><variables><variable varName=\"Title\" trait=\"textcontent\"/></variables><sampleDataSets><sampleDataSet dataSetName=\"A\">",
|
||||
"<variableSets><variableSet><variables><variable varName=\"Title\" trait=\"textcontent\"/></variables><sampleDataSets><sampleDataSet dataSetName=\"A\"><Title><p>",
|
||||
"<!DOCTYPE svg [<!ENTITY a \"",
|
||||
])) {
|
||||
variable_library("variable library garbage", json!({"xml": format!("{head}{s}")}))?;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_library_bytes_never_panic(bytes in prop::collection::vec(any::<u8>(), 0..300)) {
|
||||
variable_library("variable library bytes", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes)}))?;
|
||||
}
|
||||
|
||||
/// The library cut short and with characters replaced, then with `nest` elements nested in one
|
||||
/// another inside its `svg` element.
|
||||
#[test]
|
||||
fn mutated_variable_libraries_never_panic(
|
||||
cut in 0usize..1_200,
|
||||
edits in prop::collection::vec((0usize..1_200, prop::sample::select(vec!['<', '>', '"', '/', '=', '&', ';', '!', '[', ']', 'p', 'x', ' '])), 0..10),
|
||||
nest in prop::sample::select(vec![0usize, 0, 0, 240, 100_000]),
|
||||
) {
|
||||
let xml = mutate_text(VARIABLE_LIBRARY, cut, &edits);
|
||||
let xml = xml.replacen("<svg>", &format!("<svg>{}", "<a>".repeat(nest)), 1).replacen("</svg>", &format!("{}</svg>", "</a>".repeat(nest)), 1);
|
||||
variable_library("mutated variable library", json!({"xml": xml}))?;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- shortcut sets ----------
|
||||
|
||||
/// Shortcut set `bytes` read with `shortcutSet.read`, with import matches saved to annotate it: an
|
||||
|
||||
@@ -4202,6 +4202,7 @@ axis Both И двете
|
||||
@msg unknown workspace (one of {_1}) непознато работно пространство (едно от {_1})
|
||||
@msg unsupported: {0} не се поддържа: {0}
|
||||
@msg variables are locked променливите са заключени
|
||||
@msg variables that aren't text were left out: {kinds} променливите, които не са текстови, бяха пропуснати: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files файловете от версия {0} не могат да бъдат компресирани: само версия {COMPRESSED_SINCE} и по-нови отварят компресирани файлове
|
||||
@msg width and height are at least 1 pixel ширината и височината са най-малко 1 пиксел
|
||||
@msg workspace name can't be empty името на работното пространство не може да бъде празно
|
||||
@@ -5184,6 +5185,7 @@ theme Appearance Външен вид
|
||||
Make Image Dynamic Динамично изображение
|
||||
Import Data Sets… Импортиране на набори от данни…
|
||||
Export Data Sets… Експортиране на набори от данни…
|
||||
Load Variable Library… Зареждане на библиотека с променливи…
|
||||
Make Graph Dynamic Динамична диаграма
|
||||
Batch… Пакет…
|
||||
Source Folder: Изходна папка:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4597,6 +4597,7 @@ imageTrace Outline Obrys
|
||||
@msg unknown workspace (one of {_1}) neznámý pracovní prostor (jeden z {_1})
|
||||
@msg unsupported: {0} nepodporováno: {0}
|
||||
@msg variables are locked proměnné jsou zamčené
|
||||
@msg variables that aren't text were left out: {kinds} proměnné, které nejsou textové, byly vynechány: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files soubory verze {0} nelze komprimovat: komprimované soubory otevírá až verze {COMPRESSED_SINCE} a novější
|
||||
@msg width and height are at least 1 pixel šířka i výška jsou alespoň 1 obrazový bod
|
||||
@msg workspace name can't be empty název pracovního prostoru nesmí být prázdný
|
||||
@@ -5230,6 +5231,7 @@ mark-style Japanese Japonské
|
||||
Make Image Dynamic Nastavit obraz jako dynamický
|
||||
Import Data Sets… Importovat sady dat…
|
||||
Export Data Sets… Exportovat sady dat…
|
||||
Load Variable Library… Načíst knihovnu proměnných…
|
||||
Make Graph Dynamic Nastavit graf jako dynamický
|
||||
Batch… Dávka…
|
||||
Source Folder: Zdrojová složka:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4969,6 +4969,8 @@ mark-style Japanese Japansk
|
||||
Make Image Dynamic Gør billede dynamisk
|
||||
Import Data Sets… Importér datasæt…
|
||||
Export Data Sets… Eksportér datasæt…
|
||||
Load Variable Library… Indlæs variabelbibliotek…
|
||||
@msg variables that aren't text were left out: {kinds} variabler, der ikke er tekst, blev udeladt: {kinds}
|
||||
Make Graph Dynamic Gør diagram dynamisk
|
||||
Batch… Batch…
|
||||
Source Folder: Kildemappe:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4228,6 +4228,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg unknown workspace (one of {_1}) unbekannter Arbeitsbereich (einer von {_1})
|
||||
@msg unsupported: {0} nicht unterstützt: {0}
|
||||
@msg variables are locked die Variablen sind gesperrt
|
||||
@msg variables that aren't text were left out: {kinds} die Variablen, die kein Text sind, wurden ausgelassen: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files Dateien der Version {0} können nicht komprimiert werden: Erst Version {COMPRESSED_SINCE} und neuer öffnen komprimierte Dateien
|
||||
@msg width and height are at least 1 pixel Breite und Höhe betragen mindestens 1 Pixel
|
||||
@msg workspace name can't be empty der Name des Arbeitsbereichs darf nicht leer sein
|
||||
@@ -4809,6 +4810,7 @@ effect Soft Weich
|
||||
Make Image Dynamic Bild dynamisch machen
|
||||
Import Data Sets… Datensätze importieren…
|
||||
Export Data Sets… Datensätze exportieren…
|
||||
Load Variable Library… Variablenbibliothek laden…
|
||||
Make Graph Dynamic Diagramm dynamisch machen
|
||||
Batch… Stapelverarbeitung…
|
||||
Source Folder: Quellordner:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4260,6 +4260,7 @@ theme Appearance Εμφάνιση
|
||||
@msg unknown workspace (one of {_1}) άγνωστος χώρος εργασίας (ένας από τους {_1})
|
||||
@msg unsupported: {0} δεν υποστηρίζεται: {0}
|
||||
@msg variables are locked οι μεταβλητές είναι κλειδωμένες
|
||||
@msg variables that aren't text were left out: {kinds} οι μεταβλητές που δεν είναι κειμένου παραλείφθηκαν: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files τα αρχεία έκδοσης {0} δεν μπορούν να συμπιεστούν: μόνο η έκδοση {COMPRESSED_SINCE} και νεότερες ανοίγουν συμπιεσμένα αρχεία
|
||||
@msg width and height are at least 1 pixel το πλάτος και το ύψος πρέπει να είναι τουλάχιστον 1 εικονοστοιχείο
|
||||
@msg workspace name can't be empty το όνομα χώρου εργασίας δεν μπορεί να είναι κενό
|
||||
@@ -5095,6 +5096,7 @@ effect Points Σημεία
|
||||
Drawing : real size — 1:10 labels 1 unit on the page as 10. Σχέδιο : πραγματικό μέγεθος — με 1:10 η 1 μονάδα στη σελίδα εμφανίζεται ως 10.
|
||||
Editable Επεξεργάσιμο
|
||||
Export Data Sets… Εξαγωγή συνόλων δεδομένων…
|
||||
Load Variable Library… Φόρτωση βιβλιοθήκης μεταβλητών…
|
||||
Filter Φίλτρο
|
||||
Filter Layers Panel Rows Φιλτράρισμα γραμμών πίνακα επιπέδων
|
||||
Flatten Ισοπέδωση
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2031 and column 19.
|
@@ -4296,6 +4296,7 @@ axis Both Ambos
|
||||
@msg unknown workspace (one of {_1}) espacio de trabajo desconocido (uno de estos: {_1})
|
||||
@msg unsupported: {0} no compatible: {0}
|
||||
@msg variables are locked las variables están bloqueadas
|
||||
@msg variables that aren't text were left out: {kinds} se omitieron las variables que no son de texto: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files los archivos de la versión {0} no se pueden comprimir: solo la versión {COMPRESSED_SINCE} y posteriores abren archivos comprimidos
|
||||
@msg width and height are at least 1 pixel el ancho y el alto son de al menos 1 píxel
|
||||
@msg workspace name can't be empty el nombre del espacio de trabajo no puede estar vacío
|
||||
@@ -5172,6 +5173,7 @@ mark-style Japanese Japonesas
|
||||
Make Image Dynamic Hacer imagen dinámica
|
||||
Import Data Sets… Importar conjuntos de datos…
|
||||
Export Data Sets… Exportar conjuntos de datos…
|
||||
Load Variable Library… Cargar biblioteca de variables…
|
||||
Make Graph Dynamic Hacer gráfico dinámico
|
||||
Batch… Lote…
|
||||
Source Folder: Carpeta de origen:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4506,6 +4506,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg unknown workspace (one of {_1}) tuntematon työtila (jokin seuraavista: {_1})
|
||||
@msg unsupported: {0} ei tuettu: {0}
|
||||
@msg variables are locked muuttujat on lukittu
|
||||
@msg variables that aren't text were left out: {kinds} muut kuin tekstimuuttujat jätettiin pois: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files version {0} tiedostoja ei voi pakata: pakattuja tiedostoja voi avata vain versiosta {COMPRESSED_SINCE} alkaen
|
||||
@msg width and height are at least 1 pixel leveys ja korkeus ovat vähintään 1 pikseli
|
||||
@msg workspace name can't be empty työtilan nimi ei voi olla tyhjä
|
||||
@@ -5107,6 +5108,7 @@ mark-style Japanese Japanilainen
|
||||
Drawing : real size — 1:10 labels 1 unit on the page as 10. Piirustus : todellinen koko — 1:10 merkitsee sivun 1 yksikön arvoksi 10.
|
||||
Editable Muokattava
|
||||
Export Data Sets… Vie tietojoukot…
|
||||
Load Variable Library… Lataa muuttujakirjasto…
|
||||
Filter Suodata
|
||||
Filter Layers Panel Rows Suodata Tasot-paneelin rivit
|
||||
Flatten Litistä
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4293,6 +4293,7 @@ axis Both Les deux
|
||||
@msg unknown workspace (one of {_1}) espace de travail inconnu (valeurs possibles : {_1})
|
||||
@msg unsupported: {0} non pris en charge : {0}
|
||||
@msg variables are locked les variables sont verrouillées
|
||||
@msg variables that aren't text were left out: {kinds} les variables qui ne sont pas du texte ({kinds}) ont été ignorées
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files les fichiers de version {0} ne peuvent pas être compressés : seules les versions {COMPRESSED_SINCE} et ultérieures ouvrent les fichiers compressés
|
||||
@msg width and height are at least 1 pixel la largeur et la hauteur valent au moins 1 pixel
|
||||
@msg workspace name can't be empty le nom de l’espace de travail ne peut pas être vide
|
||||
@@ -5229,6 +5230,7 @@ theme Appearance Apparence
|
||||
Make Image Dynamic Rendre l'image dynamique
|
||||
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…
|
||||
Make Graph Dynamic Rendre le graphe dynamique
|
||||
Batch… Traitement par lots…
|
||||
Source Folder: Dossier source :
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5023,6 +5023,7 @@ imageTrace Outline Körvonal
|
||||
@msg Updated {_1} image(s) Frissített képek: {_1}
|
||||
@msg Updated {_1} linked file(s) changed on disk Frissített, a lemezen módosult csatolt fájlok: {_1}
|
||||
@msg variables are locked a változók zárolva vannak
|
||||
@msg variables that aren't text were left out: {kinds} a nem szöveges változók kimaradtak: {kinds}
|
||||
@msg VectorCraft doesn't search other apps' or the system's folders: {path} a VectorCraft nem keres más alkalmazások és a rendszer mappáiban: {path}
|
||||
@msg VectorCraft doesn't search the Shared or Public folder in Users: {path} a VectorCraft nem keres a Users alatti Shared és Public mappában: {path}
|
||||
@msg VectorCraft failed to start: {e} A VectorCraft nem tudott elindulni: {e}
|
||||
@@ -5216,6 +5217,7 @@ imageTrace Outline Körvonal
|
||||
Make Image Dynamic Kép dinamikussá tétele
|
||||
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…
|
||||
Make Graph Dynamic Diagram dinamikussá tétele
|
||||
Batch… Kötegelt feldolgozás…
|
||||
Source Folder: Forrásmappa:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4969,6 +4969,8 @@ mark-style Japanese Jepang
|
||||
Make Image Dynamic Jadikan gambar dinamis
|
||||
Import Data Sets… Impor himpunan data…
|
||||
Export Data Sets… Ekspor himpunan data…
|
||||
Load Variable Library… Muat pustaka variabel…
|
||||
@msg variables that aren't text were left out: {kinds} variabel yang bukan teks dihilangkan: {kinds}
|
||||
Make Graph Dynamic Jadikan grafik dinamis
|
||||
Batch… Batch…
|
||||
Source Folder: Folder sumber:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3801 and column 19.
|
@@ -4336,6 +4336,7 @@ axis Both Entrambi
|
||||
@msg unknown workspace (one of {_1}) area di lavoro sconosciuta (una delle seguenti: {_1})
|
||||
@msg unsupported: {0} non supportato: {0}
|
||||
@msg variables are locked le variabili sono bloccate
|
||||
@msg variables that aren't text were left out: {kinds} le variabili che non sono di testo sono state escluse: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files i file della versione {0} non possono essere compressi: solo la versione {COMPRESSED_SINCE} e successive aprono file compressi
|
||||
@msg width and height are at least 1 pixel larghezza e altezza sono di almeno 1 pixel
|
||||
@msg workspace name can't be empty il nome dell'area di lavoro non può essere vuoto
|
||||
@@ -4843,6 +4844,7 @@ lighting Light Luce
|
||||
Make Image Dynamic Rendi immagine dinamica
|
||||
Import Data Sets… Importa set di dati…
|
||||
Export Data Sets… Esporta set di dati…
|
||||
Load Variable Library… Carica libreria di variabili…
|
||||
Make Graph Dynamic Rendi grafico dinamico
|
||||
Batch… Elaborazione batch…
|
||||
Source Folder: Cartella di origine:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3924,6 +3924,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg Make Guides: select paths ガイドを作成: パスを選択してください
|
||||
@msg Release Guides: no guides ガイドを解除: ガイドがありません
|
||||
@msg variables are locked 変数がロックされています
|
||||
@msg variables that aren't text were left out: {kinds} テキスト以外の変数は除外されました: {kinds}
|
||||
@msg guides are locked ガイドがロックされています
|
||||
@msg handle must be in or out handle には in または out を指定してください
|
||||
@msg no saved selection recalled 保存した選択範囲を呼び出していません
|
||||
@@ -4883,6 +4884,7 @@ lighting Light ライト
|
||||
Make Image Dynamic 画像を動的にする
|
||||
Import Data Sets… データセットを読み込み…
|
||||
Export Data Sets… データセットを書き出し…
|
||||
Load Variable Library… 変数ライブラリを読み込み…
|
||||
Make Graph Dynamic グラフを動的にする
|
||||
Batch… バッチ…
|
||||
Source Folder: ソースフォルダー:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4901,6 +4901,7 @@ imageTrace Outline კონტური
|
||||
Make Image Dynamic გამოსახულების დინამიკურად ქცევა
|
||||
Import Data Sets… მონაცემთა ნაკრებების იმპორტი…
|
||||
Export Data Sets… მონაცემთა ნაკრებების ექსპორტი…
|
||||
Load Variable Library… ცვლადების ბიბლიოთეკის ჩატვირთვა…
|
||||
Make Graph Dynamic დიაგრამის დინამიკურად ქცევა
|
||||
Batch… პაკეტური დამუშავება…
|
||||
Source Folder: წყარო საქაღალდე:
|
||||
@@ -5177,6 +5178,7 @@ imageTrace Outline კონტური
|
||||
@msg the spine has no segments ხერხემალს სეგმენტები არ აქვს
|
||||
@msg too many library panels were opened in this session ამ სესიაში ზედმეტად ბევრი ბიბლიოთეკის პანელი გაიხსნა
|
||||
@msg variables are locked ცვლადები ჩაკეტილია
|
||||
@msg variables that aren't text were left out: {kinds} ცვლადები, რომლებიც ტექსტი არ არის, გამოტოვებულია: {kinds}
|
||||
@msg x and y must be finite x და y სასრული უნდა იყოს
|
||||
@msg {id} opens another app: only the person at the keyboard can run it {id} სხვა აპს ხსნის: მისი გაშვება მხოლოდ კლავიატურასთან მყოფ ადამიანს შეუძლია
|
||||
@msg “{name}” is not a file „{name}“ ფაილი არ არის
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3856,6 +3856,7 @@ theme Appearance 외관
|
||||
@msg Make Guides: select paths 안내선 만들기: 패스를 선택하세요
|
||||
@msg Release Guides: no guides 안내선 해제: 안내선이 없습니다
|
||||
@msg variables are locked 변수가 잠겨 있습니다
|
||||
@msg variables that aren't text were left out: {kinds} 텍스트가 아닌 변수는 제외되었습니다: {kinds}
|
||||
@msg guides are locked 안내선이 잠겨 있습니다
|
||||
@msg handle must be in or out handle은 in 또는 out이어야 합니다
|
||||
@msg no saved selection recalled 불러온 저장된 선택 영역이 없습니다
|
||||
@@ -4806,6 +4807,7 @@ lighting Light 광원
|
||||
Make Image Dynamic 이미지를 동적으로 만들기
|
||||
Import Data Sets… 데이터 세트 가져오기…
|
||||
Export Data Sets… 데이터 세트 내보내기…
|
||||
Load Variable Library… 변수 라이브러리 불러오기…
|
||||
Make Graph Dynamic 그래프를 동적으로 만들기
|
||||
Batch… 일괄 처리…
|
||||
Source Folder: 소스 폴더:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4981,6 +4981,7 @@ mark-style Japanese Japans
|
||||
Make Image Dynamic Afbeelding dynamisch maken
|
||||
Import Data Sets… Gegevenssets importeren…
|
||||
Export Data Sets… Gegevenssets exporteren…
|
||||
Load Variable Library… Variabelenbibliotheek laden…
|
||||
Make Graph Dynamic Grafiek dynamisch maken
|
||||
Batch… Batch…
|
||||
Source Folder: Bronmap:
|
||||
@@ -5294,6 +5295,7 @@ swatch Bright Violet Felviolet
|
||||
@msg the image's pixels can't be read de pixels van de afbeelding kunnen niet worden gelezen
|
||||
@msg the spine has no segments de as heeft geen segmenten
|
||||
@msg variables are locked variabelen zijn vergrendeld
|
||||
@msg variables that aren't text were left out: {kinds} variabelen die geen tekst zijn, zijn weggelaten: {kinds}
|
||||
@msg x and y must be finite x en y moeten eindig zijn
|
||||
@msg Persistent libraries not found: {files} Permanente bibliotheken niet gevonden: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` is geen bibliotheekpaneel (zie window.swatchLibrary)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4422,6 +4422,7 @@ imageTrace Outline Kontur
|
||||
@msg unknown workspace (one of {_1}) nieznany obszar roboczy (jeden z: {_1})
|
||||
@msg unsupported: {0} nieobsługiwane: {0}
|
||||
@msg variables are locked zmienne są zablokowane
|
||||
@msg variables that aren't text were left out: {kinds} pominięto zmienne, które nie są tekstowe: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files plików w wersji {0} nie można kompresować: skompresowane pliki otwierają tylko wersje {COMPRESSED_SINCE} i nowsze
|
||||
@msg width and height are at least 1 pixel szerokość i wysokość muszą wynosić co najmniej 1 piksel
|
||||
@msg workspace name can't be empty nazwa obszaru roboczego nie może być pusta
|
||||
@@ -5081,6 +5082,7 @@ mark-style Japanese Japońskie
|
||||
Make Image Dynamic Uczyń obraz dynamicznym
|
||||
Import Data Sets… Importuj zestawy danych…
|
||||
Export Data Sets… Eksportuj zestawy danych…
|
||||
Load Variable Library… Wczytaj bibliotekę zmiennych…
|
||||
Make Graph Dynamic Uczyń wykres dynamicznym
|
||||
Batch… Przetwarzanie wsadowe…
|
||||
Source Folder: Folder źródłowy:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -908,6 +908,7 @@
|
||||
Export CSS Exportar CSS
|
||||
Export CSS… Exportar CSS…
|
||||
Export Data Sets… Exportar Conjuntos de Dados…
|
||||
Load Variable Library… Carregar Biblioteca de Variáveis…
|
||||
Export DXF Exportar DXF
|
||||
Export Document Exportar Documento
|
||||
Export EPS Exportar EPS
|
||||
@@ -3874,6 +3875,7 @@ axis Both Ambos
|
||||
@msg unknown workspace (one of {_1}) espaço de trabalho desconhecido (um de {_1})
|
||||
@msg unsupported: {0} não suportado: {0}
|
||||
@msg variables are locked as variáveis estão bloqueadas
|
||||
@msg variables that aren't text were left out: {kinds} as variáveis que não são de texto foram omitidas: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files arquivos da versão {0} não podem ser compactados: somente a versão {COMPRESSED_SINCE} e posteriores abrem arquivos compactados
|
||||
@msg width and height are at least 1 pixel largura e altura têm pelo menos 1 pixel
|
||||
@msg workspace name can't be empty o nome do espaço de trabalho não pode estar vazio
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4332,6 +4332,7 @@ axis Both Оба
|
||||
@msg unknown workspace (one of {_1}) неизвестная рабочая среда (одна из: {_1})
|
||||
@msg unsupported: {0} не поддерживается: {0}
|
||||
@msg variables are locked переменные заблокированы
|
||||
@msg variables that aren't text were left out: {kinds} переменные, которые не являются текстовыми, пропущены: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files файлы версии {0} не могут быть сжаты: только версия {COMPRESSED_SINCE} и новее открывают сжатые файлы
|
||||
@msg width and height are at least 1 pixel ширина и высота должны быть не менее 1 пикселя
|
||||
@msg workspace name can't be empty имя рабочей среды не может быть пустым
|
||||
@@ -4839,6 +4840,7 @@ lighting Light Свет
|
||||
Make Image Dynamic Сделать изображение динамическим
|
||||
Import Data Sets… Импортировать наборы данных…
|
||||
Export Data Sets… Экспортировать наборы данных…
|
||||
Load Variable Library… Загрузить библиотеку переменных…
|
||||
Make Graph Dynamic Сделать диаграмму динамической
|
||||
Batch… Пакетная обработка…
|
||||
Source Folder: Исходная папка:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4207,6 +4207,7 @@ axis Both Båda
|
||||
@msg unknown workspace (one of {_1}) okänd arbetsyta (en av {_1})
|
||||
@msg unsupported: {0} stöds inte: {0}
|
||||
@msg variables are locked variablerna är låsta
|
||||
@msg variables that aren't text were left out: {kinds} variabler som inte är text utelämnades: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files filer i version {0} kan inte komprimeras: endast version {COMPRESSED_SINCE} och senare kan öppna komprimerade filer
|
||||
@msg width and height are at least 1 pixel bredd och höjd är minst 1 pixel
|
||||
@msg workspace name can't be empty arbetsytans namn får inte vara tomt
|
||||
@@ -5099,6 +5100,7 @@ theme Appearance Utseende
|
||||
Drawing : real size — 1:10 labels 1 unit on the page as 10. Ritning : verklig storlek — 1:10 visar 1 enhet på sidan som 10.
|
||||
Editable Redigerbar
|
||||
Export Data Sets… Exportera datauppsättningar…
|
||||
Load Variable Library… Läs in variabelbibliotek…
|
||||
Filter Filter
|
||||
Filter Layers Panel Rows Filtrera rader i panelen Lager
|
||||
Flatten Platta till
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4320,6 +4320,7 @@ axis Both Обидві
|
||||
@msg unknown workspace (one of {_1}) невідоме робоче середовище (одна з: {_1})
|
||||
@msg unsupported: {0} не підтримується: {0}
|
||||
@msg variables are locked змінні заблоковані
|
||||
@msg variables that aren't text were left out: {kinds} змінні, що не є текстовими, пропущено: {kinds}
|
||||
@msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files файли версії {0} не можуть бути стиснуті: тільки версія {COMPRESSED_SINCE} і новіші відкривають стислі файли
|
||||
@msg width and height are at least 1 pixel ширина та висота повинні бути не менше 1 пікселя
|
||||
@msg workspace name can't be empty ім’я робочого середовища не може бути порожнім
|
||||
@@ -4823,6 +4824,7 @@ lighting Light Світло
|
||||
Make Image Dynamic Зробити зображення динамічним
|
||||
Import Data Sets… Імпортувати набори даних…
|
||||
Export Data Sets… Експортувати набори даних…
|
||||
Load Variable Library… Завантажити бібліотеку змінних…
|
||||
Make Graph Dynamic Зробити діаграму динамічною
|
||||
Batch… Пакетна обробка…
|
||||
Source Folder: Вихідна папка:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 726 and column 40.
|
@@ -4682,6 +4682,7 @@ theme Appearance Giao diện
|
||||
@msg Updated {_1} image(s) Đã cập nhật {_1} ảnh
|
||||
@msg Updated {_1} linked file(s) changed on disk Đã cập nhật {_1} tệp liên kết đã thay đổi trên đĩa
|
||||
@msg variables are locked biến số bị khóa
|
||||
@msg variables that aren't text were left out: {kinds} các biến số không phải văn bản đã bị bỏ qua: {kinds}
|
||||
@msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft không tìm kiếm trong thư mục của ứng dụng khác hoặc hệ thống: {path}
|
||||
@msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft không tìm kiếm thư mục Shared hoặc Public trong Users: {path}
|
||||
@msg VectorCraft failed to start: {e} VectorCraft khởi động thất bại: {e}
|
||||
@@ -4823,6 +4824,7 @@ theme Appearance Giao diện
|
||||
Make Image Dynamic Tạo hình ảnh động
|
||||
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ố…
|
||||
Make Graph Dynamic Tạo biểu đồ động
|
||||
Batch… Hàng loạt…
|
||||
Source Folder: Thư mục nguồn:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4003 and column 10.
|
@@ -3340,6 +3340,7 @@ font-script Japanese 汉字
|
||||
Lock Variables 锁定变量
|
||||
Unlock Variables 解锁变量
|
||||
@msg variables are locked 变量已锁定
|
||||
@msg variables that aren't text were left out: {kinds} 已省略非文本变量:{kinds}
|
||||
List Variables 列出变量
|
||||
Bind Variable 绑定变量
|
||||
Unbind Variable 解除变量绑定
|
||||
@@ -5070,6 +5071,7 @@ lighting Light 光源
|
||||
Make Image Dynamic 使图像动态化
|
||||
Import Data Sets… 导入数据集…
|
||||
Export Data Sets… 导出数据集…
|
||||
Load Variable Library… 载入变量库…
|
||||
Make Graph Dynamic 使图表动态化
|
||||
Batch… 批处理…
|
||||
Source Folder: 源文件夹:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4249,6 +4249,7 @@ font-script Japanese 漢字
|
||||
Lock Variables 鎖定變數
|
||||
Unlock Variables 解除鎖定變數
|
||||
@msg variables are locked 變數已鎖定
|
||||
@msg variables that aren't text were left out: {kinds} 已略過非文字變數:{kinds}
|
||||
List Variables 列出變數
|
||||
Bind Variable 綁定變數
|
||||
Unbind Variable 解除變數綁定
|
||||
@@ -4879,6 +4880,7 @@ lighting Light 光源
|
||||
Make Image Dynamic 使影像動態化
|
||||
Import Data Sets… 匯入資料集…
|
||||
Export Data Sets… 匯出資料集…
|
||||
Load Variable Library… 載入變數資料庫…
|
||||
Make Graph Dynamic 使圖表動態化
|
||||
Batch… 批次處理…
|
||||
Source Folder: 來源資料夾:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -322,6 +322,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if widgets::menu_item(ui, tl!("Export Data Sets…"), !vars.is_empty(), false) {
|
||||
crate::picks::button(app, export_datasets);
|
||||
}
|
||||
// Its file dialog, shown off the UI thread, loads the library when it answers.
|
||||
if widgets::menu_item(ui, tl!("Load Variable Library…"), true, false) {
|
||||
crate::picks::button(app, load_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) {
|
||||
@@ -350,6 +354,16 @@ fn export_datasets(app: &mut VectorcraftApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Variables panel: Load Variable Library… opens a file dialog for an XML file and runs
|
||||
/// `variable.loadLibrary` on the chosen file.
|
||||
fn load_library(app: &mut VectorcraftApp) {
|
||||
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("XML", &["xml"])], ..Default::default() };
|
||||
let Some(path) = crate::picks::open(app, &pick) else { return };
|
||||
if let Err(e) = app.run("variable.loadLibrary", json!({"path": path})) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -436,6 +450,7 @@ mod tests {
|
||||
"Update Data Set",
|
||||
"Rename Data Set…",
|
||||
"Delete Data Set",
|
||||
"Load Variable Library…",
|
||||
"Select Bound Object",
|
||||
"Select All Bound Objects",
|
||||
] {
|
||||
@@ -457,6 +472,23 @@ mod tests {
|
||||
assert!(text.contains("Name") && text.contains("Data Set 1"), "{text}");
|
||||
}
|
||||
|
||||
/// Load Variable Library's warning about variables that aren't text shows in the status bar,
|
||||
/// translated in every language but English.
|
||||
#[test]
|
||||
fn the_variable_library_warning_shows_in_the_status_bar() {
|
||||
use crate::i18n::{Lang, message};
|
||||
let mut app = app_with_data();
|
||||
let xml = r#"<variableSets><variableSet><variables><variable varName="Logo" trait="other"/></variables></variableSet></variableSets>"#;
|
||||
app.run("variable.loadLibrary", json!({"xml": xml})).unwrap();
|
||||
assert_eq!(app.ui.status, "variables that aren't text were left out: other (1)");
|
||||
let langs: Vec<Lang> = Lang::all().filter(|l| l.code() != "en").collect();
|
||||
assert!(langs.len() >= 23, "{} languages", langs.len());
|
||||
for l in langs {
|
||||
let shown = message(l, &app.ui.status);
|
||||
assert!(!shown.contains("left out") && shown.contains("other") && shown.contains('1'), "{}: {shown}", l.code());
|
||||
}
|
||||
}
|
||||
|
||||
/// What the panel acts on is the highlight, and only while it names a variable: a stale one
|
||||
/// (deleted elsewhere, undone) highlights nothing, so Delete and Options… stay disabled.
|
||||
#[test]
|
||||
|
||||
@@ -290,6 +290,29 @@ fn other_graphic_style_library_asks_off_the_ui_thread() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// The Variables panel menu's Load Variable Library… asks off the UI thread: the library picked
|
||||
/// loads when the dialog answers.
|
||||
#[test]
|
||||
fn load_variable_library_asks_off_the_ui_thread() {
|
||||
let (mut app, shown, _) = app();
|
||||
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
|
||||
let dir = std::env::temp_dir().join(format!("vc-picks-variables-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let library = dir.join("library.xml");
|
||||
let xml = r#"<variableSets><variableSet><variables><variable varName="Name" trait="textcontent"/></variables>
|
||||
<sampleDataSets><sampleDataSet dataSetName="A"><Name><p>x</p></Name></sampleDataSet></sampleDataSets></variableSet></variableSets>"#;
|
||||
std::fs::write(&library, xml).unwrap();
|
||||
let sets = |app: &VectorcraftApp| app.session.doc().unwrap().doc.variables.datasets.iter().map(|d| d.name.clone()).collect::<Vec<_>>();
|
||||
|
||||
click(&mut app, "Load Variable Library…", crate::panels::variables::menu);
|
||||
assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(_)]));
|
||||
assert!(sets(&app).is_empty(), "nothing loads before the dialog answers");
|
||||
shown.answer(&[&library.to_string_lossy()]);
|
||||
poll(&mut app);
|
||||
assert_eq!(sets(&app), ["A"], "{}", app.ui.status);
|
||||
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.
|
||||
|
||||
+20
-3
@@ -1288,9 +1288,10 @@ already walks Image Trace. Short agent entry points:
|
||||
`variable.unbind`, `variable.rename`, `variable.delete`, `variable.highlight` (image values are file paths, graph
|
||||
values CSV as `graph.setData`). `variable.lock {locked?}` (Lock Variables) locks or unlocks the variables;
|
||||
without `locked` it toggles, and the lock is saved with the document. While the variables are locked,
|
||||
`variable.define`, `variable.delete`, `variable.rename` and the four `variable.make…Dynamic` commands fail with
|
||||
"variables are locked"; `variable.bind`, `variable.unbind` and the `dataset.*` commands still run (except a
|
||||
`dataset.import` whose header would define a variable, below), and `variable.list` reports `locked`.
|
||||
`variable.define`, `variable.delete`, `variable.rename`, the four `variable.make…Dynamic` commands and a
|
||||
`variable.loadLibrary` that would delete or define a variable (below) fail with "variables are locked";
|
||||
`variable.bind`, `variable.unbind` and the `dataset.*` commands still run (except a `dataset.import` whose header
|
||||
would define a variable, below), and `variable.list` reports `locked`.
|
||||
- Data sets: `dataset.list`, `dataset.new {name, values?}`, `dataset.set {name, values?}`, `dataset.rename`,
|
||||
`dataset.delete`, `dataset.capture`, `dataset.update`, `dataset.select {name}`, `dataset.next` /
|
||||
`dataset.prev` (each `dataset.select` / next / prev is one undo step over every bound object), `dataset.import {path|data|dataBase64|csv}` /
|
||||
@@ -1303,6 +1304,22 @@ already walks Image Trace. Short agent entry points:
|
||||
one whose name is a variable of another kind fails the import. A `%` cell that isn't a variable's exact name (graph
|
||||
data files) is refused. The reply's `defined` lists the variables the import defined. While the variables are
|
||||
locked, an import whose header would define a variable fails with "variables are locked".
|
||||
- Variable libraries: `variable.loadLibrary {path|xml|dataBase64}` (the Variables panel menu's Load Variable
|
||||
Library…) replaces the document's variables and data sets with a variable library's in one undo step. The library
|
||||
is XML whose `variableSets` element holds `variableSet` elements, each with a `variables` element of `variable`
|
||||
elements (`varName`, `trait`) and a `sampleDataSets` element of `sampleDataSet` elements (`dataSetName`). Every
|
||||
variable of the document is deleted with its bindings and values, and every data set is deleted. Then the library's
|
||||
variables whose `trait` is `textcontent` are defined as text variables, unbound. In each data set, the element named
|
||||
after a text variable gives its value: the text of its `p` elements, one paragraph each. An element without a `p`
|
||||
element gives no value. Variables of other kinds are left out with their values: the reply lists them in `skipped`
|
||||
(`name`, `trait`), and its `warning` names each `trait` with its count. The reply's `deleted` and `deletedDataSets`
|
||||
list the variables and data sets the load deleted. While the variables are locked, a library that would delete or
|
||||
define a variable fails with "variables are locked". The reply is `{variables, deleted, deletedDataSets, names,
|
||||
skipped, warning?}`. A library larger than 8 MiB, one whose elements nest more than 256 deep, and one with more
|
||||
than 256 variables or 1,000 data sets are refused. Its document type declaration may hold an external identifier,
|
||||
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.
|
||||
- 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
|
||||
|
||||
@@ -463,6 +463,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Variables (data merge): text, visibility and image variables bound to objects (an object may carry several), dataset rows applied in one undo step (`variable.*`, `dataset.*`) from a Variables panel and Window › Variables, with dialogs to define, bind and edit, Make Text/Visibility/Image Dynamic, Capture/Update Data Set, CSV import/export (`dataset.import` / `dataset.export`), and a row in italics when an edit leaves the art not matching it; an image dataset value is a file path and apply keeps the object's bounds; bindings let go of deleted art and locked art is left alone; every command, like the rest, is drivable over MCP and the CLI.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user