#![doc = include_str!("../README.md")]
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
use std::{fmt::Write, sync::LazyLock};
use tsrun::{Interpreter as JsRuntime, JsValue, api as js_api};
#[derive(Debug)]
pub enum Error {
InvalidSource(PathBuf),
IoError(std::io::Error),
ParseError { path: PathBuf, message: String },
UnresolvedDependency { path: PathBuf, dependency: String },
TransformError { message: String },
CodegenError(std::fmt::Error),
ConfigError { message: String },
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidSource(path) => {
write!(
f,
"Source path does not exist or is not a directory: {}",
path.display()
)
}
Error::IoError(e) => write!(f, "IO error: {e}"),
Error::ParseError { path, message } => {
write!(f, "Parse error in {}: {message}", path.display())
}
Error::UnresolvedDependency { path, dependency } => {
write!(
f,
"Unresolved dependency in {}: {dependency}",
path.display()
)
}
Error::TransformError { message } => write!(f, "Transform error: {message}"),
Error::CodegenError(e) => write!(f, "Code generation error: {e}"),
Error::ConfigError { message } => write!(f, "Config error: {message}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::IoError(e) => Some(e),
Error::CodegenError(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::IoError(e)
}
}
impl From<std::fmt::Error> for Error {
fn from(e: std::fmt::Error) -> Self {
Error::CodegenError(e)
}
}
pub struct Builder {
source: PathBuf,
output: PathBuf,
transform_script: Option<PathBuf>,
escape_func: Option<String>,
rerun_if_changed: bool,
}
impl Builder {
pub fn new(source: impl Into<PathBuf>, output: impl Into<PathBuf>) -> Self {
Self {
source: source.into(),
output: output.into(),
transform_script: None,
escape_func: None,
rerun_if_changed: false,
}
}
pub fn transform_script(mut self, path: impl Into<PathBuf>) -> Self {
self.transform_script = Some(path.into());
self
}
pub fn transform_script_option(mut self, path: Option<&str>) -> Self {
self.transform_script = path.map(PathBuf::from);
self
}
pub fn escape_func(mut self, func: impl Into<String>) -> Self {
self.escape_func = Some(func.into());
self
}
pub fn escape_func_option(mut self, func: Option<&str>) -> Self {
self.escape_func = func.map(String::from);
self
}
pub fn rerun_if_changed(mut self, enabled: bool) -> Self {
self.rerun_if_changed = enabled;
self
}
pub fn build(self) -> Result<(), Error> {
let source_path = &self.source;
if !source_path.is_dir() {
return Err(Error::InvalidSource(source_path.clone()));
}
let transform_script_str = self
.transform_script
.as_ref()
.map(|p| p.to_string_lossy().to_string());
let generated = process_directory(
source_path,
transform_script_str.as_deref(),
self.escape_func.as_deref(),
)?;
write_output(&self.output.to_string_lossy(), &generated)?;
println!("Output: {}", self.output.display());
if self.rerun_if_changed {
let template_files = collect_template_files(source_path);
for file in &template_files {
println!("cargo:rerun-if-changed={}", file.display());
}
if let Some(ref ts) = self.transform_script {
println!("cargo:rerun-if-changed={}", ts.display());
}
}
Ok(())
}
}
#[doc(hidden)]
pub fn build_from_config(path: impl Into<PathBuf>, rerun_if_changed: bool) -> Result<(), Error> {
let config_path = path.into();
let configs = evaluate_config_file(&config_path, None, None, None, None)?;
if configs.is_empty() {
return Ok(());
}
for config in &configs {
let mut builder = Builder::new(&config.source, &config.output);
if let Some(ref ts) = config.transform_script {
builder = builder.transform_script(ts);
}
if let Some(ref ef) = config.escape_func {
builder = builder.escape_func(ef);
}
builder = builder.rerun_if_changed(rerun_if_changed);
builder.build()?;
}
if rerun_if_changed {
println!("cargo:rerun-if-changed={}", config_path.display());
}
Ok(())
}
#[doc(hidden)]
pub fn build_from_config_auto(rerun_if_changed: bool) -> Result<(), Error> {
let config_path = discover_config_file(None).ok_or_else(|| Error::ConfigError {
message: "No formefile.ts or formefile.js found in current directory".to_string(),
})?;
build_from_config(config_path, rerun_if_changed)
}
#[doc(hidden)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormeConfig {
pub source: String,
pub output: String,
pub transform_script: Option<String>,
pub escape_func: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
struct FormeContext {
cwd: String,
env: HashMap<String, String>,
cli: FormeContextCli,
}
#[derive(Debug, Clone, Serialize)]
struct FormeContextCli {
source: Option<String>,
output: Option<String>,
transform_script: Option<String>,
escape_func: Option<String>,
}
#[doc(hidden)]
pub fn discover_config_file(explicit_path: Option<&str>) -> Option<PathBuf> {
if let Some(path) = explicit_path {
let p = PathBuf::from(path);
if p.exists() {
return Some(p);
}
return None;
}
let formefile_ts = PathBuf::from("formefile.ts");
if formefile_ts.exists() {
return Some(formefile_ts);
}
let formefile_js = PathBuf::from("formefile.js");
if formefile_js.exists() {
return Some(formefile_js);
}
None
}
#[doc(hidden)]
pub fn evaluate_config_file(
config_path: &Path,
cli_source: Option<&str>,
cli_output: Option<&str>,
cli_transform_script: Option<&str>,
cli_escape_func: Option<&str>,
) -> Result<Vec<FormeConfig>, Error> {
let code = fs::read_to_string(config_path)?;
let mut js_runtime = JsRuntime::new();
let virtual_path = config_path.to_string_lossy().to_string();
if let Err(e) = js_runtime.prepare(&code, Some(virtual_path.into())) {
return Err(Error::ConfigError {
message: format!("Failed to prepare config file: {e}"),
});
}
loop {
match js_runtime.step() {
Ok(tsrun::StepResult::Continue) => continue,
Ok(tsrun::StepResult::Complete(_)) => break,
Ok(tsrun::StepResult::Done) => break,
Ok(tsrun::StepResult::NeedImports(imports)) => {
return Err(Error::ConfigError {
message: format!(
"Config file requires unsupported imports: {:?}",
imports.iter().map(|i| &i.specifier).collect::<Vec<_>>()
),
});
}
Ok(tsrun::StepResult::Suspended { .. }) => {
return Err(Error::ConfigError {
message: "Config file unexpectedly suspended".to_string(),
});
}
Err(e) => {
return Err(Error::ConfigError {
message: format!("Config file error: {e}"),
});
}
}
}
let config_export =
js_api::get_export(&js_runtime, "config").ok_or_else(|| Error::ConfigError {
message: "Config file must export a 'config' function".to_string(),
})?;
let cwd = std::env::current_dir()
.map_err(|e| Error::ConfigError {
message: format!("Failed to get current directory: {e}"),
})?
.to_string_lossy()
.to_string();
let env: HashMap<String, String> = std::env::vars().collect();
let ctx = FormeContext {
cwd,
env,
cli: FormeContextCli {
source: cli_source.map(|s| s.to_string()),
output: cli_output.map(|s| s.to_string()),
transform_script: cli_transform_script.map(|s| s.to_string()),
escape_func: cli_escape_func.map(|s| s.to_string()),
},
};
let guard = js_api::create_guard(&js_runtime);
let ctx_json = serde_json::to_value(&ctx).map_err(|e| Error::ConfigError {
message: format!("Failed to serialize context: {e}"),
})?;
let ctx_js = js_api::create_from_json(&mut js_runtime, &guard, &ctx_json).map_err(|e| {
Error::ConfigError {
message: format!("Failed to create context object: {e}"),
}
})?;
let result_value =
js_api::call_function(&mut js_runtime, &guard, &config_export, None, &[ctx_js]).map_err(
|e| Error::ConfigError {
message: format!("Failed to call config function: {e}"),
},
)?;
let result_json = tsrun::js_value_to_json(&result_value).map_err(|e| Error::ConfigError {
message: format!("Failed to convert config result to JSON: {e}"),
})?;
let configs = if let Ok(configs) =
serde_json::from_value::<Vec<FormeConfig>>(result_json.clone())
{
configs
} else {
let config =
serde_json::from_value::<FormeConfig>(result_json).map_err(|e| Error::ConfigError {
message: format!("Failed to parse config result: {e}"),
})?;
vec![config]
};
let merged: Vec<FormeConfig> = configs
.iter()
.map(|c| {
merge_config_with_cli(
c,
cli_source,
cli_output,
cli_transform_script,
cli_escape_func,
)
})
.collect();
Ok(merged)
}
fn merge_config_with_cli(
config: &FormeConfig,
cli_source: Option<&str>,
cli_output: Option<&str>,
cli_transform_script: Option<&str>,
cli_escape_func: Option<&str>,
) -> FormeConfig {
FormeConfig {
source: cli_source
.map(|s| s.to_string())
.unwrap_or_else(|| config.source.clone()),
output: cli_output
.map(|s| s.to_string())
.unwrap_or_else(|| config.output.clone()),
transform_script: cli_transform_script
.map(|s| s.to_string())
.or_else(|| config.transform_script.clone()),
escape_func: cli_escape_func
.map(|s| s.to_string())
.or_else(|| config.escape_func.clone()),
}
}
pub fn html_escape(out: &mut impl std::fmt::Write, input: &str) -> std::fmt::Result {
write!(
out,
"{}",
input
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
)
}
#[doc(hidden)]
pub fn resolve_relative_path(base_path: &str, rel_path: &str) -> String {
if rel_path.starts_with(['/', '\\']) {
return rel_path.trim_start_matches(['/', '\\']).to_string();
}
let base = Path::new(base_path);
let base_dir = base.parent().unwrap_or_else(|| Path::new(""));
let joined = base_dir.join(rel_path);
let mut components = Vec::new();
for component in joined.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::CurDir => {
}
_ => {
components.push(component);
}
}
}
let mut result = PathBuf::new();
for component in components {
result.push(component);
}
result.to_string_lossy().to_string()
}
const INDENT: &str = "\t";
const HTML_VOID_ELEMENTS: [&str; 16] = [
"area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link",
"meta", "param", "source", "track", "wbr",
];
#[derive(Debug, Clone)]
struct Template {
prefix: String,
doctype: Option<String>,
args: Vec<TemplateArg>,
nodes: Vec<Node>,
}
#[derive(Debug, Clone)]
struct TemplateArg {
name: String,
ty: String,
default: Option<String>,
}
#[derive(Debug, Clone)]
enum Node {
FormeElement(FormeElement),
Element(Element),
Text(String),
Expr(Expr),
}
#[derive(Debug, Clone)]
struct FormeElement {
name: String,
attributes: AttributesList,
#[allow(dead_code)]
children: Vec<Node>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct JsElement {
name: String,
attributes: Vec<JsAttr>,
is_void: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct JsConditions {
ty: String,
expr: String,
list: Vec<JsConditions>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct JsAttr {
name: String,
conditions: JsConditions,
values: Vec<JsAttrValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct JsAttrValue {
ty: String,
conditions: JsConditions,
content: String,
}
#[derive(Debug, Clone)]
enum Conditions {
Empty,
Expr(String),
And(Vec<Conditions>),
Or(Vec<Conditions>),
}
impl Conditions {
fn empty() -> Self {
Self::Empty
}
fn single(condition: String) -> Self {
Self::Expr(condition)
}
fn is_empty(&self) -> bool {
matches!(self, Conditions::Empty)
}
fn to_rust_code(&self) -> String {
match self {
Conditions::Empty => String::new(),
Conditions::Expr(expr) => expr.clone(),
Conditions::And(conditions) => {
let parts: Vec<String> = conditions
.iter()
.map(|c| {
let code = c.to_rust_code();
if matches!(c, Conditions::Or(_)) {
format!("({})", code)
} else {
code
}
})
.collect();
parts.join(" && ")
}
Conditions::Or(conditions) => {
let parts: Vec<String> = conditions.iter().map(|c| c.to_rust_code()).collect();
parts.join(" || ")
}
}
}
}
impl From<Conditions> for JsConditions {
fn from(conditions: Conditions) -> Self {
match conditions {
Conditions::Empty => JsConditions {
ty: "Empty".to_string(),
expr: String::new(),
list: vec![],
},
Conditions::Expr(expr) => JsConditions {
ty: "Expr".to_string(),
expr,
list: vec![],
},
Conditions::And(conds) => JsConditions {
ty: "And".to_string(),
expr: String::new(),
list: conds.into_iter().map(|c| c.into()).collect(),
},
Conditions::Or(conds) => JsConditions {
ty: "Or".to_string(),
expr: String::new(),
list: conds.into_iter().map(|c| c.into()).collect(),
},
}
}
}
impl TryFrom<JsConditions> for Conditions {
type Error = String;
fn try_from(value: JsConditions) -> Result<Self, Self::Error> {
match value.ty.as_str() {
"Empty" => Ok(Conditions::Empty),
"Expr" => {
if value.expr.is_empty() {
return Err("Expr type must have non-empty expr field".to_string());
}
Ok(Conditions::Expr(value.expr))
}
"And" => {
let parsed: Result<Vec<Conditions>, String> = value
.list
.into_iter()
.map(|js_cond| js_cond.try_into())
.collect();
Ok(Conditions::And(parsed?))
}
"Or" => {
let parsed: Result<Vec<Conditions>, String> = value
.list
.into_iter()
.map(|js_cond| js_cond.try_into())
.collect();
Ok(Conditions::Or(parsed?))
}
_ => Err(format!("Unknown ConditionType: {}", value.ty)),
}
}
}
#[derive(Debug, Clone)]
struct Element {
name: String,
forme_attributes: Vec<(String, Option<String>)>,
attributes: AttributesList,
is_void: bool,
children: Vec<Node>,
}
#[derive(Debug, Clone)]
struct AttributesList {
list: Vec<Attr>,
}
impl AttributesList {
fn new() -> Self {
Self { list: vec![] }
}
fn add(&mut self, name: &str, value: Option<AttrValue>, conditions: Conditions) {
match self.list.iter().position(|exst| exst.name == name) {
Some(index) => {
let Some(attr) = self.list.get_mut(index) else {
unreachable!()
};
match value {
Some(new_value) => {
attr.conditions = conditions;
attr.values.push(new_value);
}
None => {
attr.conditions = conditions;
}
}
}
None => match value {
Some(value) => self.list.push(Attr {
name: name.to_string(),
conditions,
values: vec![value],
}),
None => self.list.push(Attr {
name: name.to_string(),
conditions,
values: vec![],
}),
},
}
}
}
#[derive(Debug, Clone)]
struct Attr {
name: String,
conditions: Conditions,
values: Vec<AttrValue>,
}
#[derive(Debug, Clone)]
struct AttrValue {
conditions: Conditions,
ty: AttrValueType,
content: String,
}
#[derive(Debug, Clone)]
enum AttrValueType {
Text,
Expr,
}
impl std::fmt::Display for AttrValueType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AttrValueType::Text => write!(f, "Text"),
AttrValueType::Expr => write!(f, "Expr"),
}
}
}
impl TryFrom<JsAttrValue> for AttrValue {
type Error = String;
fn try_from(value: JsAttrValue) -> Result<Self, Self::Error> {
let ty = match value.ty.as_str() {
"Text" => AttrValueType::Text,
"Expr" => AttrValueType::Expr,
_ => return Err(format!("Unknown AttrValueType: {}", value.ty)),
};
Ok(AttrValue {
conditions: value.conditions.try_into()?,
ty,
content: value.content,
})
}
}
#[derive(Debug, Clone)]
enum Expr {
Condition(Condition),
Repeat(Repeat),
Text(TextExpr),
Raw(RawExpr),
Render(Render),
}
#[derive(Debug, Clone)]
struct Condition {
condition: String,
children: Vec<Node>,
}
#[derive(Debug, Clone)]
struct Repeat {
repeat: String,
children: Vec<Node>,
}
#[derive(Debug, Clone)]
struct TextExpr(String);
#[derive(Debug, Clone)]
struct RawExpr(String);
#[derive(Debug, Clone)]
struct Render {
template: String,
args: Vec<RenderArg>,
}
#[derive(Debug, Clone)]
struct RenderArg {
name: String,
value: RenderArgValue,
}
#[derive(Debug, Clone)]
enum RenderArgValue {
Expr(String),
SlotItem(Vec<Node>),
SlotItems(Vec<Node>),
}
fn process_directory(
source_path: &Path,
transform_script: Option<&str>,
escape_func: Option<&str>,
) -> Result<String, Error> {
println!("Processing directory: {}", source_path.display());
let template_files = collect_template_files(source_path);
println!("Found {} template file(s)", template_files.len());
let escape_func = escape_func.unwrap_or("forme::html_escape");
let mut processor = TemplateProcessor::new(source_path, transform_script, escape_func)?;
for template_file in template_files {
processor.queue_template(template_file);
}
let output = processor.process_all()?;
println!("All templates parsed successfully!");
Ok(output)
}
fn write_output(output_path: &str, content: &str) -> Result<(), Error> {
let content = format!(
"#![cfg_attr(rustfmt, rustfmt::skip)]\n#![allow(clippy::needless_borrow)]\n\n{content}"
);
fs::write(output_path, content)?;
Ok(())
}
fn collect_template_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension()
&& (ext == "html" || ext == "htm")
{
files.push(path);
}
} else if path.is_dir() {
files.extend(collect_template_files(&path));
}
}
}
files.sort();
files
}
struct TemplateProcessor<'a> {
source_path: &'a Path,
queue: VecDeque<PathBuf>,
delayed: VecDeque<(PathBuf, Template, Vec<String>)>,
registry: HashMap<String, Vec<TemplateArg>>,
completed: HashSet<String>,
output: String,
nodes_output: Vec<String>,
js_code: Option<String>,
escape_func: String,
}
impl<'a> TemplateProcessor<'a> {
fn new(
source_path: &'a Path,
transform_script: Option<&str>,
escape_func: &str,
) -> Result<Self, Error> {
let js_code = if let Some(script_path) = transform_script {
Some(fs::read_to_string(script_path)?)
} else {
None
};
Ok(Self {
source_path,
queue: VecDeque::new(),
delayed: VecDeque::new(),
registry: HashMap::new(),
completed: HashSet::new(),
output: String::new(),
nodes_output: Vec::new(),
js_code,
escape_func: escape_func.to_string(),
})
}
fn queue_template(&mut self, template_file: PathBuf) {
self.queue.push_back(template_file);
}
fn process_all(&mut self) -> Result<String, Error> {
while !self.queue.is_empty() || !self.delayed.is_empty() {
self.process_main_queue()?;
self.process_delayed_queue()?;
}
Ok(self.output.clone())
}
fn process_main_queue(&mut self) -> Result<(), Error> {
while let Some(template_file) = self.queue.pop_front() {
self.process_template_file(template_file)?;
}
Ok(())
}
fn process_template_file(&mut self, template_file: PathBuf) -> Result<(), Error> {
let source = fs::read_to_string(&template_file)?;
let mut template = parse_template(&source);
extract_template_args(&mut template);
process_template_directives(&mut template);
if let Some(js_code) = &self.js_code {
let mut js_runtime = JsRuntime::new();
if let Err(e) = js_runtime.prepare(js_code, Some("/main.ts".into())) {
panic!("Failed to prepare JS code: {e}");
}
loop {
match js_runtime.step() {
Ok(tsrun::StepResult::Continue) => continue,
Ok(tsrun::StepResult::Complete(_)) => break,
Ok(tsrun::StepResult::Done) => break,
Ok(tsrun::StepResult::NeedImports(imports)) => {
panic!(
"Unexpected imports needed: {:?}",
imports.iter().map(|i| &i.specifier).collect::<Vec<_>>()
);
}
Ok(tsrun::StepResult::Suspended { .. }) => {
panic!("Unexpected suspension");
}
Err(e) => {
panic!("Uncaught {e}");
}
}
}
let js_processor =
js_api::get_export(&js_runtime, "processor").expect("processor should be exported");
for node in &mut template.nodes {
process_js_node_directives(node, &js_processor, &mut js_runtime);
}
}
let template_name = normalize_template_path(
&self.source_path.to_string_lossy(),
&template_file.to_string_lossy(),
);
self.registry
.insert(template_name.clone(), template.args.clone());
let dependencies = extract_dependencies(&template_name, &template);
if self.all_dependencies_met(&dependencies) {
self.nodes_output.push(format!("{template:#?}"));
self.generate_and_add_code(&template_name, &template)?;
self.completed.insert(template_name);
println!("Processed: {}", template_file.display());
} else {
self.delayed
.push_back((template_file, template, dependencies));
}
Ok(())
}
fn process_delayed_queue(&mut self) -> Result<(), Error> {
if self.delayed.is_empty() {
return Ok(());
}
let delayed_count_before = self.delayed.len();
let mut retry_queue = VecDeque::new();
while let Some((template_file, template, dependencies)) = self.delayed.pop_front() {
let template_name = normalize_template_path(
&self.source_path.to_string_lossy(),
&template_file.to_string_lossy(),
);
if self.all_dependencies_met(&dependencies) {
self.generate_and_add_code(&template_name, &template)?;
self.registry
.insert(template_name.clone(), template.args.clone());
self.completed.insert(template_name);
println!("Processed: {}", template_file.display());
} else {
retry_queue.push_back((template_file, template, dependencies));
}
}
if !retry_queue.is_empty() && retry_queue.len() == delayed_count_before {
return Err(self.create_dependency_error(&retry_queue));
}
self.delayed = retry_queue;
Ok(())
}
fn all_dependencies_met(&self, dependencies: &[String]) -> bool {
dependencies
.iter()
.all(|dep| self.registry.contains_key(dep))
}
fn generate_and_add_code(
&mut self,
template_name: &str,
template: &Template,
) -> Result<(), Error> {
let code = generate_code(template_name, template, &self.registry, &self.escape_func)?;
self.output.push_str(&code);
self.output.push_str("\n\n");
Ok(())
}
fn create_dependency_error(
&self,
retry_queue: &VecDeque<(PathBuf, Template, Vec<String>)>,
) -> Error {
let mut error_msg = String::from(
"\nUnable to resolve template dependencies!\n\nTemplates that could not be processed:\n",
);
for (template_file, _template, dependencies) in retry_queue {
let template_name = resolve_relative_path(
&self.source_path.to_string_lossy(),
&template_file.to_string_lossy(),
);
error_msg.push_str(&format!(" - {}\n", template_name));
let missing_deps: Vec<&String> = dependencies
.iter()
.filter(|dep| !self.completed.contains(*dep))
.collect();
if !missing_deps.is_empty() {
error_msg.push_str(&format!(
" Missing dependencies: {}\n",
missing_deps
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
));
}
}
Error::ConfigError { message: error_msg }
}
}
fn normalize_template_path(base_dir: &str, template_path: &str) -> String {
template_path
.strip_prefix(base_dir)
.unwrap_or(template_path)
.trim_start_matches(['/', '\\', '.'])
.to_string()
}
fn generate_function_name(path_str: &str) -> String {
let without_ext = path_str
.rsplit_once('.')
.map(|(base, _)| base)
.unwrap_or(path_str);
let sanitized: String = without_ext
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect();
let sanitized = if sanitized.chars().next().is_some_and(|c| c.is_numeric()) {
format!("_{}", sanitized)
} else {
sanitized
};
let sanitized = sanitized.trim_start_matches("_");
format!("render_{}", sanitized)
}
fn safe_format_literal(text: &str) -> String {
let mut result = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\\' => result.push_str(r"\\"),
'"' => result.push_str(r#"\""#),
'\n' => result.push_str(r"\n"),
'\r' => result.push_str(r"\r"),
'\t' => result.push_str(r"\t"),
'\0' => result.push_str(r"\0"),
'{' => result.push_str("{{"),
'}' => result.push_str("}}"),
_ => result.push(ch),
}
}
result
}
fn parse_template(source: &str) -> Template {
let mut template = Template {
prefix: "tpl".to_string(),
doctype: None,
args: vec![],
nodes: vec![],
};
let dom = tl::parse(source, tl::ParserOptions::default()).unwrap();
template.doctype = dom.version().map(|version| match version {
tl::HTMLVersion::HTML5 => "<!DOCTYPE html>".to_string(),
tl::HTMLVersion::StrictHTML401 => "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">".to_string(),
tl::HTMLVersion::TransitionalHTML401 => "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">".to_string(),
tl::HTMLVersion::FramesetHTML401 => "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Frameset//EN\" \"http://www.w3.org/TR/html4/frameset.dtd\">".to_string(),
});
for node_handle in dom.children() {
add_node(
&template.prefix,
&mut template.nodes,
node_handle,
dom.parser(),
);
}
template
}
fn add_node(
prefix: &str,
nodes: &mut Vec<Node>,
node_handle: &tl::NodeHandle,
parser: &tl::Parser<'_>,
) {
let node = node_handle.get(parser).unwrap();
match node {
tl::Node::Tag(htmltag) => {
let name = htmltag.name().as_utf8_str().to_string();
let mut forme_attributes = vec![];
let mut attributes = AttributesList::new();
let is_void = HTML_VOID_ELEMENTS.contains(&name.as_ref());
let mut children = vec![];
for (attr_name, attr_value) in htmltag.attributes().iter() {
if attr_name.starts_with(&format!("{prefix}-")) {
forme_attributes.push((
attr_name
.strip_prefix(&format!("{prefix}-"))
.unwrap()
.to_string(),
attr_value.map(|v| v.to_string()),
));
} else {
attributes.add(
&attr_name,
attr_value.map(|v| AttrValue {
ty: AttrValueType::Text,
conditions: Conditions::empty(),
content: v.to_string(),
}),
Conditions::empty(),
);
}
}
if !is_void {
for child in htmltag.children().top().iter() {
add_node(prefix, &mut children, child, parser);
}
}
if name.starts_with(&format!("{prefix}-")) {
let name = name
.strip_prefix(&format!("{prefix}-"))
.unwrap()
.to_string();
nodes.push(Node::FormeElement(FormeElement {
name,
attributes,
children,
}));
} else {
nodes.push(Node::Element(Element {
name,
forme_attributes,
attributes,
is_void,
children,
}));
}
}
tl::Node::Raw(bytes) => nodes.push(Node::Text(bytes.as_utf8_str().to_string())),
tl::Node::Comment(_bytes) => (),
}
}
fn extract_template_args(template: &mut Template) {
let args = template
.nodes
.iter()
.filter_map(|node| match node {
Node::FormeElement(tpl_element) => {
if &tpl_element.name == "arg" {
let mut name = None;
let mut ty = None;
let mut default = None;
for Attr {
name: attr_name,
conditions: _,
values,
} in &tpl_element.attributes.list
{
if attr_name == "name" {
for value in values {
if let AttrValue {
ty: AttrValueType::Text,
content,
conditions: _,
} = value
{
name = Some(content.clone());
break;
}
}
} else if attr_name == "type" {
for value in values {
if let AttrValue {
ty: AttrValueType::Text,
content,
conditions: _,
} = value
{
ty = Some(content.clone());
break;
}
}
} else if attr_name == "default" {
for value in values {
if let AttrValue {
ty: AttrValueType::Text,
content,
conditions: _,
} = value
{
default = Some(content.clone());
break;
}
}
}
}
match (name, ty, default) {
(Some(name), Some(ty), default) => Some(TemplateArg { name, ty, default }),
_ => None,
}
} else {
None
}
}
Node::Element(_) => None,
Node::Text(_) => None,
Node::Expr(_) => None,
})
.collect();
template.args = args;
}
fn process_template_directives(template: &mut Template) {
for node in &mut template.nodes {
process_node_directives(node);
}
}
fn process_node_directives(node: &mut Node) {
process_repeat_directive(node);
process_if_directive(node);
process_text_directive(node);
process_html_directive(node);
process_outer_html_directive(node);
process_attr_directive(node);
process_optional_attr_directive(node);
process_template_directive(node);
process_include_directive(node);
match node {
Node::Element(element) => {
for child in &mut element.children {
process_node_directives(child);
}
}
Node::Expr(expr) => match expr {
Expr::Condition(condition) => {
for child in &mut condition.children {
process_node_directives(child);
}
}
Expr::Repeat(repeat) => {
for child in &mut repeat.children {
process_node_directives(child);
}
}
Expr::Text(_) => (),
Expr::Raw(_) => (),
Expr::Render(Render { template: _, args }) => {
for arg in args {
match &mut arg.value {
RenderArgValue::Expr(_) => (),
RenderArgValue::SlotItem(nodes) => {
for node in nodes {
process_node_directives(node);
}
}
RenderArgValue::SlotItems(templates) => {
for template in templates {
process_node_directives(template);
}
}
}
}
}
},
Node::FormeElement(_) => (),
Node::Text(_) => (),
}
}
fn process_if_directive(node: &mut Node) {
let Some((_, Some(condition))) = extract_directive(node, "if", false) else {
return;
};
*node = Node::Expr(Expr::Condition(Condition {
condition,
children: vec![node.clone()],
}));
}
fn process_repeat_directive(node: &mut Node) {
let Some((_, Some(repeat))) = extract_directive(node, "repeat", false) else {
return;
};
*node = Node::Expr(Expr::Repeat(Repeat {
repeat,
children: vec![node.clone()],
}));
}
fn process_text_directive(node: &mut Node) {
let Some((_, Some(text))) = extract_directive(node, "text", false) else {
return;
};
match node {
Node::Element(element) => {
let text_node = Node::Expr(Expr::Text(TextExpr(text)));
element.children = vec![text_node];
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
}
fn process_html_directive(node: &mut Node) {
let Some((_, Some(raw))) = extract_directive(node, "html", false) else {
return;
};
match node {
Node::Element(element) => {
let raw_node = Node::Expr(Expr::Raw(RawExpr(raw)));
element.children = vec![raw_node];
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
}
fn process_outer_html_directive(node: &mut Node) {
let Some((_, Some(raw))) = extract_directive(node, "outer-html", false) else {
return;
};
let new_node = match node {
Node::Element(_) => Node::Expr(Expr::Raw(RawExpr(raw))),
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
};
*node = new_node;
}
fn process_attr_directive(node: &mut Node) {
while let Some((attr_name, Some(attr_value))) = extract_directive(node, "attr:", true) {
static EXPRS_LIST_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\n\s*-").unwrap());
let exprs: Vec<&str> = EXPRS_LIST_RE
.split(&attr_value)
.filter_map(|expr| {
let trimmed = expr.trim();
let res = match trimmed.strip_prefix("-") {
Some(e) => e.trim(),
None => trimmed,
};
if res.is_empty() { None } else { Some(res) }
})
.collect();
match node {
Node::Element(element) => {
if let Some(attr_name) = attr_name.strip_prefix("attr:") {
for expr in exprs {
element.attributes.add(
attr_name,
Some(AttrValue {
ty: AttrValueType::Expr,
content: expr.to_string(),
conditions: Conditions::empty(),
}),
Conditions::empty(),
);
}
} else {
unreachable!();
}
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
}
}
fn process_optional_attr_directive(node: &mut Node) {
while let Some((attr_name, Some(condition))) = extract_directive(node, "optional-attr:", true) {
match node {
Node::Element(element) => {
if let Some(attr_name) = attr_name.strip_prefix("optional-attr:") {
element
.attributes
.add(attr_name, None, Conditions::single(condition));
} else {
unreachable!();
}
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
}
}
fn process_template_directive(node: &mut Node) {
let Some((_attr_name, Some(attr_value))) = extract_directive(node, "template", false) else {
return;
};
let template = attr_value;
let args = extract_render_args(node);
match node {
Node::Element(element) => {
element.children = vec![Node::Expr(Expr::Render(Render { template, args }))];
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
}
fn process_include_directive(node: &mut Node) {
let Some((_attr_name, Some(attr_value))) = extract_directive(node, "include", false) else {
return;
};
let template = attr_value;
let args = extract_render_args(node);
let new_node = Node::Expr(Expr::Render(Render { template, args }));
*node = new_node;
}
fn extract_render_args(node: &mut Node) -> Vec<RenderArg> {
let mut args = Vec::new();
match node {
Node::Element(Element {
name: _,
forme_attributes,
attributes: _,
is_void: _,
children,
}) => {
let mut to_remove = Vec::new();
for (i, (attr_name, attr_value)) in forme_attributes.iter().enumerate() {
if let Some(arg_name) = attr_name.strip_prefix("arg:")
&& let Some(value) = attr_value
{
args.push(RenderArg {
name: arg_name.to_string(),
value: RenderArgValue::Expr(value.clone()),
});
to_remove.push(i);
}
}
for i in to_remove.iter().rev() {
forme_attributes.remove(*i);
}
{
fn is_template_arg(node: &Node) -> bool {
match node {
Node::Element(element) => element
.forme_attributes
.iter()
.any(|(name, _)| name.starts_with("slot:")),
Node::FormeElement(_) => false,
Node::Text(_) => false,
Node::Expr(_) => false,
}
}
while let Some(child_arg_index) = children.iter().position(is_template_arg) {
let child = children.remove(child_arg_index);
match child {
Node::Element(element) => {
let arg = element
.forme_attributes
.iter()
.find(|(name, _)| name.starts_with("slot:"));
let Some((arg_name, _)) = arg else {
unreachable!()
};
let Some(arg_name) = arg_name.strip_prefix("slot:") else {
unreachable!()
};
args.push(RenderArg {
name: arg_name.to_string(),
value: RenderArgValue::SlotItem(element.children),
})
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
}
}
{
fn is_template_arg(node: &Node) -> bool {
match node {
Node::Element(element) => element
.forme_attributes
.iter()
.any(|(name, _)| name.starts_with("slot-items:")),
Node::FormeElement(_) => false,
Node::Text(_) => false,
Node::Expr(_) => false,
}
}
while let Some(child_arg_index) = children.iter().position(is_template_arg) {
let child = children.remove(child_arg_index);
match child {
Node::Element(element) => {
let arg = element
.forme_attributes
.iter()
.find(|(name, _)| name.starts_with("slot-items:"));
let Some((arg_name, _)) = arg else {
unreachable!()
};
let Some(arg_name) = arg_name.strip_prefix("slot-items:") else {
unreachable!()
};
let templates = element
.children
.iter()
.filter(|node| match node {
Node::Element(_) => true,
Node::Expr(_) => true,
Node::FormeElement(_) => false,
Node::Text(_) => false,
})
.cloned()
.collect();
args.push(RenderArg {
name: arg_name.to_string(),
value: RenderArgValue::SlotItems(templates),
})
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
}
}
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
Node::Expr(_) => unreachable!(),
}
args
}
fn extract_directive(
node: &mut Node,
directive: &str,
prefixed: bool,
) -> Option<(String, Option<String>)> {
match node {
Node::Element(element) => {
let index = element.forme_attributes.iter().position(|(name, _value)| {
if prefixed {
name.starts_with(directive)
} else {
name == directive
}
})?;
let removed = element.forme_attributes.remove(index);
Some(removed)
}
Node::FormeElement(_) => None,
Node::Text(_) => None,
Node::Expr(_) => None,
}
}
fn process_js_node_directives(node: &mut Node, js_processor: &JsValue, js_runtime: &mut JsRuntime) {
match node {
Node::FormeElement(_) => (),
Node::Element(element) => {
process_js_element_directives(js_processor, js_runtime, element);
for child in &mut element.children {
process_js_node_directives(child, js_processor, js_runtime);
}
}
Node::Text(_) => (),
Node::Expr(expr) => match expr {
Expr::Condition(Condition {
condition: _,
children,
}) => {
for child in children {
process_js_node_directives(child, js_processor, js_runtime);
}
}
Expr::Repeat(Repeat {
repeat: _,
children,
}) => {
for child in children {
process_js_node_directives(child, js_processor, js_runtime);
}
}
Expr::Text(_) => (),
Expr::Raw(_) => (),
Expr::Render(Render { template: _, args }) => {
for arg in args {
match &mut arg.value {
RenderArgValue::Expr(_) => (),
RenderArgValue::SlotItem(nodes) => {
for node in nodes {
process_js_node_directives(node, js_processor, js_runtime);
}
}
RenderArgValue::SlotItems(nodes) => {
for node in nodes {
process_js_node_directives(node, js_processor, js_runtime);
}
}
}
}
}
},
}
}
fn process_js_element_directives(
js_processor: &JsValue,
js_runtime: &mut JsRuntime,
element: &mut Element,
) {
let guard = js_api::create_guard(js_runtime);
let has_callback = js_api::get_property(js_processor, "elementHeader")
.map(|v| !v.is_nullish())
.unwrap_or(false);
if !has_callback {
println!("Element header callback is null or undefined");
return;
}
let js_element = JsElement {
name: element.name.to_string(),
attributes: element
.attributes
.list
.iter()
.map(
|Attr {
name,
conditions,
values,
}| {
JsAttr {
name: name.clone(),
conditions: conditions.clone().into(),
values: values
.iter()
.map(
|AttrValue {
conditions,
ty,
content,
}| {
JsAttrValue {
ty: ty.to_string(),
conditions: conditions.clone().into(),
content: content.clone(),
}
},
)
.collect(),
}
},
)
.collect(),
is_void: element.is_void,
};
let js_element_json = serde_json::to_value(&js_element).expect("Failed to serialize element");
let js_element_obj = js_api::create_from_json(js_runtime, &guard, &js_element_json)
.expect("Failed to convert element to JS object");
let resulting_js_element = js_api::call_method(
js_runtime,
&guard,
js_processor,
"elementHeader",
&[js_element_obj],
)
.expect("Failed to call elementHeader");
let result_json =
tsrun::js_value_to_json(&resulting_js_element).expect("Failed to convert result to JSON");
let new_element: JsElement = serde_json::from_value(result_json)
.expect("Failed to deserialize JS element to Rust element");
element.name = new_element.name;
let mut new_attributes = AttributesList::new();
for attr in new_element.attributes {
if attr.values.is_empty() {
new_attributes.add(
&attr.name,
None,
attr.conditions
.try_into()
.expect("Failed to convert JsConditions to Conditions"),
);
} else {
let values: Vec<AttrValue> = attr
.values
.into_iter()
.map(|js_attr_value| {
js_attr_value
.try_into()
.expect("Failed to convert JsAttrValue to AttrValue")
})
.collect();
for value in values {
new_attributes.add(
&attr.name,
Some(value),
attr.conditions
.clone()
.try_into()
.expect("Failed to convert JsConditions to Conditions"),
);
}
}
}
element.attributes = new_attributes;
element.is_void = new_element.is_void;
}
fn extract_dependencies(template_path: &str, template: &Template) -> Vec<String> {
fn extract_from_nodes(template_path: &str, nodes: &[Node], deps: &mut HashSet<String>) {
for node in nodes {
extract_from_node(template_path, node, deps);
}
}
fn extract_from_node(template_path: &str, node: &Node, deps: &mut HashSet<String>) {
match node {
Node::FormeElement(_) => {}
Node::Element(element) => {
extract_from_nodes(template_path, &element.children, deps);
}
Node::Text(_) => {}
Node::Expr(expr) => match expr {
Expr::Condition(Condition { children, .. }) => {
extract_from_nodes(template_path, children, deps);
}
Expr::Repeat(Repeat { children, .. }) => {
extract_from_nodes(template_path, children, deps);
}
Expr::Text(_) => {}
Expr::Raw(_) => {}
Expr::Render(Render { template, .. }) => {
let path = resolve_relative_path(template_path, template);
deps.insert(path);
}
},
}
}
let mut deps = HashSet::new();
extract_from_nodes(template_path, &template.nodes, &mut deps);
deps.into_iter().collect()
}
fn generate_code(
template_path: &str,
template: &Template,
registry: &HashMap<String, Vec<TemplateArg>>,
escape_func: &str,
) -> Result<String, std::fmt::Error> {
let mut code = String::new();
let fn_name = generate_function_name(template_path);
writeln!(code, "/* {template_path} */")?;
if template.args.len() + 1 > 7 {
writeln!(code, "#[allow(clippy::too_many_arguments)]")?;
}
write!(code, "pub fn {fn_name}(out: &mut impl std::fmt::Write")?;
for arg in &template.args {
let ty = if arg.ty.starts_with("&Vec<") {
format!("&[{}]", &arg.ty[5..arg.ty.len() - 1])
} else {
arg.ty.clone()
};
write!(code, ", {}: {ty}", arg.name)?;
}
writeln!(code, ") -> std::fmt::Result {{")?;
if let Some(doctype) = &template.doctype {
let doctype = safe_format_literal(doctype);
writeln!(code, r#"{INDENT}write!(out, "{doctype}")?;"#)?;
}
generate_nodes_code(
&mut code,
template_path,
INDENT,
&template.nodes,
registry,
escape_func,
)?;
writeln!(code, "{INDENT}Ok(())")?;
writeln!(code, "}}")?;
Ok(code)
}
fn generate_nodes_code(
code: &mut impl std::fmt::Write,
template_path: &str,
indent: &str,
nodes: &[Node],
registry: &HashMap<String, Vec<TemplateArg>>,
escape_func: &str,
) -> std::fmt::Result {
let mut produce_blank = true;
for node in nodes {
match node {
Node::FormeElement(_) => (),
Node::Element(_) => produce_blank = true,
Node::Text(text) => {
if text.chars().all(|c| c.is_whitespace()) {
let skip = !produce_blank;
produce_blank = false;
if skip {
continue;
}
}
}
Node::Expr(_) => produce_blank = true,
}
generate_node_code(code, template_path, indent, node, registry, escape_func)?;
}
Ok(())
}
fn generate_node_code(
code: &mut impl std::fmt::Write,
template_path: &str,
indent: &str,
node: &Node,
registry: &HashMap<String, Vec<TemplateArg>>,
escape_func: &str,
) -> std::fmt::Result {
match node {
Node::FormeElement(_tpl_element) => {}
Node::Element(element) => {
generate_element_code(code, template_path, indent, element, registry, escape_func)?;
}
Node::Text(text) => {
if text.ends_with('\n') {
let prefix = &text[..text.len() - 1];
if prefix.is_empty() {
writeln!(code, r#"{indent}writeln!(out)?;"#)?;
} else {
let rust_safe_string = safe_format_literal(prefix);
writeln!(code, r#"{indent}writeln!(out, "{rust_safe_string}")?;"#)?;
}
} else {
let rust_safe_string = safe_format_literal(text);
writeln!(code, r#"{indent}write!(out, "{rust_safe_string}")?;"#)?;
}
}
Node::Expr(expr) => match expr {
Expr::Condition(Condition {
condition,
children,
}) => {
writeln!(code, r#"{indent}if {condition} {{"#)?;
generate_nodes_code(
code,
template_path,
&format!("{INDENT}{indent}"),
children,
registry,
escape_func,
)?;
writeln!(code, r#"{indent}}}"#)?;
}
Expr::Repeat(Repeat { repeat, children }) => {
writeln!(code, r#"{indent}for {repeat} {{"#)?;
generate_nodes_code(
code,
template_path,
&format!("{INDENT}{indent}"),
children,
registry,
escape_func,
)?;
writeln!(code, r#"{indent}}}"#)?;
}
Expr::Text(TextExpr(text)) => {
writeln!(code, r#"{indent}{escape_func}(out, &({text}))?;"#)?;
}
Expr::Raw(RawExpr(raw)) => {
writeln!(code, r#"{indent}write!(out, "{{}}", &({raw}))?;"#)?;
}
Expr::Render(Render {
template: render_template_path,
args,
}) => generate_render_call(
code,
template_path,
indent,
registry,
render_template_path,
args,
escape_func,
)?,
},
}
Ok(())
}
fn generate_element_code(
code: &mut impl std::fmt::Write,
template_path: &str,
indent: &str,
element: &Element,
registry: &HashMap<String, Vec<TemplateArg>>,
escape_func: &str,
) -> std::fmt::Result {
write!(
code,
r#"{indent}write!(out, "<{elem_name}")?;"#,
elem_name = safe_format_literal(&element.name)
)?;
let mut close_start_tag_indent = false;
{
let attr_indent = format!("{INDENT}{INDENT}{indent}");
let mut list = element.attributes.list.clone();
list.sort_by(|a, b| a.name.cmp(&b.name));
for attr in &list {
let mut quotes = "\\\"";
let condition_indent = attr_indent.to_string();
let mut attr_indent = attr_indent.to_string();
if !attr.conditions.is_empty() {
writeln!(code)?;
let condition_code = attr.conditions.to_rust_code();
write!(code, r#"{condition_indent}if {condition_code} {{"#)?;
attr_indent = format!("{INDENT}{attr_indent}");
}
writeln!(code)?;
write!(
code,
r#"{attr_indent}write!(out, " {attr_name}")?;"#,
attr_name = safe_format_literal(&attr.name)
)?;
let mut values: Vec<String> = vec![];
for AttrValue {
conditions,
ty,
content,
} in &attr.values
{
let statement = match ty {
AttrValueType::Text => {
if content.contains('"') {
quotes = "'";
}
let escaped = safe_format_literal(content);
format!(r#" write!(out, "{escaped}")?;"#)
}
AttrValueType::Expr => {
format!(r#" {escape_func}(out, &({content}))?;"#)
}
};
let value = if !conditions.is_empty() {
let condition_code = conditions.to_rust_code();
format!(r#"if {condition_code} {{ {statement} }}"#)
} else {
statement
};
values.push(value);
}
if !values.is_empty() {
write!(code, r#" write!(out, "={quotes}")?;"#)?;
for (index, value) in values.iter().enumerate() {
if index > 0 {
write!(code, "\n{INDENT}{INDENT}{attr_indent}")?;
write!(code, r#"write!(out, " ")?;"#)?;
close_start_tag_indent = true;
}
write!(code, "{value}")?;
}
write!(code, r#" write!(out, "{quotes}")?;"#)?;
}
if !attr.conditions.is_empty() {
writeln!(code)?;
writeln!(code, r#"{condition_indent}}}"#)?;
}
}
}
if close_start_tag_indent {
write!(code, r#"{indent}"#)?;
} else {
write!(code, r#" "#)?;
}
writeln!(code, r#"write!(out, ">")?;"#)?;
if !element.is_void {
generate_nodes_code(
code,
template_path,
&format!("{INDENT}{indent}"),
&element.children,
registry,
escape_func,
)?;
writeln!(
code,
r#"{indent}write!(out, "</{elem_name}>")?;"#,
elem_name = safe_format_literal(&element.name)
)?;
}
Ok(())
}
fn generate_render_call(
code: &mut impl Write,
template_path: &str,
indent: &str,
registry: &HashMap<String, Vec<TemplateArg>>,
render_template_path: &str,
args: &[RenderArg],
escape_func: &str,
) -> Result<(), std::fmt::Error> {
let render_template_path = resolve_relative_path(template_path, render_template_path);
let fn_name = generate_function_name(&render_template_path);
let ordered_args = match registry.get(&render_template_path) {
Some(template_args) => {
let arg_map: HashMap<&str, &RenderArgValue> = args
.iter()
.map(|arg| (arg.name.as_str(), &arg.value))
.collect();
let mut args: Vec<String> = vec![];
for template_arg in template_args {
let TemplateArg {
name: template_arg_name,
ty: template_arg_type,
default: template_arg_default,
} = template_arg;
let value = match arg_map.get(template_arg_name.as_str()) {
Some(value) => match value {
RenderArgValue::Expr(expr) => expr.clone(),
RenderArgValue::SlotItem(nodes) => {
let indent = format!("{INDENT}{INDENT}{INDENT}{indent}");
let mut arg_out_code = String::new();
writeln!(arg_out_code, r#"&({{"#)?;
writeln!(arg_out_code, r#"{indent}/* slot-arg */"#)?;
writeln!(arg_out_code, r#"{indent}use std::fmt::Write;"#)?;
writeln!(arg_out_code, r#"{indent}let mut out_s = String::new();"#)?;
writeln!(arg_out_code, r#"{indent}let out = &mut out_s;"#)?;
generate_nodes_code(
&mut arg_out_code,
template_path,
&indent,
nodes,
registry,
escape_func,
)?;
writeln!(arg_out_code, r#"{indent}out_s"#)?;
writeln!(arg_out_code, r#"{indent}}})"#)?;
arg_out_code
}
RenderArgValue::SlotItems(items) => {
let indent = format!("{INDENT}{INDENT}{INDENT}{indent}");
let mut arg_out_code = String::new();
writeln!(arg_out_code, r#"{{"#)?;
writeln!(arg_out_code, r#"{indent}/* children slot-arg */"#)?;
writeln!(arg_out_code, r#"{indent}let mut out_v = Vec::new();"#)?;
for item in items {
match item {
Node::Element(element) => {
writeln!(arg_out_code, r#"{indent}{{"#)?;
let indent = format!("{INDENT}{indent}");
writeln!(arg_out_code, r#"{indent}use std::fmt::Write;"#)?;
writeln!(
arg_out_code,
r#"{indent}let mut out_s = String::new();"#
)?;
writeln!(arg_out_code, r#"{indent}let out = &mut out_s;"#)?;
generate_nodes_code(
&mut arg_out_code,
template_path,
&indent,
&element.children,
registry,
escape_func,
)?;
writeln!(arg_out_code, r#"{indent}out_v.push(out_s);"#)?;
writeln!(arg_out_code, r#"{indent}}}"#)?;
}
Node::Expr(expr) => {
fn render_expr(
arg_out_code: &mut impl std::fmt::Write,
expr: &Expr,
registry: &HashMap<String, Vec<TemplateArg>>,
indent: &str,
template_path: &str,
escape_func: &str,
) -> std::fmt::Result
{
fn render_item(
arg_out_code: &mut impl std::fmt::Write,
item: &Node,
registry: &HashMap<String, Vec<TemplateArg>>,
indent: &str,
template_path: &str,
escape_func: &str,
) -> std::fmt::Result
{
match item {
Node::Element(element) => {
writeln!(
arg_out_code,
r#"{indent}use std::fmt::Write;"#
)?;
writeln!(
arg_out_code,
r#"{indent}let mut out_s = String::new();"#
)?;
writeln!(
arg_out_code,
r#"{indent}let out = &mut out_s;"#
)?;
generate_nodes_code(
arg_out_code,
template_path,
&format!("{INDENT}{indent}"),
&element.children,
registry,
escape_func,
)?;
writeln!(
arg_out_code,
r#"{indent}if !out_s.is_empty() {{ out_v.push(out_s); }}"#
)?;
}
Node::Expr(expr) => render_expr(
arg_out_code,
expr,
registry,
&format!("{INDENT}{indent}"),
template_path,
escape_func,
)?,
Node::FormeElement(_) => {
unreachable!()
}
Node::Text(_) => unreachable!(),
}
Ok(())
}
match expr {
Expr::Condition(Condition {
condition,
children: items,
}) => {
writeln!(
arg_out_code,
r#"{indent}if {condition} {{"#
)?;
for item in items {
render_item(
arg_out_code,
item,
registry,
&format!("{INDENT}{indent}"),
template_path,
escape_func,
)?;
}
writeln!(arg_out_code, r#"{indent}}}"#)?;
}
Expr::Repeat(Repeat {
repeat,
children: items,
}) => {
writeln!(
arg_out_code,
r#"{indent}for {repeat} {{"#
)?;
for item in items {
render_item(
arg_out_code,
item,
registry,
&format!("{INDENT}{indent}"),
template_path,
escape_func,
)?;
}
writeln!(arg_out_code, r#"{indent}}}"#)?;
}
Expr::Text(_text_expr) => todo!(),
Expr::Raw(_raw_expr) => todo!(),
Expr::Render(_render) => todo!(),
}
Ok(())
}
render_expr(
&mut arg_out_code,
expr,
registry,
&indent,
template_path,
escape_func,
)?;
}
Node::FormeElement(_) => unreachable!(),
Node::Text(_) => unreachable!(),
}
}
writeln!(arg_out_code, r#"{indent}out_v"#)?;
writeln!(arg_out_code, r#"{indent}}}"#)?;
arg_out_code
}
},
None => match template_arg_default {
Some(default) => default.clone(),
None => {
panic!(
"{template_path}: Required template argument for {render_template_path} is not provided: {template_arg_name}"
);
}
},
};
let value = format!("/* {template_arg_name}: {template_arg_type} */ {value}");
args.push(value);
}
args.join(&format!(",\n{INDENT}{INDENT}{indent}"))
}
None => {
panic!("Template not found in registry: {render_template_path}")
}
};
writeln!(code, r#"{indent}/* {render_template_path} */"#)?;
writeln!(code, r#"{indent}{fn_name}(out, {ordered_args}{indent})?;"#)?;
Ok(())
}
#[allow(unused)]
fn print_tl_node_handle(level: usize, node_handle: &tl::NodeHandle, parser: &tl::Parser<'_>) {
let node = node_handle.get(parser).unwrap();
match node {
tl::Node::Tag(htmltag) => {
for _ in 0..level {
print!(" ");
}
let name_string = htmltag.name().as_utf8_str().to_string();
let name = name_string.as_str();
print!("<{}", name);
for (attr_name, attr_value) in htmltag.attributes().iter() {
print!(" {}=\"{}\"", attr_name, attr_value.unwrap_or_default());
}
print!(">");
let is_void_element = HTML_VOID_ELEMENTS.contains(&name);
if !is_void_element {
for child in htmltag.children().top().iter() {
print_tl_node_handle(level + 1, child, parser);
}
print!("</{}>", name);
}
}
tl::Node::Raw(bytes) => print!("{}", bytes.as_utf8_str()),
tl::Node::Comment(_bytes) => (),
}
}