///|
/// Errors raised by the code generator.
///
/// `InvalidArguments` reports CLI misuse, `InvalidBinding` reports malformed
/// `#tpl.path` source bindings, `MissingTemplate` reports files that cannot be
/// found, and `UnexpandedInclude` protects in-memory generation from emitting a
/// renderer that still contains static include nodes.
pub(all) suberror CodegenError {
InvalidArguments(String)
InvalidBinding(source_path~ : String, offset~ : Int, message~ : String)
MissingTemplate(String)
UnexpandedInclude(path~ : String)
} derive(Eq, Debug)
///|
/// A source binding plus its loaded template content.
///
/// `source_path`, `template_path`, and `struct_name` identify the source
/// binding. `field_names` is used to rewrite bare template field references to
/// `self.`, and `content` stores the template text after any static
/// includes have been expanded.
pub(all) struct TemplateInput {
source_path : String
template_path : String
struct_name : String
field_names : Array[String]
content : String
} derive(Eq, ToJson, Debug)
///|
/// Creates an in-memory generation input.
///
/// Use this constructor when tests or custom tools already have template text
/// available. Pass `field_names` when template expressions omit `self.` for
/// fields declared on the bound struct.
pub fn template_input(
source_path~ : String,
template_path~ : String,
struct_name~ : String,
field_names? : Array[String] = [],
content~ : String,
) -> TemplateInput {
{ source_path, template_path, struct_name, field_names, content }
}
///|
/// Scans MoonBit source for `#tpl.path("...")` bindings.
///
/// Each binding records the template path and the following struct name. This
/// scanner intentionally handles only the small attribute/struct shape needed by
/// codegen; MoonBit syntax and type checking remain the compiler's job. Field
/// names from the struct body are collected so generated template code can add
/// `self.` automatically where needed.
pub fn scan_source(
source_path : String,
source : String,
) -> Array[@ast.TemplateBinding] raise CodegenError {
let bindings : Array[@ast.TemplateBinding] = []
let marker = "#tpl.path("
let len = source.length()
for offset = 0; offset < len; {
let rest = source.unsafe_substring(start=offset, end=len)
match rest.find(marker) {
None => continue len
Some(relative_marker) => {
let attr_start = offset + relative_marker
let path_start = attr_start + marker.length()
match read_quoted_string(source, path_start) {
None =>
raise InvalidBinding(
source_path~,
offset=attr_start,
message="expected quoted template path",
)
Some((template_path, after_path)) => {
let after_attr = skip_ws(source, after_path)
if after_attr >= len ||
source.unsafe_substring(start=after_attr, end=after_attr + 1) !=
")" {
raise InvalidBinding(
source_path~,
offset=after_attr,
message="expected closing ')' after template path",
)
}
let search_start = after_attr + 1
match find_struct_name(source, search_start) {
None =>
raise InvalidBinding(
source_path~,
offset=search_start,
message="expected struct after #tpl.path",
)
Some((struct_name, after_name, _struct_start)) => {
let field_names = read_struct_fields(source, _struct_start)
let span = @ast.span(
file=source_path,
start=position_at(source, attr_start),
end=position_at(source, after_name),
)
bindings.push(
@ast.template_binding(
source_path~,
template_path~,
struct_name~,
field_names~,
span~,
),
)
continue after_name
}
}
}
}
}
}
} nobreak {
bindings
}
}
///|
/// Generates a complete MoonBit source file from loaded template inputs.
///
/// The returned source contains one `Render` implementation per input. Package
/// imports are not emitted; callers should import `justjavac/template` in the
/// target package's `moon.pkg`.
pub fn generate(inputs : Array[TemplateInput]) -> String raise {
let out = StringBuilder::new()
out.write_string("// Generated using `template_codegen`, DON'T EDIT IT\n")
for input in inputs {
out.write_string("\n")
out.write_string(generate_one(input))
}
out.to_string()
}
///|
/// Generates one `Render` implementation from an in-memory template.
///
/// This function is useful for tests and tools that already loaded template
/// text. Static include nodes must be expanded before calling it; otherwise it
/// raises `CodegenError::UnexpandedInclude`.
pub fn generate_one(input : TemplateInput) -> String raise {
let template = @parser.parse(input.template_path, input.content)
let out = StringBuilder::new()
out.write_string("///|\n")
out.write_string("/// Generated render implementation for ")
out.write_string(input.template_path)
out.write_string(".\n")
out.write_string("pub impl @template.Render for ")
out.write_string(input.struct_name)
out.write_string(" with render(self) {\n")
out.write_string(" let out = StringBuilder::new()\n")
write_nodes(out, template.nodes, input.field_names)
out.write_string(" out.to_string()\n")
out.write_string("}\n")
out.to_string()
}
///|
/// Generates `output_path` by scanning source files and reading their templates.
///
/// Only `.mbt` inputs are scanned, and the output path is ignored if it appears
/// in the input list. Template paths are resolved relative to the source file
/// that owns the `#tpl.path` attribute, and static includes are expanded before
/// source generation.
pub fn generate_from_files(
source_files : Array[String],
output_path : String,
) -> Unit raise {
let inputs : Array[TemplateInput] = []
for source_path in source_files {
if source_path == output_path || !source_path.has_suffix(".mbt") {
continue
}
let source = @fs.read_file_to_string(source_path)
let bindings = scan_source(source_path, source)
for binding in bindings {
let template_path = resolve_template_path(
binding.source_path,
binding.template_path,
)
if !@fs.path_exists(template_path) {
raise MissingTemplate(template_path)
}
let content = read_template_with_includes(template_path)
inputs.push(
template_input(
source_path=binding.source_path,
template_path=binding.template_path,
struct_name=binding.struct_name,
field_names=binding.field_names,
content~,
),
)
}
}
@fs.write_string_to_file(output_path, generate(inputs))
}
///|
/// Parses command line arguments and runs file-based generation.
///
/// The supported CLI shape is `template_codegen --scan -o