aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig46
-rw-r--r--flake.nix3
-rw-r--r--src/api/root.zig2
-rw-r--r--src/api/sketch.zig29
-rw-r--r--src/frontend/Component.zig30
-rw-r--r--src/frontend/Document.zig46
-rw-r--r--src/frontend/File.zig91
-rw-r--r--src/frontend/Head.zig58
-rw-r--r--src/frontend/root.zig12
-rw-r--r--src/frontend/web/hello-world/index.css7
-rw-r--r--src/frontend/web/hello-world/index.html3
-rw-r--r--src/frontend/web/hello-world/index.js3
-rw-r--r--src/frontend/web/index.html1
-rw-r--r--src/frontend/z-component.js17
-rw-r--r--src/html/Element.zig104
-rw-r--r--src/html/root.zig7
-rw-r--r--src/http/Server.zig50
-rw-r--r--src/http/Url.zig39
-rw-r--r--src/http/root.zig3
-rw-r--r--src/main.zig27
-rw-r--r--src/web/badge/index.css5
-rw-r--r--src/web/badge/index.html3
-rw-r--r--src/web/badge/index.js1
-rw-r--r--src/web/badge/root.zig8
-rw-r--r--src/web/index.css (renamed from src/frontend/web/index.css)0
-rw-r--r--src/web/index.html3
-rw-r--r--src/web/index.js0
-rw-r--r--src/z/Component.zig18
-rw-r--r--src/z/File.zig18
-rw-r--r--src/z/Minifier.zig168
-rw-r--r--src/z/root.zig52
-rw-r--r--src/z/z.js78
32 files changed, 654 insertions, 278 deletions
diff --git a/build.zig b/build.zig
index 9f8d9dc..989f6c4 100644
--- a/build.zig
+++ b/build.zig
@@ -22,16 +22,24 @@ 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,
});
+ api_mod.addImport("http", http_mod);
+
const exe = b.addExecutable(.{
.name = "memora",
.root_module = b.createModule(.{
@@ -41,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 },
}
}),
@@ -76,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(.{
@@ -101,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);
@@ -128,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 c164e8e..92ff2d9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -21,12 +21,15 @@
buildInputs = [
pkgs.zig
+ pkgs.sqlite
];
};
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [
pkgs.zig
+ pkgs.sqlite
+ pkgs.entr
];
};
};
diff --git a/src/api/root.zig b/src/api/root.zig
index 5128aa1..c28991c 100644
--- a/src/api/root.zig
+++ b/src/api/root.zig
@@ -3,6 +3,8 @@
const std = @import("std");
+pub const _ = @import("sketch.zig");
+
test {
_ = std.testing.refAllDecls(@This());
}
diff --git a/src/api/sketch.zig b/src/api/sketch.zig
new file mode 100644
index 0000000..409606f
--- /dev/null
+++ b/src/api/sketch.zig
@@ -0,0 +1,29 @@
+const std = @import("std");
+const http = @import("http");
+
+pub const Url = http.Url.Template(
+ "/api/images/{id}/metadata/{name}",
+ .{
+ .guard = Auth(.admin),
+ }
+);
+
+const Query = struct {
+ condition: []const u8,
+ start: usize,
+ end: usize,
+}
+
+pub fn post(request: http.Request(Url)) !http.Response {
+ const query = try request.body.json(Query, request.arena);
+
+ return .string(request.params.id);
+}
+
+pub fn get(request: http.Request(Url)) !http.Response {
+ return .file("./some/path.txt");
+}
+
+test {
+ _ = std.testing.refAllDecls(@This());
+}
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 "<template id=\"--" ++ self.name ++ "\">" ++ css.to_html_string() ++ html.to_html_string() ++ js.to_html_string() ++ "</template>";
-}
-
-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 = "<!DOCTYPE html><html>";
- content = content ++ comptime self.head.to_html_string(self.components, self.styles);
- content = content ++ "<body>";
-
- inline for (self.contents) |c| {
- content = content ++ comptime c.to_html_string();
- }
-
- content = content ++ "</body></html>";
-
- 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 e7c3b6d..0000000
--- a/src/frontend/File.zig
+++ /dev/null
@@ -1,91 +0,0 @@
-const std = @import("std");
-
-pub fn File(comptime tag: ?[]const u8, Iterator: type) type {
- const opening = if (tag) |t| "<" ++ t ++ ">" else "";
- const closing = if (tag) |t| "</" ++ 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,
- };
- }
-
- 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 ++ " ";
- }
- };
-}
-
-pub const Style = File("style", StringIterator("\"'"));
-pub const Script = File("script", StringIterator("\"'`"));
-pub const Html = File(null, StringIterator("\"'"));
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 "<meta name=\"" ++ self.name ++ "\" content=\"" ++ self.content ++ "\">";
- }
-};
-
-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 = "<head>" ++ z_component_code.to_html_string();
-
- content = content ++ "<title>" ++ self.title ++ "</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 ++ "</head>";
- return content;
-}
diff --git a/src/frontend/root.zig b/src/frontend/root.zig
deleted file mode 100644
index 39908b5..0000000
--- a/src/frontend/root.zig
+++ /dev/null
@@ -1,12 +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("File.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 a9b0be7..0000000
--- a/src/frontend/web/hello-world/index.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<div id="container" onclick="clicked()">
- Click Me!
-</div>
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.html b/src/frontend/web/index.html
deleted file mode 100644
index 885f2c5..0000000
--- a/src/frontend/web/index.html
+++ /dev/null
@@ -1 +0,0 @@
-<z-component type="hello-world"></z-component>
diff --git a/src/frontend/z-component.js b/src/frontend/z-component.js
deleted file mode 100644
index e9b63ff..0000000
--- a/src/frontend/z-component.js
+++ /dev/null
@@ -1,17 +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.id = 'root';
- 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 ++ "</" ++ self.tag ++ ">";
+ },
+ .elements => |els| {
+ for (els) |el| {
+ string = string ++ el.toHtml();
+ }
+
+ string = string ++ "</" ++ self.tag ++ ">";
+ }
+ }
+
+ string = string ++ if (self.tag.len == 0) "" else "</" ++ self.tag ++ ">";
+
+ return string;
+}
+
+pub fn toDocument(comptime self: Self) []const u8 {
+ return "<!DOCTYPE html>" ++ 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
new file mode 100644
index 0000000..71037bb
--- /dev/null
+++ b/src/http/Server.zig
@@ -0,0 +1,50 @@
+const std = @import("std");
+
+const Self = @This();
+
+server: std.Io.net.Server,
+threaded: std.Io.Threaded,
+
+pub fn init(
+ gpa: std.mem.Allocator,
+ address: std.Io.net.IpAddress,
+) !Self {
+ var threaded: std.Io.Threaded = .init(gpa, .{});
+
+ return .{
+ .threaded = threaded,
+ .server = try address.listen(threaded.io(), .{ .reuse_address = true }),
+ };
+}
+
+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/http/Url.zig b/src/http/Url.zig
new file mode 100644
index 0000000..4a0a2b0
--- /dev/null
+++ b/src/http/Url.zig
@@ -0,0 +1,39 @@
+const std = @import("std");
+
+const Segment = union(enum) {
+ literal: []const u8,
+ parameter: []const u8,
+
+ pub fn format(self: @This(), writer: *std.Io.Writer) !void {
+ try writer.print("{s}{{ {s} }}", .{ @tagName(self), switch (self) {
+ inline else => |s| s,
+ } });
+ }
+};
+
+pub fn Template(comptime template: []const u8) []const Segment {
+ var segments: []const Segment = &.{};
+ var t = template;
+
+ if (template.len == 0 or template[0] != '/') {
+ @compileError("template needs to be absolute");
+ }
+
+ while (t.len > 0) {
+ const index = std.mem.indexOfScalar(u8, t, '/');
+ const segment = t[0 .. index orelse t.len];
+ t = t[(index orelse t.len - 1) + 1 ..];
+
+ if (segment.len > 0) {
+ const s: []const Segment = &.{seg: {
+ if (segment[0] == '{' and segment[segment.len - 1] == '}') {
+ break :seg .{ .parameter = segment[1 .. segment.len - 1] };
+ } else {
+ break :seg .{ .literal = segment };
+ }
+ }};
+ segments = segments ++ s;
+ }
+ }
+ return segments;
+}
diff --git a/src/http/root.zig b/src/http/root.zig
index 743e1c4..317dacd 100644
--- a/src/http/root.zig
+++ b/src/http/root.zig
@@ -2,6 +2,9 @@
const std = @import("std");
+pub const Server = @import("Server.zig");
+pub const Url = @import("Url.zig");
+
test {
_ = std.testing.refAllDecls(@This());
}
diff --git a/src/main.zig b/src/main.zig
index 1a66b63..f17214c 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -4,9 +4,34 @@
//! but also the actual running of the server / webapp.
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 {
- _ = init;
+ var server: http.Server = try .init(init.gpa, try .parseLiteral("127.0.0.1:8080"));
+ 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 @@
+<div id="container">
+ <slot name="content"></slot>
+</div>
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/frontend/web/index.css b/src/web/index.css
index a93ea47..a93ea47 100644
--- a/src/frontend/web/index.css
+++ b/src/web/index.css
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 @@
+<z-component type="badge">
+ <span slot="content">Hello Z!</span>
+</z-component>
diff --git a/src/web/index.js b/src/web/index.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/web/index.js
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 =
+ \\<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/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);