katana_render_runtime/markdown/
runtime_assets.rs1use 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;