diff --git a/src/errors/writer.zig b/src/errors/writer.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/interfaces/node.zig b/src/interfaces/node.zig index b653d9e..98dcc4f 100644 --- a/src/interfaces/node.zig +++ b/src/interfaces/node.zig @@ -2,6 +2,29 @@ const std = @import("std"); const assert = @import("std").debug.assert; const renderErr = @import("../errors/render.zig").RenderError; +pub fn Writer(comptime T: type, comptime initial: []const T, comptime new: []const T) type { + return struct { + const store: []const T = initial ++ new; + + fn Write(comptime value: []const T) type { + return Writer(T, store, value); + } + + fn write(comptime value: []const T) type { + return Writer(T, store, value); + } + + pub fn Value() []const T { + return store; + } + }; +} + +test "writer" { + const writer = Writer(u8, "Hello", "World"); + std.testing.expectEqualStrings(writer.Value(), "HelloWorld"); +} + pub const NodeType = enum { element, text, childless, attribute, nodes }; pub const Node = struct { @@ -33,6 +56,66 @@ pub const Node = struct { value: ?[]const u8, }; + pub fn render2(self: *const Node, _mem: type) type { + switch (self.data) { + .element => |elem| { + var mem = _mem.write("<"); + mem = mem.write(elem.name); + if (elem.children) |children| { + for (children) |*child| { + if (child.node_type == .attribute) { + mem = child.render2(mem); + } + } + } + mem = mem.write(">"); + if (elem.children) |children| { + for (children) |*child| { + if (child.node_type != .attribute) { + mem = child.render2(mem); + } + } + } + mem = mem.write(""); + return mem; + }, + .text => |txt| { + return _mem.write(txt.content); + }, + .childless => |childlessElem| { + var mem = _mem.write("<"); + mem = mem.write(childlessElem.name); + if (childlessElem.children) |children| { + for (children) |*child| { + assert(child.node_type == .attribute); + mem = child.render2(mem); + } + } + return mem.write("/>"); + }, + .attribute => |attr| { + var mem = _mem.write(" "); + mem = mem.write(attr.name); + if (attr.value) |attrValue| { + mem = mem.write("=\""); + mem = mem.write(attrValue); + mem = mem.write("\""); + } + return mem; + }, + .nodes => |nodes| { + var mem = _mem; + if (nodes.children) |children| { + for (children) |*child| { + mem = child.render2(mem); + } + } + return mem; + }, + } + } pub fn render(self: *const Node, mem: std.ArrayList(u8).Writer) renderErr!void { switch (self.data) { .element => |elem| { diff --git a/src/main.zig b/src/main.zig index 1064475..25f375c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,18 +7,23 @@ const sample = @import("sample.zig"); const server = @import("server.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - defer { - const deinit_status = gpa.deinit(); - if (deinit_status == .leak) std.testing.expect(false) catch @panic("Leak detected!"); - } - var arr: std.ArrayList(u8) = .{}; //.init(allocator); - defer arr.deinit(allocator); - const writer = arr.writer(allocator); - try sample.run(writer); - const samplePage = arr.items; - try server.runServer(samplePage); + // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + // const allocator = gpa.allocator(); + // defer { + // const deinit_status = gpa.deinit(); + // if (deinit_status == .leak) std.testing.expect(false) catch @panic("Leak detected!"); + // } + // var arr: std.ArrayList(u8) = .{}; //.init(allocator); + // defer arr.deinit(allocator); + // const writer = arr.writer(allocator); + // try sample.run(writer); + // const samplePage = arr.items; + // comptime { + // = sample.run2(); + // } + @setEvalBranchQuota(5000); + const samplePage = comptime sample.run2(); + try server.runServer(@constCast(samplePage)); } fn Page(n: []const Node) Node { @@ -34,7 +39,7 @@ test "components" { const html = Page(&.{ el.Head(&.{ - el.Title(&.{el.Text("My Page")}), + el.Title("My Page"), }), el.Body(&.{el.Div(&.{el.Text("Hello world!")})}), }); @@ -46,6 +51,31 @@ test "components" { try std.testing.expectEqualStrings(res, "My Page
Hello world!
"); } +fn Writer(comptime T: type, comptime initial: []const T, comptime new: []const T) type { + return struct { + const store: []const T = initial ++ new; + + fn Write(comptime value: []const T) type { + return Writer(T, store, value); + } + + fn Value() []const T { + return store; + } + // store: []const T = initial ++ new, + + // const Self = @This(); + + // fn Write(self: *Self, comptime value: []const T) type { + // return Writer(T, self.store, value); + // } + + // fn Value(self: *Self) []const T { + // return self.store; + // } + }; +} + test "html5" { const allocator = std.heap.page_allocator; var arr: std.ArrayList(u8) = .{}; //.init(allocator); @@ -57,7 +87,7 @@ test "html5" { el.Head(&.{ el.Meta(&.{attr.Charset("UTF-8")}), el.Meta(&.{ attr.Name("viewport"), attr.Content("width=device-width, initial-scale=1") }), - el.Title(&.{el.Text("Document")}), + el.Title("Document"), }), el.Body(&.{el.Div(&.{ attr.Class("my-class"), @@ -72,3 +102,23 @@ test "html5" { try std.testing.expectEqualStrings(res, "Document
Hello world!
"); } + +test "comptime allocation" { + const a: []const u8 = "Hello"; + const b: []const u8 = " World"; + var c: []const u8 = ""; + c = comptime blk: { + break :blk a ++ b; + }; + + // c = c ++ "!!"; + + std.debug.print("{d}", .{c.len}); + + try std.testing.expectEqualStrings(c, "Hello World"); + + const writer = Writer(u8, "Hello", " World"); + + const e = writer.Write("!!").Write("==").Value(); + try std.testing.expectEqualStrings(e, "Hello World!!=="); +} diff --git a/src/root.zig b/src/root.zig index c3c5287..5da6a18 100644 --- a/src/root.zig +++ b/src/root.zig @@ -3,6 +3,7 @@ pub const el = @import("./components/elements.zig"); pub const attr = @import("./components/atributes.zig"); pub const wrappers = @import("./components/wrapper.zig"); pub const Node = @import("./interfaces/node.zig").Node; +pub const Writer = @import("./interfaces/node.zig").Writer; test { std.testing.log_level = .warn; diff --git a/src/sample.zig b/src/sample.zig index a87858b..0bf750e 100644 --- a/src/sample.zig +++ b/src/sample.zig @@ -9,6 +9,7 @@ const mainExample = eg.mainExample; const addDependency = eg.addDependency; const importDependency = eg.importDependency; const useComponents = eg.useComponents; +const Writer = z.Writer; fn NavLink(label: []const u8, href: []const u8) [3]z.Node { return .{ @@ -405,3 +406,340 @@ pub fn run(writer: std.ArrayList(u8).Writer) !void { try html.render(writer); } + +pub fn run2() []const u8 { + const head = el.Head(&.{ + el.Meta(&.{attr.Charset("UTF-8")}), + el.Meta(&.{ attr.Name("viewport"), attr.Content("width=device-width, initial-scale=1, maximum-scale=5, user-scalable=1") }), + el.Meta(&.{ attr.Name("description"), attr.Content("Zigomponent is a library for generating HTML in Zig") }), + el.Meta(&.{ attr.Name("keywords"), attr.Content("Zig, HTML, generation, library, Zigomponent") }), + el.Meta(&.{ attr.Name("author"), attr.Content("eltneg") }), + el.Title("Zigomponent - HTML Generation for Zig"), + el.Script(&.{attr.Src("https://cdn.tailwindcss.com")}), + el.Link(&.{ + attr.Href("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css"), + attr.Rel("stylesheet"), + }), + el.Link(&.{ + attr.Rel("apple-touch-icon"), + attr.Sizes("180x180"), + attr.Href("/apple-touch-icon.png"), + }), + el.Link(&.{ + attr.Rel("icon"), + attr.Type("image/png"), + attr.Sizes("32x32"), + attr.Href("/favicon-32x32.png"), + }), + el.Link(&.{ + attr.Rel("icon"), + attr.Type("image/png"), + attr.Sizes("16x16"), + attr.Href("/favicon-16x16.png"), + }), + el.Link(&.{ + attr.Rel("manifest"), + attr.Href("/site.webmanifest"), + }), + el.Link(&.{ + attr.Rel("shortcut icon"), + attr.Href("/favicon.ico"), + }), + el.Link(&.{ + attr.Rel("android-chrome-icon-192x192"), + attr.Sizes("192x192"), + attr.Href("/android-chrome-192x192.png"), + }), + el.Link(&.{ + attr.Rel("android-chrome-icon-512x512"), + attr.Sizes("512x512"), + attr.Href("/android-chrome-512x512.png"), + }), + el.Script(&.{ + attr.Src("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"), + }), + el.Script(&.{ + attr.Src("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"), + }), + }); + + const header = el.Header(&.{ + attr.Class("bg-white shadow-sm border-b"), + el.Div(&.{ + attr.Class("max-w-6xl mx-auto px-4 py-6"), + el.Div(&.{ + attr.Class("flex items-center justify-between"), + el.Div(&.{ + attr.Class("flex items-center space-x-3 flex-shrink-0"), + el.Div(&.{ + attr.Class("w-10 h-10 bg-orange-500 rounded-lg flex items-center justify-center"), + el.Span(&.{ + attr.Class("text-white font-bold text-xl"), + el.Text("Z"), + }), + }), + el.H1(&.{ + attr.Class("text-2xl font-bold text-gray-900"), + el.Text("zigomponent"), + }), + }), + el.Nav(&.{ + attr.Class("hidden md:flex items-center space-x-6"), + el.A(&NavLink("Features", "#features")), + el.A(&NavLink("Examples", "#examples")), + el.A(&NavLink("Getting Started", "#getting-started")), + el.A(&.{ + attr.Href("https://github.com/eltNEG/zigomponent"), + attr.Class("bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600 transition-colors"), + el.Text("GitHub"), + }), + }), + }), + }), + }); + + const hero = el.Section(&.{ + attr.Class("bg-gradient-to-br from-orange-50 to-amber-50 py-20"), + el.Div(&.{ + attr.Class("max-w-6xl mx-auto px-4 text-center"), + el.H2(&.{ + attr.Class("text-4xl md:text-5xl font-bold text-gray-900 mb-6"), + el.Text("HTML Generation for "), + el.Span(&.{ + attr.Class("text-orange-500"), + el.Text("Zig"), + }), + }), + el.P(&.{ + attr.Class("text-lg md:text-xl text-gray-600 mb-8 max-w-3xl mx-auto px-4"), + el.Text("Zigomponent is a Zig library inspired by gomponents that allows you to create HTML elements using native Zig syntax. Build type-safe, composable HTML components with the power of Zig."), + }), + el.Div(&.{ + attr.Class("flex flex-col md:flex-row justify-center space-y-4 md:space-y-0 md:space-x-4"), + el.A(&.{ + attr.Href("#getting-started"), + attr.Class("bg-orange-500 text-white px-8 py-3 rounded-lg text-lg font-semibold hover:bg-orange-600 transition-colors"), + el.Text("Get Started"), + }), + el.A(&.{ + attr.Href("#examples"), + attr.Class("border-2 border-orange-500 text-orange-500 px-8 py-3 rounded-lg text-lg font-semibold hover:bg-orange-50 transition-colors"), + el.Text("View Examples"), + }), + }), + }), + }); + + const features = el.Section(&.{ + attr.ID("features"), + attr.Class("py-16 bg-gray-50"), + el.Div(&.{ + attr.Class("max-w-6xl mx-auto px-4"), + el.Div(&.{ + attr.Class("text-center mb-12"), + el.H3(&.{ + attr.Class("text-3xl font-bold text-gray-900 mb-4"), + el.Text("Why Zigomponent?"), + }), + el.P(&.{ + attr.Class("text-lg text-gray-600"), + el.Text("Powerful features for modern web development"), + }), + }), + el.Div(&.{ + attr.Class("grid md:grid-cols-3 gap-8"), + el.Div(&Features("Type Safety", "Leverage Zig's compile-time type checking to catch HTML errors before runtime.").New()), + el.Div(&Features("Performance", "Zero-cost abstractions and compile-time optimizations for blazing fast HTML generation.").New()), + el.Div(&Features("Composable", "Build reusable components and compose them together for complex layouts.").New()), + }), + }), + }); + + const codeExample = el.Section(&.{ + attr.Class("py-16 bg-white"), + el.Div(&.{ + attr.Class("max-w-6xl mx-auto px-4"), + el.Div(&.{ + attr.Class("text-center mb-12"), + el.H3(&.{ + attr.Class("text-3xl font-bold text-gray-900 mb-4"), + el.Text("Simple and Intuitive"), + }), + el.P(&.{ + attr.Class("text-lg text-gray-600"), + el.Text("Create HTML elements with native Zig syntax"), + }), + }), + Code(mainExample).New(), + }), + }); + + const script = el.JSScript( + \\document.querySelectorAll('a[href^="#"]').forEach((anchor) => { + \\ anchor.addEventListener("click", function (e) { + \\ e.preventDefault(); + \\ const target = document.querySelector(this.getAttribute("href")); + \\ if (target) { + \\ target.scrollIntoView({ + \\ behavior: "smooth", + \\ block: "start", + \\ }); + \\ } + \\ }); + \\}); + ); + + const footer = el.Footer(&.{ + attr.Class("bg-gray-900 text-white py-12"), + el.Div(&.{ + attr.Class("max-w-6xl mx-auto px-4"), + el.Div(&.{ + attr.Class("grid md:grid-cols-4 gap-8"), + el.Div(&.{ + attr.Class("md:col-span-2"), + el.Div(&.{ + attr.Class("flex items-center space-x-3 mb-4"), + el.Div(&.{ + attr.Class("w-8 h-8 bg-orange-500 rounded-lg flex items-center justify-center"), + el.Span(&.{ + attr.Class("text-white font-bold"), + el.Text("Z"), + }), + }), + el.H5(&.{ + attr.Class("text-xl font-bold"), + el.Text("zigomponent"), + }), + }), + el.P(&.{ + attr.Class("text-gray-400 mb-4"), + el.Text("HTML generation for Zig, inspired by gomponents. Build type-safe, composable HTML components with the power of Zig."), + }), + }), + el.Div(&.{ + el.H6(&.{ + attr.Class("font-semibold mb-4"), + el.Text("Resources"), + }), + el.Ul(&.{ + attr.Class("space-y-2 text-gray-400"), + el.Li(&.{ + el.A(&.{ + attr.Href("#"), + attr.Class("hover:text-white transition-colors"), + el.Text("Documentation"), + }), + }), + el.Li(&.{ + el.A(&.{ + attr.Href("https://github.com/eltNEG/zigomponent/blob/master/src/sample.zig"), + attr.Target("_blank"), + attr.Class("hover:text-white transition-colors"), + el.Text("Example"), + }), + }), + }), + }), + el.Div(&.{ + el.H6(&.{ + attr.Class("font-semibold mb-4"), + el.Text("Community"), + }), + el.Ul(&.{ + attr.Class("space-y-2 text-gray-400"), + el.Li(&.{ + el.A(&.{ + attr.Href("https://github.com/eltNEG/zigomponent"), + attr.Target("_blank"), + attr.Class("hover:text-white transition-colors"), + el.Text("GitHub"), + }), + }), + }), + }), + }), + el.Div(&.{ + attr.Class("border-t border-gray-800 mt-8 pt-8 text-center text-gray-400"), + el.P(&.{ + el.Text("© 2025 Zigomponent. MIT License."), + }), + }), + }), + }); + + const moreExamples = el.Section(&.{ + attr.ID("examples"), + attr.Class("py-16 bg-white"), + el.Div(&.{ + attr.Class("max-w-6xl mx-auto px-4"), + el.Div(&.{ + attr.Class("text-center mb-12"), + el.H3(&.{ + attr.Class("text-3xl font-bold text-gray-900 mb-4"), + el.Text("More Examples"), + }), + el.P(&.{ + attr.Class("text-lg text-gray-600"), + el.Text("See zigomponent in action"), + }), + }), + el.Div(&.{ + attr.Class("grid grid-cols-1 lg:grid-cols-2 gap-8 px-4"), + Snippet("Creating a Form", formExample).New(), + Snippet("Custom Components", cardExample).New(), + }), + }), + }); + + const gettingStarted = el.Section(&.{ + attr.ID("getting-started"), + attr.Class("py-16 bg-gray-50"), + el.Div(&.{ + attr.Class("max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"), + el.Div(&.{ + attr.Class("text-center mb-12"), + el.H3(&.{ + attr.Class("text-3xl font-bold text-gray-900 mb-4"), + el.Text("Getting Started"), + }), + el.P(&.{ + attr.Class("text-lg text-gray-600"), + el.Text("Add zigomponent to your Zig project in minutes"), + }), + }), + el.Div(&.{ + attr.Class("space-y-8"), + Snippet("1. Add zigomponent to your project", "$ zig fetch --save git+https://github.com/eltNEG/zigomponent;").New(), + Snippet("2. Update your build.zig", addDependency).New(), + Snippet("3. Import in your code", importDependency).New(), + Snippet("4. Use in your code", useComponents).New(), + }), + }), + }); + + const body = el.Body(&.{ + attr.Class("bg-gray-50 text-gray-900"), + header, + hero, + codeExample, + features, + moreExamples, + gettingStarted, + script, + footer, + }); + + const html = el.ToNode(&.{ + el.Raw(""), + el.Html(&.{ + attr.Lang("en"), + head, + body, + }), + }); + + const writer = Writer(u8, "", ""); + const rendered = html.render2(writer); + + return rendered.Value(); +}