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 }