1//! Removes whitespace between HTML/CSS markup.
2//! This function is exclusively intended for `template.html`,
3//! and doesn't handle many edge cases.
4input: []const u8,
5output: std.ArrayList(u8),
6
7const ws = " \t\r\n";
8
9pub 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
24pub 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
35fn 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
44fn 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
135fn 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
152fn 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
206fn 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
210fn 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
217fn 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
224fn 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
237const NaiveHtmlMinifier = @This();
238const std = @import("std");
239const mem = std.mem;
240const Allocator = mem.Allocator;