From cd32a3ca0173676ed31486b5fa4dbd8c744cdec5 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Thu, 28 May 2026 16:30:26 +0200 Subject: implement frontend framework --- build.zig | 44 +++++++-- flake.nix | 1 + src/frontend/Component.zig | 30 ------ src/frontend/Document.zig | 46 --------- src/frontend/File.zig | 41 -------- src/frontend/Head.zig | 58 ----------- src/frontend/Minifier.zig | 168 -------------------------------- src/frontend/root.zig | 13 --- src/frontend/web/hello-world/index.css | 7 -- src/frontend/web/hello-world/index.html | 6 -- src/frontend/web/hello-world/index.js | 3 - src/frontend/web/index.css | 4 - src/frontend/web/index.html | 3 - src/frontend/z-component.js | 16 --- src/html/Element.zig | 104 ++++++++++++++++++++ src/html/root.zig | 7 ++ src/http/Server.zig | 40 +++++++- src/main.zig | 25 ++++- src/web/badge/index.css | 5 + src/web/badge/index.html | 3 + src/web/badge/index.js | 1 + src/web/badge/root.zig | 8 ++ src/web/index.css | 4 + src/web/index.html | 3 + src/web/index.js | 0 src/z/Component.zig | 18 ++++ src/z/File.zig | 18 ++++ src/z/Minifier.zig | 168 ++++++++++++++++++++++++++++++++ src/z/root.zig | 52 ++++++++++ src/z/z.js | 78 +++++++++++++++ 30 files changed, 564 insertions(+), 410 deletions(-) delete mode 100644 src/frontend/Component.zig delete mode 100644 src/frontend/Document.zig delete mode 100644 src/frontend/File.zig delete mode 100644 src/frontend/Head.zig delete mode 100644 src/frontend/Minifier.zig delete mode 100644 src/frontend/root.zig delete mode 100644 src/frontend/web/hello-world/index.css delete mode 100644 src/frontend/web/hello-world/index.html delete mode 100644 src/frontend/web/hello-world/index.js delete mode 100644 src/frontend/web/index.css delete mode 100644 src/frontend/web/index.html delete mode 100644 src/frontend/z-component.js create mode 100644 src/html/Element.zig create mode 100644 src/html/root.zig create mode 100644 src/web/badge/index.css create mode 100644 src/web/badge/index.html create mode 100644 src/web/badge/index.js create mode 100644 src/web/badge/root.zig create mode 100644 src/web/index.css create mode 100644 src/web/index.html create mode 100644 src/web/index.js create mode 100644 src/z/Component.zig create mode 100644 src/z/File.zig create mode 100644 src/z/Minifier.zig create mode 100644 src/z/root.zig create mode 100644 src/z/z.js diff --git a/build.zig b/build.zig index db5a525..989f6c4 100644 --- a/build.zig +++ b/build.zig @@ -22,11 +22,17 @@ pub fn build(b: *std.Build) void { .target = target, }); - const frontend_mod = b.addModule("frontend", .{ - .root_source_file = b.path("src/frontend/root.zig"), + const html_mod = b.addModule("html", .{ + .root_source_file = b.path("src/html/root.zig"), .target = target, }); + const z_mod = b.addModule("z", .{ + .root_source_file = b.path("src/z/root.zig"), + .target = target, + }); + z_mod.addImport("html", html_mod); + const http_mod = b.addModule("http", .{ .root_source_file = b.path("src/http/root.zig"), .target = target, @@ -43,7 +49,8 @@ pub fn build(b: *std.Build) void { .imports = &.{ .{ .name = "db", .module = db_mod }, .{ .name = "api", .module = api_mod }, - .{ .name = "frontend", .module = frontend_mod }, + .{ .name = "z", .module = z_mod }, + .{ .name = "html", .module = html_mod }, .{ .name = "http", .module = http_mod }, } }), @@ -78,12 +85,19 @@ pub fn build(b: *std.Build) void { api_test_step.dependOn(&run_api_tests.step); - const frontend_tests = b.addTest(.{ - .root_module = frontend_mod, + const z_tests = b.addTest(.{ + .root_module = z_mod, + }); + const run_z_tests = b.addRunArtifact(z_tests); + const z_test_step = b.step("test-z", "Run z tests"); + z_test_step.dependOn(&run_z_tests.step); + + const html_tests = b.addTest(.{ + .root_module = html_mod, }); - const run_frontend_tests = b.addRunArtifact(frontend_tests); - const frontend_test_step = b.step("test-frontend", "Run frontend tests"); - frontend_test_step.dependOn(&run_frontend_tests.step); + const run_html_tests = b.addRunArtifact(html_tests); + const html_test_step = b.step("test-html", "Run html tests"); + html_test_step.dependOn(&run_html_tests.step); const http_tests = b.addTest(.{ @@ -103,7 +117,8 @@ pub fn build(b: *std.Build) void { const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_db_tests.step); test_step.dependOn(&run_api_tests.step); - test_step.dependOn(&run_frontend_tests.step); + test_step.dependOn(&run_z_tests.step); + test_step.dependOn(&run_html_tests.step); test_step.dependOn(&run_http_tests.step); test_step.dependOn(&run_exe_tests.step); @@ -130,4 +145,15 @@ pub fn build(b: *std.Build) void { .source_dir = exe.getEmittedDocs(), }); docs_step.dependOn(&docs_install.step); + + + const hot_cmd = b.addSystemCommand(&.{ + "sh", + "-c", + "clear; echo 'building in hot mode'; find src -type f | entr -r zig build run", + }); + + const hot_step = b.step("hot", "Run server in hot reload mode"); + hot_step.dependOn(&hot_cmd.step); + run_cmd.step.dependOn(b.getInstallStep()); } diff --git a/flake.nix b/flake.nix index a461455..92ff2d9 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,7 @@ packages = [ pkgs.zig pkgs.sqlite + pkgs.entr ]; }; }; diff --git a/src/frontend/Component.zig b/src/frontend/Component.zig deleted file mode 100644 index d90ed3b..0000000 --- a/src/frontend/Component.zig +++ /dev/null @@ -1,30 +0,0 @@ -const std = @import("std"); - -const File = @import("File.zig"); - -const Self = @This(); - -name: []const u8, -parent_dir: []const u8, - -pub fn path(p: []const u8) Self { - const name = p[if (std.mem.lastIndexOfScalar( - u8, - std.mem.trim(u8, p, "/"), - '/', - )) |index| index + 1 else 0..]; - - return .{ - .name = name, - .parent_dir = p, - }; -} - -pub fn to_html_string(comptime self: Self) []const u8 { - const html: File.Html = .minify(self.parent_dir ++ "/index.html"); - const js: File.Script = .minify(self.parent_dir ++ "/index.js"); - const css: File.Style = .minify(self.parent_dir ++ "/index.css"); - return ""; -} - -test {} diff --git a/src/frontend/Document.zig b/src/frontend/Document.zig deleted file mode 100644 index 0ba46b1..0000000 --- a/src/frontend/Document.zig +++ /dev/null @@ -1,46 +0,0 @@ -const std = @import("std"); - -const Component = @import("Component.zig"); -const Head = @import("Head.zig"); -const File = @import("File.zig"); - -const Self = @This(); - -components: []const Component, -head: Head, -styles: []const File.Style, -contents: []const File.Html, - -pub const empty: Self = .{ - .head = .empty, - .components = &.{}, - .styles = &.{}, - .contents = &.{}, -}; - -pub fn to_string(comptime self: Self) []const u8 { - comptime var content: []const u8 = ""; - content = content ++ comptime self.head.to_html_string(self.components, self.styles); - content = content ++ ""; - - inline for (self.contents) |c| { - content = content ++ comptime c.to_html_string(); - } - - content = content ++ ""; - - return content; -} - -test { - const document: Self = comptime .{ - .head = .default("test"), - .components = &.{ - .path("./web/hello-world"), - }, - .styles = &.{}, - .contents = &.{.minify("./web/index.html")}, - }; - - std.debug.print("{s}", .{document.to_string()}); -} diff --git a/src/frontend/File.zig b/src/frontend/File.zig deleted file mode 100644 index fac6a03..0000000 --- a/src/frontend/File.zig +++ /dev/null @@ -1,41 +0,0 @@ -const Minifier = @import("Minifier.zig"); - -pub fn File(comptime tag: ?[]const u8, comptime minifier: Minifier) type { - const opening = if (tag) |t| "<" ++ t ++ ">" else ""; - const closing = if (tag) |t| "" else ""; - - return struct { - file: []const u8, - do_minify: bool, - - pub fn path(comptime p: []const u8) @This() { - return .{ - .file = p, - .do_minify = false, - }; - } - - pub fn minify(comptime p: []const u8) @This() { - return .{ - .file = p, - .do_minify = true, - }; - } - - pub fn to_html_string(comptime self: @This()) []const u8 { - return opening ++ (if (self.do_minify) - minifier.minify(@embedFile(self.file)) - else - @embedFile(self.file)) ++ closing; - } - }; -} - -/// 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/Head.zig b/src/frontend/Head.zig deleted file mode 100644 index 0eb090f..0000000 --- a/src/frontend/Head.zig +++ /dev/null @@ -1,58 +0,0 @@ -const File = @import("File.zig"); -const Component = @import("Component.zig"); - -const z_component_code: File.Script = .minify("z-component.js"); - -pub const Meta = struct { - name: []const u8, - content: []const u8, - - pub fn to_html_string(comptime self: @This()) []const u8 { - return ""; - } -}; - -title: []const u8, -meta: []const Meta, - -pub const empty: @This() = .{ - .title = "", - .meta = &.{}, -}; - -pub fn default(comptime title: []const u8) @This() { - return .{ - .title = title, - .meta = &.{ - .{ - .name = "viewport", - .content = "width=device-width, initial-scale=1.0", - }, - }, - }; -} - -pub fn to_html_string( - comptime self: @This(), - comptime components: []const Component, - comptime styles: []const File.Style, -) []const u8 { - var content: []const u8 = "" ++ z_component_code.to_html_string(); - - content = content ++ "" ++ self.title ++ ""; - - inline for (components) |component| { - content = content ++ comptime component.to_html_string(); - } - - inline for (styles) |style| { - content = content ++ comptime style.to_html_string(); - } - - inline for (self.meta) |meta| { - content = content ++ comptime meta.to_html_string(); - } - - content = content ++ ""; - return content; -} diff --git a/src/frontend/Minifier.zig b/src/frontend/Minifier.zig deleted file mode 100644 index 65dd9fb..0000000 --- a/src/frontend/Minifier.zig +++ /dev/null @@ -1,168 +0,0 @@ -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 = - \\ - \\ - \\ Some Title - \\ - \\ - \\ 123 - \\ - \\ Hello World! - \\ - \\ - ; - - try std.testing.expectEqualStrings( - "Some Title123Hello World!", - 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 deleted file mode 100644 index 8ff1055..0000000 --- a/src/frontend/root.zig +++ /dev/null @@ -1,13 +0,0 @@ -//! Implementation of the frontend contains mainly javascript files -//! which will be embedded right into the ELF file to improve deployment. - -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("Head.zig"); -pub const Minifier = @import("Minifier.zig"); - -test { - _ = std.testing.refAllDecls(@This()); -} diff --git a/src/frontend/web/hello-world/index.css b/src/frontend/web/hello-world/index.css deleted file mode 100644 index 41b0b29..0000000 --- a/src/frontend/web/hello-world/index.css +++ /dev/null @@ -1,7 +0,0 @@ -#container { - background: #efefef; - padding: 10px; - border-radius: 4px; - user-select: none; - cursor: pointer; -} diff --git a/src/frontend/web/hello-world/index.html b/src/frontend/web/hello-world/index.html deleted file mode 100644 index 047a283..0000000 --- a/src/frontend/web/hello-world/index.html +++ /dev/null @@ -1,6 +0,0 @@ -
- Click Me! - - Hello World! - -
diff --git a/src/frontend/web/hello-world/index.js b/src/frontend/web/hello-world/index.js deleted file mode 100644 index 8e55c52..0000000 --- a/src/frontend/web/hello-world/index.js +++ /dev/null @@ -1,3 +0,0 @@ -function clicked() { - console.log('clicked'); -} diff --git a/src/frontend/web/index.css b/src/frontend/web/index.css deleted file mode 100644 index a93ea47..0000000 --- a/src/frontend/web/index.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - padding: 0; - margin: 0; -} diff --git a/src/frontend/web/index.html b/src/frontend/web/index.html deleted file mode 100644 index 646cdb0..0000000 --- a/src/frontend/web/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -

lulululu

-
diff --git a/src/frontend/z-component.js b/src/frontend/z-component.js deleted file mode 100644 index 5e430b5..0000000 --- a/src/frontend/z-component.js +++ /dev/null @@ -1,16 +0,0 @@ -customElements.define( - "z-component", - 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); - } - } -); diff --git a/src/html/Element.zig b/src/html/Element.zig new file mode 100644 index 0000000..8ecf31e --- /dev/null +++ b/src/html/Element.zig @@ -0,0 +1,104 @@ +const Self = @This(); + +pub const Content = union(enum) { + elements: []const Self, + string: []const u8, + void: void, + + pub fn content(string: []const u8) @This() { + return .{ .string = string }; + } + + pub fn children(elements: []const Self) @This() { + return .{ .elements = elements }; + } +}; + +pub const Attribute = struct { + name: []const u8, + value: []const u8, +}; + +tag: []const u8, +attributes: []const Attribute, +content: Content, + +fn attributesFrom(attributes: anytype) []const Attribute { + const T = @TypeOf(attributes); + const info = @typeInfo(T); + + if (info != .@"struct") { + @compileError("struct needed"); + } + + const fields = info.@"struct".fields; + + var attrs: []const Attribute = &.{}; + + for (fields) |field| { + const name = field.name; + const value = @field(attributes, name); + + const next_attr: []const Attribute = &.{ .{ .name = name, .value = value } }; + attrs = attrs ++ next_attr; + } + + return attrs; +} + +pub fn native(name: @EnumLiteral(), attrs: anytype, content: Content) Self { + return .{ + .tag = @tagName(name), + .attributes = attributesFrom(attrs), + .content = content, + }; +} + +pub fn transparent(content: Content) Self { + return .{ + .tag = "", + .attributes = &.{}, + .content = content, + }; +} + +pub fn toHtml(comptime self: Self) []const u8 { + var string: []const u8 = string: { + if (self.tag.len == 0) { + break :string ""; + } + + var string: []const u8 = "<" ++ self.tag; + + inline for (self.attributes) |attr| { + string = string ++ " " ++ attr.name ++ "='" ++ attr.value ++ "'"; + } + + string = string ++ ">"; + + break :string string; + }; + + + switch (self.content) { + .void => {}, + .string => |s| { + string = string ++ s ++ ""; + }, + .elements => |els| { + for (els) |el| { + string = string ++ el.toHtml(); + } + + string = string ++ ""; + } + } + + string = string ++ if (self.tag.len == 0) "" else ""; + + return string; +} + +pub fn toDocument(comptime self: Self) []const u8 { + return "" ++ self.toHtml(); +} diff --git a/src/html/root.zig b/src/html/root.zig new file mode 100644 index 0000000..14a9b3c --- /dev/null +++ b/src/html/root.zig @@ -0,0 +1,7 @@ +const std = @import("std"); + +pub const Element = @import("Element.zig"); + +test { + _ = std.testing.refAllDecls(@This()); +} diff --git a/src/http/Server.zig b/src/http/Server.zig index b6e2d90..71037bb 100644 --- a/src/http/Server.zig +++ b/src/http/Server.zig @@ -3,18 +3,48 @@ const std = @import("std"); const Self = @This(); server: std.Io.net.Server, -io: std.Io.Threaded, +threaded: std.Io.Threaded, pub fn init( gpa: std.mem.Allocator, address: std.Io.net.IpAddress, ) !Self { - var io: std.Io.Threaded = .init(gpa, .{}); + var threaded: std.Io.Threaded = .init(gpa, .{}); return .{ - .io = io, - .server = try address.listen(io.io(), .{ .reuse_address = true }), + .threaded = threaded, + .server = try address.listen(threaded.io(), .{ .reuse_address = true }), }; } -pub fn serve(_: *Self) !void {} +pub fn serve(self: *Self, content: []const u8) !void { + var read_buffer: [1024 * 8]u8 = undefined; + var write_buffer: [1024 * 8]u8 = undefined; + + while (true) { + const stream = try self.server.accept(self.threaded.io()); + defer stream.close(self.threaded.io()); + + var reader = stream.reader(self.threaded.io(), &read_buffer); + var writer = stream.writer(self.threaded.io(), &write_buffer); + + var server: std.http.Server = .init(&reader.interface, &writer.interface); + var request = server.receiveHead() catch |e| { + std.debug.print("error: {}\n", .{e}); + continue; + }; + + std.debug.print("{s} {s}\n", .{ @tagName(request.head.method), request.head.target }); + + if (std.mem.eql(u8, request.head.target, "/")) { + request.respond(content, .{ + .status = .ok, + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "text/html" }, + }, + }) catch continue; + } else { + request.respond("Not Found", .{ .status = .not_found }) catch continue; + } + } +} diff --git a/src/main.zig b/src/main.zig index df0c061..f17214c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,10 +5,33 @@ const std = @import("std"); const http = @import("http"); +const html = @import("html"); +const z = @import("z"); + +const badge = @import("web/badge/root.zig"); + +const document: html.Element = .native(.html, .{}, .children(&.{ + .native(.head, .{}, .children(&.{ + .native(.title, .{}, .content("Memora")), + .native(.meta, .{ + .name = "viewport", + .content = "width=device-width, initial-scale=1.0", + }, .void), + z.env(&.{ + badge.component, + }), + })), + .native(.body, .{}, .children(&.{ + z.html.load(@embedFile("web/index.html")), + z.component(.badge, .{}, .children(&.{ + .native(.span, .{ .slot = "content" }, .content("Hello Z (direct)!")), + })), + })), +})); pub fn main(init: std.process.Init) !void { var server: http.Server = try .init(init.gpa, try .parseLiteral("127.0.0.1:8080")); - try server.serve(); + try server.serve(comptime document.toDocument()); } test { diff --git a/src/web/badge/index.css b/src/web/badge/index.css new file mode 100644 index 0000000..3f237d6 --- /dev/null +++ b/src/web/badge/index.css @@ -0,0 +1,5 @@ +#container { + background: red; + padding: 5px; + border-radius: 5px; +} diff --git a/src/web/badge/index.html b/src/web/badge/index.html new file mode 100644 index 0000000..dcb38e1 --- /dev/null +++ b/src/web/badge/index.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/web/badge/index.js b/src/web/badge/index.js new file mode 100644 index 0000000..60fbac5 --- /dev/null +++ b/src/web/badge/index.js @@ -0,0 +1 @@ +Z.by_id('sdf') diff --git a/src/web/badge/root.zig b/src/web/badge/root.zig new file mode 100644 index 0000000..5e8ea25 --- /dev/null +++ b/src/web/badge/root.zig @@ -0,0 +1,8 @@ +const z = @import("z"); + +pub const component: z.Component = .{ + .name = .badge, + .body = @embedFile("index.html"), + .style = @embedFile("index.css"), + .script = @embedFile("index.js"), +}; diff --git a/src/web/index.css b/src/web/index.css new file mode 100644 index 0000000..a93ea47 --- /dev/null +++ b/src/web/index.css @@ -0,0 +1,4 @@ +body { + padding: 0; + margin: 0; +} diff --git a/src/web/index.html b/src/web/index.html new file mode 100644 index 0000000..3bf9953 --- /dev/null +++ b/src/web/index.html @@ -0,0 +1,3 @@ + + Hello Z! + diff --git a/src/web/index.js b/src/web/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/z/Component.zig b/src/z/Component.zig new file mode 100644 index 0000000..3211335 --- /dev/null +++ b/src/z/Component.zig @@ -0,0 +1,18 @@ +const std = @import("std"); +const html = @import("html"); +const z = @import("root.zig"); + +name: @EnumLiteral(), +body: []const u8, +style: []const u8, +script: []const u8, + +pub fn toElement(comptime self: @This()) html.Element { + return .transparent(.children(&.{ + .native(.template, .{ .id = "@" ++ @tagName(self.name) }, .children(&.{ + z.style.load(self.style), + z.body.load(self.body), + })), + z.script.load("Zenv.constructors['component__" ++ @tagName(self.name) ++ "'] = (Z) => {" ++ self.script ++ "}"), + })); +} diff --git a/src/z/File.zig b/src/z/File.zig new file mode 100644 index 0000000..cd7094f --- /dev/null +++ b/src/z/File.zig @@ -0,0 +1,18 @@ +const Minifier = @import("Minifier.zig"); +const Element = @import("html").Element; + +tag: ?@EnumLiteral(), +minifier: Minifier, + +pub fn load(comptime self: @This(), comptime content: []const u8) Element { + if (self.tag) |tag| { + return .native( + tag, + .{}, + .content(comptime self.minifier.minify(content)), + ); + } else { + return .transparent(.content(comptime self.minifier.minify(content))); + } +} + diff --git a/src/z/Minifier.zig b/src/z/Minifier.zig new file mode 100644 index 0000000..65dd9fb --- /dev/null +++ b/src/z/Minifier.zig @@ -0,0 +1,168 @@ +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 = + \\ + \\ + \\ Some Title + \\ + \\ + \\ 123 + \\ + \\ Hello World! + \\ + \\ + ; + + try std.testing.expectEqualStrings( + "Some Title123Hello World!", + 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/z/root.zig b/src/z/root.zig new file mode 100644 index 0000000..26bbffc --- /dev/null +++ b/src/z/root.zig @@ -0,0 +1,52 @@ +const std = @import("std"); + +pub const Minifier = @import("Minifier.zig"); +pub const Component = @import("Component.zig"); +pub const File = @import("File.zig"); + +const Element = @import("html").Element; + +pub const script: File = .{ + .tag = .script, + .minifier = .js, +}; +pub const style: File = .{ + .tag = .style, + .minifier = .css, +}; +pub const body: File = .{ + .tag = .body, + .minifier = .html, +}; + +pub const html: File = .{ + .tag = null, + .minifier = .html, +}; + +pub fn env(comptime components: []const Component) Element { + var children: []const Element = &.{script.load(@embedFile("z.js"))}; + + inline for (components) |c| { + const child: []const Element = &.{c.toElement()}; + children = children ++ child; + } + + return .transparent(.children(children)); +} + +pub fn component(kind: @EnumLiteral(), attrs: anytype, content: Element.Content) Element { + var element: Element = .native(.@"z-component", attrs, content); + + const type_attr: []const Element.Attribute = &.{.{ + .name = "type", + .value = @tagName(kind), + }}; + element.attributes = type_attr ++ element.attributes; + + return element; +} + +test { + _ = std.testing.refAllDecls(@This()); +} diff --git a/src/z/z.js b/src/z/z.js new file mode 100644 index 0000000..a5a1b92 --- /dev/null +++ b/src/z/z.js @@ -0,0 +1,78 @@ +class ZComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + const type = this.getAttribute('type'); + + if (type == null) { + throw `component without any type given.`; + } + + const template = document.getElementById(`@${type}`); + + if (template == null) { + throw `component '${type}' does not exist.`; + } + + const node = document.importNode(template.content, true); + this.shadowRoot.appendChild(node); + + Zenv.constructors[`component__${type}`](new Zenv(this)); + } +} + +class Zenv { + static constructors = {}; + + constructor(doc) { + this.document = doc; + } + + component(type, slots) { + const element = document.createElement('z-component'); + element.setAttribute('type', type); + + for (const name in slots) { + const slot = slots[name]; + slot.setAttribute('slot', name); + element.appendChild(slot); + } + + return element; + } + + populate_attributes(obj, attrs) { + for (const attr in attrs) { + if (typeof(obj[attr]) == 'object' && typeof(attrs[attr]) == 'object') { + populate_attributes(obj[attr], attrs[attr]); + } else { + obj[attr] = attrs[attr]; + } + } + } + + native(name, options) { + const element = document.createElement(name); + Zenv.populate_attributes(element, options); + return element; + } + + by_id(id) { + return this.document.shadowRoot.getElementById(id); + } + + by_selector(query) { + return this.document.shadowRoot.querySelector(query); + } + + by_selector_all(query) { + return this.document.shadowRoot.querySelectorAll(query); + } +} + +Z = new Zenv(document.body); + +customElements.define("z-component", ZComponent); -- cgit v1.2.3-70-g09d2