///|
/// HTML template rendering support.
///
/// HTML template rendering with glob/file loading.
/// In MoonBit, template rendering is simplified — HTML templates are plain
/// strings with `{{ .key }}` placeholders replaced via simple string substitution.
///|
/// Template store — maps template name to template string.
pub(all) struct TemplateStore {
templates : Map[String, String]
mut delims_left : String
mut delims_right : String
}
///|
pub fn TemplateStore::new() -> TemplateStore {
{ templates: Map([]), delims_left: "{{", delims_right: "}}" }
}
///|
/// Set template delimiters (default: `{{` and `}}`).
pub fn TemplateStore::set_delims(
self : TemplateStore,
left : String,
right : String,
) -> Unit {
self.delims_left = left
self.delims_right = right
}
///|
/// Load a single template from a string.
pub fn TemplateStore::add_template(
self : TemplateStore,
name : String,
content : String,
) -> Unit {
self.templates.set(name, content)
}
///|
/// Load templates from a glob pattern. Reads all matching files.
pub async fn TemplateStore::load_glob(
self : TemplateStore,
pattern : String,
) -> Unit {
// Simple glob: just load a single file or directory listing
try {
// If pattern ends with *, load all files in directory
if pattern.has_suffix("*") {
let dir = pattern[:pattern.length() - 1].to_owned()
let files = @fs.readdir(dir)
for file in files {
if file.has_suffix(".html") || file.has_suffix(".tmpl") {
let content = @fs.read_file(dir + "/" + file).text()
let name = file
.replace_all(old=".html", new="")
.replace_all(old=".tmpl", new="")
self.templates.set(name, content)
}
}
} else {
// Single file
let content = @fs.read_file(pattern).text()
// Extract filename as template name
let name = match pattern.rev_find("/") {
Some(pos) => pattern[pos + 1:].to_owned()
None => pattern
}
let name = name
.replace_all(old=".html", new="")
.replace_all(old=".tmpl", new="")
self.templates.set(name, content)
}
} catch {
_ => ()
}
}
///|
/// Load templates from a list of file paths.
pub async fn TemplateStore::load_files(
self : TemplateStore,
files : Array[String],
) -> Unit {
for file in files {
try {
let content = @fs.read_file(file).text()
let name = match file.rev_find("/") {
Some(pos) => file[pos + 1:].to_owned()
None => file
}
let name = name
.replace_all(old=".html", new="")
.replace_all(old=".tmpl", new="")
self.templates.set(name, content)
} catch {
_ => ()
}
}
}
///|
/// Render a template with data. Replaces `{{ .key }}` placeholders.
pub fn TemplateStore::render(
self : TemplateStore,
name : String,
data : Map[String, String],
) -> String? {
match self.templates.get(name) {
Some(tmpl) => {
let mut result = tmpl
for key, value in data {
let placeholder = self.delims_left + " ." + key + " " + self.delims_right
result = result.replace_all(old=placeholder, new=value)
// Also support {{ .key }} without spaces
let placeholder2 = self.delims_left + "." + key + self.delims_right
result = result.replace_all(old=placeholder2, new=value)
}
Some(result)
}
None => None
}
}
///|
/// Global template store for engine-wide template access.
let global_templates : TemplateStore = TemplateStore::new()
///|
/// Set the HTML template delimiters.
pub fn set_template_delims(left : String, right : String) -> Unit {
global_templates.set_delims(left, right)
}
///|
/// Load HTML templates from a glob pattern.
pub async fn load_html_glob(pattern : String) -> Unit {
global_templates.load_glob(pattern)
}
///|
/// Load HTML templates from file paths.
pub async fn load_html_files(files : Array[String]) -> Unit {
global_templates.load_files(files)
}
///|
/// Set a single HTML template by name and content.
pub fn set_html_template(name : String, content : String) -> Unit {
global_templates.add_template(name, content)
}
///|
/// Render an HTML template response.
pub async fn Context::html_template(
self : Context,
code : Int,
name : String,
data : Map[String, String],
) -> Unit {
match global_templates.render(name, data) {
Some(html) => self.html(code, html)
None => self.abort_with_status(500, "Template not found: " + name)
}
}