diff options
Diffstat (limited to 'src/z')
| -rw-r--r-- | src/z/Component.zig | 18 | ||||
| -rw-r--r-- | src/z/File.zig | 18 | ||||
| -rw-r--r-- | src/z/Minifier.zig | 168 | ||||
| -rw-r--r-- | src/z/root.zig | 52 | ||||
| -rw-r--r-- | src/z/z.js | 78 |
5 files changed, 334 insertions, 0 deletions
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); |