lib/zen/src/html.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const Allocator = std.mem.Allocator;
4
5 pub fn appendEscaped(out: *std.ArrayList(u8), allocator: Allocator, value: []const u8) Allocator.Error!void {
6 for (value) |byte| try appendEscapedByte(out, allocator, byte);
7 }
8
9 pub fn appendAttributeEscaped(out: *std.ArrayList(u8), allocator: Allocator, value: []const u8) Allocator.Error!void {
10 try appendEscaped(out, allocator, value);
11 }
12
13 pub fn appendEscapedByte(out: *std.ArrayList(u8), allocator: Allocator, byte: u8) Allocator.Error!void {
14 switch (byte) {
15 '&' => try out.appendSlice(allocator, "&"),
16 '<' => try out.appendSlice(allocator, "<"),
17 '>' => try out.appendSlice(allocator, ">"),
18 '"' => try out.appendSlice(allocator, """),
19 '\'' => try out.appendSlice(allocator, "'"),
20 else => try out.append(allocator, byte),
21 }
22 }
23
24 pub fn escapedLength(value: []const u8) error{CapacityOverflow}!usize {
25 var length: usize = 0;
26 for (value) |byte| {
27 length = std.math.add(usize, length, escapedByteLength(byte)) catch
28 return error.CapacityOverflow;
29 }
30 return length;
31 }
32
33 pub fn writeEscaped(out: []u8, value: []const u8) []u8 {
34 const length = escapedLength(value) catch unreachable;
35 std.debug.assert(out.len >= length);
36 var index: usize = 0;
37 for (value) |byte| switch (byte) {
38 '&' => copy(out, &index, "&"),
39 '<' => copy(out, &index, "<"),
40 '>' => copy(out, &index, ">"),
41 '"' => copy(out, &index, """),
42 '\'' => copy(out, &index, "'"),
43 else => {
44 out[index] = byte;
45 index += 1;
46 },
47 };
48 std.debug.assert(index == length);
49 return out[0..index];
50 }
51
52 pub fn escapedAlloc(allocator: Allocator, value: []const u8) Allocator.Error![]u8 {
53 var out: std.ArrayList(u8) = .empty;
54 errdefer out.deinit(allocator);
55 try appendEscaped(&out, allocator, value);
56 return try out.toOwnedSlice(allocator);
57 }
58
59 pub fn slug(allocator: Allocator, value: []const u8) Allocator.Error![]u8 {
60 const out = try allocator.alloc(u8, slugLength(value));
61 _ = writeSlug(out, value);
62 return out;
63 }
64
65 pub fn slugLength(value: []const u8) usize {
66 var length: usize = 0;
67 var previous_dash = false;
68
69 for (value) |byte| {
70 if (isAsciiAlnum(byte)) {
71 length += 1;
72 previous_dash = false;
73 } else if (byte == ' ' or byte == '\t' or byte == '-' or byte == '_') {
74 if (!previous_dash and length != 0) {
75 length += 1;
76 previous_dash = true;
77 }
78 }
79 }
80
81 if (length != 0 and previous_dash) length -= 1;
82 return if (length == 0) "section".len else length;
83 }
84
85 pub fn writeSlug(out: []u8, value: []const u8) []u8 {
86 std.debug.assert(out.len >= slugLength(value));
87 var index: usize = 0;
88 var pending_dash = false;
89
90 for (value) |byte| {
91 if (isAsciiAlnum(byte)) {
92 if (pending_dash) {
93 out[index] = '-';
94 index += 1;
95 }
96 out[index] = std.ascii.toLower(byte);
97 index += 1;
98 pending_dash = false;
99 } else if (byte == ' ' or byte == '\t' or byte == '-' or byte == '_') {
100 if (index != 0) pending_dash = true;
101 }
102 }
103
104 if (index == 0) {
105 @memcpy(out[0.."section".len], "section");
106 index = "section".len;
107 }
108 std.debug.assert(index == slugLength(value));
109 return out[0..index];
110 }
111
112 fn isAsciiAlnum(byte: u8) bool {
113 return (byte >= 'a' and byte <= 'z') or
114 (byte >= 'A' and byte <= 'Z') or
115 (byte >= '0' and byte <= '9');
116 }
117
118 fn escapedByteLength(byte: u8) usize {
119 return switch (byte) {
120 '&' => "&".len,
121 '<' => "<".len,
122 '>' => ">".len,
123 '"' => """.len,
124 '\'' => "'".len,
125 else => 1,
126 };
127 }
128
129 fn copy(out: []u8, index: *usize, value: []const u8) void {
130 @memcpy(out[index.*..][0..value.len], value);
131 index.* += value.len;
132 }
133
134 test "html escaping covers text and attributes" {
135 const escaped = try escapedAlloc(std.testing.allocator, "<a href=\"x&y\">'");
136 defer std.testing.allocator.free(escaped);
137 try std.testing.expectEqualStrings("<a href="x&y">'", escaped);
138 var exact: [44]u8 = undefined;
139 const written = writeEscaped(&exact, "<a href=\"x&y\">'");
140 try std.testing.expectEqual(try escapedLength("<a href=\"x&y\">'"), written.len);
141 try std.testing.expectEqualStrings(escaped, written);
142 }
143
144 test "slug normalizes ascii heading text" {
145 const value = try slug(std.testing.allocator, "Fast, Robust Sites!");
146 defer std.testing.allocator.free(value);
147 try std.testing.expectEqualStrings("fast-robust-sites", value);
148 }
149
150 test "slug length and caller storage cover fallback and trailing separators" {
151 var normal: [17]u8 = undefined;
152 try std.testing.expectEqualStrings(
153 "fast-robust-sites",
154 writeSlug(&normal, "Fast, Robust Sites!--"),
155 );
156 var fallback: [7]u8 = undefined;
157 try std.testing.expectEqualStrings("section", writeSlug(&fallback, "!?"));
158 try std.testing.expectEqual(@as(usize, 7), slugLength("!?"));
159 }