diff --git a/build.zig b/build.zig index 9b07cc6db28aa2eaacbc0711ed3bad0b259f00b6..6707e7e23a2b762fe64e6c6d2ba05b6a21def336 100644 --- a/build.zig +++ b/build.zig @@ -14,15 +14,15 @@ pub fn build(b: *std.Build) void { deps.addAllTo(exe); b.installArtifact(exe); - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { - run_cmd.addArgs(args); - } + const tests = b.addTest(.{ + .root_source_file = b.path("test.zig"), + .target = target, + .optimize = mode, + }); + deps.addAllTo(tests); - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); - - const test_step = b.step("test", "dummy test step to pass CI checks"); - _ = test_step; + const test_step = b.step("test", "Run all library tests"); + const tests_run = b.addRunArtifact(tests); + tests_run.has_side_effects = true; + test_step.dependOn(&tests_run.step); } diff --git a/licenses.txt b/licenses.txt index de393c6167e7b50a0c7c59a240ed29c1f0997f5a..a0b42e8fbd5d71fef3c7492a22023185e81b272b 100644 --- a/licenses.txt +++ b/licenses.txt @@ -4,8 +4,7 @@ MPL-2.0: MIT: = https://spdx.org/licenses/MIT +- git https://github.com/nektro/zig-expect - git https://github.com/nektro/zig-extras - git https://github.com/nektro/zig-tracer - -Unspecified: - git https://github.com/kivikakk/htmlentities.zig diff --git a/src/lib.zig b/src/lib.zig index b59e555da076f32743f5a10ccc9e156c4b51273a..bc56cd9a6864bbd1a5b6b955c634658e2d51641c 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -12,6 +12,7 @@ const string = []const u8; const htmlentities = @import("htmlentities"); const root = @import("root"); const tracer = @import("tracer"); +const extras = @import("extras"); const tokenize = @import("./tokenize.zig"); const astgen = @import("./astgen.zig"); @@ -65,7 +66,7 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d .string => try writer.print(" {s}=\"{}\"", .{ it.key, std.zig.fmtEscapes(it.value.string[1 .. it.value.string.len - 1]) }), .body => { try writer.print(" {s}=\"", .{it.key}); - try do(alloc, writer, astgen.Value{ .body = it.value.body }, data, ctx, opts); + inline for (it.value.body) |bval| try do(alloc, writer, bval, data, ctx, opts); try writer.print("\"", .{}); }, } @@ -103,7 +104,7 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d const TO = @TypeOf(x); const TI = @typeInfo(TO); - if (comptime std.meta.trait.isZigString(TO)) { + if (comptime extras.isZigString(TO)) { if (repl.raw) { try writer.writeAll(x); return; @@ -117,10 +118,10 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d try writer.print("{d}", .{x}); return; } - if (comptime std.meta.trait.hasFn("format")(TO)) { + if (comptime extras.hasFn("format")(TO)) { return std.fmt.format(writer, "{}", .{x}); } - if (comptime std.meta.trait.hasFn("toString")(TO)) { + if (comptime extras.hasFn("toString")(TO)) { try writer.writeAll(try x.toString(alloc)); return; } @@ -167,7 +168,7 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d return; } comptime assertEqual(v.args.len, 1); - if (comptime std.meta.trait.isIndexable(T)) { + if (comptime extras.isIndexable(T)) { try doif(alloc, writer, body, bottom, data, ctx, opts, x.len > 0); return; } @@ -197,7 +198,7 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d return; } comptime assertEqual(v.args.len, 1); - if (comptime std.meta.trait.isIndexable(T)) { + if (comptime extras.isIndexable(T)) { try doif(alloc, writer, body, bottom, data, ctx, opts, x.len == 0); return; } @@ -210,10 +211,10 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d .ifequal => { comptime assertEqual(v.args.len, 2); const y = try resolveArg(v.args[1], alloc, data, ctx, opts); - if (@typeInfo(@TypeOf(x)) == .Enum and comptime std.meta.trait.isZigString(@TypeOf(y))) { + if (@typeInfo(@TypeOf(x)) == .Enum and comptime extras.isZigString(@TypeOf(y))) { return try doif(alloc, writer, body, bottom, data, ctx, opts, std.mem.eql(u8, @tagName(x), y)); } - if (comptime std.meta.trait.isSlice(@TypeOf(x, y))) { + if (comptime extras.isSlice(@TypeOf(x, y))) { return try doif(alloc, writer, body, bottom, data, ctx, opts, std.mem.eql(u8, x, y)); } try doif(alloc, writer, body, bottom, data, ctx, opts, x == y); @@ -221,10 +222,10 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d .ifnotequal => { comptime assertEqual(v.args.len, 2); const y = try resolveArg(v.args[1], alloc, data, ctx, opts); - if (@typeInfo(@TypeOf(x)) == .Enum and comptime std.meta.trait.isZigString(@TypeOf(y))) { + if (@typeInfo(@TypeOf(x)) == .Enum and comptime extras.isZigString(@TypeOf(y))) { return try doif(alloc, writer, body, bottom, data, ctx, opts, !std.mem.eql(u8, @tagName(x), y)); } - if (comptime std.meta.trait.isSlice(@TypeOf(x, y))) { + if (comptime extras.isSlice(@TypeOf(x, y))) { return try doif(alloc, writer, body, bottom, data, ctx, opts, !std.mem.eql(u8, x, y)); } try doif(alloc, writer, body, bottom, data, ctx, opts, x != y); @@ -265,6 +266,7 @@ fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, d const ATT = std.meta.fieldInfo(AT, .@"3").type; if (v.args.len != std.meta.fields(ATT).len) @compileError(std.fmt.comptimePrint("expected:{d} - actual:{d}", .{ std.meta.fields(ATT).len, v.args.len })); var tupargs = @as(ATT, undefined); + _ = &tupargs; var args = .{ alloc, list.writer(), @@ -342,7 +344,7 @@ fn FieldSearch(comptime T: type, comptime args: []const string) type { } fn Field(comptime T: type, comptime field_name: string) type { - if (std.meta.trait.isIndexable(T) and std.mem.eql(u8, field_name, "len")) { + if (extras.isIndexable(T) and std.mem.eql(u8, field_name, "len")) { return usize; } switch (@typeInfo(T)) { @@ -464,7 +466,7 @@ fn docap(alloc: std.mem.Allocator, writer: anytype, comptime top: astgen.Value, } } -fn isArrayOf(comptime T: type) std.meta.trait.TraitFn { +fn isArrayOf(comptime T: type) fn (type) bool { const Closure = struct { pub fn trait(comptime C: type) bool { return switch (@typeInfo(C)) { diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index b53aae337cf4bbf90331727e5ff974a6c48ea59f..0000000000000000000000000000000000000000 --- a/src/main.zig +++ /dev/null @@ -1,79 +0,0 @@ -const std = @import("std"); -const pek = @import("pek"); - -const example_document = - \\html[lang="en"]( - \\ head( - \\ meta[charset="utf-8"] - \\ title("Pek Example") - \\ meta[name="viewport" content="width=device-width,initial-scale=1"] - \\ meta[http-equiv="X-UA-Compatible" content="IE=edge"] - \\ ) - \\ body( - \\ h1("Pek Example") - \\ hr - \\ p("This is an example HTML document written in "a[href="https://github.com/nektro/zig-pek"]("Pek")".") - \\ p("Pek is written by "{author}".") - \\ p("Her favorite plant is the "{favorite.flower}) - \\ p("Hello, 世界") - \\ p("The most populous US cities are:") - \\ ul( - \\ {#each top_cities} - \\ li({this.name}", "{this.state.code}) - \\ /each/ - \\ ) - \\ p("Spooky text: "{spooky}) - \\ - \\ {#if am_i_a_girl} - \\ p("#1") - \\ /if/ - \\ - \\ {#ifnot is_it_my_birthday} - \\ p("#2") - \\ /ifnot/ - \\ - \\ {#ifequal top_cities.len best_rating} - \\ p("#3") - \\ /ifequal/ - \\ - \\ {#ifnotequal favorite.color sky} - \\ p("#4") - \\ /ifnotequal/ - \\ ) - \\) -; - -const S = struct { - name: []const u8, - state: struct { - code: []const u8, - }, -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const alloc = gpa.allocator(); - - var name: []const u8 = "Meghan D"; - const doc = comptime pek.parse(example_document); - try pek.compile(@This(), alloc, std.io.getStdOut().writer(), doc, .{ - .author = name, - .favorite = .{ - .flower = "Sunflower", - .program_lang = "Zig", - .color = "Pink", - }, - .top_cities = &[_]S{ - .{ .name = "New York", .state = .{ .code = "NY" } }, - .{ .name = "Los Angeles", .state = .{ .code = "CA" } }, - .{ .name = "Chicago", .state = .{ .code = "IL" } }, - .{ .name = "Houston", .state = .{ .code = "TX" } }, - .{ .name = "Phoenix", .state = .{ .code = "AZ" } }, - }, - .spooky = "I better not be in bold.", - .am_i_a_girl = true, - .sky = "Blue", - .best_rating = @as(usize, 5), - .is_it_my_birthday = false, - }); -} diff --git a/test.zig b/test.zig new file mode 100644 index 0000000000000000000000000000000000000000..0d6bee6829340d27820ff471ae10d373c6c8dd5f --- /dev/null +++ b/test.zig @@ -0,0 +1,867 @@ +const std = @import("std"); +const pek = @import("pek"); +const expect = @import("expect").expect; + +test { + std.testing.refAllDeclsRecursive(pek); +} + +test "document" { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\html[lang="en"]( + \\ head( + \\ title("Pek Example") + \\ meta[charset="UTF-8"] + \\ meta[name="viewport" content="width=device-width,initial-scale=1"] + \\ ) + \\ body( + \\ h1("Pek Example") + \\ hr + \\ p("This is an example HTML document written in "a[href="https://github.com/nektro/zig-pek"]("Pek")".") + \\ ) + \\) + ); + try pek.compile( + @This(), + alloc, + builder.writer(), + doc, + .{}, + ); + try expect(builder.items).toEqualString( + \\ + \\ + \\
+ \\This is an example HTML document written in Pek.
+ \\ + \\ + \\ + ); +} + +test "fragment" { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ h1("Pek Example") + \\ hr + \\ p("This is an example HTML document written in "a[href="https://github.com/nektro/zig-pek"]("Pek")".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{}, + ); + try expect(builder.items).toEqualString( + \\ + \\This is an example HTML document written in Pek.
+ \\ + \\ + ); +} + +test "if: basic" { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ h1("Pek Example") + \\ hr + \\ {#if foo} + \\ p("This is an example HTML document written in "a[href="https://github.com/nektro/zig-pek"]("Pek")".") + \\ /if/ + \\ {#if bar} + \\ p("This will show up because "code("bar")" is true instead.") + \\ /if/ + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .foo = false, .bar = true }, + ); + try expect(builder.items).toEqualString( + \\ + \\This will show up because bar is true instead.
This will show up because bar is false.
This will show up because bar is true.
This will show up because bar is false.
This will show up because bar is true.
This will show up because bar is false.
This will show up because bar is 'foo'.
This will show up because bar is 'qux'.
This will show up because bar is .foo.
This will show up because bar is .qux.
This will show up because bar is false.
This will show up because bar is true.
This will show up because bar is 'qux'.
This will show up because bar is 'foo'.
This will show up because bar is .qux.
This will show up because bar is .foo.
This text for this link is dynamic.
+ \\ + \\ + ); +} +test { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ p("This url for this link is "a[href=({url})]("dynamic")".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .url = @as([]const u8, "https://github.com/nektro/zig-pek") }, + ); + try expect(builder.items).toEqualString( + \\ + \\This url for this link is dynamic.
+ \\ + \\ + ); +} +test { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ p("This host for this link is "a[href=("https://"{host}"/nektro/zig-pek")]("dynamic")".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .host = @as([]const u8, "github.com") }, + ); + try expect(builder.items).toEqualString( + \\ + \\This host for this link is dynamic.
+ \\ + \\ + ); +} +test { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ p("This host for this link is "a[href=("https://"{host}"/nektro/zig-pek")]("dynamic")".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .host = "github.com".* }, + ); + try expect(builder.items).toEqualString( + \\ + \\This host for this link is dynamic.
+ \\ + \\ + ); +} + +// replacement with custom serializer +test { + const S = struct { + name: []const u8, + + pub fn format(s: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + _ = fmt; + _ = options; + try writer.writeAll(s.name); + try writer.writeAll(".com"); + } + }; + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ p("This host for this link is "a[href=("https://"{host}"/nektro/zig-pek")]("dynamic")".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .host = S{ .name = "github" } }, + ); + try expect(builder.items).toEqualString( + \\ + \\This host for this link is dynamic.
+ \\ + \\ + ); +} +test { + const S = struct { + name: []const u8, + + pub fn toString(s: @This(), alloc: std.mem.Allocator) ![]const u8 { + return std.fmt.allocPrint(alloc, "{s}.com", .{s.name}); + } + }; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ p("This host for this link is "a[href=("https://"{host}"/nektro/zig-pek")]("dynamic")".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .host = S{ .name = "github" } }, + ); + try expect(builder.items).toEqualString( + \\ + \\This host for this link is dynamic.
+ \\ + \\ + ); +} + +// raw replacement +test { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ p("This url for this link is "a[href=({{url}})]("dynamic but not escaped")".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .url = "https://github.com/nektro/zig-pek".* }, + ); + try expect(builder.items).toEqualString( + \\ + \\This url for this link is dynamic but not escaped.
+ \\ + \\ + ); +} +test { + const alloc = std.testing.allocator; + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + const doc = comptime pek.parse( + \\body( + \\ p("Raw replacements can also "{{foo}}".") + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = @This(), .indent = 0, .flag1 = false }, + .{ .foo = "contain HTML such as as an escape hatch when you know content is safe".* }, + ); + try expect(builder.items).toEqualString( + \\ + \\Raw replacements can also contain HTML such as as an escape hatch when you know content is safe.
+ \\ + \\ + ); +} + +// custom function +test { + const C = struct { + pub fn pek__input_text(alloc: std.mem.Allocator, writer: pek.Writer, comptime opts: pek.DoOptions, args: struct { []const u8, []const u8, []const u8, []const u8 }) !void { + const tmpl = comptime pek.parse( + \\label( + \\ div({label}) + \\ textarea[type="text" required="" name=({name}) placeholder=({placeholder})]({value}) + \\) + ); + try pek.compileInner(alloc, writer, tmpl, opts, .{ + .name = args[0], + .label = args[1], + .value = args[2], + .placeholder = args[3], + }); + } + }; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + var person: struct { name: []const u8 } = .{ .name = "meghan" }; + _ = &person; + const doc = comptime pek.parse( + \\body( + \\ form( + \\ {##input_text "name" "Name" prev.name ""} + \\ button("Submit ▶") + \\ ) + \\) + ); + try pek.compileInner( + alloc, + builder.writer(), + doc, + .{ .Ctx = C, .indent = 0, .flag1 = false }, + .{ .prev = person }, + ); + try expect(builder.items).toEqualString( + \\ + \\ + \\ + \\ + ); +} + +// custom bool function +test { + const S = struct { + id: u64, + }; + const C = struct { + pub fn pek_is_admin(alloc: std.mem.Allocator, user: S) !bool { + _ = alloc; + return user.id < 10; + } + }; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + var person: S = .{ .id = 0 }; + _ = &person; + const doc = comptime pek.parse( + \\body( + \\ {#if#is_admin user} + \\ p("only an admin user can see this") + \\only an admin user can see this
+ \\ + \\ + ); +} +test { + const S = struct { + id: u64, + }; + const C = struct { + pub fn pek_is_admin(alloc: std.mem.Allocator, user: S) !bool { + _ = alloc; + return user.id < 10; + } + }; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + var person: S = .{ .id = 25 }; + _ = &person; + const doc = comptime pek.parse( + \\body( + \\ {#if#is_admin user} + \\ p("only an admin user can see this") + \\unauthorized users will see this
+ \\ + \\ + ); +} diff --git a/zig.mod b/zig.mod index 138741e5c1ee9f5b3f5ffc932a861361bdf6f2d0..adcae393819b15f145a56a731882268503e198cd 100644 --- a/zig.mod +++ b/zig.mod @@ -4,6 +4,9 @@ main: src/lib.zig license: MPL-2.0 description: An HTML preprocessor with a builtin template engine. dependencies: - - src: git https://github.com/kivikakk/htmlentities.zig + - src: git https://github.com/kivikakk/htmlentities.zig commit-bd5d569a245c7c8e83812eadcb5761b7ba76ef04 - src: git https://github.com/nektro/zig-extras - src: git https://github.com/nektro/zig-tracer + +root_dependencies: + - src: git https://github.com/nektro/zig-expect