From 6b13dd776bada37f6f624ae6bb3c17d90c4d8caa Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 17 Oct 2026 05:49:33 -0700 Subject: [PATCH] Export: names typed into file names follow Windows' naming rules Export for Screens, Batch over data sets, Save for Web and the Libraries panel name files after text the person typed: a prefix, a suffix, a name pattern, the name of an artboard, asset, layer, slice, data set or library, or the document's title. The prefix, the suffix, name patterns and Batch's data set names had only /, \ and : replaced, so on Windows a name holding <, >, ", |, ?, * or a control character failed to save, and Save for Web without a path named its files after the document's title as it was. A row folder name could end in a period, which Windows does not allow, and names such as CON, NUL, COM1 or LPT1, which Windows reserves for devices, were written as they were. file_part, which Export for Screens' prefix and suffix and Batch's data set names use, now writes all of those characters as -, and name patterns use it in place of their own copy. A new safe_file_name runs a whole file or folder name through file_part, removes trailing periods and spaces, and adds - after a device name that is the whole name or comes before its first period (CON.png becomes CON-.png). Export for Screens uses it for each file, folder and zip it names, and keeps item names distinct after it. Save for Web uses it for slice images and, without a path, for files named after the title. Library files use it too. This happens on every platform. Other names are written as before, except where a changed name now equals another item's name, which then gets a number added. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/fileio/action_batch.rs | 31 ++++++++++++- crates/engine/src/cmd/fileio/mod.rs | 5 ++- crates/engine/src/cmd/fileio/naming.rs | 23 +++++----- crates/engine/src/cmd/fileio/screens.rs | 43 ++++++++++++++----- crates/engine/src/cmd/fileio/tests_batch.rs | 11 +++++ crates/engine/src/cmd/fileio/tests_screens.rs | 36 ++++++++++++++++ crates/engine/src/cmd/library.rs | 5 ++- crates/engine/src/cmd/webexport.rs | 11 +++-- crates/engine/src/tests_library.rs | 12 ++++++ crates/engine/src/tests_webexport.rs | 30 +++++++++++++ docs/mcp.md | 20 ++++++--- docs/target-app-parity.md | 3 +- 12 files changed, 192 insertions(+), 38 deletions(-) diff --git a/crates/engine/src/cmd/fileio/action_batch.rs b/crates/engine/src/cmd/fileio/action_batch.rs index 783587878..7e757eb19 100644 --- a/crates/engine/src/cmd/fileio/action_batch.rs +++ b/crates/engine/src/cmd/fileio/action_batch.rs @@ -140,8 +140,9 @@ fn batch_datasets(s: &mut Session, cmd: &str, run: &Run<'_>) -> Result { } let doc_stem = st.path.as_deref().and_then(|p| std::path::Path::new(p).file_stem().and_then(|s| s.to_str())).unwrap_or("Untitled").to_string(); // Data set files are named File + Data Set Name (helpx, for Override Save/Export; the folder - // destination uses the same name). `file_part` writes `/`, `\` and `:` in the data set's name as - // `-`, and `distinct_names` adds `-2`, `-3`… to a name an earlier data set used. + // destination uses the same name). `file_part` writes the characters Windows does not allow in a + // file name (`/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters) in the data set's + // name as `-`, and `distinct_names` adds `-2`, `-3`… to a name an earlier data set used. let stems = distinct_names(names.iter().map(|name| format!("{doc_stem}_{}", file_part(name)))); let steps = filter_steps(run.steps, true, false); let run = Run { steps: &steps, ..*run }; @@ -598,6 +599,32 @@ mod tests { assert_eq!(file_names(&exports), ["Untitled_a-b.svg", "Untitled_c-d.svg", "Untitled_e-f.svg"], "{r}"); } + /// Windows does not allow `<`, `>`, `"`, `|`, `?`, `*` or control characters in a file name: + /// Batch writes them as `-` in a data set's name, on every platform. + #[test] + fn batch_saves_data_sets_named_with_characters_windows_does_not_allow() { + let dest = tmp("set-names-win"); + let mut s = with_data_sets(&["a?b", "c*d", "f\"g|h", "i\u{7}j"]); + let r = s + .execute( + "action.batch", + &json!({ + "source": "datasets", + "destination": "folder", + "destinationFolder": dest.to_string_lossy(), + "stopOnError": false, + "commands": [{"command": "object.unlockAll", "params": {}}] + }), + ) + .unwrap(); + assert_eq!(r["failed"], 0, "{r}"); + assert_eq!( + file_names(&dest), + ["Untitled_a-b.vectorcraft", "Untitled_c-d-e-.vectorcraft", "Untitled_f-g-h.vectorcraft", "Untitled_i-j.vectorcraft"], + "{r}" + ); + } + #[test] fn batch_gives_each_data_set_a_file_of_its_own() { let dest = tmp("set-names-same"); diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index 5da4fa250..64d000b37 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -78,7 +78,8 @@ pub use screens::{ builtin_presets as screen_export_builtin_presets, preset_rows as screen_preset_rows, }; pub(crate) use screens::{ - SHARED_KEYS as SCREEN_SETTINGS_KEYS, check_settings as check_screen_settings, export as export_screens, store_settings as store_screen_settings, + SHARED_KEYS as SCREEN_SETTINGS_KEYS, check_settings as check_screen_settings, export as export_screens, safe_file_name, + store_settings as store_screen_settings, }; pub use svg::options_map as svg_options; /// Atomic file writes (a temporary file renamed over the target: a failed write never damages the @@ -186,7 +187,7 @@ pub fn specs() -> Vec { "Batch…", [], None, - "{commands: [{command, params}] (the action's steps), source?: folder (default) | datasets, folder? (required for folder: the source folder), includeSubdirs?: false, destination?: none (default: leave each file open) | saveAndClose (save over the source and close) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), overrideOpen?: true (skip document.open / file.open steps: the batch opens each source file), overrideSave?: true (point document.save / file.save / file.saveAs at destinationFolder/{stem}.{ext}), overrideExport?: true (point document.export / document.exportForScreens / document.exportSelection the same way), stopOnError?: true, label?} play the commands on every readable file in folder (and its subfolders when includeSubdirs), or on every data set of the open document (source: datasets; File + Data Set Name for the folder destination and Override Save/Export, with `/`, `\\` and `:` in the data set's name as `-`, and `-2`, `-3`… added to a name already used). Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped}", + "{commands: [{command, params}] (the action's steps), source?: folder (default) | datasets, folder? (required for folder: the source folder), includeSubdirs?: false, destination?: none (default: leave each file open) | saveAndClose (save over the source and close) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), overrideOpen?: true (skip document.open / file.open steps: the batch opens each source file), overrideSave?: true (point document.save / file.save / file.saveAs at destinationFolder/{stem}.{ext}), overrideExport?: true (point document.export / document.exportForScreens / document.exportSelection the same way), stopOnError?: true, label?} play the commands on every readable file in folder (and its subfolders when includeSubdirs), or on every data set of the open document (source: datasets; File + Data Set Name for the folder destination and Override Save/Export, with `/`, `\\`, `:`, `<`, `>`, `\"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and `-2`, `-3`… added to a name already used). Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped}", always, action_batch::action_batch ), diff --git a/crates/engine/src/cmd/fileio/naming.rs b/crates/engine/src/cmd/fileio/naming.rs index 950cdbbbd..9e18e8212 100644 --- a/crates/engine/src/cmd/fileio/naming.rs +++ b/crates/engine/src/cmd/fileio/naming.rs @@ -12,7 +12,10 @@ //! (YYYYMMDD), `