diff --git a/crates/engine/src/cmd/file/export.rs b/crates/engine/src/cmd/file/export.rs index 542ee246c..de426ba89 100644 --- a/crates/engine/src/cmd/file/export.rs +++ b/crates/engine/src/cmd/file/export.rs @@ -1,7 +1,7 @@ use super::load::{ext, write}; use super::save::thumbnail; use super::*; -use crate::params::f64_of; +use crate::params::{f64_of, req_targets}; /// Encode `ids` (or the page) in `format` at `scale`. pub fn export_bytes(doc: &Document, ids: &[NodeId], format: ExportFormat, scale: f64) -> Result> { @@ -86,7 +86,7 @@ pub(super) fn export(s: &mut Session, p: &Value) -> Result { _ => {} } let doc = s.doc()?; - let mut ids = targets(s, p); + let mut ids = export_targets(cmd, s, p)?; if ids.is_empty() { ids = doc.get(s.page()?)?.children.clone(); } @@ -325,11 +325,22 @@ pub fn exportable_layers(doc: &Document, page: NodeId) -> Vec { doc.subtree(page).into_iter().skip(1).filter(|i| doc.node(*i).is_some_and(|n| !n.exports.is_empty())).collect() } +/// Named layers (`ids` / `id`), checked like other commands; empty when none are named, so the +/// caller can fall back to its default instead of silently dropping a bad id. +fn export_targets(cmd: &str, s: &Session, p: &Value) -> Result> { + let named = match get(p, "ids") { + Some(Value::Array(a)) if !a.is_empty() => true, + Some(Value::Array(_) | Value::Null) | None => get(p, "id").is_some_and(|v| !v.is_null()), + Some(_) => true, + }; + if named { req_targets(cmd, s, p) } else { Ok(targets(s, p)) } +} + pub(super) fn export_layers(s: &mut Session, p: &Value) -> Result { let cmd = "file.exportLayers"; let doc = s.doc()?; let assets = boolean(p, "assets").unwrap_or(false); - let mut ids = targets(s, p); + let mut ids = export_targets(cmd, s, p)?; if ids.is_empty() { ids = if assets { vec![s.page()?] } else { exportable_layers(doc, s.page()?) }; } diff --git a/crates/engine/src/export_behaviour_tests.rs b/crates/engine/src/export_behaviour_tests.rs index 1cd836784..492fbf881 100644 --- a/crates/engine/src/export_behaviour_tests.rs +++ b/crates/engine/src/export_behaviour_tests.rs @@ -384,3 +384,17 @@ fn copy_as_png_svg_and_code() { let css = crate::codegen::code_for(doc, a, crate::codegen::CodeLang::Css); assert!(css.contains("background: #FF0000;"), "Copy as CSS is the layer's CSS from Inspect"); } + +#[test] +fn export_rejects_an_invalid_layer_id_instead_of_exporting_the_page() { + let mut s = Session::new(); + let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0, "#ff0000", None); + ex(&mut s, "export.add", json!({"ids": [r]})); + for cmd in ["file.export", "file.exportLayers"] { + for p in [json!({"ids": ["abc"], "format": "svg"}), json!({"id": "abc", "format": "svg"}), json!({"ids": [], "id": "abc", "format": "svg"})] { + let res = s.execute(cmd, &p); + assert!(res.is_err(), "{cmd} {p} exported for an invalid id: {res:?}"); + } + } + assert!(s.execute("file.export", &json!({"format": "svg"})).is_ok()); +}