1//! Pek HTML Preprocessor Language
2//!
3//! Shortening of Pekingese
4//! https://en.wikipedia.org/wiki/Pekingese
5//!
6//! Loosely inspired by Pug + Handlebars
7//! https://pugjs.org/
8//! https://handlebarsjs.com/
9
10const std = @import("std");
11const string = []const u8;
12const htmlentities = @import("htmlentities");
13const root = @import("root");
14const tracer = @import("tracer");
15const extras = @import("extras");
16const builtin = @import("builtin");
17const nio = @import("nio");
18
19const tokenize = @import("./tokenize.zig");
20const astgen = @import("./astgen.zig");
21
22pub fn parse(comptime input: string) astgen.Value {
23 return astgen.Value{ .element = astgen.do(tokenize.do(input, &.{ '[', '=', ']', '(', ')', '{', '}', '#', '/', '.', '<', '>' })) };
24}
25
26pub fn compile(comptime Ctx: type, alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, data: anytype) !void {
27 const t = tracer.trace(@src(), "", .{});
28 defer t.end();
29
30 try writer.writeAll("<!DOCTYPE html>\n");
31 try do(alloc, writer, value, data, data, .{
32 .Ctx = Ctx,
33 .indent = 0,
34 .doindent = builtin.mode == .Debug,
35 .doindent2 = builtin.mode == .Debug,
36 });
37}
38
39pub fn compileInner(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, comptime opts: DoOptions, data: anytype) !void {
40 try do(alloc, writer, value, data, data, opts);
41}
42
43pub const Writer = *nio.AllocatingWriter;
44
45fn do(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, data: anytype, ctx: anytype, comptime opts: DoOptions) anyerror!void {
46 comptime var skipindent = false;
47 if (value == .element and comptime std.mem.eql(u8, value.element.name, "pre")) skipindent = true;
48 return doInner(alloc, writer, value, data, ctx, .{
49 .Ctx = opts.Ctx,
50 .indent = opts.indent,
51 .doindent = opts.doindent and !skipindent,
52 .doindent2 = opts.doindent,
53 .escaped = opts.escaped,
54 });
55}
56fn doInner(alloc: std.mem.Allocator, writer: anytype, comptime value: astgen.Value, data: anytype, ctx: anytype, comptime opts: DoOptions) anyerror!void {
57 switch (comptime value) {
58 .element => |v| {
59 const hastext = comptime for (v.children) |x| {
60 switch (x) {
61 .string, .replacement, .function => break true,
62 .element, .attr, .block, .body => {},
63 }
64 } else false;
65 _ = hastext;
66
67 if (comptime std.mem.eql(u8, v.name, "_")) {
68 inline for (v.children) |it| {
69 try do(alloc, writer, it, data, ctx, .{
70 .Ctx = opts.Ctx,
71 .indent = opts.indent,
72 .doindent = opts.doindent,
73 .doindent2 = opts.doindent2,
74 });
75 }
76 return;
77 }
78
79 if (opts.doindent2) for (0..opts.indent) |_| try writer.writeAll(" ");
80 try writer.writeAll("<");
81 try writer.writeAll(v.name);
82
83 inline for (v.attrs) |it| {
84 switch (comptime it.value) {
85 .string => try writer.print(" {s}={s}", .{ it.key, it.value.string }),
86 .body => {
87 try writer.print(" {s}=\"", .{it.key});
88 inline for (it.value.body) |bval| try do(alloc, writer, bval, data, ctx, opts);
89 try writer.print("\"", .{});
90 },
91 }
92 }
93
94 if (v.children.len == 0) {
95 if (comptime contains(std.meta.fieldNames(HtmlVoidElement), v.name)) {
96 try writer.writeAll(">");
97 if (opts.doindent2) try writer.writeAll("\n");
98 } else {
99 try writer.print("></{s}>", .{v.name});
100 if (opts.doindent2) try writer.writeAll("\n");
101 }
102 } else {
103 const shouldindent = opts.doindent and v.children[0] != .string and v.children[0] != .replacement;
104 try writer.writeAll(">");
105 if (shouldindent) try writer.writeAll("\n");
106 inline for (v.children) |it| {
107 try do(alloc, writer, it, data, ctx, .{
108 .Ctx = opts.Ctx,
109 .indent = if (shouldindent) opts.indent + 1 else opts.indent,
110 .doindent = shouldindent,
111 .doindent2 = opts.doindent2,
112 });
113 }
114 if (shouldindent) for (0..opts.indent) |_| try writer.writeAll(" ");
115 try writer.print("</{s}>", .{v.name});
116 if (opts.doindent2) try writer.writeAll("\n");
117 }
118 },
119 .string => |v| {
120 if (opts.escaped) try writeEscaped(v[1 .. v.len - 1], writer);
121 if (!opts.escaped) try writer.writeAll(v[1 .. v.len - 1]);
122 },
123 .replacement => |repl| {
124 const v = repl.arms;
125 const x = search(v, ctx);
126 const TO = @TypeOf(x);
127 const TI = @typeInfo(TO);
128
129 if (comptime extras.isZigString(TO)) {
130 if (repl.raw) return writer.writeAll(x);
131 return writeReplacementString(writer, opts.escaped, x);
132 }
133 if (TI == .int or TI == .float or TI == .comptime_int or TI == .comptime_float) {
134 try writer.print("{d}", .{x});
135 return;
136 }
137 if (comptime isArrayOf(u8)(TO)) {
138 if (repl.raw) return writer.writeAll(&x);
139 const s = std.mem.trim(u8, &x, "\n");
140 if (opts.escaped) try writeEscaped(s, writer);
141 if (!opts.escaped) try writer.writeAll(s);
142 return;
143 }
144 if (TI == .@"enum") {
145 const s = @tagName(x);
146 if (opts.escaped) try writeEscaped(s, writer);
147 if (!opts.escaped) try writer.writeAll(s);
148 return;
149 }
150 if (TI == .bool) {
151 const s = if (x) "true" else "false";
152 if (opts.escaped) try writeEscaped(s, writer);
153 if (!opts.escaped) try writer.writeAll(s);
154 return;
155 }
156 if (TI == .optional) {
157 if (comptime extras.isZigString(std.meta.Child(TO))) {
158 if (repl.raw) return writer.writeAll(x.?);
159 return writeReplacementString(writer, opts.escaped, x.?);
160 }
161 return x.?.nprint(writer);
162 }
163 return x.nprint(writer);
164 },
165 .block => |v| {
166 const body = astgen.Value{ .body = v.body };
167 const bottom = astgen.Value{ .body = v.bttm };
168 const x = try resolveArg(v.args[0], alloc, data, ctx, opts);
169 const T = @TypeOf(x);
170 const TI = @typeInfo(T);
171 switch (v.name) {
172 .each => {
173 switch (v.args.len) {
174 1 => {
175 for (x) |item| {
176 if (@hasField(@TypeOf(ctx), "this")) {
177 try do(alloc, writer, body, null, extras.join(.{ extras.omit(ctx, "this"), .{ .@"^this" = ctx.this, .this = item } }), opts);
178 } else {
179 try do(alloc, writer, body, null, extras.join(.{ ctx, .{ .this = item } }), opts);
180 }
181 }
182 },
183 2 => {
184 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
185 for (x, y) |item, jtem| {
186 try do(alloc, writer, body, null, extras.join(.{ ctx, .{ .this = item, .that = jtem } }), opts);
187 }
188 },
189 else => @compileError(nio.fmt.comptimePrint("#each block cannot have {d} iterators", .{v.args.len})),
190 }
191 },
192 .@"if" => {
193 if (v.func) |n| {
194 const f = @field(opts.Ctx, "pek_" ++ n);
195 const x2 = try switch (@typeInfo(@TypeOf(f)).@"fn".params.len) {
196 2 => f(alloc, x),
197 3 => blk: {
198 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
199 break :blk f(alloc, x, y);
200 },
201 4 => blk: {
202 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
203 const z = try resolveArg(v.args[2], alloc, data, ctx, opts);
204 break :blk f(alloc, x, y, z);
205 },
206 else => unreachable, // TODO
207 };
208 try doif(alloc, writer, body, bottom, data, ctx, opts, x2);
209 return;
210 }
211 comptime assertEqual(v.args.len, 1);
212 if (comptime extras.isIndexable(T)) {
213 try doif(alloc, writer, body, bottom, data, ctx, opts, x.len > 0);
214 return;
215 }
216 switch (comptime TI) {
217 .bool => try doif(alloc, writer, body, bottom, data, ctx, opts, x),
218 .optional => try doif(alloc, writer, body, bottom, data, ctx, opts, x != null),
219 .int => try doif(alloc, writer, body, bottom, data, ctx, opts, x != 0),
220 else => @compileError(nio.fmt.comptimePrint("pek: unable to use '{s}' in an #if block", .{@typeName(T)})),
221 }
222 },
223 .ifnot => {
224 if (v.func) |n| {
225 const f = @field(opts.Ctx, "pek_" ++ n);
226 const x2 = try switch (@typeInfo(@TypeOf(f)).@"fn".params.len) {
227 2 => f(alloc, x),
228 3 => blk: {
229 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
230 break :blk f(alloc, x, y);
231 },
232 4 => blk: {
233 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
234 const z = try resolveArg(v.args[2], alloc, data, ctx, opts);
235 break :blk f(alloc, x, y, z);
236 },
237 else => unreachable, // TODO
238 };
239 try doif(alloc, writer, body, bottom, data, ctx, opts, !x2);
240 return;
241 }
242 comptime assertEqual(v.args.len, 1);
243 if (comptime extras.isIndexable(T)) {
244 try doif(alloc, writer, body, bottom, data, ctx, opts, x.len == 0);
245 return;
246 }
247 switch (comptime TI) {
248 .bool => try doif(alloc, writer, body, bottom, data, ctx, opts, !x),
249 .optional => try doif(alloc, writer, body, bottom, data, ctx, opts, x == null),
250 .int => try doif(alloc, writer, body, bottom, data, ctx, opts, x == 0),
251 else => @compileError(nio.fmt.comptimePrint("pek: unable to use '{s}' in an #ifnot block", .{@typeName(T)})),
252 }
253 },
254 .ifequal => {
255 comptime assertEqual(v.args.len, 2);
256 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
257 const Y = @TypeOf(y);
258 if (@typeInfo(@TypeOf(x)) == .@"enum" and comptime extras.isZigString(@TypeOf(y))) {
259 return try doif(alloc, writer, body, bottom, data, ctx, opts, std.mem.eql(u8, @tagName(x), y));
260 }
261 if (TI == .@"enum" and @typeInfo(@TypeOf(y)) == .int) {
262 return doif(alloc, writer, body, bottom, data, ctx, opts, @intFromEnum(x) == y);
263 }
264 if (TI == .@"union" and comptime extras.isZigString(Y)) {
265 const tag = std.meta.stringToEnum(TI.@"union".tag_type.?, y) orelse {
266 return doif(alloc, writer, body, bottom, data, ctx, opts, false);
267 };
268 return doif(alloc, writer, body, bottom, data, ctx, opts, x == tag);
269 }
270 if (comptime extras.isSlice(@TypeOf(x, y))) {
271 return try doif(alloc, writer, body, bottom, data, ctx, opts, std.mem.eql(u8, x, y));
272 }
273 if (@typeInfo(@TypeOf(x)) == .@"struct") {
274 return doif(alloc, writer, body, bottom, data, ctx, opts, x.eql(y));
275 }
276 try doif(alloc, writer, body, bottom, data, ctx, opts, x == y);
277 },
278 .ifnotequal => {
279 comptime assertEqual(v.args.len, 2);
280 const y = try resolveArg(v.args[1], alloc, data, ctx, opts);
281 if (@typeInfo(@TypeOf(x)) == .@"enum" and comptime extras.isZigString(@TypeOf(y))) {
282 return try doif(alloc, writer, body, bottom, data, ctx, opts, !std.mem.eql(u8, @tagName(x), y));
283 }
284 if (TI == .@"enum" and @typeInfo(@TypeOf(y)) == .int) {
285 return doif(alloc, writer, body, bottom, data, ctx, opts, @intFromEnum(x) != y);
286 }
287 if (comptime extras.isSlice(@TypeOf(x, y))) {
288 return try doif(alloc, writer, body, bottom, data, ctx, opts, !std.mem.eql(u8, x, y));
289 }
290 if (@typeInfo(@TypeOf(x)) == .@"struct") {
291 return doif(alloc, writer, body, bottom, data, ctx, opts, !x.eql(y));
292 }
293 try doif(alloc, writer, body, bottom, data, ctx, opts, x != y);
294 },
295 }
296 },
297 .body => |v| {
298 inline for (v) |val| {
299 try do(alloc, writer, val, data, ctx, opts);
300 }
301 },
302 .function => |v| {
303 if (!v.raw) {
304 if (@hasDecl(opts.Ctx, "pek__" ++ v.name)) @compileError("pek: attempted to call raw custom function: '_" ++ v.name ++ "' but did not use '{#" ++ v.name ++ "}'");
305 const func = @field(opts.Ctx, "pek_" ++ v.name);
306 var list: nio.AllocatingWriter = .init(alloc);
307 defer list.deinit();
308 comptime var types: [v.args.len]type = @splat(void);
309 inline for (v.args, &types) |arg, *T| T.* = ResolveArg(arg, @TypeOf(data), @TypeOf(ctx));
310 var args: std.meta.Tuple(&types) = undefined;
311 inline for (v.args, 0..) |arg, i| args[i] = try resolveArg(arg, alloc, data, ctx, opts);
312 try @call(.auto, func, .{ alloc, &list } ++ args);
313 try writeReplacementString(writer, opts.escaped, list.items);
314 return;
315 }
316 if (v.raw) {
317 if (@hasDecl(opts.Ctx, "pek_" ++ v.name)) @compileError("pek: attempted to call safe custom function: '" ++ v.name ++ "' but did not use '{" ++ v.name ++ "}'");
318 const func = @field(opts.Ctx, "pek__" ++ v.name);
319 comptime var types: [v.args.len]type = @splat(void);
320 inline for (v.args, &types) |arg, *T| T.* = ResolveArg(arg, @TypeOf(data), @TypeOf(ctx));
321 var args: std.meta.Tuple(&types) = undefined;
322 inline for (v.args, 0..) |arg, i| args[i] = try resolveArg(arg, alloc, data, ctx, opts);
323 try @call(.auto, func, .{ alloc, writer, opts, args });
324 return;
325 }
326 comptime unreachable;
327 },
328 else => unreachable,
329 }
330}
331
332pub const DoOptions = struct {
333 Ctx: type,
334 indent: usize,
335 doindent: bool,
336 doindent2: bool,
337 escaped: bool = true,
338};
339
340fn resolveArg(comptime arg: astgen.Arg, alloc: std.mem.Allocator, data: anytype, ctx: anytype, comptime opts: DoOptions) !ResolveArg(arg, @TypeOf(data), @TypeOf(ctx)) {
341 return switch (arg) {
342 .plain => |av| av[1 .. av.len - 1],
343 .lookup => |av| search(av, ctx),
344 .int => |av| av,
345 .value => |av| {
346 var list: nio.AllocatingWriter = .init(alloc);
347 comptime var newopts = opts;
348 newopts.escaped = false;
349 try do(alloc, &list, astgen.Value{ .body = av }, data, ctx, newopts);
350 return list.items;
351 },
352 };
353}
354
355fn ResolveArg(comptime arg: astgen.Arg, comptime This: type, comptime Ctx: type) type {
356 _ = This;
357 return switch (arg) {
358 .plain => string,
359 .lookup => |av| FieldSearch(Ctx, av),
360 .int => u64,
361 .value => string,
362 };
363}
364
365fn search(comptime args: []const string, ctx: anytype) FieldSearch(@TypeOf(ctx), args) {
366 if (args.len == 0) return ctx;
367 if (args[0][0] == '"') return std.mem.trim(u8, args[0], "\"");
368 if (comptime std.mem.eql(u8, args[0], "true")) return true;
369 if (comptime std.mem.eql(u8, args[0], "false")) return false;
370 if (@typeInfo(@TypeOf(ctx)) == .optional) return search(args, ctx.?);
371 const arg_is_number = if (comptime extras.parseDigits(usize, args[0], 10)) |_| true else |_| false;
372 const f = if (!arg_is_number) @field(ctx, args[0]) else ctx[comptime extras.parseDigits(usize, args[0], 10) catch unreachable];
373 if (args.len == 1) return f;
374 return search(args[1..], f);
375}
376
377fn FieldSearch(comptime T: type, comptime args: []const string) type {
378 if (args.len > 0 and args[0][0] == '"') return string;
379 if (args.len > 0 and std.mem.eql(u8, args[0], "true")) return bool;
380 if (args.len > 0 and std.mem.eql(u8, args[0], "false")) return bool;
381 if (args.len == 0) return T;
382 if (args.len == 1) return Field(T, args[0]);
383 return FieldSearch(Field(T, args[0]), args[1..]);
384}
385
386fn Field(comptime T: type, comptime field_name: string) type {
387 if (extras.isIndexable(T) and std.mem.eql(u8, field_name, "len")) {
388 return usize;
389 }
390 if (extras.isTuple(T) and (if (extras.parseDigits(usize, field_name, 10)) |_| true else |_| false)) {
391 const info = @typeInfo(T).@"struct".fields;
392 const idx = extras.parseDigits(usize, field_name, 10) catch unreachable;
393 return info[idx].type;
394 }
395 if (extras.isIndexable(T) and (if (extras.parseDigits(usize, field_name, 10)) |_| true else |_| false)) {
396 return std.meta.Child(T);
397 }
398 switch (@typeInfo(T)) {
399 .optional => |info| return Field(info.child, field_name),
400 .pointer => |info| return Field(info.child, field_name),
401 else => {},
402 }
403 for (std.meta.fields(T)) |fld| {
404 if (std.mem.eql(u8, fld.name, field_name)) {
405 return fld.type;
406 }
407 }
408 _ = @field(@as(T, undefined), field_name);
409}
410
411pub fn writeEscaped(s: string, writer: anytype) !void {
412 const view = std.unicode.Utf8View.initUnchecked(s);
413 var iter = view.iterator();
414 while (nextCodepointSliceLossy(&iter)) |sl| {
415 const cp = std.unicode.utf8Decode(sl) catch unreachable;
416 if (isCodepointAnEntity(cp)) |ent| {
417 try writer.writeAll(ent.entity);
418 } else {
419 try writer.writeAll(sl);
420 }
421 }
422}
423
424fn writeReplacementString(writer: anytype, escaped: bool, bytes: []const u8) !void {
425 const s = std.mem.trim(u8, bytes, "\n");
426 if (escaped) try writeEscaped(s, writer);
427 if (!escaped) try writer.writeAll(s);
428}
429
430fn nextCodepointSliceLossy(it: *std.unicode.Utf8Iterator) ?[]const u8 {
431 if (it.i >= it.bytes.len) return null;
432 const cp_len = std.unicode.utf8ByteSequenceLength(it.bytes[it.i]) catch {
433 it.i += 1;
434 return "�";
435 };
436 if (it.i + cp_len > it.bytes.len) return null;
437 const maybe = it.bytes[it.i..][0..cp_len];
438 _ = std.unicode.utf8Decode(maybe) catch {
439 it.i += 1;
440 return "�";
441 };
442 it.i += cp_len;
443 return maybe;
444}
445
446fn isCodepointAnEntity(cp: u21) ?htmlentities.Entity {
447 switch (cp) {
448 // '<', MUST NOT skip
449 // '<', MUST NOT skip
450 // '"', MUST NOT skip
451 // '&', MUST NOT skip
452 // ';', MUST NOT skip
453 '\n',
454 '.',
455 ':',
456 '(',
457 ')',
458 '%',
459 '+',
460 '/',
461 '@',
462 ' ',
463 'a'...'z',
464 'A'...'Z',
465 '0'...'9',
466 '_',
467 '=',
468 '-',
469 '#',
470 '{',
471 '}',
472 ',',
473 '*',
474 '!',
475 '\'',
476 '[',
477 ']',
478 '|',
479 '?',
480 '`',
481 => return null,
482 else => {},
483 }
484 for (htmlentities.ENTITIES) |e| {
485 if (e.entity.len == 0) continue;
486 if (e.entity[e.entity.len - 1] != ';') continue;
487
488 if (e.codepoints == .Single and e.codepoints.Single == cp) {
489 return e;
490 }
491 }
492 return null;
493}
494
495fn assertEqual(comptime a: usize, comptime b: usize) void {
496 if (a != b) @compileError(nio.fmt.comptimePrint("{d} != {d}", .{ a, b }));
497}
498
499const HtmlVoidElement = enum {
500 area,
501 base,
502 br,
503 col,
504 embed,
505 hr,
506 img,
507 input,
508 link,
509 meta,
510 param,
511 source,
512 track,
513 wbr,
514};
515
516fn contains(haystack: []const string, needle: string) bool {
517 for (haystack) |v| {
518 if (std.mem.eql(u8, v, needle)) {
519 return true;
520 }
521 }
522 return false;
523}
524
525fn doif(alloc: std.mem.Allocator, writer: anytype, comptime top: astgen.Value, comptime bottom: astgen.Value, data: anytype, ctx: anytype, comptime opts: DoOptions, flag2: bool) anyerror!void {
526 if (flag2) {
527 try do(alloc, writer, top, data, ctx, opts);
528 } else {
529 try do(alloc, writer, bottom, data, ctx, opts);
530 }
531}
532
533fn isArrayOf(comptime T: type) fn (type) bool {
534 const Closure = struct {
535 pub fn trait(comptime C: type) bool {
536 return switch (@typeInfo(C)) {
537 .array => |ti| ti.child == T,
538 else => false,
539 };
540 }
541 };
542 return Closure.trait;
543}