lib/zen/src/site/path/transform.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const model = @import("model.zig");
  4 
  5 const index_name = "index.html";
  6 const html_suffix = ".html";
  7 
  8 pub fn targetLength(relative_path: []const u8, clean_urls: bool) error{CapacityOverflow}!usize {
  9     const base = std.fs.path.basename(relative_path);
 10     const directory = std.fs.path.dirname(relative_path);
 11     const stem = markdownStem(base);
 12     if (clean_urls) {
 13         if (std.mem.eql(u8, stem, "index") or std.mem.eql(u8, stem, "README")) {
 14             return joinedLength(&.{ directory orelse "", index_name });
 15         }
 16         return joinedLength(&.{ directory orelse "", stem, index_name });
 17     }
 18     return added(try joinedLength(&.{ directory orelse "", stem }), html_suffix.len);
 19 }
 20 
 21 pub fn writeTarget(output: []u8, relative_path: []const u8, clean_urls: bool) []u8 {
 22     const base = std.fs.path.basename(relative_path);
 23     const directory = std.fs.path.dirname(relative_path);
 24     const stem = markdownStem(base);
 25     var index: usize = 0;
 26     if (clean_urls) {
 27         if (std.mem.eql(u8, stem, "index") or std.mem.eql(u8, stem, "README")) {
 28             writeJoined(output, &index, &.{ directory orelse "", index_name });
 29         } else {
 30             writeJoined(output, &index, &.{ directory orelse "", stem, index_name });
 31         }
 32     } else {
 33         writeJoined(output, &index, &.{ directory orelse "", stem });
 34         appendRaw(output, &index, html_suffix);
 35     }
 36     std.debug.assert(index == output.len);
 37     return output;
 38 }
 39 
 40 pub fn publicLength(
 41     target_bytes: usize,
 42     clean_urls: bool,
 43     kind: model.PublicKind,
 44 ) error{CapacityOverflow}!usize {
 45     const content_bytes = switch (kind) {
 46         .target => target_bytes,
 47         .url => if (clean_urls) target_bytes - index_name.len else target_bytes,
 48     };
 49     return added(1, content_bytes);
 50 }
 51 
 52 pub fn writePublic(
 53     output: []u8,
 54     target: []const u8,
 55     clean_urls: bool,
 56     kind: model.PublicKind,
 57 ) []u8 {
 58     const content = switch (kind) {
 59         .target => target,
 60         .url => if (clean_urls) target[0 .. target.len - index_name.len] else target,
 61     };
 62     output[0] = '/';
 63     for (content, output[1..]) |byte, *destination| {
 64         destination.* = if (std.fs.path.isSep(byte)) '/' else byte;
 65     }
 66     return output;
 67 }
 68 
 69 fn markdownStem(base: []const u8) []const u8 {
 70     if (std.mem.endsWith(u8, base, ".markdown")) return base[0 .. base.len - ".markdown".len];
 71     if (std.mem.endsWith(u8, base, ".md")) return base[0 .. base.len - ".md".len];
 72     return base;
 73 }
 74 
 75 fn joinedLength(parts: []const []const u8) error{CapacityOverflow}!usize {
 76     var total: usize = 0;
 77     var previous: []const u8 = "";
 78     for (parts) |part| {
 79         if (part.len == 0) continue;
 80         if (previous.len != 0) {
 81             const previous_sep = std.fs.path.isSep(previous[previous.len - 1]);
 82             const current_sep = std.fs.path.isSep(part[0]);
 83             if (!previous_sep and !current_sep) total = try added(total, 1);
 84             total = try added(total, if (previous_sep and current_sep) part.len - 1 else part.len);
 85         } else {
 86             total = try added(total, part.len);
 87         }
 88         previous = part;
 89     }
 90     return total;
 91 }
 92 
 93 fn writeJoined(output: []u8, index: *usize, parts: []const []const u8) void {
 94     var previous: []const u8 = "";
 95     for (parts) |part| {
 96         if (part.len == 0) continue;
 97         if (previous.len == 0) {
 98             appendRaw(output, index, part);
 99         } else {
100             const previous_sep = std.fs.path.isSep(previous[previous.len - 1]);
101             const current_sep = std.fs.path.isSep(part[0]);
102             if (!previous_sep and !current_sep) {
103                 output[index.*] = std.fs.path.sep;
104                 index.* += 1;
105             }
106             appendRaw(output, index, if (previous_sep and current_sep) part[1..] else part);
107         }
108         previous = part;
109     }
110 }
111 
112 fn appendRaw(output: []u8, index: *usize, value: []const u8) void {
113     @memcpy(output[index.*..][0..value.len], value);
114     index.* += value.len;
115 }
116 
117 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
118     return std.math.add(usize, left, right) catch error.CapacityOverflow;
119 }
120 
121 test "site path transforms match allocating path semantics" {
122     const relative = try std.fs.path.join(std.testing.allocator, &.{ "guides", "start.md" });
123     defer std.testing.allocator.free(relative);
124     const target_bytes = try targetLength(relative, true);
125     var target_buffer: [128]u8 = undefined;
126     const target = writeTarget(target_buffer[0..target_bytes], relative, true);
127     const expected_target = try std.fs.path.join(std.testing.allocator, &.{ "guides", "start", index_name });
128     defer std.testing.allocator.free(expected_target);
129     try std.testing.expectEqualStrings(expected_target, target);
130 
131     const public_bytes = try publicLength(target.len, true, .url);
132     var public_buffer: [128]u8 = undefined;
133     const public = writePublic(public_buffer[0..public_bytes], target, true, .url);
134     try std.testing.expectEqualStrings("/guides/start/", public);
135 
136     const flat_bytes = try targetLength(relative, false);
137     const flat = writeTarget(target_buffer[0..flat_bytes], relative, false);
138     const expected_flat = try std.fs.path.join(std.testing.allocator, &.{ "guides", "start.html" });
139     defer std.testing.allocator.free(expected_flat);
140     try std.testing.expectEqualStrings(expected_flat, flat);
141 }
142 
143 test "bounded and allocating site paths preserve bytes" {
144     const cases = [_][]const u8{
145         "index.md",
146         "README.markdown",
147         "guides/start.md",
148         "guides/reference/index.markdown",
149         "notes",
150     };
151     var target_buffer: [256]u8 = undefined;
152     var public_buffer: [256]u8 = undefined;
153     for (cases) |relative_path| {
154         for ([_]bool{ true, false }) |clean_urls| {
155             const expected_target = try allocatingTarget(
156                 std.testing.allocator,
157                 relative_path,
158                 clean_urls,
159             );
160             defer std.testing.allocator.free(expected_target);
161             const target_bytes = try targetLength(relative_path, clean_urls);
162             const target = writeTarget(
163                 target_buffer[0..target_bytes],
164                 relative_path,
165                 clean_urls,
166             );
167             try std.testing.expectEqualStrings(expected_target, target);
168             for ([_]model.PublicKind{ .target, .url }) |kind| {
169                 const expected_public = try allocatingPublic(
170                     std.testing.allocator,
171                     expected_target,
172                     clean_urls,
173                     kind,
174                 );
175                 defer std.testing.allocator.free(expected_public);
176                 const public_bytes = try publicLength(target.len, clean_urls, kind);
177                 const public = writePublic(
178                     public_buffer[0..public_bytes],
179                     target,
180                     clean_urls,
181                     kind,
182                 );
183                 try std.testing.expectEqualStrings(expected_public, public);
184             }
185         }
186     }
187 }
188 
189 fn allocatingTarget(
190     allocator: std.mem.Allocator,
191     relative_path: []const u8,
192     clean_urls: bool,
193 ) std.mem.Allocator.Error![]u8 {
194     const base = std.fs.path.basename(relative_path);
195     const directory = std.fs.path.dirname(relative_path);
196     const stem = markdownStem(base);
197     if (clean_urls) {
198         if (std.mem.eql(u8, stem, "index") or std.mem.eql(u8, stem, "README")) {
199             if (directory) |parent| return try std.fs.path.join(allocator, &.{ parent, index_name });
200             return try allocator.dupe(u8, index_name);
201         }
202         if (directory) |parent| return try std.fs.path.join(allocator, &.{ parent, stem, index_name });
203         return try std.fs.path.join(allocator, &.{ stem, index_name });
204     }
205     const file = try std.fmt.allocPrint(allocator, "{s}{s}", .{ stem, html_suffix });
206     defer allocator.free(file);
207     if (directory) |parent| return try std.fs.path.join(allocator, &.{ parent, file });
208     return try allocator.dupe(u8, file);
209 }
210 
211 fn allocatingPublic(
212     allocator: std.mem.Allocator,
213     target: []const u8,
214     clean_urls: bool,
215     kind: model.PublicKind,
216 ) std.mem.Allocator.Error![]u8 {
217     const content = switch (kind) {
218         .target => target,
219         .url => if (clean_urls) target[0 .. target.len - index_name.len] else target,
220     };
221     var output = try allocator.alloc(u8, content.len + 1);
222     output[0] = '/';
223     for (content, output[1..]) |byte, *destination| {
224         destination.* = if (std.fs.path.isSep(byte)) '/' else byte;
225     }
226     return output;
227 }