Skip to main content

katana_render_runtime/markdown/
runtime_assets.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::atomic::{AtomicU64, Ordering},
4};
5
6static RUNTIME_ASSET_WRITE_SEQUENCE: AtomicU64 = AtomicU64::new(1);
7
8pub const MERMAID_JS_VERSION: &str = "11.15.0";
9pub const MERMAID_JS_CHECKSUM: &str =
10    "70137e77bb273bb2ef972b86e8b0400cca8be53cb25bfc45911a186dc98665de";
11pub const MERMAID_DOWNLOAD_URL: &str =
12    "https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/mermaid.min.js";
13
14pub const MERMAID_ZENUML_JS_VERSION: &str = "0.2.3";
15pub const MERMAID_ZENUML_JS_CHECKSUM: &str =
16    "28eeec88021d9e9728df4d005ff723a3d71da29a21dbcfa2a628232c35ef2ab6";
17pub const MERMAID_ZENUML_DOWNLOAD_URL: &str =
18    "https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-zenuml@0.2.3/dist/mermaid-zenuml.min.js";
19
20pub const ZENUML_CORE_JS_VERSION: &str = "3.47.9";
21pub const ZENUML_CORE_JS_CHECKSUM: &str =
22    "ece11a311907401113f965e110c25c04c6a9b3dcbbb234bf2cd593a3f3ebe3df";
23pub const ZENUML_CORE_DOWNLOAD_URL: &str =
24    "https://cdn.jsdelivr.net/npm/@zenuml/core@3.47.9/dist/zenuml.js";
25
26pub const DRAWIO_JS_VERSION: &str = "30.0.2";
27pub const DRAWIO_JS_CHECKSUM: &str =
28    "0435d7a829549490482d576a37556224fa190d538610c96908632e5cda7c601f";
29pub const DRAWIO_DOWNLOAD_URL: &str = "https://github.com/jgraph/drawio/releases/tag/v30.0.2";
30
31pub const MATHJAX_JS_VERSION: &str = "4.1.2";
32pub const MATHJAX_JS_CHECKSUM: &str =
33    "e201dba4a20191563337e7f95ebeef6724bd2fbdc079c431b4bb8ecdfc059c33";
34pub const MATHJAX_DOWNLOAD_URL: &str = "https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-svg.js";
35
36pub(crate) struct RuntimeAsset {
37    kind: &'static str,
38    version: &'static str,
39    filename: &'static str,
40    bytes: &'static [u8],
41}
42
43impl RuntimeAsset {
44    pub(crate) fn mermaid() -> Self {
45        Self {
46            kind: "mermaid",
47            version: MERMAID_JS_VERSION,
48            filename: "mermaid.min.js",
49            bytes: include_bytes!("../../vendor/mermaid/11.15.0/mermaid.min.js"),
50        }
51    }
52
53    pub(crate) fn drawio() -> Self {
54        Self {
55            kind: "drawio",
56            version: DRAWIO_JS_VERSION,
57            filename: "drawio.min.js",
58            bytes: include_bytes!("../../vendor/drawio/30.0.2/drawio.min.js"),
59        }
60    }
61
62    #[cfg(test)]
63    pub(crate) fn zenuml_core() -> Self {
64        Self {
65            kind: "zenuml-core",
66            version: ZENUML_CORE_JS_VERSION,
67            filename: "zenuml.js",
68            bytes: include_bytes!("../../vendor/zenuml-core/3.47.9/zenuml.js"),
69        }
70    }
71
72    pub(crate) fn materialized_path(&self) -> PathBuf {
73        std::env::temp_dir()
74            .join("katana-render-runtime")
75            .join("vendor")
76            .join(self.kind)
77            .join(self.version)
78            .join(self.filename)
79    }
80
81    pub(crate) fn materialize_at(&self, path: PathBuf) -> Result<PathBuf, String> {
82        if self.exists_with_same_bytes(&path)? {
83            return Ok(path);
84        }
85        let Some(parent) = path.parent() else {
86            return Err(format!("{} runtime asset path has no parent", self.kind));
87        };
88        std::fs::create_dir_all(parent).map_err(runtime_asset_error)?;
89        self.write_atomically(&path, parent)?;
90        Ok(path)
91    }
92
93    fn write_atomically(&self, path: &Path, parent: &Path) -> Result<(), String> {
94        let temp_path = self.temporary_write_path(parent);
95        std::fs::write(&temp_path, self.bytes).map_err(runtime_asset_error)?;
96        match std::fs::rename(&temp_path, path) {
97            Ok(()) => Ok(()),
98            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
99                self.handle_existing_destination(path, &temp_path)
100            }
101            Err(error) => Self::cleanup_temp_and_report(temp_path, error),
102        }
103    }
104
105    fn temporary_write_path(&self, parent: &Path) -> PathBuf {
106        let sequence = RUNTIME_ASSET_WRITE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
107        parent.join(format!(
108            ".{}.{}.{}.tmp",
109            self.filename,
110            std::process::id(),
111            sequence
112        ))
113    }
114
115    fn handle_existing_destination(&self, path: &Path, temp_path: &Path) -> Result<(), String> {
116        if self.exists_with_same_bytes(path)? {
117            std::fs::remove_file(temp_path).map_err(runtime_asset_error)?;
118            return Ok(());
119        }
120        remove_existing_destination(path)?;
121        std::fs::rename(temp_path, path).map_err(runtime_asset_error)
122    }
123
124    fn cleanup_temp_and_report(temp_path: PathBuf, error: std::io::Error) -> Result<(), String> {
125        let _ = std::fs::remove_file(temp_path);
126        Err(runtime_asset_error(error))
127    }
128
129    fn exists_with_same_bytes(&self, path: &Path) -> Result<bool, String> {
130        match std::fs::read(path) {
131            Ok(existing) => Ok(existing == self.bytes),
132            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
133            Err(error) => Err(runtime_asset_error(error)),
134        }
135    }
136}
137
138fn runtime_asset_error(error: std::io::Error) -> String {
139    error.to_string()
140}
141
142fn remove_existing_destination(path: &Path) -> Result<(), String> {
143    match std::fs::remove_file(path) {
144        Ok(()) => Ok(()),
145        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
146        Err(error) => Err(runtime_asset_error(error)),
147    }
148}
149
150#[cfg(test)]
151#[path = "runtime_assets_tests.rs"]
152mod tests;