From aa036fde8b09c6d2e91b586c12c980fb39b17ee0 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Mon, 11 Mar 2024 22:26:45 +0000 Subject: [PATCH] Implement layouts Latest Zmpl provides `template.renderWithLayout(other_template, data)`, allowing a template to be renedered within another template. Create layouts in `src/app/views/layouts/` or use `jetzig generate layout [name]` and set `pub const layout = "name";` in each view file. --- build.zig.zon | 4 +- cli/commands/generate.zig | 18 +++---- cli/commands/generate/layout.zig | 54 +++++++++++++++++++++ demo/src/app/views/iguanas.zig | 17 +++++++ demo/src/app/views/iguanas/index.zmpl | 3 ++ demo/src/app/views/layouts/application.zmpl | 8 +++ demo/src/app/views/layouts/bangbang.zmpl | 6 +++ src/GenerateRoutes.zig | 2 + src/compile_static_routes.zig | 26 +++++++++- src/jetzig.zig | 12 +++++ src/jetzig/App.zig | 1 + src/jetzig/http/Server.zig | 30 +++++++++++- src/jetzig/views/Route.zig | 1 + 13 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 cli/commands/generate/layout.zig create mode 100644 demo/src/app/views/iguanas.zig create mode 100644 demo/src/app/views/iguanas/index.zmpl create mode 100644 demo/src/app/views/layouts/application.zmpl create mode 100644 demo/src/app/views/layouts/bangbang.zmpl diff --git a/build.zig.zon b/build.zig.zon index 955aeff..6488c32 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,8 +3,8 @@ .version = "0.0.0", .dependencies = .{ .zmpl = .{ - .url = "https://github.com/jetzig-framework/zmpl/archive/a39adeb83fdd4363a69754b016988fe092f307e9.tar.gz", - .hash = "1220f7fae403e34015012ce486cc3b98c447177353b303978bd267c4cdd938fcc8d5", + .url = "https://github.com/jetzig-framework/zmpl/archive/0c889d88a6e84cf821a75219a2473cf3620538a1.tar.gz", + .hash = "12200e7ae229283fe737b9d9bee413e3860f76e5d823efd1ee742695463345806bf8", }, .args = .{ .url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz", diff --git a/cli/commands/generate.zig b/cli/commands/generate.zig index 7898aa5..b6fb6e9 100644 --- a/cli/commands/generate.zig +++ b/cli/commands/generate.zig @@ -2,19 +2,14 @@ const std = @import("std"); const args = @import("args"); const view = @import("generate/view.zig"); const partial = @import("generate/partial.zig"); +const layout = @import("generate/layout.zig"); const middleware = @import("generate/middleware.zig"); const util = @import("../util.zig"); /// Command line options for the `generate` command. pub const Options = struct { - path: ?[]const u8 = null, - - pub const shorthands = .{ - .p = "path", - }; - pub const meta = .{ - .usage_summary = "[view|middleware] [options]", + .usage_summary = "[view|partial|layout|middleware] [options]", .full_text = \\Generates scaffolding for views, middleware, and other objects in future. \\ @@ -33,7 +28,7 @@ pub const Options = struct { }; }; -/// Run the `jetzig init` command. +/// Run the `jetzig generate` command. pub fn run( allocator: std.mem.Allocator, options: Options, @@ -49,7 +44,7 @@ pub fn run( try args.printHelp(Options, "jetzig generate", writer); return; } - var generate_type: ?enum { view, partial, middleware } = null; + var generate_type: ?enum { view, partial, layout, middleware } = null; var sub_args = std.ArrayList([]const u8).init(allocator); defer sub_args.deinit(); @@ -58,6 +53,8 @@ pub fn run( generate_type = .view; } else if (generate_type == null and std.mem.eql(u8, arg, "partial")) { generate_type = .partial; + } else if (generate_type == null and std.mem.eql(u8, arg, "layout")) { + generate_type = .layout; } else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) { generate_type = .middleware; } else if (generate_type == null) { @@ -72,10 +69,11 @@ pub fn run( return switch (capture) { .view => view.run(allocator, cwd, sub_args.items), .partial => partial.run(allocator, cwd, sub_args.items), + .layout => layout.run(allocator, cwd, sub_args.items), .middleware => middleware.run(allocator, cwd, sub_args.items), }; } else { - std.debug.print("Missing sub-command. Expected: [view|middleware]\n", .{}); + std.debug.print("Missing sub-command. Expected: [view|partial|layout|middleware]\n", .{}); return error.JetzigCommandError; } } diff --git a/cli/commands/generate/layout.zig b/cli/commands/generate/layout.zig new file mode 100644 index 0000000..51feac2 --- /dev/null +++ b/cli/commands/generate/layout.zig @@ -0,0 +1,54 @@ +const std = @import("std"); + +/// Run the layout generator. Create a layout template in `src/app/views/layouts` +pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void { + if (args.len != 1) { + std.debug.print( + \\Expected a layout name. + \\ + \\Example: + \\ + \\ jetzig generate layout standard + \\ + , .{}); + return error.JetzigCommandError; + } + + const dir_path = try std.fs.path.join( + allocator, + &[_][]const u8{ "src", "app", "views", "layouts" }, + ); + defer allocator.free(dir_path); + + var dir = try cwd.makeOpenPath(dir_path, .{}); + defer dir.close(); + + const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zmpl" }); + defer allocator.free(filename); + + const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| { + switch (err) { + error.PathAlreadyExists => { + std.debug.print("Layout already exists: {s}\n", .{filename}); + return error.JetzigCommandError; + }, + else => return err, + } + }; + + try file.writeAll( + \\ + \\ + \\ + \\
{zmpl.content}
+ \\ + \\ + \\ + ); + + file.close(); + + const realpath = try dir.realpathAlloc(allocator, filename); + defer allocator.free(realpath); + std.debug.print("Generated layout: {s}\n", .{realpath}); +} diff --git a/demo/src/app/views/iguanas.zig b/demo/src/app/views/iguanas.zig new file mode 100644 index 0000000..e96c180 --- /dev/null +++ b/demo/src/app/views/iguanas.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +/// This example uses a layout. A layout is a template that exists in `src/app/views/layouts` and +/// references `{zmpl.content}`. +/// +/// The content is the rendered template for the current view which is then injected into the +/// layout in place of `{zmpl.content}`. +/// +/// See `demo/src/app/views/layouts/application.zmpl` +/// and `demo/src/app/views/iguanas/index.zmpl` +pub const layout = "application"; + +pub fn index(request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View { + _ = data; + return request.render(.ok); +} diff --git a/demo/src/app/views/iguanas/index.zmpl b/demo/src/app/views/iguanas/index.zmpl new file mode 100644 index 0000000..76457d0 --- /dev/null +++ b/demo/src/app/views/iguanas/index.zmpl @@ -0,0 +1,3 @@ +
+ Content goes here +
diff --git a/demo/src/app/views/layouts/application.zmpl b/demo/src/app/views/layouts/application.zmpl new file mode 100644 index 0000000..6701f4d --- /dev/null +++ b/demo/src/app/views/layouts/application.zmpl @@ -0,0 +1,8 @@ + + + + + +
{zmpl.content}
+ + diff --git a/demo/src/app/views/layouts/bangbang.zmpl b/demo/src/app/views/layouts/bangbang.zmpl new file mode 100644 index 0000000..2778ea4 --- /dev/null +++ b/demo/src/app/views/layouts/bangbang.zmpl @@ -0,0 +1,6 @@ + + + +
{zmpl.content}
+ + diff --git a/src/GenerateRoutes.zig b/src/GenerateRoutes.zig index c8b3033..a88e90c 100644 --- a/src/GenerateRoutes.zig +++ b/src/GenerateRoutes.zig @@ -165,6 +165,7 @@ fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !v \\ .action = "{s}", \\ .uri_path = "{s}", \\ .template = "{s}", + \\ .module = @import("{s}"), \\ .function = @import("{s}").{s}, \\ .params = {s}, \\ }}, @@ -196,6 +197,7 @@ fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !v uri_path, full_name, module_path, + module_path, route.name, params_buf.items, }); diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig index 149306e..9f3c377 100644 --- a/src/compile_static_routes.zig +++ b/src/compile_static_routes.zig @@ -32,12 +32,18 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void { comptime var params: [static_params_len][]const u8 = undefined; inline for (static_route.params, 0..) |json, index| params[index] = json; + const layout: ?[]const u8 = if (@hasDecl(static_route.module, "layout")) + static_route.module.layout + else + null; + const route = jetzig.views.Route{ .name = static_route.name, .action = @field(jetzig.views.Route.Action, static_route.action), .view = static_view, .static = true, .uri_path = static_route.uri_path, + .layout = layout, .template = static_route.template, .json_params = ¶ms, }; @@ -95,6 +101,24 @@ fn writeContent( std.debug.print("[jetzig] Compiled static route: {s}\n", .{json_path}); if (zmpl.find(route.template)) |template| { + var content: []const u8 = undefined; + defer allocator.free(content); + + if (route.layout) |layout_name| { + // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ + const prefixed_name = try std.mem.concat(allocator, u8, &[_][]const u8{ "layouts_", layout_name }); + defer allocator.free(prefixed_name); + + if (zmpl.find(prefixed_name)) |layout| { + content = try template.renderWithLayout(layout, view.data); + } else { + std.debug.print("Unknown layout: {s}\n", .{layout_name}); + content = try allocator.dupe(u8, ""); + } + } else { + content = try template.render(view.data); + } + const html_path = try std.mem.concat( allocator, u8, @@ -102,7 +126,7 @@ fn writeContent( ); defer allocator.free(html_path); const html_file = try dir.createFile(html_path, .{ .truncate = true }); - try html_file.writeAll(try template.render(view.data)); + try html_file.writeAll(content); defer html_file.close(); std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path}); } diff --git a/src/jetzig.zig b/src/jetzig.zig index 7a84745..c23ac7f 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -112,12 +112,18 @@ pub fn route(comptime routes: anytype) []views.Route { ), }; + const layout: ?[]const u8 = if (@hasDecl(dynamic_route.module, "layout")) + dynamic_route.module.layout + else + null; + detected[index] = .{ .name = dynamic_route.name, .action = @field(views.Route.Action, dynamic_route.action), .view = view, .static = false, .uri_path = dynamic_route.uri_path, + .layout = layout, .template = dynamic_route.template, .json_params = &.{}, }; @@ -138,12 +144,18 @@ pub fn route(comptime routes: anytype) []views.Route { comptime var static_params: [params_size][]const u8 = undefined; inline for (static_route.params, 0..) |json, params_index| static_params[params_index] = json; + const layout: ?[]const u8 = if (@hasDecl(static_route.module, "layout")) + static_route.module.layout + else + null; + detected[index] = .{ .name = static_route.name, .action = @field(views.Route.Action, static_route.action), .view = view, .static = true, .uri_path = static_route.uri_path, + .layout = layout, .template = static_route.template, .json_params = &static_params, }; diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig index 96d3d15..1432554 100644 --- a/src/jetzig/App.zig +++ b/src/jetzig/App.zig @@ -36,6 +36,7 @@ pub fn start(self: Self, comptime_routes: []jetzig.views.Route) !void { .static = comptime_route.static, .render = comptime_route.render, .renderStatic = comptime_route.renderStatic, + .layout = comptime_route.layout, .template = comptime_route.template, .json_params = comptime_route.json_params, }; diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 23fb40c..db2d0b2 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -210,9 +210,35 @@ fn renderView( if (isBadRequest(err)) return try self.renderBadRequest(request); return try self.renderInternalServerError(request, err); }; - const content = if (template) |capture| try capture.render(view.data) else ""; + if (template) |capture| { + return .{ + .view = view, + .content = try self.renderTemplateWithLayout(capture, view, route), + }; + } else { + // We are rendering JSON, content is ignored. + return .{ .view = view, .content = "" }; + } +} - return .{ .view = view, .content = content }; +fn renderTemplateWithLayout( + self: *Self, + template: zmpl.manifest.Template, + view: jetzig.views.View, + route: *jetzig.views.Route, +) ![]const u8 { + if (route.layout) |layout_name| { + // TODO: Allow user to configure layouts directory other than src/app/views/layouts/ + const prefixed_name = try std.mem.concat(self.allocator, u8, &[_][]const u8{ "layouts_", layout_name }); + defer self.allocator.free(prefixed_name); + + if (zmpl.manifest.find(prefixed_name)) |layout| { + return try template.renderWithLayout(layout, view.data); + } else { + self.logger.debug("Unknown layout: {s}", .{layout_name}); + return try template.render(view.data); + } + } else return try template.render(view.data); } fn isBadRequest(err: anyerror) bool { diff --git a/src/jetzig/views/Route.zig b/src/jetzig/views/Route.zig index 0d29b3a..40bbf8d 100644 --- a/src/jetzig/views/Route.zig +++ b/src/jetzig/views/Route.zig @@ -44,6 +44,7 @@ static_view: ?StaticViewType = null, static: bool, render: RenderFn = renderFn, renderStatic: RenderStaticFn = renderStaticFn, +layout: ?[]const u8, template: []const u8, json_params: [][]const u8, params: std.ArrayList(*jetzig.data.Data) = undefined,