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,