diff --git a/crates/engine/src/cmd/fileio/screens.rs b/crates/engine/src/cmd/fileio/screens.rs index 1d9c0303c..aed45c0cd 100644 --- a/crates/engine/src/cmd/fileio/screens.rs +++ b/crates/engine/src/cmd/fileio/screens.rs @@ -382,7 +382,7 @@ struct Naming { } impl Naming { - /// The naming of params `o` for a document titled `title`. + /// The naming of params `o` for a document named `title`: its file's path, or its title before it is saved. fn of(o: &ScreenParams, title: &str) -> Result { let folders = Folders::of(o)?; // A prefix names files, not folders. @@ -669,7 +669,9 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result { let o = ScreenParams::of(p)?; let rows = rows_of(s, p, o.preset.as_deref())?; o.one_source()?; - let naming = Naming::of(&o, &s.doc()?.doc.title)?; + // ``: the document's file, as Save names it, or its title before it is saved. + let st = s.doc()?; + let naming = Naming::of(&o, st.path.as_deref().unwrap_or(&st.doc.title))?; let selection = o.selection_mode()?; let selected = if selection.is_some() { selected_artwork(s)? } else { vec![] }; let doc = s.doc()?.doc.clone(); @@ -846,13 +848,14 @@ pub fn first_file(s: &Session, p: &Value) -> Result { let rows = rows_of(s, &p, o.preset.as_deref())?; o.one_source()?; let selection = o.selection_mode()?; - let doc = &s.doc()?.doc; + let st = s.doc()?; + let doc = &st.doc; let mut selected = if selection.is_some() { selected_artwork(s)? } else { vec![] }; // A file per object: objects that draw nothing write no file. if selection == Some(true) { selected = selected.into_iter().find(|id| export_crop(doc, &[*id]).is_some()).into_iter().collect(); } - let naming = Naming::of(&o, &doc.title)?; + let naming = Naming::of(&o, st.path.as_deref().unwrap_or(&doc.title))?; let items = items(doc, &o, selection, &selected, &naming.filename, false, false)?; let (Some(item), Some(r)) = (items.first(), rows.first()) else { return Err(bad(C, "nothing to export")) }; let (sub, file) = naming.file(item, 0, 0, r); diff --git a/crates/engine/src/cmd/fileio/tests_screens.rs b/crates/engine/src/cmd/fileio/tests_screens.rs index 867911681..5634c0da7 100644 --- a/crates/engine/src/cmd/fileio/tests_screens.rs +++ b/crates/engine/src/cmd/fileio/tests_screens.rs @@ -205,6 +205,39 @@ fn the_first_file_is_the_file_the_export_writes_first() { } } +/// A saved document's ``, Full Document file and ZIP are named after its file without the +/// extension, as Save names it, whatever its File Info title says. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn a_saved_document_exports_under_its_file_name() { + let mut s = session(1); + s.execute("file.info", &json!({"title": "Logo: final version"})).unwrap(); + let dir = std::env::temp_dir().join(format!("vc-screens-filename-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + s.execute("document.save", &json!({"path": dir.join("my-logo.vectorcraft").to_string_lossy()})).unwrap(); + for (p, name) in [ + (json!({"name": "__", "formats": [{"format": "png"}]}), "my-logo__Artboard 1.png"), + (json!({"fullDocument": true, "formats": [{"format": "png"}]}), "my-logo.png"), + ] { + assert_eq!(names(&export(&mut s, p.clone())), [name], "{p}"); + assert_eq!(super::screens::first_file(&s, &p).unwrap(), name, "{p}"); + } + assert_eq!(export(&mut s, json!({"zip": true, "formats": [{"format": "svg"}]}))["name"], "my-logo.zip"); + let _ = std::fs::remove_dir_all(dir); +} + +/// A document that has not been saved has no file: `` is its title. +#[test] +fn an_unsaved_document_exports_under_its_title() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 40, "height": 30, "name": "Poster"})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 5, "y": 5, "width": 20, "height": 20})).unwrap(); + s.execute("file.info", &json!({"title": "Spring poster"})).unwrap(); + let p = json!({"name": "", "formats": [{"format": "png"}]}); + assert_eq!(names(&export(&mut s, p.clone())), ["Spring-poster.png"]); + assert_eq!(super::screens::first_file(&s, &p).unwrap(), "Spring-poster.png"); +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn sub_folders_are_made_in_the_export_folder() { diff --git a/docs/mcp.md b/docs/mcp.md index d7d6f6890..1b0dea3cc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3628,8 +3628,9 @@ fallback font…), each note once; the export still writes or returns every file Batch exports (#1935): - **What:** `layers: [id…]` (each layer's art alone) or `selection: "one" | "each"` instead of artboards (`assets`, `fullDocument` as before). -- **Names:** `name` is a pattern for every file: `` (the document), `` (the artboard, asset, layer - or object), ``, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"` +- **Names:** `name` is a pattern for every file: `` (the document's file name without its extension, or + its title before it is saved; the Full Document file and the `.zip` use the same name), `` (the artboard, + asset, layer or object), ``, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"` or `"file"`), `YYYYMMDD`, `YYYY-MM-DD`, ``, `