Skip to main content

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}