lib/zen/src/theme/load.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const model = @import("model.zig");
3
4 const Allocator = std.mem.Allocator;
5 const io = std.Options.debug_io;
6 const Files = model.Files;
7 const Include = model.Include;
8 const Theme = model.Theme;
9
10 pub const Loaded = struct {
11 value: Theme,
12 layout: ?[]u8 = null,
13 style: ?[]u8 = null,
14 includes: []Include = &.{},
15
16 pub fn deinit(self: *Loaded, allocator: Allocator) void {
17 if (self.layout) |value| allocator.free(value);
18 if (self.style) |value| allocator.free(value);
19 for (self.includes) |include| {
20 allocator.free(include.name);
21 allocator.free(include.html);
22 }
23 if (self.includes.len != 0) allocator.free(self.includes);
24 self.* = undefined;
25 }
26 };
27
28 const ThemeError = error{
29 DuplicateInclude,
30 };
31
32 pub const Error = Allocator.Error || ThemeError;
33
34 pub fn load(allocator: Allocator, source_root: []const u8, files: Files, base: Theme) !Loaded {
35 var loaded = Loaded{ .value = base };
36 errdefer loaded.deinit(allocator);
37 if (files.dir.len == 0) return loaded;
38 if (files.layout.len != 0) {
39 if (try readOptional(
40 allocator,
41 source_root,
42 files.dir,
43 files.layout,
44 files.max_layout_bytes,
45 )) |value| {
46 loaded.layout = value;
47 loaded.value.layout = value;
48 }
49 }
50 if (files.style.len != 0) {
51 if (try readOptional(
52 allocator,
53 source_root,
54 files.dir,
55 files.style,
56 files.max_style_bytes,
57 )) |value| {
58 loaded.style = value;
59 loaded.value.style = value;
60 }
61 }
62 if (files.includes.len != 0) {
63 loaded.includes = try loadIncludes(
64 allocator,
65 source_root,
66 files.dir,
67 files.includes,
68 files.max_include_bytes,
69 );
70 loaded.value.includes = loaded.includes;
71 }
72 return loaded;
73 }
74
75 pub fn ownsPath(path: []const u8, files: Files) bool {
76 if (files.dir.len == 0) return false;
77 if (std.mem.eql(u8, path, files.dir)) return true;
78 return path.len > files.dir.len and
79 std.mem.startsWith(u8, path, files.dir) and
80 std.fs.path.isSep(path[files.dir.len]);
81 }
82
83 fn includeHtml(active: Theme, name: []const u8) ?[]const u8 {
84 for (active.includes) |include| {
85 if (std.mem.eql(u8, include.name, name)) return include.html;
86 }
87 return null;
88 }
89
90 fn readOptional(
91 allocator: Allocator,
92 source_root: []const u8,
93 dir: []const u8,
94 file: []const u8,
95 limit: usize,
96 ) !?[]u8 {
97 const path = try std.fs.path.join(allocator, &.{ source_root, dir, file });
98 defer allocator.free(path);
99 if (!pathExists(path)) return null;
100 return try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(limit));
101 }
102
103 fn loadIncludes(
104 allocator: Allocator,
105 source_root: []const u8,
106 dir: []const u8,
107 includes_dir: []const u8,
108 limit: usize,
109 ) ![]Include {
110 const root = try std.fs.path.join(allocator, &.{ source_root, dir, includes_dir });
111 defer allocator.free(root);
112 if (!pathExists(root)) return &.{};
113
114 var include_dir = try std.Io.Dir.openDirAbsolute(io, root, .{ .iterate = true });
115 defer include_dir.close(io);
116 var walker = try include_dir.walk(allocator);
117 defer walker.deinit();
118
119 var includes: std.ArrayList(Include) = .empty;
120 errdefer {
121 for (includes.items) |include| {
122 allocator.free(include.name);
123 allocator.free(include.html);
124 }
125 includes.deinit(allocator);
126 }
127
128 while (try walker.next(io)) |entry| {
129 if (entry.kind != .file) continue;
130 const maybe_name = try includeName(allocator, entry.path);
131 const name = maybe_name orelse continue;
132 if (includeHtml(.{ .includes = includes.items }, name) != null) {
133 allocator.free(name);
134 return error.DuplicateInclude;
135 }
136 const path = std.fs.path.join(allocator, &.{ root, entry.path }) catch |err| {
137 allocator.free(name);
138 return err;
139 };
140 defer allocator.free(path);
141 const bytes = std.Io.Dir.cwd().readFileAlloc(
142 io,
143 path,
144 allocator,
145 .limited(limit),
146 ) catch |err| {
147 allocator.free(name);
148 return err;
149 };
150 includes.append(allocator, .{ .name = name, .html = bytes }) catch |err| {
151 allocator.free(name);
152 allocator.free(bytes);
153 return err;
154 };
155 }
156
157 if (includes.items.len == 0) return &.{};
158 return try includes.toOwnedSlice(allocator);
159 }
160
161 fn includeName(allocator: Allocator, path: []const u8) Allocator.Error!?[]u8 {
162 if (!std.mem.endsWith(u8, path, ".html")) return null;
163 const stem = path[0 .. path.len - ".html".len];
164 if (stem.len == 0) return null;
165 const name = try allocator.dupe(u8, stem);
166 for (name) |*byte| {
167 if (std.fs.path.isSep(byte.*)) byte.* = '.';
168 }
169 return name;
170 }
171
172 fn pathExists(path: []const u8) bool {
173 std.Io.Dir.cwd().access(io, path, .{}) catch return false;
174 return true;
175 }