pub mod codegen;
pub mod error;
pub mod format;
pub mod parser;
pub mod suggest;
use std::fs;
use std::path::{Path, PathBuf};
pub const CODEGEN_VERSION: u32 = 2;
const HASH_HEADER_PREFIX: &str = "// ruitl-hash: ";
pub use codegen::CodeGenerator;
pub use error::{CompileError, Result};
pub use parser::{
Attribute, AttributeValue, ComponentDef, ImportDef, MatchArm, ParamDef, PropDef, PropValue,
RuitlFile, RuitlParser, TemplateAst, TemplateDef,
};
pub fn parse_str(source: &str) -> Result<RuitlFile> {
RuitlParser::new(source.to_string()).parse()
}
pub fn generate(file: RuitlFile) -> Result<String> {
let mut gen = CodeGenerator::new(file);
let tokens = gen.generate()?;
Ok(format_rust(tokens.to_string()))
}
pub fn compile_file_sibling(source: &Path) -> Result<PathBuf> {
let stem = source
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| CompileError::parse(format!("invalid file name: {}", source.display())))?;
let parent = source.parent().unwrap_or_else(|| Path::new("."));
let out = parent.join(format!("{}_ruitl.rs", sanitize_stem(stem)));
compile_file(source, &out)?;
Ok(out)
}
pub fn compile_file(source: &Path, output: &Path) -> Result<()> {
let src = fs::read_to_string(source)?;
let hash = compute_hash(&src);
if output.exists() {
if let Ok(existing) = fs::read_to_string(output) {
if let Some(existing_hash) = extract_hash(&existing) {
if existing_hash == hash {
return Ok(());
}
}
}
}
let ast = parse_str(&src)?;
let code = generate(ast)?;
let final_text = format!("{}{}\n{}", HASH_HEADER_PREFIX, hash, code);
if let Some(parent) = output.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
fs::write(output, final_text)?;
Ok(())
}
fn compute_hash(source: &str) -> String {
let digest = md5::compute(format!("{}|v{}", source, CODEGEN_VERSION));
format!("{:x}", digest)
}
fn extract_hash(content: &str) -> Option<&str> {
let first_line = content.lines().next()?;
first_line.strip_prefix(HASH_HEADER_PREFIX).map(str::trim)
}
pub fn compile_dir_sibling(dir: &Path) -> Result<Vec<PathBuf>> {
if !dir.exists() {
return Ok(Vec::new());
}
let inputs: Vec<PathBuf> = walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path().is_file()
&& e.path().extension().map(|x| x == "ruitl").unwrap_or(false)
})
.map(|e| e.path().to_path_buf())
.collect();
let results: Vec<Result<PathBuf>> = {
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
inputs
.par_iter()
.map(|p| compile_file_sibling(p))
.collect()
}
#[cfg(not(feature = "parallel"))]
{
inputs.iter().map(|p| compile_file_sibling(p)).collect()
}
};
let mut outputs = Vec::with_capacity(results.len());
let mut first_err: Option<CompileError> = None;
for r in results {
match r {
Ok(p) => outputs.push(p),
Err(e) => {
if first_err.is_none() {
first_err = Some(e);
}
}
}
}
if let Some(e) = first_err {
return Err(e);
}
let mut module_stems: Vec<String> = outputs
.iter()
.filter_map(|o| o.file_stem().and_then(|s| s.to_str()).map(String::from))
.collect();
module_stems.sort();
if !module_stems.is_empty() {
write_sibling_mod_file(dir, &module_stems)?;
}
Ok(outputs)
}
fn write_sibling_mod_file(dir: &Path, stems: &[String]) -> Result<()> {
let mut sorted = stems.to_vec();
sorted.sort();
let mut content = String::from(
"// @generated by ruitl_compiler — do not edit. Regenerated on each compile.\n\n",
);
for stem in &sorted {
content.push_str(&format!("#[allow(non_snake_case)] pub mod {};\n", stem));
}
content.push('\n');
for stem in &sorted {
content.push_str(&format!(
"#[allow(unused_imports)] pub use {}::*;\n",
stem
));
}
fs::write(dir.join("mod.rs"), content)?;
Ok(())
}
fn sanitize_stem(stem: &str) -> String {
stem.to_string()
}
fn format_rust(raw: String) -> String {
use std::io::Write;
use std::process::{Command, Stdio};
let child = Command::new("rustfmt")
.args(["--edition", "2021", "--emit", "stdout"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn();
let Ok(mut child) = child else {
return raw;
};
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(raw.as_bytes());
}
match child.wait_with_output() {
Ok(out) if out.status.success() => {
String::from_utf8(out.stdout).unwrap_or(raw)
}
_ => raw,
}
}