diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0af9951..b775779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,39 +1,39 @@ -name: CI - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - schedule: - - cron: "0 0 * * *" - workflow_dispatch: - -jobs: - test: - strategy: - matrix: - platform: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.platform }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup Zig - uses: mlugg/setup-zig@v2 - with: - version: master - - - name: Print Zig version - run: zig version - - - name: Build - run: zig build --verbose - - - name: Run Tests - run: | - zig build test --summary all - - - name: Formatting check - if: matrix.platform != 'windows-latest' - run: zig fmt --check . +# name: CI +# +# on: +# push: +# branches: [ main ] +# pull_request: +# branches: [ main ] +# schedule: +# - cron: "0 0 * * *" +# workflow_dispatch: +# +# jobs: +# test: +# strategy: +# matrix: +# platform: [ubuntu-latest, windows-latest, macos-latest] +# runs-on: ${{ matrix.platform }} +# +# steps: +# - uses: actions/checkout@v4 +# +# - name: Setup Zig +# uses: mlugg/setup-zig@v2 +# with: +# version: master +# +# - name: Print Zig version +# run: zig version +# +# - name: Build +# run: zig build --verbose +# +# - name: Run Tests +# run: | +# zig build test --summary all +# +# - name: Formatting check +# if: matrix.platform != 'windows-latest' +# run: zig fmt --check . diff --git a/build.zig b/build.zig index 75eec1f..f8bbd4c 100755 --- a/build.zig +++ b/build.zig @@ -11,11 +11,17 @@ pub fn build(b: *std.Build) !void { const optimize = b.standardOptimizeOption(.{}); const options_files = b.addWriteFiles(); + var threaded: std.Io.Threaded = .init_single_threaded; + // This deinit is not necessary since it's stack allocated but it's ok to + // keep, in case we want to change the threaded initialization at some point + defer threaded.deinit(); + const io = threaded.io(); + const use_llvm = b.option( bool, "use_llvm", "Use LLVM", - ) orelse true; + ); const test_filters = b.option( []const []const u8, @@ -58,6 +64,7 @@ pub fn build(b: *std.Build) !void { "zmpl_templates_paths", "Directories to search for .zmpl templates. Format: `prefix=...,path=...", ) orelse try templatesPaths( + io, b.allocator, &.{.{ .prefix = "templates", @@ -181,14 +188,14 @@ pub fn build(b: *std.Build) !void { const manifest_lazy_path = manifest_exe_run.addOutputFileArg("zmpl.manifest.zig"); manifest_exe_run.setCwd(.{ - .cwd_relative = try std.fs.cwd().realpathAlloc(b.allocator, "."), + .cwd_relative = try std.Io.Dir.cwd().realPathFileAlloc(io, ".", b.allocator), }); manifest_exe_run.expectExitCode(0); manifest_exe_run.addArg(try std.mem.join(b.allocator, ";", templates_paths)); lib.step.dependOn(&manifest_exe_run.step); - for (try findTemplates(b, templates_paths)) |path| + for (try findTemplates(b, io, templates_paths)) |path| manifest_exe_run.addFileArg(.{ .cwd_relative = path }); const compile_step = b.step("compile", "Compile Zmpl templates"); @@ -264,7 +271,7 @@ const TemplatesPath = struct { path: []const []const u8, }; -pub fn templatesPaths(allocator: Allocator, paths: []const TemplatesPath) ![]const []const u8 { +pub fn templatesPaths(io: std.Io, allocator: Allocator, paths: []const TemplatesPath) ![]const []const u8 { var buf: ArrayList([]const u8) = .empty; defer buf.deinit(allocator); for (paths) |path| { @@ -274,7 +281,7 @@ pub fn templatesPaths(allocator: Allocator, paths: []const TemplatesPath) ![]con const absolute_path = if (std.fs.path.isAbsolute(joined)) try allocator.dupe(u8, joined) else - std.fs.cwd().realpathAlloc(allocator, joined) catch |err| + std.Io.Dir.cwd().realPathFileAlloc(io, joined, allocator) catch |err| switch (err) { error.FileNotFound => "_", else => return err, @@ -293,24 +300,24 @@ pub fn templatesPaths(allocator: Allocator, paths: []const TemplatesPath) ![]con return buf.toOwnedSlice(allocator); } -// pub fn addTemplateConstants(b: *Build, comptime constants: type) ![]const u8 { -// const fields = switch (@typeInfo(constants)) { -// .@"struct" => |info| info.fields, -// else => @panic("Expected struct, found: " ++ @typeName(constants)), -// }; -// var array: [fields.len][]const u8 = undefined; -// -// inline for (fields, 0..) |field, index| { -// array[index] = std.fmt.comptimePrint( -// "{s}#{s}", -// .{ field.name, @typeName(field.type) }, -// ); -// } -// -// return std.mem.join(b.allocator, "|", &array); -// } - -fn findTemplates(b: *Build, templates_paths: []const []const u8) ![][]const u8 { +pub fn addTemplateConstants(b: *Build, comptime constants: type) ![]const u8 { + const fields = switch (@typeInfo(constants)) { + .@"struct" => |info| info.fields, + else => @panic("Expected struct, found: " ++ @typeName(constants)), + }; + var array: [fields.len][]const u8 = undefined; + + inline for (fields, 0..) |field, index| { + array[index] = std.fmt.comptimePrint( + "{s}#{s}", + .{ field.name, @typeName(field.type) }, + ); + } + + return std.mem.join(b.allocator, "|", &array); +} + +fn findTemplates(b: *Build, io: std.Io, templates_paths: []const []const u8) ![][]const u8 { var templates: ArrayList([]const u8) = .empty; defer templates.deinit(b.allocator); @@ -326,7 +333,8 @@ fn findTemplates(b: *Build, templates_paths: []const []const u8) ![][]const u8 { for (templates_paths_buf.items) |templates_path| { if (std.mem.eql(u8, templates_path, "_")) continue; - var dir = std.fs.cwd().openDir( + var dir = std.Io.Dir.cwd().openDir( + io, templates_path, .{ .iterate = true }, ) catch |err| { @@ -345,11 +353,11 @@ fn findTemplates(b: *Build, templates_paths: []const []const u8) ![][]const u8 { var walker = try dir.walk(b.allocator); defer walker.deinit(); - while (try walker.next()) |entry| { + while (try walker.next(io)) |entry| { if (entry.kind != .file) continue; const extension = std.fs.path.extension(entry.path); if (!std.mem.eql(u8, extension, ".zmpl")) continue; - try templates.append(b.allocator, try dir.realpathAlloc(b.allocator, entry.path)); + try templates.append(b.allocator, try dir.realPathFileAlloc(io, entry.path, b.allocator)); } } return templates.toOwnedSlice(b.allocator); diff --git a/build.zig.zon b/build.zig.zon index 11ece3c..003e7a2 100755 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,14 +4,14 @@ .fingerprint = 0xef2931206468149, .minimum_zig_version = "0.15.1", .dependencies = .{ - .jetcommon = .{ - .url = "https://github.com/jetzig-framework/jetcommon/archive/ed326c74b98fae893acd89d2e84c0d894a200df3.tar.gz", - .hash = "jetcommon-0.1.0-jPY_DbNIAAA0utNExN3zMeqXA4fhZXQ6LDDP1eg8vodj", - }, .zmd = .{ .url = "https://github.com/jetzig-framework/zmd/archive/87b1c46b517f47b3384eba7ce98d197c463a9d5e.tar.gz", .hash = "zmd-0.1.0-H8YV7bvdAABAVFR2lherqOup7valUMg9UTEiiMFxlTjZ", }, + .jetcommon = .{ + .url = "https://github.com/emneo-dev/jetcommon/archive/c4da8d68813ab6cd384323475753c07d18627656.tar.gz", + .hash = "jetcommon-0.1.0-jPY_DetFAAA8wBYM9TKN5HJ1TAH5qSioHzmZuRTg4H_Z", + }, }, .paths = .{ @@ -24,4 +24,3 @@ "README.md", }, } - diff --git a/src/main.zig b/src/main.zig index ef2022b..8ce6fd6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,8 @@ const std = @import("std"); const ArenaAllocator = std.heap.ArenaAllocator; const GeneralPurposeAllocator = std.heap.GeneralPurposeAllocator; +const Writer = std.Io.Writer; +const Allocator = std.mem.Allocator; const zmpl = @import("zmpl"); @@ -10,20 +12,23 @@ pub fn main() !void { var arena: ArenaAllocator = .init(allocator); var data = zmpl.Data.init(arena.allocator()); + // https://github.com/json-iterator/test-data/blob/master/large-file.json const stat = try std.fs.cwd().statFile("large-file.json"); const json = try std.fs.cwd().readFileAlloc(allocator, "large-file.json", stat.size); // Time to beat: Duration: 1.28s - try benchmark(zmpl.Data.fromJson, .{ &data, json }); + try benchmark(allocator, zmpl.Data.fromJson, .{ &data, json }); // Time to beat: Duration: 946.734ms - _ = try benchmark(zmpl.Data.toJson, .{&data}); + _ = try benchmark(allocator, zmpl.Data.toJson, .{&data}); } -fn benchmark(func: anytype, args: anytype) @typeInfo(@TypeOf(func)).@"fn".return_type.? { - const start = std.time.nanoTimestamp(); - const result = try @call(.auto, func, args); - const end = std.time.nanoTimestamp(); - std.debug.print("Duration: {}\n", .{std.fmt.fmtDuration(@intCast(end - start))}); - return result; +fn benchmark(allocator: Allocator, func: anytype, args: anytype) !void { + const start = std.time.microTimestamp(); + _ = try @call(.auto, func, args); + const end = std.time.microTimestamp(); + var buf: Writer.Allocating = .init(allocator); + defer buf.deinit(); + try buf.writer.printDuration((end - start) * 1000, .{}); + std.debug.print("Duration: {s}\n", .{try buf.toOwnedSlice()}); } diff --git a/src/manifest/Manifest.zig b/src/manifest/Manifest.zig index 8432c2d..e0a572d 100644 --- a/src/manifest/Manifest.zig +++ b/src/manifest/Manifest.zig @@ -28,6 +28,7 @@ pub fn init( pub fn compile( self: *Manifest, + io: std.Io, allocator: Allocator, writer: *Writer, comptime options: type, @@ -107,6 +108,7 @@ pub fn compile( // var file = try std.fs.openFileAbsolute(inner_path.path, .{}); // } try self.compileTemplates( + io, allocator, &template_defs, outer_path, @@ -190,6 +192,7 @@ pub fn compile( fn compileTemplates( self: *Manifest, + io: std.Io, allocator: Allocator, array: *ArrayList(TemplateDef), templates_path: TemplatePath, @@ -197,6 +200,7 @@ fn compileTemplates( template_map: *StringHashMap(Template.TemplateMap), comptime options: type, ) !void { + var read_buffer: [512]u8 = undefined; for (self.template_paths) |template_path| { if (!template_path.present) continue; if (!std.mem.eql(u8, template_path.prefix, templates_path.prefix)) continue; @@ -204,9 +208,10 @@ fn compileTemplates( const key = try util.templatePathStore(allocator, templates_paths_map.get(template_path.prefix).?, template_path.path); const generated_name = template_map.get(template_path.prefix).?.get(key).?; - var file = try std.fs.openFileAbsolute(template_path.path, .{}); - const size = (try file.stat()).size; - const content = try file.readToEndAlloc(allocator, @intCast(size)); + var file = try std.Io.Dir.openFileAbsolute(io, template_path.path, .{}); + const size = (try file.stat(io)).size; + var file_reader = file.reader(io, &read_buffer); + const content = try file_reader.interface.allocRemaining(allocator, .limited(size + 1)); var template: Template = .init( allocator, generated_name, @@ -217,7 +222,7 @@ fn compileTemplates( content, template_map.*, ); - const output = try template.compile(options); + const output = try template.compile(io, options); const template_def: TemplateDef = .{ .key = key, diff --git a/src/manifest/Node.zig b/src/manifest/Node.zig index c8a72b2..b2dccec 100644 --- a/src/manifest/Node.zig +++ b/src/manifest/Node.zig @@ -90,7 +90,7 @@ pub const Writer = struct { } }; -pub fn compile(self: Node, input: []const u8, writer: Writer, options: type) !void { +pub fn compile(self: Node, io: std.Io, input: []const u8, writer: Writer, options: type) !void { var compile_writer = writer; compile_writer.token = self.token; @@ -109,22 +109,22 @@ pub fn compile(self: Node, input: []const u8, writer: Writer, options: type) !vo for (self.children.items) |child_node| { if (start < child_node.token.start) { const content = input[start .. child_node.token.start - 1]; - try self.render(if (initial) .initial else .secondary, content, options, compile_writer); + try self.render(io, if (initial) .initial else .secondary, content, options, compile_writer); initial = false; } start = child_node.token.end + 1; - try child_node.compile(input, compile_writer, options); + try child_node.compile(io, input, compile_writer, options); } if (self.children.items.len == 0) { const content = input[self.token.startOfContent()..self.token.endOfContent()]; - try self.render(.initial, content, options, compile_writer); + try self.render(io, .initial, content, options, compile_writer); } else { const last_child = self.children.items[self.children.items.len - 1]; if (last_child.token.end + 1 < self.token.endOfContent()) { const content = input[last_child.token.end + 1 .. self.token.endOfContent()]; - try self.render(.secondary, content, options, compile_writer); + try self.render(io, .secondary, content, options, compile_writer); } } try self.renderClose(compile_writer); @@ -143,6 +143,7 @@ fn divFormatter(allocator: Allocator, node: ZmdNode) ![]const u8 { fn render( self: Node, + io: std.Io, context: Context, content: []const u8, options: type, @@ -155,6 +156,7 @@ fn render( const stripped_content = try self.stripComments(content); try self.renderMode( + io, self.token.mode, context, stripped_content, @@ -163,7 +165,7 @@ fn render( ); } -fn renderMode(self: Node, mode: Mode, context: Context, content: []const u8, formatters: Formatters, writer: anytype) !void { +fn renderMode(self: Node, io: std.Io, mode: Mode, context: Context, content: []const u8, formatters: Formatters, writer: anytype) !void { switch (mode) { .zig => try self.renderZig(content, writer), .html => try self.renderHtml(content, .{}, writer), @@ -172,12 +174,12 @@ fn renderMode(self: Node, mode: Mode, context: Context, content: []const u8, for .{}, writer, ), - .partial => try self.renderPartial(content, writer), + .partial => try self.renderPartial(io, content, writer), .args => try self.renderArgs(writer), .extend => try self.renderExtend(writer), .@"for" => try self.renderFor(context, content, writer, formatters), .@"if" => try self.renderIf(context, content, writer, formatters), - .block => try self.writeBlock(context, content, formatters), + .block => try self.writeBlock(io, context, content, formatters), .blocks => try self.writeBlocks(writer), } } @@ -375,7 +377,7 @@ fn renderHtml( } } -fn renderPartial(self: Node, content: []const u8, writer: anytype) !void { +fn renderPartial(self: Node, io: std.Io, content: []const u8, writer: anytype) !void { if (self.token.args == null) { std.log.err( "Expected `@partial` with name, no name was given [{}->{}]: '{s}'", @@ -418,7 +420,7 @@ fn renderPartial(self: Node, content: []const u8, writer: anytype) !void { return error.ZmplSyntaxError; } - const expected_partial_args = try self.getPartialArgsSignature(prefix, partial_name); + const expected_partial_args = try self.getPartialArgsSignature(io, prefix, partial_name); var reordered_args: ArrayList(Arg) = .empty; defer reordered_args.deinit(self.allocator); @@ -542,7 +544,7 @@ fn renderPartial(self: Node, content: []const u8, writer: anytype) !void { \\ const __slots = [_]__zmpl.Data.Slot{{ \\{[items]s} \\ }}; - \\ var __partial_data: __zmpl.Data = .init(allocator); + \\ var __partial_data: __zmpl.Data = .init(zmpl.io, allocator); \\ __partial_data.template_decls = zmpl.template_decls; \\ defer __partial_data.deinit(); \\ @@ -877,7 +879,7 @@ fn ifStatement(self: Node, input: []const u8) !IfStatement { // Write a `@block` definition - note that we write to a different output buffer here - each // block is compiled into a separate function which is written after the main manifest body. -fn writeBlock(self: Node, context: Context, content: []const u8, formatters: Formatters) !void { +fn writeBlock(self: Node, io: std.Io, context: Context, content: []const u8, formatters: Formatters) !void { if (context == .initial) { const args = self.token.args orelse { std.log.err("Missing argument to `@block` mode: `{s}`", .{self.token.mode_line}); @@ -915,12 +917,12 @@ fn writeBlock(self: Node, context: Context, content: []const u8, formatters: For .{}, writer, ), - .partial => try self.renderPartial(content, writer), + .partial => try self.renderPartial(io, content, writer), .args => try self.renderArgs(writer), .extend => try self.renderExtend(writer), .@"for" => try self.renderFor(context, content, writer, formatters), .@"if" => try self.renderIf(context, content, writer, formatters), - .block => try self.writeBlock(context, content, formatters), + .block => try self.writeBlock(io, context, content, formatters), .blocks => try self.writeBlocks(writer), } } else { @@ -1145,7 +1147,7 @@ fn renderZigLiteral( // Parse a target partial's `@args` pragma in order to re-order keyword args if needed. // We need to read direct from the file here because we can't guarantee that the target partial // has been parsed yet. -fn getPartialArgsSignature(self: Node, prefix: []const u8, partial_name: []const u8) ![]Arg { +fn getPartialArgsSignature(self: Node, io: std.Io, prefix: []const u8, partial_name: []const u8) ![]Arg { const fetch_name = try util.templatePathFetch(self.allocator, partial_name, true); std.mem.replaceScalar(u8, fetch_name, '/', std.fs.path.sep); const with_extension = try std.mem.concat(self.allocator, u8, &[_][]const u8{ fetch_name, ".zmpl" }); @@ -1159,7 +1161,7 @@ fn getPartialArgsSignature(self: Node, prefix: []const u8, partial_name: []const }; const path = try std.fs.path.join(self.allocator, &[_][]const u8{ templates_path, with_extension }); defer self.allocator.free(path); - const content = util.readFile(self.allocator, std.fs.cwd(), path) catch return &.{}; + const content = try util.readFile(io, self.allocator, std.Io.Dir.cwd(), path); defer self.allocator.free(content); var it = std.mem.splitScalar(u8, content, '\n'); diff --git a/src/manifest/Template.zig b/src/manifest/Template.zig index b14d11c..b869cd5 100644 --- a/src/manifest/Template.zig +++ b/src/manifest/Template.zig @@ -104,7 +104,7 @@ pub fn deinit(self: *Template) void { } /// Compile a template into a Zig code which can then be written out and compiled by Zig. -pub fn compile(self: *Template, comptime options: type) ![]const u8 { +pub fn compile(self: *Template, io: std.Io, comptime options: type) ![]const u8 { if (self.state != .initial) unreachable; try self.tokenize(); @@ -115,7 +115,7 @@ pub fn compile(self: *Template, comptime options: type) ![]const u8 { const writer = Node.Writer{ .allocator = self.allocator, .buf = &buf, .token = self.tokens.items[0] }; try self.renderHeader(writer, options); - try self.root_node.compile(self.input, writer, options); + try self.root_node.compile(io, self.input, writer, options); try self.renderFooter(writer); @@ -259,8 +259,8 @@ fn appendToken(self: *Template, context: Context, end: usize, depth: usize) !voi var args = std.mem.trim(u8, mode_line, &std.ascii.whitespace); args = switch (context.delimiter) { .none, .eof => args, - .string => |delimiter_string| std.mem.trimRight(u8, args, delimiter_string), - .brace => std.mem.trimRight(u8, args, "}"), + .string => |delimiter_string| std.mem.trimEnd(u8, args, delimiter_string), + .brace => std.mem.trimEnd(u8, args, "}"), }; const args_start = @tagName(context.mode).len + 1; args = if (args_start <= args.len) @@ -647,7 +647,7 @@ fn renderFooter(self: Template, writer: anytype) !void { \\ const __inner_content = try allocator.dupe(u8, try zmpl.output_buf.toOwnedSlice()); \\ zmpl.content = .{{ .data = zmpl.strip(__inner_content) }}; \\ zmpl.output_buf.clearRetainingCapacity(); - \\ const __content = try __capture.render(zmpl, Context, context, {s}{s}, .{{}}); + \\ const __content = try __capture.render(zmpl.io, zmpl, Context, context, {s}{s}, .{{}}); \\ return __content; \\ }} else {{ \\ const output = try zmpl.output_buf.toOwnedSlice(); @@ -698,7 +698,7 @@ fn renderFooter(self: Template, writer: anytype) !void { \\ ); \\ zmpl.content = .{{ .data = zmpl.strip(inner_content) }}; \\ zmpl.output_buf.clearRetainingCapacity(); - \\ const content = try layout.render(zmpl, Context, context, blocks, .{{}}); + \\ const content = try layout.render(zmpl.io, zmpl, Context, context, blocks, .{{}}); \\ return zmpl.strip(content); \\}} \\ diff --git a/src/manifest/main.zig b/src/manifest/main.zig index 9291f28..20e3e23 100644 --- a/src/manifest/main.zig +++ b/src/manifest/main.zig @@ -22,7 +22,7 @@ pub fn main() !void { if (std.mem.eql(u8, permitted_field, field.name)) break; } else { std.debug.print( - "[zmpl] Unrecgonized option: `{s}: {s}`\n", + "[zmpl] Unrecognized option: `{s}: {s}`\n", .{ field.name, @typeName(field.type) }, ); std.process.exit(1); @@ -38,6 +38,9 @@ pub fn main() !void { defer arena.deinit(); const allocator = arena.allocator(); + var threaded = std.Io.Threaded.init_single_threaded; + const io = threaded.io(); + const args = try std.process.argsAlloc(allocator); const manifest_path = args[1]; @@ -55,7 +58,7 @@ pub fn main() !void { const present = !std.mem.eql(u8, path, "_"); try templates_paths.append(allocator, .{ .prefix = prefix, - .path = if (present) try std.fs.realpathAlloc(allocator, path) else "_", + .path = if (present) try std.Io.Dir.cwd().realPathFileAlloc(io, path, allocator) else "_", .present = present, }); } @@ -79,15 +82,16 @@ pub fn main() !void { var manifest: Manifest = .init(templates_paths.items, template_paths_buf.items); - const file = try std.fs.cwd().createFile(manifest_path, .{ .truncate = true }); + const file = try std.Io.Dir.cwd().createFile(io, manifest_path, .{ .truncate = true }); var buffer: [1024]u8 = undefined; - var writer = file.writerStreaming(&buffer); + var writer = file.writerStreaming(io, &buffer); try manifest.compile( + io, allocator, &writer.interface, zmpl_options, ); - file.close(); + file.close(io); } test { diff --git a/src/manifest/util.zig b/src/manifest/util.zig index 1817482..ebc8839 100644 --- a/src/manifest/util.zig +++ b/src/manifest/util.zig @@ -5,7 +5,7 @@ const Writer = std.Io.Writer; /// The first non-whitespace character of a given input (line). pub fn firstMeaningfulChar(input: []const u8) ?u8 { - const stripped = std.mem.trimLeft(u8, input, &std.ascii.whitespace); + const stripped = std.mem.trimStart(u8, input, &std.ascii.whitespace); if (stripped.len == 0) return null; @@ -14,7 +14,7 @@ pub fn firstMeaningfulChar(input: []const u8) ?u8 { /// Detect if a given input string begins with a given value, ignoring leading whitespace. pub fn startsWithIgnoringWhitespace(haystack: []const u8, needle: []const u8) bool { - const stripped = std.mem.trimLeft(u8, haystack, &std.ascii.whitespace); + const stripped = std.mem.trimStart(u8, haystack, &std.ascii.whitespace); return std.mem.startsWith(u8, stripped, needle); } @@ -22,7 +22,7 @@ pub fn startsWithIgnoringWhitespace(haystack: []const u8, needle: []const u8) bo /// Detect if a given input string begins with a given value, ignoring leading whitespace. pub fn indexOfIgnoringWhitespace(haystack: []const u8, needle: []const u8) ?usize { // FIXME: This function makes no sense. - const trimmed = std.mem.trimLeft(u8, haystack, &std.ascii.whitespace); + const trimmed = std.mem.trimStart(u8, haystack, &std.ascii.whitespace); if (std.mem.indexOf(u8, trimmed, needle)) |index| { return (haystack.len - trimmed.len) + index; } else { @@ -160,7 +160,7 @@ pub inline fn strip(input: []const u8) []const u8 { /// Strip surrounding parentheses from a []const u8: `(foobar)` becomes `foobar`. pub inline fn trimParentheses(input: []const u8) []const u8 { - return std.mem.trimRight(u8, std.mem.trimLeft(u8, input, "("), ")"); + return std.mem.trimEnd(u8, std.mem.trimStart(u8, input, "("), ")"); } /// Strip all leading and trailing `\n` except one. @@ -219,8 +219,8 @@ pub fn normalizePathPosix(allocator: Allocator, path: []const u8) ![]const u8 { } /// Try to read a file and return content, output a helpful error on failure. -pub fn readFile(allocator: Allocator, dir: std.fs.Dir, path: []const u8) ![]const u8 { - const stat = dir.statFile(path) catch |err| { +pub fn readFile(io: std.Io, allocator: Allocator, dir: std.Io.Dir, path: []const u8) ![]const u8 { + const stat = dir.statFile(io, path, .{}) catch |err| { switch (err) { error.FileNotFound => { std.debug.print("[zmpl] File not found: {s}\n", .{path}); @@ -229,8 +229,7 @@ pub fn readFile(allocator: Allocator, dir: std.fs.Dir, path: []const u8) ![]cons else => return err, } }; - const content = std.fs.cwd().readFileAlloc(allocator, path, @intCast(stat.size)); - return content; + return std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(stat.size + 1)); } /// Output an escaped string suitable for use in generated Zig code. diff --git a/src/tests.zig b/src/tests.zig index ab235c8..48a0df2 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -1,21 +1,21 @@ const std = @import("std"); -const zmpl = @import("zmpl"); const allocator = std.testing.allocator; const ArenaAllocator = std.heap.ArenaAllocator; const ArrayList = std.ArrayList; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const expectEqualStrings = std.testing.expectEqualStrings; +const io = std.testing.io; + +const zmpl = @import("zmpl"); +const Data = zmpl.Data; const jetcommon = @import("jetcommon"); const Context = struct { foo: []const u8 = "default" }; test "readme example" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var body = try data.object(); @@ -29,7 +29,7 @@ test "readme example" { try body.put("auth", auth); const template = zmpl.find("example") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ @@ -55,10 +55,7 @@ test "readme example" { } test "object passing to partial" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -70,7 +67,7 @@ test "object passing to partial" { try root.put("user", user); const template = zmpl.find("object_root_layout") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\

User

@@ -81,10 +78,7 @@ test "object passing to partial" { } test "complex example" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var body = try data.object(); @@ -99,7 +93,7 @@ test "complex example" { try body.put("auth", auth); const template = zmpl.find("complex_example") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\
hello
\\

Slots:

@@ -175,14 +166,11 @@ test "direct rendering of slots (render [][]const u8 as line-separated string)" } test "javascript" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("javascript") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ \\ { is my favorite character @@ -196,14 +184,11 @@ test "javascript" { } test "partials without blocks" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("partials_without_blocks") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ Blah partial content \\
bar
Blah partial content @@ -212,14 +197,11 @@ test "partials without blocks" { } test "custom delimiters" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("custom_delimiters") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\

Built-in markdown support

\\
@@ -234,14 +216,11 @@ test "custom delimiters" { } test ".md.zmpl extension" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("markdown_extension") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\

Hello

\\
@@ -249,14 +228,11 @@ test ".md.zmpl extension" { } test "default partial arguments" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("default_partial_arguments") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\bar, default value \\ @@ -264,14 +240,11 @@ test "default partial arguments" { } test "escaping (HTML and backslash escaping" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("escaping") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\
<div>
         \\  @partial foo("bar")
@@ -281,10 +254,7 @@ test "escaping (HTML and backslash escaping" {
 }
 
 test "references combined with markdown" {
-    var arena: ArenaAllocator = .init(allocator);
-    defer arena.deinit();
-
-    var data: zmpl.Data = .init(arena.allocator());
+    var data: Data = .init(io, allocator);
     defer data.deinit();
 
     var object = try data.object();
@@ -292,7 +262,7 @@ test "references combined with markdown" {
     try object.put("title", data.string("jetzig.dev"));
 
     const template = zmpl.find("references_markdown") orelse return expect(false);
-    const output = try template.render(&data, Context, .{}, &.{}, .{});
+    const output = try template.render(io, &data, Context, .{}, &.{}, .{});
     try expectEqualStrings(
         \\

Test

\\ @@ -303,10 +273,7 @@ test "references combined with markdown" { } test "partial arg type coercion" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var object = try data.object(); @@ -315,7 +282,7 @@ test "partial arg type coercion" { try object.put("baz", data.string("qux")); const template = zmpl.find("partial_arg_type_coercion") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\100 \\123.456 @@ -325,14 +292,12 @@ test "partial arg type coercion" { } test "inheritance" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("inheritance_child") orelse return expect(false); const output = try template.render( + io, &data, Context, .{}, @@ -354,10 +319,7 @@ test "inheritance" { } test "root init" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -371,7 +333,7 @@ test "root init" { try root.put("auth", auth); const template = zmpl.find("example") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ @@ -397,17 +359,14 @@ test "root init" { } test "reference stripping" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); try root.put("message", data.string("hello")); const template = zmpl.find("reference_with_spaces") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\
hello
@@ -416,10 +375,7 @@ test "reference stripping" { } test "inferred type in put/append" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const TestEnum = enum { field_a, field_b }; @@ -450,7 +406,7 @@ test "inferred type in put/append" { try root.put("optional", optional); const template = zmpl.find("basic") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\hello @@ -464,10 +420,7 @@ test "inferred type in put/append" { } test "getT(.array, ...) and getT(.object, ...)" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -489,10 +442,7 @@ test "getT(.array, ...) and getT(.object, ...)" { } test "object.remove(...)" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var obj = try data.object(); @@ -506,10 +456,7 @@ test "object.remove(...)" { } test "getStruct from object" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -551,10 +498,7 @@ test "getStruct from object" { } test "Array.items()" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var array = try data.array(); @@ -567,10 +511,7 @@ test "Array.items()" { } test "Object.items()" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var object = try data.object(); @@ -588,10 +529,7 @@ test "Object.items()" { } test "toJson()" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var object = try data.object(); @@ -607,10 +545,7 @@ test "toJson()" { } test "put slice" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -633,10 +568,7 @@ test "put slice" { } test "iteration" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -652,7 +584,7 @@ test "iteration" { try root.put("objects", objects); const template = zmpl.find("iteration") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ \\
baz
@@ -683,10 +615,7 @@ test "iteration" { } test "datetime format" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -697,7 +626,7 @@ test "datetime format" { try root.put("bar", bar); const template = zmpl.find("datetime_format") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\
Tue Sep 24 19:30:35 2024
\\
2024-09-24
@@ -708,10 +637,7 @@ test "datetime format" { } test "datetime" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -722,10 +648,7 @@ test "datetime" { } test "for with partial" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -734,7 +657,7 @@ test "for with partial" { try array.append(.{ .foo = "foo2", .bar = "bar2" }); const template = zmpl.find("for_with_partial") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\foo1: bar1 \\
foo1
@@ -747,10 +670,7 @@ test "for with partial" { } test "error union" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -760,17 +680,14 @@ test "error union" { } test "xss sanitization/raw formatter" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); try root.put("foo", ""); const template = zmpl.find("xss") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\<script>alert(':)');</script> \\ @@ -779,10 +696,7 @@ test "xss sanitization/raw formatter" { } test "if/else" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -799,7 +713,7 @@ test "if/else" { try foo.put("falsey", false); const template = zmpl.find("if_else") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ \\ expected here @@ -829,10 +743,7 @@ test "if/else" { } test "for with zmpl value" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -843,7 +754,7 @@ test "for with zmpl value" { try foo.append("qux"); const template = zmpl.find("for_with_zmpl_value_main") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ \\ bar @@ -857,14 +768,11 @@ test "for with zmpl value" { } test "comments" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); const template = zmpl.find("comments") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ \\ @@ -874,10 +782,7 @@ test "comments" { } test "for with if" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.object(); @@ -887,7 +792,7 @@ test "for with if" { try things.append(.{ .foo = "quux", .bar = "corge", .time = "2024-11-24T18:51:23Z" }); const template = zmpl.find("for_with_if") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\
foo: bar \\ @@ -918,10 +823,7 @@ test "for with if" { } test "mix mardown and zig" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.object(); @@ -934,7 +836,7 @@ test "mix mardown and zig" { // markdown (i.e. the parent's mode) but the list gets broken into three parts intsead of a // single list. const template = zmpl.find("mix_markdown_and_zig") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\

Header

\\
  • list item 1
  • list item 2
  • qux
  • corge
  • last item
  • qux
@@ -949,10 +851,7 @@ test "nullable if" { // Test with null value - should be falsey { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var clip = try data.object(); @@ -962,16 +861,13 @@ test "nullable if" { try root.put("clip", clip); const template = zmpl.find("nullable_if") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings("\nThe value is null\n", output); } // Test with non-null, non-empty string - should be truthy { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var clip = try data.object(); @@ -981,17 +877,14 @@ test "nullable if" { try root.put("clip", clip); const template = zmpl.find("nullable_if") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); // Non-empty string should correctly evaluate as truthy try expectEqualStrings("\nThe value is not null\n", output); } // Test with empty string - should be falsey like null { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var clip = try data.object(); @@ -1001,16 +894,13 @@ test "nullable if" { try root.put("clip", clip); const template = zmpl.find("nullable_if") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings("\nThe value is null\n", output); } } test "if statement with indented HTML - if branch" { - var arena: ArenaAllocator = .init(allocator); - defer arena.deinit(); - - var data: zmpl.Data = .init(arena.allocator()); + var data: Data = .init(io, allocator); defer data.deinit(); var root = try data.root(.object); @@ -1020,7 +910,7 @@ test "if statement with indented HTML - if branch" { try root.put("user", user); const template = zmpl.find("if_indented_html") orelse return expect(false); - const output = try template.render(&data, Context, .{}, &.{}, .{}); + const output = try template.render(io, &data, Context, .{}, &.{}, .{}); try expectEqualStrings( \\ \\