const std = @import("std"); pub fn main() !void { const Enum = enum { foo, bar, baz }; try execute( std.io.getStdOut().writer(), .{ .test_tmpl = "{.x} {%context_foo}" }, @embedFile("./test.tmp.html"), .{ .community = .{ .name = "" }, .foo = &[_][]const u8{ "5", "4", "3", "2", "1" }, .baz = &[_][]const []const u8{ &.{ "5", "4", "3", "2", "1" }, &.{ "5", "4", "3", "2", "1" }, }, .start = 1, .end = 3, .bar = .{ .x = "x" }, .qux = false, .quxx = true, .quxx2 = true, .maybe_foo = @as(?[]const u8, "foo"), .maybe_bar = @as(?[]const u8, null), .snap = Enum.bar, .crackle = union(Enum) { foo: []const u8, bar: []const u8, baz: []const u8, }{ .foo = "abcd" }, .x = "y", }, .{ .context_foo = "foo", }, ); } pub fn execute( writer: anytype, comptime other_templates: anytype, comptime template: []const u8, args: anytype, context: anytype, ) !void { @setEvalBranchQuota(@intCast(u32, template.len * 12)); const tokens = comptime parseTemplateTokens(ControlTokenIter{ .text = template }); const tmpl = comptime parseTemplate(tokens, 0, .root); try executeTemplate(writer, other_templates, tmpl.items, args, .{}, context); } fn executeTemplate( writer: anytype, comptime templates: anytype, comptime items: []const TemplateItem, args: anytype, captures: anytype, context: anytype, ) !void { inline for (items) |it| { switch (it) { .text => |text| try writer.writeAll(text), .statement => |stmt| try executeStatement(writer, templates, stmt, args, captures, context), } } } fn executeStatement( writer: anytype, comptime templates: anytype, comptime stmt: Statement, args: anytype, captures: anytype, context: anytype, ) !void { switch (stmt) { .expression => |expr| { const val = try evaluateExpression(expr, args, captures, context); try print(writer, val); }, .@"for" => |loop| { const iterable = try evaluateExpression(loop.header.iterable, args, captures, context); const subtemplate = loop.subtemplate; //std.log.debug("{any}", .{subtemplate}); for (iterable) |v, i| { const with_item_capture = addCapture(captures, loop.header.item_capture, v); const with_idx_capture = if (comptime loop.header.idx_capture) |name| addCapture(with_item_capture, name, i) else with_item_capture; try executeTemplate( writer, templates, subtemplate, args, with_idx_capture, context, ); } }, .@"if" => |if_stmt| { const condition = try evaluateExpression(if_stmt.header.condition, args, captures, context); const subtemplate = if_stmt.subtemplate; var was_true: bool = false; if (if_stmt.header.capture) |capture| { if (condition) |val| { was_true = true; try executeTemplate( writer, templates, subtemplate, args, addCapture(captures, capture, val), context, ); } } else { if (condition) { was_true = true; try executeTemplate( writer, templates, subtemplate, args, captures, context, ); } } if (!was_true) { if (if_stmt.else_branch) |branch| switch (branch) { .@"else" => |subtmpl| try executeTemplate(writer, templates, subtmpl, args, captures, context), .elif => |elif| try executeStatement(writer, templates, .{ .@"if" = elif.* }, args, captures, context), }; } }, .@"switch" => |switch_stmt| { const expr = try evaluateExpression(switch_stmt.expression, args, captures, context); const exhaustive = switch_stmt.cases.len == std.meta.fields(@TypeOf(expr)).len; if (exhaustive and switch_stmt.else_branch != null) @compileError("Unused else branch in switch"); if (!exhaustive and switch_stmt.else_branch == null) @compileError("Not all switch cases covered"); var found = false; inline for (switch_stmt.cases) |case| { if (std.meta.isTag(expr, case.header.tag)) { found = true; if (case.header.capture) |capture| { try executeTemplate( writer, templates, case.subtemplate, args, addCapture(captures, capture, @field(expr, case.header.tag)), context, ); } else { try executeTemplate( writer, templates, case.subtemplate, args, captures, context, ); } } } else if (!found) if (switch_stmt.else_branch) |subtemplate| { try executeTemplate( writer, templates, subtemplate, args, captures, context, ); }; }, .call_template => |call| { const new_template = @field(templates, call.template_name); try execute( writer, templates, new_template, try evaluateExpression(call.args, args, captures, context), context, ); }, .format => |fmt| { try std.fmt.format( writer, "{" ++ fmt.format ++ "}", .{try evaluateExpression(fmt.value, args, captures, context)}, ); }, //else => @compileError("TODO"), } } fn htmlEscape(writer: anytype, str: []const u8) !void { for (str) |ch| switch (ch) { '<' => try writer.writeAll("<"), '>' => try writer.writeAll(">"), '"' => try writer.writeAll("""), '&' => try writer.writeAll("&"), else => try writer.writeByte(ch), }; } fn print(writer: anytype, arg: anytype) !void { const T = @TypeOf(arg); if (T == void) return; if (comptime std.meta.trait.isZigString(T)) return htmlEscape(writer, arg); if (comptime std.meta.trait.isNumber(T)) return std.fmt.format(writer, "{}", .{arg}); if (comptime std.meta.trait.is(.Optional)(T)) return if (arg) |a| try print(writer, a); try std.fmt.format(writer, "{}", .{arg}); } const ExpressionError = error{ IndexOutOfBounds, NullOptional }; fn Deref(comptime T: type, comptime field: []const u8) type { if (std.meta.trait.isIndexable(T) and std.mem.eql(u8, field, "len")) return usize; switch (@typeInfo(T)) { .Pointer => return Deref(std.meta.Child(T), field), .Struct => |info| for (info.fields) |f| { if (std.mem.eql(u8, field, f.name)) return f.field_type; } else @compileError("Field " ++ field ++ " does not exist on type " ++ @typeName(T)), else => @compileError("Cannot retrieve field " ++ field ++ " from type " ++ @typeName(T)), } } fn EvaluateExpression( comptime expression: Expression, comptime Args: type, comptime Captures: type, comptime Context: type, ) type { return switch (expression) { .args => Args, .captures => Captures, .context => Context, .deref => |expr| { const T = EvaluateExpression(expr.container, Args, Captures, Context); return Deref(T, expr.field); }, .equals => bool, .builtin => |call| switch (call.*) { .isTag => bool, .slice => |sl| []const std.meta.Elem(EvaluateExpression(sl.iterable, Args, Captures, Context)), }, .optional_unwrap => |expr| std.meta.Child(EvaluateExpression(expr.*, Args, Captures, Context)), .int => isize, }; } fn evaluateExpression( comptime expression: Expression, args: anytype, captures: anytype, context: anytype, ) ExpressionError!EvaluateExpression(expression, @TypeOf(args), @TypeOf(captures), @TypeOf(context)) { return switch (expression) { .args => args, .captures => captures, .context => context, .deref => |expr| { return @field( try evaluateExpression(expr.container, args, captures, context), expr.field, ); }, .equals => |eql| { const lhs = try evaluateExpression(eql.lhs, args, captures, context); const rhs = try evaluateExpression(eql.rhs, args, captures, context); const T = @TypeOf(lhs, rhs); if (comptime std.meta.trait.isZigString(T)) { return std.mem.eql(u8, lhs, rhs); } else if (comptime std.meta.trait.isContainer(T) and @hasDecl(T, "eql")) { return T.eql(lhs, rhs); } else return std.meta.eql(lhs, rhs); }, .builtin => |call| switch (call.*) { .isTag => |hdr| { const val = try evaluateExpression(hdr.expression, args, captures, context); return std.meta.isTag(val, hdr.tag); }, .slice => |sl| { const iterable = try evaluateExpression(sl.iterable, args, captures, context); const start = std.math.cast(usize, try evaluateExpression(sl.start, args, captures, context)) orelse return error.IndexOutOfBounds; const end = std.math.cast(usize, try evaluateExpression(sl.end, args, captures, context)) orelse return error.IndexOutOfBounds; if (comptime std.meta.trait.is(.Array)(@TypeOf(iterable))) @compileError("Cannot slice an array, pass a slice or pointer to array instead"); if (start > iterable.len or end > iterable.len) return error.IndexOutOfBounds; return iterable[start..end]; }, }, .optional_unwrap => |expr| { const val = try evaluateExpression(expr.*, args, captures, context); return val orelse error.NullOptional; }, .int => |i| return i, }; } fn AddCapture(comptime Root: type, comptime name: []const u8, comptime Val: type) type { if (std.mem.eql(u8, name, "_")) return Root; var fields = std.meta.fields(Root) ++ [_]std.builtin.Type.StructField{.{ .name = name, .field_type = Val, .default_value = null, .is_comptime = false, .alignment = @alignOf(Val), }}; const Result = @Type(.{ .Struct = .{ .layout = .Auto, .fields = fields, .decls = &.{}, .is_tuple = false, } }); return Result; } fn addCapture(root: anytype, comptime name: []const u8, val: anytype) AddCapture(@TypeOf(root), name, @TypeOf(val)) { if (comptime std.mem.eql(u8, name, "_")) return root; var result: AddCapture(@TypeOf(root), name, @TypeOf(val)) = undefined; inline for (std.meta.fields(@TypeOf(root))) |f| { @field(result, f.name) = @field(root, f.name); } @field(result, name) = val; return result; } const TemplateType = enum { root, for_block, if_block, if_else_block, switch_block, }; const TemplateParseResult = struct { new_idx: usize, items: []const TemplateItem, closing_block: ?ControlBlock, }; fn parseIfBlock( comptime header: IfHeader, comptime tokens: []const TemplateToken, comptime start: usize, ) ParseResult(usize, If) { const subtemplate = parseTemplate(tokens, start, .if_block); switch (subtemplate.closing_block.?.block) { .end_if => return .{ .new_iter = subtemplate.new_idx, .item = If{ .header = header, .subtemplate = subtemplate.items, .else_branch = null, }, }, .@"else" => { const else_subtemplate = parseTemplate(tokens, subtemplate.new_idx + 1, .if_else_block); return .{ .new_iter = else_subtemplate.new_idx, .item = If{ .header = header, .subtemplate = subtemplate.items, .else_branch = .{ .@"else" = else_subtemplate.items }, }, }; }, .elif_header => |elif_header| { const else_if = parseIfBlock(elif_header, tokens, subtemplate.new_idx + 1); return .{ .new_iter = else_if.new_iter, .item = If{ .header = header, .subtemplate = subtemplate.items, .else_branch = .{ .elif = &else_if.item }, }, }; }, else => unreachable, } } fn parseTemplate( comptime tokens: []const TemplateToken, comptime start: usize, comptime template_type: TemplateType, ) TemplateParseResult { comptime { @setEvalBranchQuota(tokens.len * 100); var i: usize = start; var current_text: []const u8 = ""; var items: []const TemplateItem = &.{}; const closing_block: ?ControlBlock = while (i < tokens.len) : (i += 1) { switch (tokens[i]) { .text => |text| current_text = current_text ++ text, .whitespace => |wsp| { if (i != tokens.len - 1 and tokens[i + 1] == .control_block) { if (tokens[i + 1].control_block.strip_before) continue; } if (i != 0 and tokens[i - 1] == .control_block) { if (tokens[i - 1].control_block.strip_after) continue; } current_text = current_text ++ wsp; }, .control_block => |cb| { if (current_text.len != 0) { items = items ++ [_]TemplateItem{.{ .text = current_text }}; current_text = ""; } switch (cb.block) { .expression => |expr| items = items ++ [_]TemplateItem{.{ .statement = .{ .expression = expr } }}, .if_header => |header| { const parsed = parseIfBlock(header, tokens, i + 1); i = parsed.new_iter; items = items ++ [_]TemplateItem{.{ .statement = .{ .@"if" = parsed.item }, }}; }, .for_header => |header| { const subtemplate = parseTemplate(tokens, i + 1, .for_block); items = items ++ [_]TemplateItem{.{ .statement = .{ .@"for" = .{ .subtemplate = subtemplate.items, .header = header, }, }, }}; i = subtemplate.new_idx; }, .switch_header => |header| { var cases: []const Case = &.{}; var else_branch: ?[]const TemplateItem = null; var last_header: CaseHeader = header.first_case; var is_else = false; while (true) { const case = parseTemplate(tokens, i + 1, .switch_block); i = case.new_idx; if (!is_else) { cases = cases ++ [_]Case{.{ .header = last_header, .subtemplate = case.items, }}; } else { else_branch = case.items; } switch (case.closing_block.?.block) { .end_switch => break, .@"else" => is_else = true, .case_header => |case_header| last_header = case_header, else => @compileError("Unexpected token"), } } items = items ++ [_]TemplateItem{.{ .statement = .{ .@"switch" = .{ .expression = header.expression, .cases = cases, .else_branch = else_branch, }, }, }}; }, .end_for => if (template_type == .for_block) break cb else @compileError("Unexpected /for tag"), .end_if => if (template_type == .if_block or template_type == .if_else_block) break cb else @compileError("Unexpected /if tag"), .elif_header => if (template_type == .if_block) break cb else @compileError("Unexpected #elif tag"), .@"else" => if (template_type == .if_block or template_type == .switch_block) break cb else @compileError("Unexpected #else tag"), .call_template => |call| items = items ++ [_]TemplateItem{.{ .statement = .{ .call_template = call }, }}, .format => |call| items = items ++ [_]TemplateItem{.{ .statement = .{ .format = call }, }}, .end_switch, .case_header => if (template_type == .switch_block) break cb else @compileError("Unexpected /switch tag"), } }, } } else null; if (current_text.len != 0) items = items ++ [_]TemplateItem{.{ .text = current_text }}; if (template_type != .root and closing_block == null) @compileError("End tag not found"); return .{ .new_idx = i, .items = items, .closing_block = closing_block, }; } } const TemplateToken = union(enum) { text: []const u8, whitespace: []const u8, control_block: ControlBlock, }; fn parseTemplateTokens(comptime tokens: ControlTokenIter) []const TemplateToken { comptime { var iter = tokens; var items: []const TemplateToken = &.{}; while (iter.next()) |token| switch (token) { .whitespace => |wsp| items = items ++ [_]TemplateToken{.{ .whitespace = wsp }}, .number, .text => |text| items = items ++ [_]TemplateToken{.{ .text = text }}, .open_bracket => { const next = iter.next() orelse @compileError("Unexpected end of template"); if (next == .open_bracket) { items = items ++ [_]TemplateToken{.{ .text = "{" }}; } else { iter.putBack(next); const result = parseControlBlock(iter); iter = result.new_iter; items = items ++ [_]TemplateToken{.{ .control_block = result.item }}; } }, .close_bracket => { const next = iter.next() orelse @compileError("Unexpected end of template"); if (next == .close_bracket) items = items ++ [_]TemplateToken{.{ .text = "}" }} else @compileError("Unpaired close bracket, did you mean \"}}\"?"); }, .period => items = items ++ [_]TemplateToken{.{ .text = "." }}, .pound => items = items ++ [_]TemplateToken{.{ .text = "#" }}, .pipe => items = items ++ [_]TemplateToken{.{ .text = "|" }}, .dollar => items = items ++ [_]TemplateToken{.{ .text = "$" }}, .slash => items = items ++ [_]TemplateToken{.{ .text = "/" }}, .equals => items = items ++ [_]TemplateToken{.{ .text = "=" }}, .at => items = items ++ [_]TemplateToken{.{ .text = "@" }}, .comma => items = items ++ [_]TemplateToken{.{ .text = "," }}, .percent => items = items ++ [_]TemplateToken{.{ .text = "%" }}, .open_paren => items = items ++ [_]TemplateToken{.{ .text = "(" }}, .close_paren => items = items ++ [_]TemplateToken{.{ .text = ")" }}, .double_quote => items = items ++ [_]TemplateToken{.{ .text = "\"" }}, .question_mark => items = items ++ [_]TemplateToken{.{ .text = "?" }}, }; return items; } } fn tryParseIdentifier(comptime tokens: ControlTokenIter) ?ParseResult(ControlTokenIter, []const u8) { comptime { var iter = skipWhitespace(tokens); var ident: []const u8 = ""; var first: bool = true; while (iter.next()) |token| switch (token) { .number, .text => |text| { if (first and token == .number) return null; ident = ident ++ text; first = false; }, else => { iter.putBack(token); break; }, }; if (first) return null; return ParseResult(ControlTokenIter, []const u8){ .new_iter = iter, .item = ident, }; } } fn parseExpression(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, Expression) { comptime { var iter = tokens; var last_valid_iter: ?ControlTokenIter = null; var expr: ?Expression = null; while (iter.next()) |token| { switch (token) { .whitespace => {}, .period => { iter = skipWhitespace(iter); if (expr == null) { expr = .{ .args = {} }; if (iter.peek()) |n| if (n == .text) iter.putBack(.{ .period = {} }); } else if (tryParseIdentifier(iter)) |ident| { iter = ident.new_iter; expr = .{ .deref = &.{ .container = expr.?, .field = ident.item, }, }; } else if (iter.peek()) |next| if (next == .question_mark) { _ = iter.next(); expr = .{ .optional_unwrap = blk: { const e = expr.?; break :blk &e; }, }; }; last_valid_iter = iter; }, .dollar => { if (expr != null) break; iter = skipWhitespace(iter); expr = .{ .captures = {} }; if (iter.peek()) |n| if (n == .text) iter.putBack(.{ .period = {} }); last_valid_iter = iter; }, .percent => { if (expr != null) break; iter = skipWhitespace(iter); expr = .{ .context = {} }; if (iter.peek()) |n| if (n == .text) iter.putBack(.{ .period = {} }); last_valid_iter = iter; }, .equals => { const next = iter.next() orelse break; if (next == .equals) { const lhs = expr orelse break; const rhs = parseExpression(iter); iter = rhs.new_iter; expr = .{ .equals = &.{ .lhs = lhs, .rhs = rhs.item, }, }; last_valid_iter = iter; } else break; }, .at => { if (expr != null) break; const builtin = parseBuiltin(iter); iter = builtin.new_iter; expr = .{ .builtin = &builtin.item }; last_valid_iter = iter; }, .number => |n| { if (expr != null) break; const num = std.fmt.parseInt(isize, n, 10) catch @compileError("Error parsing integer"); expr = .{ .int = num }; last_valid_iter = iter; }, else => break, } } return .{ .new_iter = last_valid_iter orelse @compileError("Invalid Expression"), .item = expr orelse @compileError("Invalid Expression"), }; } } fn expectToken(comptime token: ?ControlToken, comptime exp: std.meta.Tag(ControlToken)) void { comptime { if (token == null) @compileError("Unexpected End Of Template"); const token_tag = std.meta.activeTag(token.?); if (token_tag != exp) @compileError("Expected " ++ @tagName(exp) ++ ", got " ++ @tagName(token_tag)); } } fn parseBuiltin(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, BuiltinCall) { comptime { var iter = tokens; const builtin = blk: { const next = iter.next() orelse @compileError("Invalid Builtin"); if (next != .text) @compileError("Invalid Builtin"); break :blk std.meta.stringToEnum(Builtin, next.text) orelse @compileError("Invalid Builtin"); }; iter = skipWhitespace(iter); expectToken(iter.next(), .open_paren); iter = skipWhitespace(iter); const call = switch (builtin) { .isTag => blk: { const expr = parseExpression(iter); iter = skipWhitespace(expr.new_iter); expectToken(iter.next(), .comma); iter = skipWhitespace(iter); const tag = iter.next(); expectToken(tag, .text); break :blk .{ .isTag = .{ .tag = tag.?.text, .expression = expr.item, }, }; }, .slice => blk: { const expr = parseExpression(iter); iter = skipWhitespace(expr.new_iter); expectToken(iter.next(), .comma); iter = skipWhitespace(iter); const start = parseExpression(iter); iter = skipWhitespace(start.new_iter); expectToken(iter.next(), .comma); iter = skipWhitespace(iter); const end = parseExpression(iter); iter = skipWhitespace(end.new_iter); break :blk .{ .slice = .{ .iterable = expr.item, .start = start.item, .end = end.item, }, }; }, }; iter = skipWhitespace(iter); expectToken(iter.next(), .close_paren); return .{ .new_iter = iter, .item = call, }; } } fn parseControlBlock(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, ControlBlock) { comptime { var iter = tokens; const strip_before = if (iter.next()) |first| blk: { if (first == .equals) { break :blk true; } iter.putBack(first); break :blk false; } else @compileError("Unexpected end of template"); var stmt: ControlBlock.Data = while (iter.next()) |token| switch (token) { .equals => @compileError("Unexpected '='"), .whitespace => {}, .pound => { const next = iter.next() orelse @compileError("Unexpected end of template"); if (next != .text) @compileError("Expected keyword following '#' character"); const text = next.text; const keyword = std.meta.stringToEnum(Keyword, text) orelse @compileError("Unknown keyword: " ++ text); switch (keyword) { .@"for" => { const result = parseForHeader(iter); iter = result.new_iter; break .{ .for_header = result.item }; }, .@"if" => { const result = parseIfHeader(iter); iter = result.new_iter; break .{ .if_header = result.item }; }, .@"else" => break .{ .@"else" = {} }, .@"elif" => { const result = parseIfHeader(iter); iter = result.new_iter; break .{ .elif_header = result.item }; }, .template => { const result = parseCallTemplate(iter); iter = result.new_iter; break .{ .call_template = result.item }; }, .@"switch" => { const result = parseSwitchHeader(iter); iter = result.new_iter; break .{ .switch_header = result.item }; }, .case => { const result = parseCaseHeader(iter); iter = result.new_iter; break .{ .case_header = result.item }; }, .format => { const result = parseFormat(iter); iter = result.new_iter; break .{ .format = result.item }; }, //else => @compileError("TODO"), } }, .slash => { const next = iter.next() orelse @compileError("Unexpected end of template"); if (next != .text) @compileError("Expected keyword following '/' character"); const text = next.text; const keyword = std.meta.stringToEnum(EndKeyword, text) orelse @compileError("Unknown keyword: " ++ text); switch (keyword) { .@"for" => break .{ .end_for = {} }, .@"if" => break .{ .end_if = {} }, .@"switch" => break .{ .end_switch = {} }, } }, .period, .dollar, .percent => { iter.putBack(token); const expr = parseExpression(iter); iter = expr.new_iter; break .{ .expression = expr.item }; }, else => @compileError("TODO " ++ @tagName(token)), }; // search for end of statement var strip_after: bool = false; while (iter.next()) |token| switch (token) { .whitespace => {}, .equals => { if (iter.peek()) |t| { if (t == .close_bracket) { strip_after = true; continue; } } @compileError("Unexpected '='"); }, .close_bracket => return .{ .new_iter = iter, .item = .{ .block = stmt, .strip_before = strip_before, .strip_after = strip_after, }, }, else => { @compileLog(iter.row); @compileError("TODO " ++ @tagName(token)); }, }; @compileError("Unexpected end of template"); } } fn skipWhitespace(comptime tokens: ControlTokenIter) ControlTokenIter { comptime { var iter = tokens; while (iter.peek()) |token| switch (token) { .whitespace => _ = iter.next(), else => break, }; return iter; } } fn endControlBlock(comptime tokens: ControlTokenIter) ControlTokenIter { comptime { var iter = skipWhitespace(tokens); const token = iter.next() orelse @compileError("Unexpected end of template"); if (token != .close_bracket) @compileError("Unexpected token"); return iter; } } fn parseForHeader(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, ForHeader) { comptime { const iterable = parseExpression(tokens); var iter = skipWhitespace(iterable.new_iter); const captures = tryParseCapture(iter) orelse { @compileLog(iter.row); @compileError("Expected capture"); }; if (captures.item.len == 0 or captures.item.len > 2) @compileError("Expected 1 or 2 captures"); return .{ .new_iter = captures.new_iter, .item = .{ .iterable = iterable.item, .item_capture = captures.item[0], .idx_capture = if (captures.item.len == 2) captures.item[1] else null, }, }; } } fn tryParseCapture(comptime tokens: ControlTokenIter) ?ParseResult(ControlTokenIter, []const []const u8) { comptime { var iter = skipWhitespace(tokens); if ((iter.next() orelse return null) != .pipe) return null; var captures: []const []const u8 = &.{}; while (true) { iter = skipWhitespace(iter); if ((iter.next() orelse return null) != .dollar) return null; iter = skipWhitespace(iter); const name = switch (iter.next() orelse return null) { .text => |text| text, else => return null, }; iter = skipWhitespace(iter); captures = captures ++ &[_][]const u8{name}; switch (iter.next() orelse return null) { .pipe => break, .comma => {}, else => return null, } } return .{ .new_iter = iter, .item = captures, }; } } fn parseIfHeader(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, IfHeader) { comptime { const condition = parseExpression(tokens); var iter = condition.new_iter; const captures = tryParseCapture(iter); if (captures) |cap| { if (cap.item.len == 1) { return .{ .new_iter = cap.new_iter, .item = IfHeader{ .condition = condition.item, .capture = cap.item[0], }, }; } else @compileError("Only one capture allowed for if statements"); } return .{ .new_iter = iter, .item = .{ .condition = condition.item, .capture = null, }, }; } } fn parseCaseHeader(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, CaseHeader) { comptime { var iter = skipWhitespace(tokens); const tag = iter.next(); expectToken(tag, .text); const captures = tryParseCapture(iter); if (captures) |cap| { if (cap.item.len == 1) { return .{ .new_iter = cap.new_iter, .item = CaseHeader{ .tag = tag.?.text, .capture = cap.item[0], }, }; } else @compileError("Only one capture allowed for case statements"); } return .{ .new_iter = iter, .item = .{ .tag = tag.?.text, .capture = null, }, }; } } fn parseSwitchHeader(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, SwitchHeader) { comptime { const condition = parseExpression(tokens); var iter = skipWhitespace(condition.new_iter); const next = iter.next(); expectToken(next, .text); if (!std.mem.eql(u8, next.?.text, "case")) @compileError("Expected case following switch condition"); iter = skipWhitespace(iter); const first = parseCaseHeader(iter); return .{ .new_iter = first.new_iter, .item = .{ .expression = condition.item, .first_case = first.item, }, }; } } fn parseCallTemplate(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, CallTemplate) { comptime { var iter = tokens; const template_name = while (iter.next()) |token| switch (token) { .text => |t| break t, .whitespace => {}, else => @compileError("Unexpected token"), } else @compileError("Unexpected end of template"); const args = parseExpression(iter); return .{ .new_iter = args.new_iter, .item = .{ .template_name = template_name, .args = args.item, }, }; } } fn parseFormat(comptime tokens: ControlTokenIter) ParseResult(ControlTokenIter, FormatStmt) { comptime { var iter = skipWhitespace(tokens); expectToken(iter.next(), .double_quote); var fmt_str: []const u8 = ""; while (true) switch (iter.next() orelse @compileError("Unexpected end of template")) { .text, .number, .whitespace => |t| fmt_str = fmt_str ++ t, .open_bracket => fmt_str = fmt_str ++ "{", .close_bracket => fmt_str = fmt_str ++ "}", .period => fmt_str = fmt_str ++ ".", .pound => fmt_str = fmt_str ++ "#", .pipe => fmt_str = fmt_str ++ "|", .dollar => fmt_str = fmt_str ++ "$", .slash => fmt_str = fmt_str ++ "/", .equals => fmt_str = fmt_str ++ "=", .at => fmt_str = fmt_str ++ "@", .comma => fmt_str = fmt_str ++ ",", .percent => fmt_str = fmt_str ++ "%", .open_paren => fmt_str = fmt_str ++ "(", .close_paren => fmt_str = fmt_str ++ ")", .question_mark => fmt_str = fmt_str ++ "?", .double_quote => break, }; const expr = parseExpression(iter); return .{ .new_iter = expr.new_iter, .item = .{ .format = fmt_str, .value = expr.item, }, }; } } fn ParseResult(comptime It: type, comptime T: type) type { return struct { new_iter: It, item: T, }; } const TemplateItem = union(enum) { text: []const u8, statement: Statement, }; const DerefExpr = struct { container: Expression, field: []const u8, }; const EqualsExpr = struct { lhs: Expression, rhs: Expression, }; const Expression = union(enum) { args: void, captures: void, context: void, deref: *const DerefExpr, equals: *const EqualsExpr, builtin: *const BuiltinCall, optional_unwrap: *const Expression, int: isize, }; const For = struct { subtemplate: []const TemplateItem, header: ForHeader, }; const ForHeader = struct { iterable: Expression, item_capture: []const u8, idx_capture: ?[]const u8, }; const If = struct { subtemplate: []const TemplateItem, header: IfHeader, else_branch: ?union(enum) { @"else": []const TemplateItem, elif: *const If, }, }; const Case = struct { header: CaseHeader, subtemplate: []const TemplateItem, }; const SwitchHeader = struct { expression: Expression, first_case: CaseHeader, }; const CaseHeader = struct { tag: []const u8, capture: ?[]const u8, }; const Switch = struct { expression: Expression, cases: []const Case, else_branch: ?[]const TemplateItem, }; const CallTemplate = struct { template_name: []const u8, args: Expression, }; const IfHeader = struct { condition: Expression, capture: ?[]const u8, }; const FormatStmt = struct { format: []const u8, value: Expression, }; const Statement = union(enum) { expression: Expression, @"for": For, @"if": If, @"switch": Switch, call_template: CallTemplate, format: FormatStmt, }; const ControlBlock = struct { const Data = union(enum) { expression: Expression, for_header: ForHeader, end_for: void, if_header: IfHeader, end_if: void, @"else": void, elif_header: IfHeader, call_template: CallTemplate, switch_header: SwitchHeader, case_header: CaseHeader, end_switch: void, format: FormatStmt, }; block: Data, strip_before: bool, strip_after: bool, }; const Keyword = enum { @"for", @"if", @"else", @"elif", @"template", @"switch", @"case", @"format", }; const EndKeyword = enum { @"for", @"if", @"switch", }; const Builtin = enum { isTag, slice, }; const BuiltinCall = union(Builtin) { isTag: struct { tag: []const u8, expression: Expression, }, slice: struct { iterable: Expression, start: Expression, end: Expression, }, }; const ControlToken = union(enum) { text: []const u8, number: []const u8, open_bracket: void, close_bracket: void, period: void, whitespace: []const u8, pound: void, pipe: void, dollar: void, slash: void, equals: void, at: void, comma: void, percent: void, open_paren: void, close_paren: void, double_quote: void, question_mark: void, }; const ControlTokenIter = struct { start: usize = 0, text: []const u8, peeked_tokens: [2]?ControlToken = [2]?ControlToken{ null, null }, peeked_token_count: usize = 0, row: usize = 0, fn isControlChar(ch: u8) bool { return switch (ch) { '{', '}', '.', '#', '|', '$', '/', '=', '@', ',', '%', '(', ')', '"', '?' => true, else => false, }; } fn isTextChar(ch: u8) bool { return !std.ascii.isWhitespace(ch) and !std.ascii.isDigit(ch) and !isControlChar(ch); } fn next(self: *ControlTokenIter) ?ControlToken { if (self.peeked_token_count != 0) { const t = self.peeked_tokens[self.peeked_token_count - 1].?; self.peeked_tokens[self.peeked_token_count - 1] = null; self.peeked_token_count -= 1; return t; } const remaining = self.text[self.start..]; if (remaining.len == 0) return null; const ch = remaining[0]; self.start += 1; switch (ch) { '{' => return .{ .open_bracket = {} }, '}' => return .{ .close_bracket = {} }, '.' => return .{ .period = {} }, '#' => return .{ .pound = {} }, '|' => return .{ .pipe = {} }, '$' => return .{ .dollar = {} }, '/' => return .{ .slash = {} }, '=' => return .{ .equals = {} }, '@' => return .{ .at = {} }, ',' => return .{ .comma = {} }, '%' => return .{ .percent = {} }, '(' => return .{ .open_paren = {} }, ')' => return .{ .close_paren = {} }, '"' => return .{ .double_quote = {} }, '?' => return .{ .question_mark = {} }, ' ', '\t', '\n', '\r' => { var idx: usize = 0; while (idx < remaining.len and std.mem.indexOfScalar(u8, " \t\n\r", remaining[idx]) != null) : (idx += 1) {} const newline_count = std.mem.count(u8, remaining[0..idx], "\n"); self.row += newline_count; self.start += idx - 1; return .{ .whitespace = remaining[0..idx] }; }, '0'...'9' => { var idx: usize = 0; while (idx < remaining.len and std.ascii.isDigit(remaining[idx])) : (idx += 1) {} self.start += idx - 1; return .{ .number = remaining[0..idx] }; }, else => { var idx: usize = 0; while (idx < remaining.len and isTextChar(remaining[idx])) : (idx += 1) {} self.start += idx - 1; return .{ .text = remaining[0..idx] }; }, } } fn peek(self: *ControlTokenIter) ?ControlToken { const token = self.next(); if (token) |t| self.putBack(t); return token; } fn putBack(self: *ControlTokenIter, token: ControlToken) void { std.debug.assert(self.peeked_token_count < self.peeked_tokens.len); self.peeked_tokens[self.peeked_token_count] = token; self.peeked_token_count += 1; } }; test "template" { const testCase = struct { fn case(comptime tmpl: []const u8, args: anytype, expected: []const u8) !void { var stream = std.io.changeDetectionStream(expected, std.io.null_writer); try execute(stream.writer(), .{}, tmpl, args, .{}); try std.testing.expect(!stream.changeDetected()); } }.case; try testCase("", .{}, ""); try testCase("abcd", .{}, "abcd"); try testCase("{.v}", .{ .v = @as(usize, 3) }, "4"); try testCase("{#if .val}1{/if}", .{ .val = true }, "1"); try testCase("{#for .vals |$v|=} {$v} {=/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); try testCase("{#for .vals |$val|}{$val}{/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); try testCase("{#if .val}1{#else}0{/if}", .{ .val = true }, "1"); try testCase("{#if .val}1{#else}0{/if}", .{ .val = false }, "0"); try testCase("{#if .val}1{#elif .foo}2{/if}", .{ .val = false, .foo = true }, "2"); try testCase("{#if .val}1{#elif .foo}2{/if}", .{ .val = false, .foo = false }, ""); try testCase("{#if .val}1{#elif .foo}2{#else}0{/if}", .{ .val = false, .foo = false }, "0"); try testCase("{#if .val}1{#elif .foo}2{#else}0{/if}", .{ .val = true, .foo = false }, "1"); try testCase("{#if .val}1{#elif .foo}2{#else}0{/if}", .{ .val = false, .foo = true }, "2"); try testCase("{#if .val}1{#elif .foo}2{#else}0{/if}", .{ .val = true, .foo = true }, "1"); }