use std::{io::Write, path::PathBuf}; use walkdir::{DirEntry, WalkDir}; fn absoloutize(s: &str) -> PathBuf { let p = std::path::Path::new(s); if !p.is_absolute() { let d = std::env::current_dir().unwrap(); d.join(p) } else { p.to_path_buf() } } fn main() { let args: Vec = std::env::args().collect(); if args.len() < 5 { eprintln!( "Usage cargo run --bin slint_generator -- source_dir output_dir output_root data_dir" ); std::process::exit(1); } let source = &args[1]; // where .slint source lives let source_path = absoloutize(&source); // where html will get written let output_path = absoloutize(&args[2]); // the root of the output directory let output_root = absoloutize(&args[3]); // the data directory containing js and css let data_path = absoloutize(&args[4]); if !output_root.exists() { std::fs::create_dir_all(&output_root).expect("Failed to create output dir"); } let file_index = output_root.join("fileIndex"); let file_index = file_index.to_str().unwrap(); let mut file_index = std::fs::OpenOptions::new() .create(true) .write(true) .append(true) .open(file_index) .unwrap(); let slint_files = collect_slint_files(&source); for file in slint_files { if let Err(e) = generate( &file, &source_path, &output_path, &output_root, &mut file_index, &data_path, ) { eprintln!("Failed to generate {:?}. Error: {e}", file.as_os_str()); } else { println!("Wrote {file:?}"); } } } fn filter(e: &DirEntry) -> bool { if e.file_type().is_dir() { true } else { e.path().extension().is_some_and(|ext| ext == "slint") } } fn collect_slint_files(source_dir: &str) -> Vec { let slint_files: Vec<_> = WalkDir::new(source_dir) .into_iter() .filter_entry(filter) .filter_map(|e| { e.ok() .and_then(|e| e.file_type().is_file().then_some(e.into_path())) }) .collect(); slint_files } fn generate( file: &PathBuf, source: &PathBuf, output_path: &PathBuf, output_root: &PathBuf, file_index: &mut std::fs::File, data_path: &PathBuf, ) -> Result<(), String> { if !file.starts_with(source) { return Err("Unexpected file doesn't start with {source:?}".into()); } let rel_path = file.strip_prefix(source).map_err(|e| e.to_string())?; let output_file_path = output_path.join(rel_path); let output_file_dir = output_file_path .parent() .ok_or("Failed to get output_dir for file")?; std::fs::create_dir_all(&output_file_dir).map_err(|e| e.to_string())?; let source = std::fs::read_to_string(&file).map_err(|e| e.to_string())?; let mut out_file = std::fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(format!("{}.html", output_file_path.to_str().unwrap())) .map_err(|e| e.to_string())?; let relative_data_path = pathdiff::diff_paths(data_path, &output_file_dir) .ok_or("Failed to get relative data path")?; let file_name = file .file_name() .and_then(|n| n.to_str()) .ok_or("Failed to get file name")?; let mut hl = create_slint_highlighter(); gen_highlighted_html( &mut out_file, &mut hl, file_name, &relative_data_path, &source, ) .map_err(|e| e.to_string())?; // get relative path from output root directory and write to fileIndex if let Err(e) = output_file_path .strip_prefix(&output_root) .map_err(|e| e.to_string()) .and_then(|p| Ok(p.to_str().unwrap())) .and_then(|p| writeln!(file_index, "{p}").map_err(|e| e.to_string())) { eprintln!("Failed to write {output_file_path:?} to fileIndex. Error: {e}"); } Ok(()) } fn write_html_escaped_string(out: &mut dyn Write, s: &str) -> Result<(), std::io::Error> { for c in s.chars() { match c { '<' => write!(out, "<"), '>' => write!(out, ">"), '&' => write!(out, "&"), _ => write!(out, "{c}"), }? } Ok(()) } fn write_tag( out: &mut dyn Write, tag: &str, inner: &str, attribute: Option<&str>, ) -> Result<(), std::io::Error> { if let Some(attr) = attribute { write!(out, "<{tag} class=\"{attr}\">")?; } else { write!(out, "<{tag}>")?; } write_html_escaped_string(out, inner)?; write!(out, "")?; Ok(()) } fn create_slint_highlighter() -> synoptic::Highlighter { let mut hl = synoptic::Highlighter::new(4); hl.bounded("comment", r"/\*", r"\*/", false); hl.keyword("comment", "(//.*)$"); hl.keyword("boolean", r"\b(true|false)\b"); hl.keyword("digit", r"\b([0-9]+\.?[0-9]*[a-z%]*)\b"); hl.bounded("string", "\"", "\"", true); hl.keyword("keyword", r"\b(import|from|export|global|struct|enum|component|inherits|property|in\-out|in|out|function|private|public|callback|animate|states|transitions|if|for|return|@tr|@children|@image-url|@linear-gradient|@radial-gradient|when)\b[^:\.]"); hl.keyword("type", r"\b(int|bool|float|duration|angle|string|image|brush|color|length|physical\-length|relative\-font\-size)\b\s*[^:\.\-]"); hl.keyword("property", r"([a-zA-Z_][a-zA-Z_\-0-9]*)\s*:[^=]"); hl.keyword("property", r"([a-zA-Z_][a-zA-Z_\-0-9]*)\s*<=>"); hl.keyword("builtin_fns", r"\b(min|max|abs|round|ceil|floor|sin|cos|tan|asin|sqrt|pow|log|rgb|rgba|debug|animation\-tick|brighter|darker|mod)\b"); hl.keyword("builtin_comps", r"\b(Rectangle|Image|Window|TouchArea|VerticalLayout|Text|Path|FocusScope|VerticalLayout|HorizontalLayout|GridLayout|PathLayout|Flickable|TextInput|PopupWindow|Dialog|PopupWindow)\b"); hl.keyword( "builtin_structs", r"\b(Point|KeyEvent|KeyboardModifiers|PointerEvent|StandardListViewItem|TableColumn)\b", ); hl.keyword( "builtin_widgets", r"\b(AboutSlint|Button|CheckBox|ComboBox|GridBox|GroupBox|HorizontalBox|LineEdit|ListView|ProgressIndicator|ScrollView|Slider|SpinBox|StandardButton|StandardListView|StandardTableView|Switch|TabWidget|TextEdit|VerticalBox|TextInputInterface)\b", ); hl } fn gen_highlighted_html( out: &mut dyn Write, hl: &mut synoptic::Highlighter, file_name: &str, data_path: &PathBuf, source: &str, ) -> Result<(), std::io::Error> { let header = create_header(file_name, data_path.to_str().unwrap()); write!(out, "{header}")?; write!(out, "
\n")?; write!(out, "\n")?; write!(out, "\n")?; let lines = source.split("\n").map(|l| l.to_string()).collect(); hl.run(&lines); for (line_num, text) in lines.iter().enumerate() { write!( out, "\n")?; } write!(out, "
{}", line_num + 1, line_num + 1 )?; use synoptic::TokOpt; for token in hl.line(line_num, &text) { match token { TokOpt::Some(s, n) => match n.as_str() { "string" => write_tag(out, "q", &s, None), "comment" => write_tag(out, "i", &s, None), "boolean" | "digit" => write_tag(out, "var", &s, None), "keyword" => write_tag(out, "b", &s, None), "type" => write_tag(out, "b", &s, None), "property" => write_tag(out, "span", &s, Some("field")), "builtin_fns" => write_tag(out, "span", &s, Some("fn")), "builtin_comps" => write_tag(out, "span", &s, Some("type")), "builtin_structs" => write_tag(out, "span", &s, Some("type")), "builtin_widgets" => write_tag(out, "span", &s, Some("type")), _ => write_html_escaped_string(out, &s), }, TokOpt::None(s) => write_html_escaped_string(out, &s), }? } write!(out, "
\n")?; Ok(()) } // TODO: reuse ../html_generator.rs fn create_header(file_name: &str, data_path: &str) -> String { format!( r#" {} - Codebrowser "#, file_name ) }