diff options
| -rw-r--r-- | build.zig | 22 | ||||
| -rw-r--r-- | flake.nix | 2 | ||||
| -rw-r--r-- | src/api/root.zig | 2 | ||||
| -rw-r--r-- | src/api/sketch.zig | 29 | ||||
| -rw-r--r-- | src/frontend/File.zig | 78 | ||||
| -rw-r--r-- | src/frontend/Minifier.zig | 168 | ||||
| -rw-r--r-- | src/frontend/root.zig | 3 | ||||
| -rw-r--r-- | src/frontend/web/hello-world/index.html | 3 | ||||
| -rw-r--r-- | src/frontend/web/index.html | 4 | ||||
| -rw-r--r-- | src/frontend/z-component.js | 3 | ||||
| -rw-r--r-- | src/http/Server.zig | 20 | ||||
| -rw-r--r-- | src/http/Url.zig | 39 | ||||
| -rw-r--r-- | src/http/root.zig | 3 | ||||
| -rw-r--r-- | src/main.zig | 4 | ||||
| -rw-r--r-- | src/root.zig | 6 |
15 files changed, 315 insertions, 71 deletions
@@ -32,6 +32,8 @@ pub fn build(b: *std.Build) void { .target = target, }); + api_mod.addImport("http", http_mod); + const exe = b.addExecutable(.{ .name = "memora", .root_module = b.createModule(.{ @@ -43,7 +45,7 @@ pub fn build(b: *std.Build) void { .{ .name = "api", .module = api_mod }, .{ .name = "frontend", .module = frontend_mod }, .{ .name = "http", .module = http_mod }, - }, + } }), }); @@ -51,9 +53,9 @@ pub fn build(b: *std.Build) void { const run_step = b.step("run", "Run the app"); + const run_cmd = b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); - run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { @@ -67,6 +69,7 @@ pub fn build(b: *std.Build) void { const db_test_step = b.step("test-db", "Run database tests"); db_test_step.dependOn(&run_db_tests.step); + const api_tests = b.addTest(.{ .root_module = api_mod, }); @@ -74,6 +77,7 @@ pub fn build(b: *std.Build) void { const api_test_step = b.step("test-api", "Run api tests"); api_test_step.dependOn(&run_api_tests.step); + const frontend_tests = b.addTest(.{ .root_module = frontend_mod, }); @@ -81,6 +85,7 @@ pub fn build(b: *std.Build) void { const frontend_test_step = b.step("test-frontend", "Run frontend tests"); frontend_test_step.dependOn(&run_frontend_tests.step); + const http_tests = b.addTest(.{ .root_module = http_mod, }); @@ -88,11 +93,13 @@ pub fn build(b: *std.Build) void { const http_test_step = b.step("test-http", "Run http tests"); http_test_step.dependOn(&run_http_tests.step); + const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); + const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_db_tests.step); test_step.dependOn(&run_api_tests.step); @@ -100,16 +107,27 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&run_http_tests.step); test_step.dependOn(&run_exe_tests.step); + const fmt_step = b.step("fmt", "Format"); const do_fmt = b.addFmt(.{ .paths = &.{ "src" }, }); fmt_step.dependOn(&do_fmt.step); + const check_fmt_step = b.step("check", "Check format and syntax"); const do_check_fmt = b.addFmt(.{ .paths = &.{ "src" }, .check = true, }); check_fmt_step.dependOn(&do_check_fmt.step); + + + const docs_step = b.step("docs", "Emit documentation"); + const docs_install = b.addInstallDirectory(.{ + .install_dir = .prefix, + .install_subdir = "docs", + .source_dir = exe.getEmittedDocs(), + }); + docs_step.dependOn(&docs_install.step); } @@ -21,12 +21,14 @@ buildInputs = [ pkgs.zig + pkgs.sqlite ]; }; devShells.x86_64-linux.default = pkgs.mkShell { packages = [ pkgs.zig + pkgs.sqlite ]; }; }; 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/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..65dd9fb --- /dev/null +++ b/src/frontend/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/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); } } diff --git a/src/http/Server.zig b/src/http/Server.zig new file mode 100644 index 0000000..b6e2d90 --- /dev/null +++ b/src/http/Server.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +const Self = @This(); + +server: std.Io.net.Server, +io: std.Io.Threaded, + +pub fn init( + gpa: std.mem.Allocator, + address: std.Io.net.IpAddress, +) !Self { + var io: std.Io.Threaded = .init(gpa, .{}); + + return .{ + .io = io, + .server = try address.listen(io.io(), .{ .reuse_address = true }), + }; +} + +pub fn serve(_: *Self) !void {} 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..df0c061 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,9 +4,11 @@ //! but also the actual running of the server / webapp. const std = @import("std"); +const http = @import("http"); 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(); } test { diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..8d85eb4 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,6 @@ +//! Memora Software Stack + +pub const db = @import("db"); +pub const api = @import("api"); +pub const frontend = @import("frontend"); +pub const http = @import("http"); |