1use crate::formats::{
2 html::html2text_with_width, troff::convert_man_troff_to_html,
3};
4use crate::utilities::*;
5use anyhow::{Context, Result};
6use directories::ProjectDirs;
7use encre_css::{Config, Preflight};
8use handlebars::Handlebars;
9use std::{
10 fs::{self, OpenOptions},
11 io::Write,
12 path::{Path, PathBuf},
13};
14
15pub mod db;
16pub mod graph;
17pub mod node;
18pub mod pc_settings;
19pub mod user;
20
21pub fn put(key: Vec<u8>, value: Vec<u8>) {
22 println!(
23 "Storage put with key {}, value {}",
24 String::from_utf8_lossy(&key),
25 String::from_utf8_lossy(&value)
26 );
27}
28
29pub fn get(key: Vec<u8>) -> Option<Vec<u8>> {
30 println!("Storage get with key {}", String::from_utf8_lossy(&key));
31 Some(key)
32}
33
34pub fn create_store(_name: String, _password: String) {}
35
36pub fn get_asset(key: &str) -> Option<Vec<u8>> {
37 let key = if key.starts_with('/') {
38 key.strip_prefix('/').expect("Strip prefix failed")
39 } else {
40 key
41 };
42
43 let file = if key == "src.zip" {
44 crate::SOURCE_ZIP.get_file(key)
45 } else {
46 crate::PROJECT_DIR.get_file(key)
47 };
48
49 Some(file?.contents().to_vec())
50}
51
52fn _get_storage_dir() -> Option<PathBuf> {
53 Some(
54 ProjectDirs::from("com", "collectivetoolbox", "collectivetoolbox")?
55 .data_dir()
56 .to_path_buf(),
57 )
58}
59
60pub fn get_storage_dir() -> Result<PathBuf> {
61 let data_dir =
62 _get_storage_dir().ok_or(anyhow::anyhow!("Failed to get cache dir"))?;
63 std::fs::create_dir_all(data_dir.clone())
64 .with_context(|| format!("Failed to create cache dir {data_dir:?}"))?;
65 Ok(data_dir)
66}
67
68pub fn get_help_troff() -> Vec<u8> {
69 get_asset("docs/cli/ctoolbox.1").expect("Could not load help")
70}
71
72pub fn get_help_html() -> Vec<u8> {
73 let (converted, log) = convert_man_troff_to_html(get_help_troff())
74 .expect("Could not convert help");
75 log.auto_log();
76 converted
77}
78
79pub fn get_help_for_tty(width: u16) -> Vec<u8> {
80 html2text_with_width(get_help_html(), width)
81}
82
83fn inline_css_imports(css: &str, base_path: &str) -> Result<String> {
84 fn normalize_path(p: &Path) -> PathBuf {
85 let mut stack = Vec::new();
86 for component in p.components() {
87 match component {
88 std::path::Component::Normal(c) => stack.push(c),
89 std::path::Component::ParentDir => {
90 stack.pop();
91 }
92 std::path::Component::CurDir => {}
93 _ => {}
94 }
95 }
96 stack.into_iter().collect()
97 }
98
99 let mut result = String::new();
100 for line in css.lines() {
101 if line.trim_start().starts_with("@import") {
102 let trimmed = line.trim_start();
103 if trimmed == "@import \"tailwindcss\";" {
104 continue;
105 }
106 if let Some(start) = trimmed.find("url('") {
107 if let Some(end) = trimmed[start + 5..].find("')") {
108 let path = &trimmed[start + 5..start + 5 + end];
109 let resolved =
110 if path.starts_with("web/") || path.starts_with('/') {
111 path.strip_prefix('/').unwrap_or(path).to_string()
112 } else {
113 Path::new(base_path)
114 .join(path)
115 .to_string_lossy()
116 .to_string()
117 };
118 let resolved_path = Path::new(&resolved);
119 let normalized = normalize_path(resolved_path);
120 let resolved = normalized.to_string_lossy().to_string();
121 let imported_css = String::from_utf8(
122 get_asset(&resolved).ok_or_else(|| {
123 anyhow::anyhow!(
124 "Failed to load imported CSS: {}",
125 resolved
126 )
127 })?,
128 )
129 .with_context(|| {
130 format!(
131 "Failed to convert imported CSS to string: {}",
132 resolved
133 )
134 })?;
135 let new_base = Path::new(&resolved)
136 .parent()
137 .unwrap_or(Path::new(""))
138 .to_string_lossy();
139 let inlined = inline_css_imports(&imported_css, &new_base)?;
140 result.push_str(&inlined);
141 result.push('\n');
142 continue;
143 }
144 }
145 return Err(anyhow::anyhow!(
146 "Failed to parse @import line: {}",
147 line
148 ));
149 } else {
150 result.push_str(line);
151 result.push('\n');
152 }
153 }
154 Ok(result)
155}
156
157pub fn register_views<'a>() -> handlebars::Handlebars<'a> {
158 let mut handlebars = Handlebars::new();
159 handlebars.set_strict_mode(true);
160 let mut buffer = String::new();
161 let views = crate::PROJECT_DIR
162 .find("**/*.hbs")
163 .expect("Failed to find views");
164 for entry in views {
165 let path = entry.path();
166 let template_name = substr_mb(
167 path.to_str()
168 .expect("Built-in assets should all use valid UTF-8 filenames"),
169 6,
170 -4,
171 )
172 .replace('/', ".");
173 log!(format!("Registering template {}", template_name));
174 let template_contents: &str = crate::PROJECT_DIR
175 .get_file(path)
176 .unwrap()
177 .contents_utf8()
178 .unwrap();
179 assert!(
180 handlebars
181 .register_template_string(
182 template_name.as_str(),
183 template_contents
184 )
185 .is_ok()
186 );
187 buffer.push_str(template_contents);
188 }
189
190 let js = String::from_utf8(
192 get_asset("web/pretty-load.js").expect("Could not load pretty-load.js"),
193 )
194 .expect("Could not convert pretty-load.js to string");
195 buffer.push_str(js.as_str());
196
197 let mut config = Config::default();
198 let mut css: String = String::new();
199
200 let app_css = String::from_utf8(
203 get_asset("web/app.css").expect("Could not load app.css"),
204 )
205 .expect("Could not convert app.css to string");
206
207 config.preflight = Preflight::new_full();
209 css.push_str(encre_css::generate([""], &config).as_str());
210
211 config.preflight = Preflight::new_none();
212
213 let inlined_app_css =
216 inline_css_imports(&app_css, "web/").expect("Failed to inline imports");
217
218 for line in inlined_app_css.lines() {
220 if line.trim_start().starts_with("@apply") {
221 let html =
222 format!("<div class='{}'></div>", remove_suffix(line, ";"));
223 let html_css = encre_css::generate([html.as_str()], &config);
224 let html_css = html_css.as_str();
225 let mut starting_media_query;
226 let mut in_media_query = false;
227 let mut leaving_media_query;
228 for line in html_css.lines() {
229 starting_media_query = line.starts_with('@');
230 leaving_media_query = line.starts_with('}') && in_media_query;
231 if (!in_media_query && line.starts_with(' '))
232 || starting_media_query
233 || (in_media_query
234 && !line.ends_with('{')
235 && !line.ends_with('}'))
236 || leaving_media_query
237 {
238 css.push_str(format!("{line}\n").as_str());
239 }
240 if starting_media_query {
241 in_media_query = true;
242 }
243 if leaving_media_query {
244 in_media_query = false;
245 }
246 }
247 continue;
248 }
249 css.push_str(format!("{line}\n").as_str());
250 }
251
252 let css_from_views = encre_css::generate([buffer.as_str()], &config);
254 for line in css_from_views.lines() {
255 if line == "@media (prefers-color-scheme: dark) {" {
256 css.push_str(
257 format!("{}\n", "@container dark (width > 0) {").as_str(),
258 );
259 } else if line.trim_start().starts_with('@') {
260 css.push_str(format!("{line}\n").as_str());
261 } else {
262 css.push_str(
263 format!("{line}\n")
264 .as_str()
265 .replace(" {", ":not(.increase-specificity) {")
266 .as_str(),
267 );
268 }
269 }
270 css.push('\n');
271 config.preflight = Preflight::new_none();
272
273 assert!(
275 handlebars
276 .register_template_string("encre_css", css)
277 .is_ok()
278 );
279 handlebars.set_strict_mode(true);
280 handlebars
281}
282
283pub fn guarantee_dir(path: &Path) -> Result<()> {
284 if let Some(parent) = path.parent() {
285 if !parent.exists() {
286 fs::create_dir_all(parent).with_context(|| {
287 format!("Failed to create parent dir {parent:?}")
288 })?;
289 }
290 }
291 Ok(())
292}
293
294pub fn put_file(path: &Path, data: &[u8]) -> Result<()> {
295 guarantee_dir(path)?;
296 overwrite_file(path, data)
297}
298
299pub fn append_file(path: &Path, data: &[u8]) -> Result<()> {
300 guarantee_dir(path)?;
301 let mut file = OpenOptions::new()
302 .create(true)
303 .append(true)
304 .open(path)
305 .with_context(|| {
306 format!("Failed to open file for appending {path:?}")
307 })?;
308 file.write_all(data)
309 .with_context(|| format!("Failed to append data to {path:?}"))
310}
311
312pub fn overwrite_file(path: &Path, data: &[u8]) -> Result<()> {
313 guarantee_dir(path)?;
314 fs::write(path, data)
315 .with_context(|| format!("Failed to overwrite file {path:?}"))
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[crate::ctb_test]
323 fn can_get_asset() {
324 assert_eq!(
325 strtovec("Hello, world!"),
326 get_asset("fixtures/hello.txt").expect("Could not load hello.txt")
327 );
328 }
329
330 #[crate::ctb_test]
331 fn can_store_and_get_value() {
332 put("key".as_bytes().to_owned(), "value".as_bytes().to_owned());
333 assert_eq!(
334 "key",
335 String::from_utf8_lossy(&get(strtovec("key")).unwrap())
336 );
337 }
338
339 #[crate::ctb_test]
340 fn can_get_help_troff() {
341 assert!(
342 String::from_utf8_lossy(&get_help_troff()).contains(".SH SYNOPSIS")
343 );
344 }
345
346 #[crate::ctb_test]
347 fn can_get_help_html() {
348 assert!(
349 String::from_utf8_lossy(&get_help_html())
350 .contains(">Synopsis</h2>")
351 );
352 }
353
354 #[crate::ctb_test]
355 fn can_get_help_for_tty() {
356 assert!(
359 String::from_utf8_lossy(&get_help_for_tty(8)).contains("## iptio")
360 );
361 }
362}