lib/zen/src/theme/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const html = @import("../root.zig").html;
3 const model = @import("model.zig");
4 const scan = @import("scan.zig");
5 const value = @import("value.zig");
6
7 pub const Limits = struct {
8 max_output_bytes: usize,
9 };
10
11 pub const default_limits: Limits = .{
12 .max_output_bytes = 128 * 1024 * 1024,
13 };
14
15 pub const Plan = struct {
16 layout_bytes: usize,
17 placeholders: usize,
18 output_bytes: usize,
19
20 pub fn inspect(active: model.Theme, page: model.Page, limits: Limits) model.Error!Plan {
21 var placeholders: usize = 0;
22 var output_bytes: usize = 0;
23 var iterator = scan.Iterator.init(active.layout);
24 while (try iterator.next()) |event| switch (event) {
25 .literal => |literal| output_bytes = try addOutput(
26 output_bytes,
27 literal.len,
28 limits.max_output_bytes,
29 ),
30 .placeholder => |key| {
31 placeholders = try added(placeholders, 1);
32 const resolved = try value.resolve(active, page, key);
33 const bytes = switch (resolved.encoding) {
34 .raw => resolved.text.len,
35 .escaped => try html.escapedLength(resolved.text),
36 };
37 output_bytes = try addOutput(
38 output_bytes,
39 bytes,
40 limits.max_output_bytes,
41 );
42 },
43 };
44 return .{
45 .layout_bytes = active.layout.len,
46 .placeholders = placeholders,
47 .output_bytes = output_bytes,
48 };
49 }
50
51 pub fn exactLimits(self: Plan) Limits {
52 return .{ .max_output_bytes = self.output_bytes };
53 }
54
55 pub fn require(self: Plan, limits: Limits) model.Exhaustion!void {
56 if (self.output_bytes > limits.max_output_bytes) {
57 return error.ThemeOutputByteCapacityExceeded;
58 }
59 }
60 };
61
62 fn addOutput(current: usize, amount: usize, limit: usize) model.Error!usize {
63 const total = try added(current, amount);
64 if (total > limit) return error.ThemeOutputByteCapacityExceeded;
65 return total;
66 }
67
68 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
69 return std.math.add(usize, left, right) catch error.CapacityOverflow;
70 }
71
72 test "theme render plan computes exact raw and escaped bytes" {
73 const includes = [_]model.Include{
74 .{ .name = "head", .html = "<b>raw</b>" },
75 };
76 const active = model.Theme{
77 .layout = "A{{ title }}B{{content}}C{{include.head}}D",
78 .includes = &includes,
79 };
80 const page = model.Page{ .title = "<&", .content = "<i>x</i>" };
81 const plan = try Plan.inspect(active, page, default_limits);
82 try std.testing.expectEqual(@as(usize, 3), plan.placeholders);
83 try std.testing.expectEqual(
84 "A".len + "<&".len + "B".len + "<i>x</i>".len +
85 "C".len + "<b>raw</b>".len + "D".len,
86 plan.output_bytes,
87 );
88 try std.testing.expectEqual(Limits{ .max_output_bytes = plan.output_bytes }, plan.exactLimits());
89 }
90
91 test "theme render plan validates templates and output capacity" {
92 const page = model.Page{ .title = "Title", .content = "body" };
93 try std.testing.expectError(
94 error.UnclosedPlaceholder,
95 Plan.inspect(.{ .layout = "{{title" }, page, default_limits),
96 );
97 try std.testing.expectError(
98 error.UnknownPlaceholder,
99 Plan.inspect(.{ .layout = "{{unknown}}" }, page, default_limits),
100 );
101 try std.testing.expectError(
102 error.ThemeOutputByteCapacityExceeded,
103 Plan.inspect(.{ .layout = "{{content}}" }, page, .{ .max_output_bytes = 3 }),
104 );
105 }