diff --git a/ROADMAP.md b/ROADMAP.md index 796ff7306..8443ae9e6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -388,6 +388,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-31** · A `.designcraft` file opened from bytes (`file.openBytes` with its file name, as a browser or host passes it) is titled without the extension, as an IDML file opened that way is. Saving it in the browser downloaded `Brochure.designcraft.designcraft`, and the File Name variable showed the doubled name. No readiness percentage change. - **2026-10-30** · Import warnings about a story have a Show button: it closes the warnings (the status line keeps them) and selects the story's first frame, fitted in the window. `file.open` returns `warningItems`, the frame each IDML or InDesign warning is about (or null), matching the converter's INDD story numbers (`story 30892`, written to IDML as `u78ac`) and IDML `Self` names to the imported stories. No readiness percentage change. - **2026-10-30** · Parent pages: a running-header variable shows its name in angle brackets on the parent itself (it was blank), as InDesign shows it; on the document pages, and in text with no page, it shows its text as before. No readiness percentage change. - **2026-10-30** · Without facing pages, a page of a multi-page spread shows the parent page in its place, as in InDesign: the second page of a two-page spread shows the parent's second page. Every single-sided page showed the parent's first page, so an InDesign book set up without facing pages but laid out in two-page spreads repeated the left parent page's items (a black heading bar) on its right pages and lost the right parent page's. No readiness percentage change. diff --git a/crates/engine/src/cmd/file/bytes.rs b/crates/engine/src/cmd/file/bytes.rs index 0946205a8..f42925034 100644 --- a/crates/engine/src/cmd/file/bytes.rs +++ b/crates/engine/src/cmd/file/bytes.rs @@ -27,6 +27,16 @@ pub fn with_document_extension(path: &str) -> String { format!("{path}.{}", designcraft_format::EXTENSION) } +/// `name` without a trailing `.designcraft` (any case): the title of a document opened from bytes +/// under its file name. Save, Save As and the File Name variable add the extension back, so a title +/// that kept it would name the file `Brochure.designcraft.designcraft`. A bare `.designcraft` stays. +pub(super) fn without_native_extension(name: &str) -> &str { + match name.rsplit_once('.') { + Some((stem, ext)) if !stem.is_empty() && ext.eq_ignore_ascii_case(designcraft_format::EXTENSION) => stem, + _ => name, + } +} + /// Whether `bytes` are a document `file.open` reads: a DesignCraft package or an IDML package. /// The first [`DOCUMENT_SNIFF_BYTES`] of a file are enough for packages that start with their /// `mimetype` entry, as both formats write them. diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 75a7ce627..01d268635 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -1,6 +1,6 @@ #[cfg(test)] use super::bytes::{PREVIEW_PX, to_bytes}; -use super::bytes::{base64_decode, load_bytes}; +use super::bytes::{base64_decode, load_bytes, without_native_extension}; use super::{Arc, DocState, Document, EngineError, Result, Session, Value, bad, json, str_param}; pub(super) fn has_recent(s: &Session) -> std::result::Result<(), String> { @@ -150,7 +150,8 @@ pub(super) fn file_open_bytes(s: &mut Session, p: &Value) -> Result { let (mut d, warnings) = load_bytes(&b)?; crate::cmd::datamerge::resolve_sources_on_open(&mut d, None); if let Some(n) = str_param(p, "name") { - d.title = n.to_string(); + // Titled as an IDML file opened from bytes is: without its extension. + d.title = without_native_extension(n).to_string(); } let i = s.add_document(DocState::new(d, None)); Ok(json!({"index": i, "warnings": warnings})) @@ -413,6 +414,35 @@ mod document_fonts_tests { } } +#[cfg(test)] +mod open_bytes_tests { + use serde_json::json; + + use crate::Session; + + /// A `.designcraft` file opened from bytes under its file name (a browser or host passes the + /// name it has) is titled without the extension, as an IDML file is, so saving it gives + /// `Brochure.designcraft` and not `Brochure.designcraft.designcraft`. + #[test] + fn open_bytes_titles_a_native_document_without_its_extension() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let native = s.execute("file.serialize", &json!({})).unwrap(); + for (name, title) in [ + ("Brochure.designcraft", "Brochure"), + ("Brochure.DesignCraft", "Brochure"), + ("Brochure", "Brochure"), + ("Brochure.v2", "Brochure.v2"), + ("Brochure.designcraft.designcraft", "Brochure.designcraft"), + (".designcraft", ".designcraft"), + ] { + let r = s.execute("file.openBytes", &json!({"name": name, "base64": native["base64"]})).unwrap(); + let st = &s.documents()[r["index"].as_u64().unwrap() as usize]; + assert_eq!((st.doc.title.as_str(), st.title()), (title, title.to_string()), "{name}"); + } + } +} + #[cfg(test)] mod recent_tests { use serde_json::json;