aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2026-05-21 16:04:26 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2026-05-21 16:04:26 +0200
commit78ba18b890655b12650afedac3a9d57ef95567d0 (patch)
treefce51444285a4b20f28e1c74f314ddce784e861a
parentab49ab9f5cc751d185b2049896913842bd33bf7b (diff)
frontend: implement more sophisticated minimizer
-rw-r--r--src/frontend/File.zig78
-rw-r--r--src/frontend/Minifier.zig170
-rw-r--r--src/frontend/root.zig3
-rw-r--r--src/frontend/web/hello-world/index.html3
-rw-r--r--src/frontend/web/index.html4
-rw-r--r--src/frontend/z-component.js3
6 files changed, 193 insertions, 68 deletions
diff --git a/src/frontend/File.zig b/src/frontend/File.zig
index e7c3b6d..fac6a03 100644
--- a/src/frontend/File.zig
+++ b/src/frontend/File.zig
@@ -1,6 +1,6 @@
-const std = @import("std");
+const Minifier = @import("Minifier.zig");
-pub fn File(comptime tag: ?[]const u8, Iterator: type) type {
+pub fn File(comptime tag: ?[]const u8, comptime minifier: Minifier) type {
const opening = if (tag) |t| "<" ++ t ++ ">" else "";
const closing = if (tag) |t| "</" ++ t ++ ">" else "";
@@ -22,70 +22,20 @@ pub fn File(comptime tag: ?[]const u8, Iterator: type) type {
};
}
- fn content(comptime self: @This()) []const u8 {
- return opening ++ @embedFile(self.file) ++ closing;
- }
-
- fn content_minified(comptime self: @This()) []const u8 {
- var minified: []const u8 = "";
- var iterator: Iterator = .init(@embedFile(self.file));
-
- while (iterator.next()) |segment| {
- minified = minified ++ segment;
- }
-
- return opening ++ minified ++ closing;
- }
-
pub fn to_html_string(comptime self: @This()) []const u8 {
- return if (self.do_minify) self.content_minified() else self.content();
- }
- };
-}
-
-fn StringIterator(comptime quotes: []const u8) type {
- return struct {
- content: []const u8,
-
- pub fn init(content: []const u8) @This() {
- return .{ .content = content };
- }
-
- pub fn next(self: *@This()) ?[]const u8 {
- @setEvalBranchQuota(self.content.len * 20);
-
- self.content = std.mem.trimStart(u8, self.content, &std.ascii.whitespace);
-
- if (self.content.len == 0) return null;
-
- var index: usize = 0;
-
- while (self.content.len > index) {
- const char = self.content[index];
-
- if (std.mem.indexOfScalar(u8, quotes, char) != null) {
- index += 1;
- while (self.content.len > index) {
- if (self.content[index] == char) {
- index += 1;
- break;
- }
- index += 1;
- }
- } else if (std.ascii.isWhitespace(char)) {
- break;
- }
- index += 1;
- }
-
- const block = self.content[0..index];
- self.content = self.content[index..];
-
- return block ++ " ";
+ return opening ++ (if (self.do_minify)
+ minifier.minify(@embedFile(self.file))
+ else
+ @embedFile(self.file)) ++ closing;
}
};
}
-pub const Style = File("style", StringIterator("\"'"));
-pub const Script = File("script", StringIterator("\"'`"));
-pub const Html = File(null, StringIterator("\"'"));
+/// When using regex in Javascript the spaces
+/// might get trimmed since /re/ is a awful syntax
+/// I don't what to add a rule for it currently,
+/// Since a single `/` is a operator but if there are two on one line
+/// it is a regex.
+pub const Script = File("script", .js);
+pub const Style = File("style", .css);
+pub const Html = File(null, .html);
diff --git a/src/frontend/Minifier.zig b/src/frontend/Minifier.zig
new file mode 100644
index 0000000..541bbe1
--- /dev/null
+++ b/src/frontend/Minifier.zig
@@ -0,0 +1,170 @@
+const std = @import("std");
+
+/// A string contained in quotes will not be changed in any form.
+quotes: []const u8,
+
+/// This character is used to escape in strings.
+escape: ?u8,
+
+/// A delimiter does not need any padding around it.
+delimiters: []const u8,
+
+/// Kinds of comments, will be removed in the output.
+comments: []const struct { []const u8, []const u8 },
+
+pub const html: @This() = .{
+ .quotes = "\"'",
+ .escape = null,
+ .delimiters = "<>",
+ .comments = &.{.{ "<!--", "-->" }},
+};
+
+pub const js: @This() = .{
+ .quotes = "\"'`",
+ .escape = '\\',
+ .delimiters = "+-*,.:;[](){}=&|^",
+ .comments = &.{ .{ "//", "\n" }, .{ "/*", "*/" } },
+};
+
+pub const css: @This() = .{
+ .quotes = "\"'",
+ .escape = '\\',
+ .delimiters = ":;{}",
+ .comments = &.{.{ "/*", "*/" }},
+};
+
+fn skip(comptime source: *[]const u8, comptime len: usize) void {
+ source.* = (source.*)[len..];
+}
+
+fn push(
+ comptime source: *[]const u8,
+ comptime target: *[]const u8,
+ comptime len: usize,
+) void {
+ target.* = target.* ++ (source.*)[0..len];
+ skip(source, len);
+}
+
+pub fn minify(
+ comptime self: *const @This(),
+ comptime c: []const u8,
+) []const u8 {
+ @setEvalBranchQuota(c.len * 100);
+ var content = std.mem.trimStart(u8, c, &std.ascii.whitespace);
+ var block: []const u8 = "";
+
+ while (content.len > 0) {
+
+ const had_comment: bool = cmt: {
+ inline for (self.comments) |comment| {
+ const start, const end = comment;
+
+ if (std.mem.startsWith(u8, content, start)) {
+ skip(&content, start.len);
+ skip(&content, if (std.mem.indexOf(u8, content, end)) |l| l + end.len else content.len);
+ break :cmt true;
+ }
+ }
+ break :cmt false;
+ };
+
+ if (had_comment) {
+ } else if (std.mem.indexOfScalar(u8, self.quotes, content[0])) |_| {
+ const quote = content[0];
+ push(&content, &block, 1);
+ while (content.len > 0) {
+ if (content[0] == self.escape) {
+ push(&content, &block, 2);
+ } else if (content[0] == quote) {
+ push(&content, &block, 1);
+ break;
+ }
+ push(&content, &block, 1);
+ }
+ } else {
+ push(&content, &block, 1);
+ }
+
+ const was_trimmed = content.len > 0 and std.ascii.isWhitespace(content[0]);
+ content = std.mem.trimStart(u8, content, &std.ascii.whitespace);
+ if (was_trimmed and
+ block.len > 0 and
+ content.len > 0 and
+ std.mem.indexOfScalar(u8, self.delimiters, block[block.len - 1]) == null and
+ std.mem.indexOfScalar(u8, self.delimiters, content[0]) == null)
+ {
+ block = block ++ " ";
+ }
+ }
+
+ return block;
+}
+
+test "html" {
+ const buffer: []const u8 =
+ \\<html>
+ \\ <head>
+ \\ <title>Some Title</title>
+ \\ </head>
+ \\ <body>
+ \\ <z-component type="something here" tag="1 2 3"> 123 </z-component>
+ \\ <!-- some comment here -->
+ \\ Hello World!
+ \\ </body>
+ \\</html>
+ ;
+
+ try std.testing.expectEqualStrings(
+ "<html><head><title>Some Title</title></head><body><z-component type=\"something here\" tag=\"1 2 3\">123</z-component>Hello World!</body></html>",
+ comptime html.minify(buffer),
+ );
+}
+
+test "css" {
+ const buffer: []const u8 =
+ \\body {
+ \\ background: #fff;
+ \\ color: red;
+ \\}
+ \\
+ \\#content:first {
+ \\ user-select: none;
+ \\ /* comment */
+ \\ content: ' ';
+ \\ padding: 5px 5px;
+ \\}
+ ;
+
+ try std.testing.expectEqualStrings(
+ "body{background:#fff;color:red;}#content:first{user-select:none;content:' ';padding:5px 5px;}",
+ comptime css.minify(buffer),
+ );
+}
+
+test "js" {
+ const buffer: []const u8 =
+ \\class extends HTMLElement {
+ \\ // comment here
+ \\ constructor() {
+ \\ super();
+ \\ this.attachShadow({ mode: 'open' });
+ \\ }
+ \\
+ \\ /*
+ \\ * Mulitline
+ \\ */
+ \\ connectedCallback() {
+ \\ const type = this.getAttribute('type');
+ \\ const template = document.getElementById(`--${type}`);
+ \\ const node = document.importNode(template.content, true);
+ \\ this.shadowRoot.appendChild(node);
+ \\ }
+ \\}
+ ;
+
+ try std.testing.expectEqualStrings(
+ "class extends HTMLElement{constructor(){super();this.attachShadow({mode:'open'});}connectedCallback(){const type=this.getAttribute('type');const template=document.getElementById(`--${type}`);const node=document.importNode(template.content,true);this.shadowRoot.appendChild(node);}}",
+ comptime js.minify(buffer),
+ );
+}
diff --git a/src/frontend/root.zig b/src/frontend/root.zig
index 39908b5..8ff1055 100644
--- a/src/frontend/root.zig
+++ b/src/frontend/root.zig
@@ -5,7 +5,8 @@ const std = @import("std");
pub const Component = @import("Component.zig");
pub const Document = @import("Document.zig");
pub const File = @import("File.zig");
-pub const Head = @import("File.zig");
+pub const Head = @import("Head.zig");
+pub const Minifier = @import("Minifier.zig");
test {
_ = std.testing.refAllDecls(@This());
diff --git a/src/frontend/web/hello-world/index.html b/src/frontend/web/hello-world/index.html
index a9b0be7..047a283 100644
--- a/src/frontend/web/hello-world/index.html
+++ b/src/frontend/web/hello-world/index.html
@@ -1,3 +1,6 @@
<div id="container" onclick="clicked()">
Click Me!
+ <z-component type="box">
+ <span slot="title">Hello World!<span>
+ </z-component>
</div>
diff --git a/src/frontend/web/index.html b/src/frontend/web/index.html
index 885f2c5..646cdb0 100644
--- a/src/frontend/web/index.html
+++ b/src/frontend/web/index.html
@@ -1 +1,3 @@
-<z-component type="hello-world"></z-component>
+<z-component type="hello-world">
+ <p slot="ur-mom">lulululu</p>
+</z-component>
diff --git a/src/frontend/z-component.js b/src/frontend/z-component.js
index e9b63ff..5e430b5 100644
--- a/src/frontend/z-component.js
+++ b/src/frontend/z-component.js
@@ -3,14 +3,13 @@ customElements.define(
class extends HTMLElement {
constructor() {
super();
- this.attachShadow({ mode: 'open' })
+ this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const type = this.getAttribute('type');
const template = document.getElementById(`--${type}`);
const node = document.importNode(template.content, true);
- this.shadowRoot.id = 'root';
this.shadowRoot.appendChild(node);
}
}