diff --git a/src/template/lib.zig b/src/template/lib.zig index f1e141d..ff4ea23 100644 --- a/src/template/lib.zig +++ b/src/template/lib.zig @@ -8,8 +8,8 @@ pub fn main() !void { &.{ "5", "4", "3", "2", "1" }, &.{ "5", "4", "3", "2", "1" }, }, - .qux = true, - .quxx = false, + .qux = false, + .quxx = true, }); } @@ -18,7 +18,7 @@ pub fn execute(writer: anytype, comptime template: []const u8, args: anytype) !v const tokens = comptime parseTemplateTokens(ControlTokenIter{ .text = template }); const tmpl = comptime parseTemplate(tokens, 0, .root); - try executeTemplate(writer, tmpl.item, args, .{}); + try executeTemplate(writer, tmpl.items, args, .{}); } fn executeTemplate(writer: anytype, comptime items: []const TemplateItem, args: anytype, captures: anytype) !void { @@ -52,7 +52,14 @@ fn executeStatement(writer: anytype, comptime stmt: Statement, args: anytype, ca .@"if" => |if_stmt| { const condition = evaluateExpression(if_stmt.header.condition, args, captures); const subtemplate = if_stmt.subtemplate; - if (condition) try executeTemplate(writer, subtemplate, args, captures); + if (condition) { + try executeTemplate(writer, subtemplate, args, captures); + } else { + if (if_stmt.else_branch) |branch| switch (branch) { + .@"else" => |subtmpl| try executeTemplate(writer, subtmpl, args, captures), + .elif => |elif| try executeStatement(writer, .{ .@"if" = elif.* }, args, captures), + }; + } }, //else => @compileError("TODO"), } @@ -128,25 +135,77 @@ const TemplateType = enum { root, for_block, if_block, + if_else_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, -) ParseResult(usize, []const TemplateItem) { +) TemplateParseResult { comptime { var i: usize = start; var current_text: []const u8 = ""; var items: []const TemplateItem = &.{}; - while (i < tokens.len) : (i += 1) { + 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 != 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| { @@ -158,55 +217,53 @@ fn parseTemplate( switch (cb.block) { .expression => |expr| items = items ++ [_]TemplateItem{.{ .statement = .{ .expression = expr } }}, .if_header => |header| { - if (i != tokens.len - 1 and tokens[i + 1] == .whitespace and cb.strip_after) i += 1; - const subtemplate = parseTemplate(tokens, i + 1, .if_block); + const parsed = parseIfBlock(header, tokens, i + 1); + i = parsed.new_iter; items = items ++ [_]TemplateItem{.{ - .statement = .{ - .@"if" = .{ - .subtemplate = subtemplate.item, - .header = header, - }, - }, + .statement = .{ .@"if" = parsed.item }, }}; - i = subtemplate.new_iter; }, .for_header => |header| { - if (i != tokens.len - 1 and tokens[i + 1] == .whitespace and cb.strip_after) i += 1; const subtemplate = parseTemplate(tokens, i + 1, .for_block); items = items ++ [_]TemplateItem{.{ .statement = .{ .@"for" = .{ - .subtemplate = subtemplate.item, + .subtemplate = subtemplate.items, .header = header, }, }, }}; - i = subtemplate.new_iter; + i = subtemplate.new_idx; }, .end_for => if (template_type == .for_block) - break + break cb else @compileError("Unexpected /for tag"), - .end_if => if (template_type == .if_block) - break + .end_if => if (template_type == .if_block or template_type == .if_else_block) + break cb else @compileError("Unexpected /if tag"), - } - - if (i != tokens.len - 1 and tokens[i] == .control_block) { - if (tokens[i].control_block.strip_after and tokens[i + 1] == .whitespace) { - i += 1; - } + .elif_header => if (template_type == .if_block) + break cb + else + @compileError("Unexpected #elif tag"), + .@"else" => if (template_type == .if_block) + break cb + else + @compileError("Unexpected #else tag"), } }, } - } else if (template_type != .root) @compileError("End tag not found"); + } 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_iter = i, - .item = items, + .new_idx = i, + .items = items, + .closing_block = closing_block, }; } } @@ -314,6 +371,12 @@ fn parseControlBlock(comptime tokens: ControlTokenIter) ParseResult(ControlToken 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 }; + }, //else => @compileError("TODO"), } @@ -496,6 +559,10 @@ const ForHeader = struct { const If = struct { subtemplate: []const TemplateItem, header: IfHeader, + else_branch: ?union(enum) { + @"else": []const TemplateItem, + elif: *const If, + }, }; const IfHeader = struct { @@ -515,6 +582,8 @@ const ControlBlock = struct { end_for: void, if_header: IfHeader, end_if: void, + @"else": void, + elif_header: IfHeader, }; block: Data, strip_before: bool, @@ -524,6 +593,8 @@ const ControlBlock = struct { const Keyword = enum { @"for", @"if", + @"else", + @"elif", }; const EndKeyword = enum { @@ -617,4 +688,12 @@ test "template" { 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 |$v|=} {$v} {=/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"); } diff --git a/src/template/test.tmp.html b/src/template/test.tmp.html index 72435f4..d8b0353 100644 --- a/src/template/test.tmp.html +++ b/src/template/test.tmp.html @@ -14,8 +14,13 @@ {$b}: {= /for =} {= /for} - {#if .qux}qux!{/if=} - {#if .quxx}quxx!{/if} + {#if .qux=} + qux + {=#elif .quxx=} + quxx + {=#else=} + neither + {=/if}