authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2025-06-21 18:53:16 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2025-06-21 18:53:16 -07:00
log656e58d4447aec777ce61d1b971b16d00200ebc6
tree13733a3b6fc2f64fbe360253129cc8b105e99586
parent645a7dede162499ce58e9278d2070f69b7045d84

add two element multi-{#each


3 files changed, 79 insertions(+), 18 deletions(-)

src/lib.zig+38-18
...@@ -94,19 +94,12 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V...@@ -94,19 +94,12 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V
94 },94 },
95 .replacement => |repl| {95 .replacement => |repl| {
96 const v = repl.arms;96 const v = repl.arms;
97 const x = if (comptime std.mem.eql(u8, v[0], "this")) search(v[1..], data) else search(v, ctx);97 const x = search(v, ctx);
98 const TO = @TypeOf(x);98 const TO = @TypeOf(x);
99 const TI = @typeInfo(TO);99 const TI = @typeInfo(TO);
100100
101 if (comptime extras.isZigString(TO)) {101 if (comptime extras.isZigString(TO)) {
102 if (repl.raw) {102 return writeReplacementString(writer, repl.raw, opts.escaped, x);
103 try writer.writeAll(x);
104 return;
105 }
106 const s = std.mem.trim(u8, x, "\n");
107 if (opts.escaped) try writeEscaped(s, writer);
108 if (!opts.escaped) try writer.writeAll(s);
109 return;
110 }103 }
111 if (TI == .int or TI == .float or TI == .comptime_int or TI == .comptime_float) {104 if (TI == .int or TI == .float or TI == .comptime_int or TI == .comptime_float) {
112 try writer.print("{d}", .{x});105 try writer.print("{d}", .{x});
...@@ -145,8 +138,25 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V...@@ -145,8 +138,25 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V
145 const TI = @typeInfo(T);138 const TI = @typeInfo(T);
146 switch (v.name) {139 switch (v.name) {
147 .each => {140 .each => {
148 comptime assertEqual(v.args.len, 1);141 switch (v.args.len) {
149 for (x) |item| try do(alloc, writer, body, item, ctx, opts);142 1 => {
143 for (x) |item| {
144 if (@hasField(@TypeOf(ctx), "this")) {
145 // handle nested loops, should be temporary
146 try do(alloc, writer, body, null, extras.join(.{ extras.omit(ctx, "this"), .{ .this = item } }), opts);
147 } else {
148 try do(alloc, writer, body, null, extras.join(.{ ctx, .{ .this = item } }), opts);
149 }
150 }
151 },
152 2 => {
153 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
154 for (x, y) |item, jtem| {
155 try do(alloc, writer, body, null, extras.join(.{ ctx, .{ .this = item, .that = jtem } }), opts);
156 }
157 },
158 else => @compileError(std.fmt.comptimePrint("#each block cannot have {d} iterators", .{v.args.len})),
159 }
150 },160 },
151 .@"if" => {161 .@"if" => {
152 if (v.func) |n| {162 if (v.func) |n| {
...@@ -255,9 +265,8 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V...@@ -255,9 +265,8 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V
255 const field_name = comptime std.fmt.comptimePrint("{d}", .{i + 2});265 const field_name = comptime std.fmt.comptimePrint("{d}", .{i + 2});
256 @field(args, field_name) = try resolveArg(arg, alloc, data, ctx, opts);266 @field(args, field_name) = try resolveArg(arg, alloc, data, ctx, opts);
257 }267 }
258 const repvalue = astgen.Value{ .replacement = .{ .arms = &.{"this"}, .raw = v.raw } };
259 try @call(.auto, func, args);268 try @call(.auto, func, args);
260 try do(alloc, writer, repvalue, try list.toOwnedSlice(), ctx, opts);269 try writeReplacementString(writer, v.raw, opts.escaped, try list.toOwnedSlice());
261 return;270 return;
262 }271 }
263 if (v.raw and @hasDecl(opts.Ctx, "pek__" ++ v.name)) {272 if (v.raw and @hasDecl(opts.Ctx, "pek__" ++ v.name)) {
...@@ -279,9 +288,8 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V...@@ -279,9 +288,8 @@ inline fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.V
279 const field_name = comptime std.fmt.comptimePrint("{d}", .{i});288 const field_name = comptime std.fmt.comptimePrint("{d}", .{i});
280 @field(args[3], field_name) = try resolveArg(arg, alloc, data, ctx, opts);289 @field(args[3], field_name) = try resolveArg(arg, alloc, data, ctx, opts);
281 }290 }
282 const repvalue = astgen.Value{ .replacement = .{ .arms = &.{"this"}, .raw = v.raw } };
283 try @call(.auto, func, args);291 try @call(.auto, func, args);
284 try do(alloc, writer, repvalue, try list.toOwnedSlice(), ctx, opts);292 try writeReplacementString(writer, v.raw, opts.escaped, list.items);
285 return;293 return;
286 }294 }
287 if (v.raw and @hasDecl(opts.Ctx, "pek_" ++ v.name)) {295 if (v.raw and @hasDecl(opts.Ctx, "pek_" ++ v.name)) {
...@@ -306,7 +314,7 @@ pub const DoOptions = struct {...@@ -306,7 +314,7 @@ pub const DoOptions = struct {
306fn resolveArg(comptime arg: astgen.Arg, alloc: std.mem.Allocator, data: anytype, ctx: anytype, comptime opts: DoOptions) !ResolveArg(arg, @TypeOf(data), @TypeOf(ctx)) {314fn resolveArg(comptime arg: astgen.Arg, alloc: std.mem.Allocator, data: anytype, ctx: anytype, comptime opts: DoOptions) !ResolveArg(arg, @TypeOf(data), @TypeOf(ctx)) {
307 return switch (arg) {315 return switch (arg) {
308 .plain => |av| av[1 .. av.len - 1],316 .plain => |av| av[1 .. av.len - 1],
309 .lookup => |av| if (comptime std.mem.eql(u8, av[0], "this")) search(av[1..], data) else search(av, ctx),317 .lookup => |av| search(av, ctx),
310 .int => |av| av,318 .int => |av| av,
311 .value => |av| {319 .value => |av| {
312 var list = std.ArrayList(u8).init(alloc);320 var list = std.ArrayList(u8).init(alloc);
...@@ -319,9 +327,10 @@ fn resolveArg(comptime arg: astgen.Arg, alloc: std.mem.Allocator, data: anytype,...@@ -319,9 +327,10 @@ fn resolveArg(comptime arg: astgen.Arg, alloc: std.mem.Allocator, data: anytype,
319}327}
320328
321fn ResolveArg(comptime arg: astgen.Arg, comptime This: type, comptime Ctx: type) type {329fn ResolveArg(comptime arg: astgen.Arg, comptime This: type, comptime Ctx: type) type {
330 _ = This;
322 return switch (arg) {331 return switch (arg) {
323 .plain => string,332 .plain => string,
324 .lookup => |av| if (comptime std.mem.eql(u8, av[0], "this")) FieldSearch(This, av[1..]) else FieldSearch(Ctx, av),333 .lookup => |av| FieldSearch(Ctx, av),
325 .int => u64,334 .int => u64,
326 .value => string,335 .value => string,
327 };336 };
...@@ -358,7 +367,8 @@ fn Field(comptime T: type, comptime field_name: string) type {...@@ -358,7 +367,8 @@ fn Field(comptime T: type, comptime field_name: string) type {
358 return fld.type;367 return fld.type;
359 }368 }
360 }369 }
361 @compileError(std.fmt.comptimePrint("pek: unknown field {s} on type {s}", .{ field_name, @typeName(T) }));370 @compileLog(std.meta.fieldNames(T));
371 _ = @field(@as(T, undefined), field_name);
362}372}
363373
364pub fn writeEscaped(s: string, writer: anytype) !void {374pub fn writeEscaped(s: string, writer: anytype) !void {
...@@ -374,6 +384,16 @@ pub fn writeEscaped(s: string, writer: anytype) !void {...@@ -374,6 +384,16 @@ pub fn writeEscaped(s: string, writer: anytype) !void {
374 }384 }
375}385}
376386
387fn writeReplacementString(writer: anytype, raw: bool, escaped: bool, bytes: []const u8) !void {
388 if (raw) {
389 try writer.writeAll(bytes);
390 return;
391 }
392 const s = std.mem.trim(u8, bytes, "\n");
393 if (escaped) try writeEscaped(s, writer);
394 if (!escaped) try writer.writeAll(s);
395}
396
377fn nextCodepointSliceLossy(it: *std.unicode.Utf8Iterator) ?[]const u8 {397fn nextCodepointSliceLossy(it: *std.unicode.Utf8Iterator) ?[]const u8 {
378 if (it.i >= it.bytes.len) return null;398 if (it.i >= it.bytes.len) return null;
379 const cp_len = std.unicode.utf8ByteSequenceLength(it.bytes[it.i]) catch {399 const cp_len = std.unicode.utf8ByteSequenceLength(it.bytes[it.i]) catch {
test.zig+40
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1const std = @import("std");1const std = @import("std");
2const pek = @import("pek");2const pek = @import("pek");
3const expect = @import("expect").expect;3const expect = @import("expect").expect;
4const extras = @import("extras");
45
5test {6test {
6 std.testing.refAllDeclsRecursive(pek);7 std.testing.refAllDeclsRecursive(pek);
...@@ -904,3 +905,42 @@ test {...@@ -904,3 +905,42 @@ test {
904 \\905 \\
905 );906 );
906}907}
908
909// multi-each
910test {
911 const alloc = std.testing.allocator;
912 var builder = std.ArrayList(u8).init(alloc);
913 defer builder.deinit();
914 const S = struct {
915 abbr: []const u8,
916 name: []const u8,
917 index: u8,
918 };
919 const states = extras.StaticMultiList(S).initComptime(&[_]S{
920 .{ .abbr = "MA", .name = "Massachusetts", .index = 0 },
921 .{ .abbr = "OR", .name = "Oregon", .index = 0 },
922 .{ .abbr = "CA", .name = "California", .index = 0 },
923 });
924 const doc = comptime pek.parse(
925 \\body(
926 \\ {#each abbrs names}
927 \\ p("The US state '"{that}"' can be shortened to '"{this}"'.")
928 \\ /each/
929 \\)
930 );
931 try pek.compileInner(
932 alloc,
933 builder.writer(),
934 doc,
935 .{ .Ctx = @This(), .indent = 0, .flag1 = false },
936 .{ .abbrs = states.items.abbr, .names = states.items.name },
937 );
938 try expect(builder.items).toEqualString(
939 \\<body>
940 \\ <p>The US state 'Massachusetts' can be shortened to 'MA'.</p>
941 \\ <p>The US state 'Oregon' can be shortened to 'OR'.</p>
942 \\ <p>The US state 'California' can be shortened to 'CA'.</p>
943 \\</body>
944 \\
945 );
946}
zig.mod+1
...@@ -10,3 +10,4 @@ dependencies:...@@ -10,3 +10,4 @@ dependencies:
1010
11root_dependencies:11root_dependencies:
12 - src: git https://github.com/nektro/zig-expect12 - src: git https://github.com/nektro/zig-expect
13 - src: git https://github.com/nektro/zig-extras