blockworx_editor/import.rs
1//! What the "Import…" dialog brings in: a PNG or SVG, as a plain **image** in
2//! the current view. The name's extension picks the branch.
3//!
4//! No diagram comes in through here. A container's projection is not a
5//! self-contained document — its assets live beside it — so no file says the
6//! whole of one, and bringing another diagram in as a block is a placement
7//! flow of its own rather than a file door.
8
9use blockworx_doc::block_model::Asset;
10
11/// A file name without its extension — what a document is called when the
12/// file it arrived in is all there is to call it.
13pub fn file_stem(name: &str) -> String {
14 match name.rsplit_once('.') {
15 Some((stem, _)) if !stem.is_empty() => stem.to_owned(),
16 _ => name.to_owned(),
17 }
18}
19
20/// What a document with neither provenance nor a file to be named by is
21/// called. A native session is born into a container it takes its name from,
22/// so this is the browser's document — and the fallback for a container that
23/// could not be created.
24pub const UNTITLED: &str = "untitled";
25
26/// Interpret a picked file (`name` for its extension, `bytes` for its content).
27/// Returns `None` for an unreadable/unsupported file.
28pub fn interpret(name: &str, bytes: Vec<u8>) -> Option<Asset> {
29 let ext = name
30 .rsplit('.')
31 .next()
32 .unwrap_or_default()
33 .to_ascii_lowercase();
34 match ext.as_str() {
35 // Named rather than left to the catch-all so the console says why a
36 // `.kdl` file is refused.
37 "kdl" => {
38 tracing::error!(
39 "{name} is in the retired KDL document format, which this build no longer reads"
40 );
41 None
42 }
43 "png" => Some(Asset::Png(bytes.into())),
44 "svg" => {
45 let src = String::from_utf8(bytes).ok()?;
46 Some(Asset::Svg(src.into_bytes().into()))
47 }
48 _ => None,
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 /// The retired document format is refused rather than half-read: its
57 /// parser is gone, so an import of one imports nothing.
58 #[test]
59 fn the_retired_document_format_no_longer_imports() {
60 assert!(interpret("d.kdl", b"top \"b0\"".to_vec()).is_none());
61 }
62
63 /// A document file is not an image, and a diagram has no file door.
64 #[test]
65 fn a_json_document_no_longer_imports() {
66 let json = br#"{ "version": 3, "top": "b1", "blocks": { "b1": {} } }"#.to_vec();
67 assert!(interpret("motor.json", json).is_none());
68 }
69
70 #[test]
71 fn a_plain_picture_imports_as_an_image() {
72 let svg = br#"<svg viewBox="0 0 4 2"><rect/></svg>"#.to_vec();
73 assert!(matches!(interpret("logo.svg", svg), Some(Asset::Svg(_))));
74 assert!(matches!(
75 interpret("logo.png", fake_png()),
76 Some(Asset::Png(_))
77 ));
78 }
79
80 #[test]
81 fn an_unknown_extension_is_rejected() {
82 assert!(interpret("notes.txt", b"hello".to_vec()).is_none());
83 }
84
85 /// A structurally-walkable PNG (signature + IHDR + IEND). CRCs are not checked
86 /// on read, so a placeholder header is enough for the chunk scanner.
87 fn fake_png() -> Vec<u8> {
88 let mut png = vec![0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1a, b'\n'];
89 png.extend_from_slice(&13u32.to_be_bytes()); // IHDR length
90 png.extend_from_slice(b"IHDR");
91 png.extend_from_slice(&[0; 13]); // header body
92 png.extend_from_slice(&[0; 4]); // CRC placeholder
93 png
94 }
95}