use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{info, warn};
use crate::markdown::Block;
pub fn collect_sources(blocks: &[Block]) -> Vec<String> {
let mut sources = Vec::new();
collect_sources_inner(blocks, &mut sources);
sources.sort();
sources.dedup();
sources
}
fn collect_sources_inner(blocks: &[Block], out: &mut Vec<String>) {
for block in blocks {
match block {
Block::LiveComponent { source } => out.push(source.clone()),
Block::Blockquote(inner) => collect_sources_inner(inner, out),
Block::Center(inner) => collect_sources_inner(inner, out),
Block::Div { children, .. } => collect_sources_inner(children, out),
_ => {}
}
}
}
pub fn compile_all(sources: &[String], work_dir: &Path) -> HashMap<String, String> {
let mut result = HashMap::new();
if sources.is_empty() {
return result;
}
let live_dir = work_dir.join("live-blocks");
if live_dir.exists() {
std::fs::remove_dir_all(&live_dir).ok();
}
std::fs::create_dir_all(&live_dir).ok();
let crate_dir = live_dir.join("workspace");
std::fs::create_dir_all(crate_dir.join("src")).ok();
let entries: Vec<(String, String)> = sources
.iter()
.map(|s| {
let hash = short_hash(s);
(s.clone(), hash)
})
.collect();
for (source, hash) in &entries {
let file_path = crate_dir.join("src").join(format!("{hash}.rs"));
std::fs::write(&file_path, render_bin_rs(source)).ok();
}
std::fs::write(crate_dir.join("Cargo.toml"), render_cargo_toml(&entries)).ok();
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let build = Command::new(&cargo)
.args(["build", "--release", "--quiet"])
.env("RUSTC_WRAPPER", "")
.env("CARGO_BUILD_RUSTC_WRAPPER", "")
.current_dir(&crate_dir)
.output();
let build = match build {
Ok(o) => o,
Err(e) => {
warn!("live block cargo invocation failed: {e}");
return result;
}
};
if !build.status.success() {
let stderr = String::from_utf8_lossy(&build.stderr);
warn!(
"live block workspace build failed ({} snippets): {}",
entries.len(),
stderr_tail(&stderr, 800)
);
return result;
}
let target_dir = crate_dir.join("target");
for (source, hash) in &entries {
let bin = find_binary(&target_dir, hash);
match bin {
Some(path) => {
let run = Command::new(&path).output();
match run {
Ok(r) if r.status.success() => {
let html = String::from_utf8_lossy(&r.stdout).to_string();
result.insert(source.clone(), html.trim().to_string());
}
Ok(r) => {
let stderr = String::from_utf8_lossy(&r.stderr);
warn!(
"live block execution failed for {hash}: {}",
stderr_tail(&stderr, 300)
);
}
Err(e) => {
warn!("live block execution error for {hash}: {e}");
}
}
}
None => {
warn!("live block binary not found for {hash}");
}
}
}
info!(
"compiled {} of {} live component block(s)",
result.len(),
sources.len()
);
result
}
fn render_cargo_toml(entries: &[(String, String)]) -> String {
let mut bins = String::new();
for (_, hash) in entries {
bins.push_str(&format!(
"[[bin]]\nname = \"live-block-{hash}\"\npath = \"src/{hash}.rs\"\n\n"
));
}
format!(
r#"[package]
name = "live-block-workspace"
version = "0.0.0"
edition = "2021"
publish = false
# Standalone workspace so cargo doesn't try to merge this into lagrange's
# workspace (the temp crate lives under lagrange's output dir).
[workspace]
{bins}[dependencies]
tairitsu-vdom = "0.5"
tairitsu-hooks = "0.5"
tairitsu-macros = "0.5"
hikari-components = "0.3"
"#
)
}
fn render_bin_rs(source: &str) -> String {
let trimmed = source.trim();
let expr = if trimmed.starts_with("rsx")
|| trimmed.starts_with("Button")
|| trimmed.starts_with("Card")
{
trimmed.to_string()
} else {
format!("rsx! {{ {trimmed} }}")
};
format!(
r#"use tairitsu_vdom::VNode;
use hikari_components::prelude::*;
fn main() {{
let vnode: VNode = {expr};
let html = vnode.render_to_html();
print!("{{}}", html);
}}
"#
)
}
fn short_hash(s: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
s.hash(&mut h);
format!("lb{:016x}", h.finish())
}
fn find_binary(target_dir: &Path, hash: &str) -> Option<PathBuf> {
let exe_name = if cfg!(windows) {
format!("live-block-{hash}.exe")
} else {
format!("live-block-{hash}")
};
for sub in &["release", "debug"] {
let path = target_dir.join(sub).join(&exe_name);
if path.exists() {
return Some(path);
}
}
None
}
fn stderr_tail(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
format!("...{}", &s[s.len() - max..])
}
}