| 1 | //! Removes whitespace between HTML/CSS markup. |
| 2 | //! This function is exclusively intended for `template.html`, |
| 3 | //! and doesn't handle many edge cases. |
| 4 | input: []const u8, |
| 5 | output: std.ArrayList(u8), |
| 6 | |
| 7 | const ws = " \t\r\n"; |
| 8 | |
| 9 | pub fn minify(input: []const u8, allocator: Allocator) ![]u8 { |
| 10 | var m: NaiveHtmlMinifier = .{ |
| 11 | .input = input, |
| 12 | .output = std.ArrayList(u8).init(allocator), |
| 13 | }; |
| 14 | errdefer m.output.deinit(); |
| 15 | |
| 16 | while (m.input.len > 0) { |
| 17 | m.skipWhitespace(); |
| 18 | try m.processHtml(); |
| 19 | } |
| 20 | |
| 21 | return try m.output.toOwnedSlice(); |
| 22 | } |
| 23 | |
| 24 | pub inline fn minifyComptime(input: []const u8) []const u8 { |
| 25 | comptime { |
| 26 | @setEvalBranchQuota(99999999); |
| 27 | var buf: [input.len * 2]u8 = undefined; |
| 28 | var fba = std.heap.FixedBufferAllocator.init(&buf); |
| 29 | const result_slice = minify(input, fba.allocator()) catch unreachable; // should not increase size |
| 30 | const result = result_slice[0..].*; |
| 31 | return &result; |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | fn skipWhitespace(m: *NaiveHtmlMinifier) void { |
| 36 | var i: usize = 0; |
| 37 | while (m.input.len > i) switch (m.input[i]) { |
| 38 | ' ', '\t', '\n', '\r' => i += 1, |
| 39 | else => break, |
| 40 | }; |
| 41 | m.input = m.input[i..]; |
| 42 | } |
| 43 | |
| 44 | fn processHtml(m: *NaiveHtmlMinifier) !void { |
| 45 | switch (m.input[0]) { |
| 46 | ' ', '\t', '\n', '\r' => unreachable, // did not skip whitespace |
| 47 | |
| 48 | '<' => switch (m.input[1]) { |
| 49 | '!' => { |
| 50 | if (m.input[2] == '-' and m.input[3] == '-') { |
| 51 | // <!-- comment --> |
| 52 | m.input = m.input[(mem.indexOfPos(u8, m.input, 4, "-->") orelse @panic("no comment end")) + 3 ..]; |
| 53 | return; |
| 54 | } else { |
| 55 | // <!DOCTYPE ...> |
| 56 | try m.moveTokenWith(">"); |
| 57 | } |
| 58 | }, |
| 59 | '/' => try m.moveTokenWith(">"), |
| 60 | else => { |
| 61 | const first_tok = getToken(m, ws); |
| 62 | try m.moveToken(ws); |
| 63 | if (first_tok[first_tok.len - 1] == '>') { |
| 64 | try m.maybeProcessBody(first_tok[1 .. first_tok.len - 1]); |
| 65 | return; |
| 66 | } |
| 67 | const tag_name = first_tok[1..]; |
| 68 | var need_space_before_attr = true; |
| 69 | while (true) switch (m.input[0]) { |
| 70 | ' ', '\t', '\n', '\r' => unreachable, // did not skip whitespace |
| 71 | '>' => { |
| 72 | m.input = m.input[1..]; |
| 73 | try m.output.appendSlice(">"); |
| 74 | try m.maybeProcessBody(tag_name); |
| 75 | return; |
| 76 | }, |
| 77 | '/' => { |
| 78 | const tok = m.getToken(">"); |
| 79 | m.input = m.input[tok.len + 1 ..]; |
| 80 | try m.output.appendSlice("/>"); |
| 81 | try m.maybeProcessBody(tag_name); |
| 82 | return; |
| 83 | }, |
| 84 | else => { |
| 85 | if (need_space_before_attr) |
| 86 | try m.output.appendSlice(" "); |
| 87 | |
| 88 | try m.moveToken("=>" ++ ws); |
| 89 | if (m.input[0] == '=') { |
| 90 | m.input = m.input[1..]; |
| 91 | m.skipWhitespace(); |
| 92 | try m.output.appendSlice("="); |
| 93 | |
| 94 | // move attr value |
| 95 | const end = switch (m.input[0]) { |
| 96 | '"' => brk: { |
| 97 | need_space_before_attr = false; |
| 98 | try m.output.append('"'); |
| 99 | m.input = m.input[1..]; |
| 100 | break :brk "\""; |
| 101 | }, |
| 102 | '\'' => brk: { |
| 103 | need_space_before_attr = false; |
| 104 | try m.output.append('\''); |
| 105 | m.input = m.input[1..]; |
| 106 | break :brk "'"; |
| 107 | }, |
| 108 | else => brk: { |
| 109 | need_space_before_attr = true; |
| 110 | break :brk ws ++ ">"; |
| 111 | }, |
| 112 | }; |
| 113 | try m.moveTokenWith(end); |
| 114 | |
| 115 | m.skipWhitespace(); |
| 116 | } |
| 117 | }, |
| 118 | }; |
| 119 | }, |
| 120 | }, |
| 121 | |
| 122 | else => { |
| 123 | const text_block = m.getToken("<"); |
| 124 | m.input = m.input[text_block.len..]; |
| 125 | var it = mem.tokenizeAny(u8, text_block, ws); |
| 126 | try m.output.appendSlice(it.next().?); |
| 127 | while (it.next()) |token| { |
| 128 | try m.output.append(' '); |
| 129 | try m.output.appendSlice(token); |
| 130 | } |
| 131 | }, |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | fn maybeProcessBody(m: *NaiveHtmlMinifier, tag_name: []const u8) !void { |
| 136 | inline for (.{ .style, .script, .pre }) |tag| { |
| 137 | if (mem.eql(u8, tag_name, @tagName(tag))) { |
| 138 | const end = mem.indexOf(u8, m.input, "</" ++ @tagName(tag) ++ ">") orelse @panic("no end"); |
| 139 | const tok = m.input[0 .. end + @tagName(tag).len + 3]; |
| 140 | |
| 141 | if (tag == .style) { |
| 142 | try m.processCss(m.input[0..end]); |
| 143 | } else { |
| 144 | try m.output.appendSlice(tok); |
| 145 | } |
| 146 | |
| 147 | m.input = m.input[end..]; |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | fn processCss(m: *NaiveHtmlMinifier, input_css: []const u8) !void { |
| 153 | var without_comments = try std.ArrayList(u8).initCapacity(m.output.allocator, input_css.len); |
| 154 | defer without_comments.deinit(); |
| 155 | |
| 156 | { |
| 157 | var i: usize = 0; |
| 158 | while (mem.indexOfScalarPos(u8, input_css, i, '/')) |j| { |
| 159 | if (j + 1 < input_css.len and input_css[j + 1] == '*') { |
| 160 | try without_comments.appendSlice(input_css[i..j]); |
| 161 | i = mem.indexOfPos(u8, input_css, j + 2, "*/") orelse |
| 162 | std.debug.panic("no end comment in {s}", .{input_css[j..]}); |
| 163 | } else { |
| 164 | i = j + 1; |
| 165 | } |
| 166 | } |
| 167 | try without_comments.appendSlice(input_css[i..]); |
| 168 | } |
| 169 | |
| 170 | var cm: NaiveHtmlMinifier = .{ .input = without_comments.items, .output = undefined }; |
| 171 | cm.skipWhitespace(); |
| 172 | |
| 173 | while (cm.input.len > 0) { |
| 174 | const part = cm.getToken("{};"); |
| 175 | const last = cm.input[part.len]; |
| 176 | cm.input = cm.input[part.len + 1 ..]; |
| 177 | |
| 178 | switch (last) { |
| 179 | '{' => { |
| 180 | try appendWithoutCssComment(part, &m.output); |
| 181 | }, |
| 182 | ';', '}' => rule: { |
| 183 | if (part.len == 0) { |
| 184 | if (m.output.items[m.output.items.len - 1] == ';') |
| 185 | m.output.items.len -= 1; |
| 186 | break :rule; |
| 187 | } |
| 188 | |
| 189 | const i = mem.indexOfScalar(u8, part, ':') orelse |
| 190 | std.debug.panic("missing colon in {s}", .{part}); |
| 191 | const key = part[0..i]; |
| 192 | const value = part[i + 1 ..]; |
| 193 | |
| 194 | try appendWithoutCssComment(key, &m.output); |
| 195 | try m.output.append(':'); |
| 196 | try appendWithoutCssComment(value, &m.output); |
| 197 | }, |
| 198 | else => @panic("no end"), |
| 199 | } |
| 200 | |
| 201 | try m.output.append(last); |
| 202 | cm.skipWhitespace(); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | fn getToken(m: *const NaiveHtmlMinifier, ends: []const u8) []const u8 { |
| 207 | return m.input[0 .. mem.indexOfAny(u8, m.input, ends) orelse return m.input]; |
| 208 | } |
| 209 | |
| 210 | fn moveToken(m: *NaiveHtmlMinifier, ends: []const u8) !void { |
| 211 | const tok = m.getToken(ends); |
| 212 | m.input = m.input[tok.len..]; |
| 213 | m.skipWhitespace(); |
| 214 | try m.output.appendSlice(tok); |
| 215 | } |
| 216 | |
| 217 | fn moveTokenWith(m: *NaiveHtmlMinifier, ends: []const u8) !void { |
| 218 | const tok = m.getToken(ends); |
| 219 | try m.output.appendSlice(m.input[0 .. tok.len + 1]); |
| 220 | m.input = m.input[tok.len + 1 ..]; |
| 221 | m.skipWhitespace(); |
| 222 | } |
| 223 | |
| 224 | fn appendWithoutCssComment(text: []const u8, output: *std.ArrayList(u8)) !void { |
| 225 | var it = mem.tokenizeAny(u8, text, ws); |
| 226 | var needs_space = false; |
| 227 | while (it.next()) |tok| { |
| 228 | if (needs_space) try output.append(' '); |
| 229 | try output.appendSlice(tok); |
| 230 | needs_space = switch (tok[tok.len - 1]) { |
| 231 | ',' => false, |
| 232 | else => true, |
| 233 | }; |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | const NaiveHtmlMinifier = @This(); |
| 238 | const std = @import("std"); |
| 239 | const mem = std.mem; |
| 240 | const Allocator = mem.Allocator; |