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, "&lt;"),
 17         '>' => try out.appendSlice(allocator, "&gt;"),
 18         '"' => try out.appendSlice(allocator, "&quot;"),
 19         '\'' => try out.appendSlice(allocator, "&#39;"),
 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, "&amp;"),
 39         '<' => copy(out, &index, "&lt;"),
 40         '>' => copy(out, &index, "&gt;"),
 41         '"' => copy(out, &index, "&quot;"),
 42         '\'' => copy(out, &index, "&#39;"),
 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         '&' => "&amp;".len,
121         '<' => "&lt;".len,
122         '>' => "&gt;".len,
123         '"' => "&quot;".len,
124         '\'' => "&#39;".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("&lt;a href=&quot;x&amp;y&quot;&gt;&#39;", 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 }