mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 02:16:56 -06:00
M0.9: figcraft-cli export names files as the app's export does (#1154)
export into a folder replaced every character outside letters, digits, '-', '_', space and '.' with '_', so a frame called "Homepage - desktop (live + verbeteringen)" became "Homepage - desktop _live _ verbeteringen_.png". The app's file.exportLayers keeps those names and only replaces what file systems refuse. The CLI now uses the engine's safe_file_name, and docs/cli.md says which characters are replaced. Also adds my own entry to contributors/people.toml.
This commit is contained in:
@@ -207,9 +207,11 @@ pub fn info(path: &str, as_json: bool) -> CliResult<ExitCode> {
|
|||||||
Ok(ExitCode::SUCCESS)
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The file name a layer exports to: the app's rule ([`figcraft_engine::cmd::file::safe_file_name`],
|
||||||
|
/// so `/` becomes `_` too), without leading or trailing spaces and dots.
|
||||||
fn sanitize(name: &str) -> String {
|
fn sanitize(name: &str) -> String {
|
||||||
let s: String = name.chars().map(|c| if c.is_alphanumeric() || matches!(c, '-' | '_' | ' ' | '.') { c } else { '_' }).collect();
|
let s = figcraft_engine::cmd::file::safe_file_name(name);
|
||||||
let s = s.trim().trim_matches('.').to_string();
|
let s = s.trim().trim_matches('.').trim().to_string();
|
||||||
if s.is_empty() { "layer".into() } else { s.chars().take(100).collect() }
|
if s.is_empty() { "layer".into() } else { s.chars().take(100).collect() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +413,12 @@ mod tests {
|
|||||||
assert_eq!(sanitize("Home / Hero"), "Home _ Hero");
|
assert_eq!(sanitize("Home / Hero"), "Home _ Hero");
|
||||||
assert_eq!(sanitize("../.."), "_");
|
assert_eq!(sanitize("../.."), "_");
|
||||||
assert_eq!(sanitize(""), "layer");
|
assert_eq!(sanitize(""), "layer");
|
||||||
|
// The same characters the app's export keeps (#1154): only what file systems refuse goes.
|
||||||
|
assert_eq!(sanitize("Homepage - desktop (live + verbeteringen)"), "Homepage - desktop (live + verbeteringen)");
|
||||||
|
assert_eq!(sanitize("Q&A, 50% #1 [v2] {x} ~é"), "Q&A, 50% #1 [v2] {x} ~é");
|
||||||
|
assert_eq!(sanitize("a\\b:c*d?e\"f<g>h|i\tj"), "a_b_c_d_e_f_g_h_i_j");
|
||||||
|
assert_eq!(sanitize(" .hidden. "), "hidden");
|
||||||
|
assert_eq!(sanitize(&"x".repeat(300)).chars().count(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -81,6 +81,30 @@ fn run_export_info_convert() {
|
|||||||
std::fs::remove_dir_all(&d).ok();
|
std::fs::remove_dir_all(&d).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `export` into a folder names the files as the app's export does (#1154): brackets, `+`, `&`,
|
||||||
|
/// `%`, `#` and commas stay, what file systems refuse becomes `_`.
|
||||||
|
#[test]
|
||||||
|
fn export_file_names_keep_what_the_app_keeps() {
|
||||||
|
let d = tmp("names");
|
||||||
|
let script = d.join("s.json");
|
||||||
|
let names = ["Homepage - desktop (live + verbeteringen)", "Q&A, 50% #1 [v2]", "a:b*c?d|e"];
|
||||||
|
let steps: Vec<serde_json::Value> = names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, n)| serde_json::json!({"command": "frame.create", "params": {"x": i * 200, "y": 0, "width": 20, "height": 20, "name": n}}))
|
||||||
|
.collect();
|
||||||
|
std::fs::write(&script, serde_json::to_string(&steps).unwrap()).unwrap();
|
||||||
|
let file = d.join("doc.figcraft");
|
||||||
|
assert!(bin().arg("run").arg(&script).arg("--out").arg(&file).output().unwrap().status.success());
|
||||||
|
let dir = d.join("out");
|
||||||
|
let out = bin().arg("export").arg(&file).arg("-o").arg(&dir).output().unwrap();
|
||||||
|
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
for want in ["Homepage - desktop (live + verbeteringen).png", "Q&A, 50% #1 [v2].png", "a_b_c_d_e.png"] {
|
||||||
|
assert!(dir.join(want).is_file(), "{want} in {:?}", std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.file_name()).collect::<Vec<_>>());
|
||||||
|
}
|
||||||
|
std::fs::remove_dir_all(&d).ok();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_takes_the_documented_gradient_shorthand() {
|
fn run_takes_the_documented_gradient_shorthand() {
|
||||||
// #252: only MCP used to accept it; `run` said "`fills` must be paints or colours".
|
// #252: only MCP used to accept it; `run` said "`fills` must be paints or colours".
|
||||||
|
|||||||
@@ -71,3 +71,8 @@ sync_github_name = true
|
|||||||
|
|
||||||
[people.AganzeFelicite]
|
[people.AganzeFelicite]
|
||||||
sync_github_name = true
|
sync_github_name = true
|
||||||
|
|
||||||
|
[people.switch87]
|
||||||
|
real_name = "Gert Pellin"
|
||||||
|
display_name = "Gert Pellin"
|
||||||
|
sync_github_name = false
|
||||||
|
|||||||
@@ -1169,8 +1169,10 @@ fn export_path(name: &str) -> String {
|
|||||||
if parts.is_empty() { "Untitled".into() } else { parts.join("/") }
|
if parts.is_empty() { "Untitled".into() } else { parts.join("/") }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `name` without path separators and characters file systems reject.
|
/// `name` without path separators and characters file systems reject (`/ \ : * ? " < > |` and
|
||||||
fn safe_file_name(name: &str) -> String {
|
/// control characters become `_`); everything else is kept. `figcraft-cli export` names its files
|
||||||
|
/// by this rule too.
|
||||||
|
pub fn safe_file_name(name: &str) -> String {
|
||||||
name.chars().take(200).map(|c| if c.is_control() || "/\\:*?\"<>|".contains(c) { '_' } else { c }).collect()
|
name.chars().take(200).map(|c| if c.is_control() || "/\\:*?\"<>|".contains(c) { '_' } else { c }).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ page 1 "Screens": 380 nodes, 4 top-level
|
|||||||
|
|
||||||
- `<out>` is a directory: one file per top-level frame of the page (default the first page), named
|
- `<out>` is a directory: one file per top-level frame of the page (default the first page), named
|
||||||
after the frame (`Home.png`, `Settings.png`…); with no frames, one file per top-level layer.
|
after the frame (`Home.png`, `Settings.png`…); with no frames, one file per top-level layer.
|
||||||
|
The name is kept as it is, except for the characters file systems refuse (`/ \ : * ? " < > |`
|
||||||
|
and control characters), which become `_`, and leading or trailing spaces and dots, which are
|
||||||
|
dropped. The app's export replaces the same characters (there a `/` makes a folder). Two layers
|
||||||
|
with the same name get `Name 2`, `Name 3`…
|
||||||
- `<out>` has an image extension: one file with the given `--node`, or the whole page composited.
|
- `<out>` has an image extension: one file with the given `--node`, or the whole page composited.
|
||||||
- `--node` takes a layer id (`12` or `0:12`) or a layer name.
|
- `--node` takes a layer id (`12` or `0:12`) or a layer name.
|
||||||
- `--format fig` (or an `<out>` ending in `.fig`) writes the whole file as `.fig`, experimental:
|
- `--format fig` (or an `<out>` ending in `.fig`) writes the whole file as `.fig`, experimental:
|
||||||
|
|||||||
Reference in New Issue
Block a user