diff --git a/Cargo.lock b/Cargo.lock index 36bd121b6..5fdb9e27b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1157,10 +1157,21 @@ dependencies = [ "zip 9.1.1", ] +[[package]] +name = "craft-web-api" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505" +dependencies = [ + "js-sys", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "craft-web-host" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=2e2312ddaf21bc3d85cd3395d0955c7d239306f6#2e2312ddaf21bc3d85cd3395d0955c7d239306f6" +source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505" dependencies = [ "wasm-bindgen", "web-sys", @@ -2130,6 +2141,7 @@ name = "figcraft-web" version = "0.15.0" dependencies = [ "base64 0.22.1", + "craft-web-api", "craft-web-host", "eframe", "egui", diff --git a/ROADMAP.md b/ROADMAP.md index a75992ad3..3f9e03f2b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -104,7 +104,7 @@ native speakers' reviews of the nine language drafts need people. | Traditional Chinese (Taiwan) | zh-Hant | 2,647 / 2,647 entries (100%, counted 2026-11-03) | draft (needs native review) | | Spanish | es, es-419 | 100% (es-419: an overlay of 101 entries) | draft (needs native review) | | Hindi | hi | 0% | none | -| Arabic | ar | 2,508 / 2,508 entries (100%, counted 2026-10-28) | draft; native review and panel mirroring pending | +| Arabic | ar | 2,508 / 2,508 entries (100%, counted 2026-10-28) | draft; the window mirrors (panels swap sides); native review and mirrored panel rows pending | | French | fr | 95% | draft (needs native review) | | Portuguese (Brazil) | pt-BR | 95% | draft (needs native review) | | Indonesian | id | 99% | draft (needs native review) | diff --git a/apps/figcraft-cli/src/files.rs b/apps/figcraft-cli/src/files.rs index 29f8cd5b6..a7bddeb3b 100644 --- a/apps/figcraft-cli/src/files.rs +++ b/apps/figcraft-cli/src/files.rs @@ -10,6 +10,12 @@ use serde_json::{Value, json}; use crate::{CliError, CliResult}; +/// A document's or plugin's text as a terminal should show it: control characters escaped, and +/// everything else (quotes, backslashes in paths) left as it is. +fn plain(text: &str) -> String { + text.chars().map(|c| if c.is_control() { c.escape_default().to_string() } else { c.to_string() }).collect() +} + pub fn load(path: &str) -> CliResult { let (doc, notes) = load_noted(path)?; if ext(path) == "penpot" { @@ -199,26 +205,33 @@ pub fn info(path: &str, as_json: bool) -> CliResult { return Ok(ExitCode::SUCCESS); } for n in ¬es { - eprintln!("note: {n}"); + eprintln!("note: {}", plain(&n.to_string())); } println!( "{} — {} ({} nodes, {} styles, {} variables, {} images)", - path, - doc.name, + plain(path), + plain(&doc.name), doc.node_count(), doc.styles.len(), doc.variables.variables.len(), doc.images.len() ); for p in &pages { - println!("page {} \"{}\": {} nodes, {} top-level", p["id"], p["name"].as_str().unwrap_or(""), p["nodes"], p["topLevel"]); + println!("page {} \"{}\": {} nodes, {} top-level", p["id"], plain(p["name"].as_str().unwrap_or("")), p["nodes"], p["topLevel"]); for f in p["frames"].as_array().into_iter().flatten() { - println!(" {} {} \"{}\" {}×{}", f["id"], f["type"].as_str().unwrap_or(""), f["name"].as_str().unwrap_or(""), f["width"], f["height"]); + println!( + " {} {} \"{}\" {}×{}", + f["id"], + f["type"].as_str().unwrap_or(""), + plain(f["name"].as_str().unwrap_or("")), + f["width"], + f["height"] + ); } } for m in &missing { let layers = if m.layers == 1 { "text layer" } else { "text layers" }; - println!("missing font \"{}\": {} {layers} (drawn in a fallback font)", m.family, m.layers); + println!("missing font \"{}\": {} {layers} (drawn in a fallback font)", plain(&m.family), m.layers); } Ok(ExitCode::SUCCESS) } @@ -438,7 +451,8 @@ pub fn run_script(script: &str, input: Option<&str>, out: Option<&str>) -> CliRe let cmd = steps.get(failed_at).map_or("", |step| step.command.as_str()); let i = failed_at; println!("{}", json!({"step": i, "command": cmd, "ok": false, "error": e.to_string(), "rolledBack": true})); - eprintln!("error: step {i} ({cmd}) failed: {e}"); + // Plugin-supplied text: escape terminal controls and line breaks. + eprintln!("error: step {i} ({}) failed: {}", plain(cmd), plain(&e.to_string())); eprintln!("the script's document changes were rolled back"); return Ok(ExitCode::from(1)); } @@ -491,6 +505,25 @@ pub fn connect_publish(file: &str, mappings: &str, dry_run: bool) -> CliResult"#).unwrap(); + std::fs::write(&script, r#"[{"command":"no.such\u001b"}]"#).unwrap(); + assert_eq!(info(svg.to_str().unwrap(), false).unwrap(), ExitCode::SUCCESS); + assert_eq!(run_script(script.to_str().unwrap(), None, None).unwrap(), ExitCode::from(1)); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn plain_escapes_controls_and_leaves_quotes_and_paths() { + assert_eq!(plain("a\u{1b}[31m\nb"), r"a\u{1b}[31m\nb"); + assert_eq!(plain(r"Bob's C:\files"), r"Bob's C:\files"); + } + #[test] fn script_formats() { assert_eq!(parse_script(r#"[{"command":"a"},{"command":"b","params":{"x":1}}]"#).unwrap().len(), 2); diff --git a/apps/figcraft-web/Cargo.toml b/apps/figcraft-web/Cargo.toml index 7f0b4ded5..c37663258 100644 --- a/apps/figcraft-web/Cargo.toml +++ b/apps/figcraft-web/Cargo.toml @@ -20,7 +20,9 @@ figcraft-ui-egui = { workspace = true } figcraft-collab-net = { workspace = true } figcraft-text = { workspace = true } # The host messages (storytold/craft-libs): one contract and one bridge for the web builds. -craft-web-host = { git = "https://github.com/storytold/craft-libs", rev = "2e2312ddaf21bc3d85cd3395d0955c7d239306f6", version = "0.1.0" } +# The page command API (storytold/craft-libs#413): the request plumbing behind window.figcraft. +craft-web-api = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" } +craft-web-host = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" } base64 = { workspace = true } egui = { workspace = true } log = { workspace = true } diff --git a/apps/figcraft-web/src/api.rs b/apps/figcraft-web/src/api.rs index 7f6a0ec4d..167ea7687 100644 --- a/apps/figcraft-web/src/api.rs +++ b/apps/figcraft-web/src/api.rs @@ -1,36 +1,54 @@ -//! `window.figcraft`: the control channel for the page, as promises. +//! `window.figcraft`: the page command API (`craft-web-api`, storytold/craft-libs#413) plus FigCraft's +//! own file functions. //! -//! The same JSON methods as the desktop control server (`docs/control-protocol.md`), so an agent -//! driving FigCraft in a browser tab (or an `