module generator
import (
std.collections
std.fs
std.io
std.strings
)
let append_value(
engine: *mut Engine,
output: *mut std.strings.StringBuilder,
value: Value,
): bool =
match value {
.String(string) => append_text(output, string),
.Number(number) => {
let writer = output.(*mut dyn std.io.Writer)
let error = std.io.wprint(writer, "{}", number)
if error.is_error() { return fail(engine, "could not render value") }
true
}
.Boolean(boolean) => {
let writer = output.(*mut dyn std.io.Writer)
let error = std.io.wprint(writer, "{}", boolean)
if error.is_error() { return fail(engine, "could not render value") }
true
}
}
let values_equal(left: Value, right: Value): bool =
match left, right {
.String(left_string), .String(right_string) => left_string.eq(right_string),
.Number(left_number), .Number(right_number) => left_number == right_number,
.Boolean(left_boolean), .Boolean(right_boolean) => left_boolean == right_boolean,
_ => false,
}
let evaluate_condition(
engine: *mut Engine,
expression: str,
context: *Context,
): (bool, bool) {
let text = expression.trim()
let equal_at, has_equal = text.find("==")
let not_equal_at, has_not_equal = text.find("!=")
if has_equal && has_not_equal {
fail(engine, "condition may contain only one comparison")
return false, false
}
if has_equal || has_not_equal {
let operator_at = if has_equal { equal_at } else { not_equal_at }
let key = text[:operator_at].trim()
let literal_text = text[operator_at + 2:].trim()
if !valid_identifier(key) {
fail(engine, "invalid conditional variable")
return false, false
}
let actual, found = find_field(context.fields.as_slice(), key)
if !found {
fail_path(engine, "unknown conditional variable: ", key)
return false, false
}
let literal, valid = parse_value(literal_text)
if !valid {
fail(engine, "invalid conditional literal")
return false, false
}
let equal = values_equal(actual, literal)
return if has_equal { equal } else { !equal }, true
}
if !valid_identifier(text) {
fail(engine, "invalid conditional variable")
return false, false
}
let value, found = find_field(context.fields.as_slice(), text)
if !found {
fail_path(engine, "unknown conditional variable: ", text)
return false, false
}
return match value {
.Boolean(boolean) => boolean,
.Number(number) => number != 0,
.String(string) => @len(string) != 0,
}, true
}
let append_cycle(
engine: *mut Engine,
stack: *mut std.collections.DynamicArray(str),
name: str,
): bool {
_ = append_text(engine.error, "template cycle: ")
let mut start: usz = 0
for item, index in stack.as_slice() {
if item.eq(name) {
start = index
break
}
}
for index in start..stack.count {
if index > start { _ = append_text(engine.error, " -> ") }
_ = append_text(engine.error, stack.ptr[index])
}
_ = append_text(engine.error, " -> ")
_ = append_text(engine.error, name)
return false
}
let render_template(
engine: *mut Engine,
name: str,
context: *Context,
content: str,
stack: *mut std.collections.DynamicArray(str),
): (std.strings.StringBuilder, bool) {
let mut result = std.strings.StringBuilder.init(engine.allocator)
if !template_name_valid(name) {
fail_path(engine, "invalid template path: ", name)
return result, false
}
for active in stack.as_slice() {
if active.eq(name) {
append_cycle(engine, stack, name)
return result, false
}
}
if !stack.append(name) {
fail(engine, "out of memory")
return result, false
}
let path, path_ok = make_path(engine, "templates", name)
if !path_ok {
_ = stack.pop()
return result, false
}
let source, read_error = std.fs.read_file(engine.allocator, path.as_str())
if read_error.is_error() {
fail_path(engine, "could not read template: ", name)
_ = stack.pop()
return result, false
}
let document, parsed = parse_document(engine, source.as_str(), name)
if !parsed {
_ = stack.pop()
return result, false
}
let mut local, copied = copy_context(engine.allocator, context)
if !copied {
fail(engine, "out of memory")
_ = stack.pop()
return result, false
}
for field in document.fields.as_slice() {
if !set_field(local.&mut, field) {
fail(engine, "out of memory")
_ = stack.pop()
return result, false
}
}
let mut cursor: usz = 0
let stop = render_region(
engine,
document.body,
cursor.&mut,
local.&,
content,
stack,
result.&mut,
true,
)
if stop != .Eof {
if stop != .Error { fail_path(engine, "unexpected conditional terminator in ", name) }
_ = stack.pop()
return result, false
}
let parent, has_parent = find_field(document.fields.as_slice(), "template")
if has_parent {
match parent {
.String(parent_name) => {
let parent_result, parent_ok = render_template(
engine,
parent_name,
local.&,
result.as_str(),
stack,
)
_ = stack.pop()
return parent_result, parent_ok
}
_ => {
fail_path(engine, "template front matter must be a string in ", name)
_ = stack.pop()
return result, false
}
}
}
_ = stack.pop()
return result, true
}
let render_region(
engine: *mut Engine,
text: str,
cursor: *mut usz,
context: *Context,
content: str,
stack: *mut std.collections.DynamicArray(str),
output: *mut std.strings.StringBuilder,
emit: bool,
): Stop {
for cursor.* < @len(text) {
let remaining = text[cursor.*:]
let variable_at, has_variable = remaining.find("{{")
let tag_at, has_tag = remaining.find("{%")
if !has_variable && !has_tag {
if emit && !append_text(output, remaining) {
fail(engine, "out of memory")
return .Error
}
cursor.* = @len(text)
return .Eof
}
let use_variable = has_variable && (!has_tag || variable_at < tag_at)
let token_at = if use_variable { variable_at } else { tag_at }
if emit && !append_text(output, remaining[:token_at]) {
fail(engine, "out of memory")
return .Error
}
cursor.* += token_at
if use_variable {
let close_at, found_close = text[cursor.* + 2:].find("}}")
if !found_close {
fail(engine, "unterminated variable substitution")
return .Error
}
let name = text[cursor.* + 2:cursor.* + 2 + close_at].trim()
if !valid_identifier(name) {
fail(engine, "invalid variable substitution")
return .Error
}
if emit {
if name.eq("content") {
if !append_text(output, content) {
fail(engine, "out of memory")
return .Error
}
} else {
let value, found = find_field(context.fields.as_slice(), name)
if !found {
fail_path(engine, "unknown template variable: ", name)
return .Error
}
if !append_value(engine, output, value) { return .Error }
}
}
cursor.* += 2 + close_at + 2
continue
}
let close_at, found_close = text[cursor.* + 2:].find("%}")
if !found_close {
fail(engine, "unterminated template tag")
return .Error
}
let tag = text[cursor.* + 2:cursor.* + 2 + close_at].trim()
cursor.* += 2 + close_at + 2
if tag.eq("else") { return .Else }
if tag.eq("endif") { return .EndIf }
if tag.starts_with("include ") || tag.starts_with("include\t") {
let argument = tag[7:].trim()
let include_value, valid = parse_value(argument)
match include_value {
.String(include_name) if valid => {
if emit {
let included, ok = render_template(
engine,
include_name,
context,
content,
stack,
)
if !ok { return .Error }
if !append_text(output, included.as_str()) {
fail(engine, "out of memory")
return .Error
}
continue
}
}
_ => {
fail(engine, "include expects one quoted path")
return .Error
}
}
}
if tag.starts_with("if ") || tag.starts_with("if\t") {
let expression = tag[2:].trim()
if @len(expression) == 0 {
fail(engine, "if expects a condition")
return .Error
}
let condition, valid = evaluate_condition(engine, expression, context)
if !valid { return .Error }
let first_stop = render_region(
engine,
text,
cursor,
context,
content,
stack,
output,
emit && condition,
)
if first_stop == .Error { return .Error }
if first_stop == .Eof {
fail(engine, "if is missing endif")
return .Error
}
if first_stop == .Else {
let second_stop = render_region(
engine,
text,
cursor,
context,
content,
stack,
output,
emit && !condition,
)
if second_stop == .Error { return .Error }
if second_stop != .EndIf {
fail(engine, "else is missing endif")
return .Error
}
}
continue
}
fail_path(engine, "unknown template tag: ", tag)
return .Error
}
return .Eof
}