mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
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.
This commit is contained in:
parent
9be2032e65
commit
aa036fde8b
@ -3,8 +3,8 @@
|
|||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.zmpl = .{
|
.zmpl = .{
|
||||||
.url = "https://github.com/jetzig-framework/zmpl/archive/a39adeb83fdd4363a69754b016988fe092f307e9.tar.gz",
|
.url = "https://github.com/jetzig-framework/zmpl/archive/0c889d88a6e84cf821a75219a2473cf3620538a1.tar.gz",
|
||||||
.hash = "1220f7fae403e34015012ce486cc3b98c447177353b303978bd267c4cdd938fcc8d5",
|
.hash = "12200e7ae229283fe737b9d9bee413e3860f76e5d823efd1ee742695463345806bf8",
|
||||||
},
|
},
|
||||||
.args = .{
|
.args = .{
|
||||||
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz",
|
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz",
|
||||||
|
@ -2,19 +2,14 @@ const std = @import("std");
|
|||||||
const args = @import("args");
|
const args = @import("args");
|
||||||
const view = @import("generate/view.zig");
|
const view = @import("generate/view.zig");
|
||||||
const partial = @import("generate/partial.zig");
|
const partial = @import("generate/partial.zig");
|
||||||
|
const layout = @import("generate/layout.zig");
|
||||||
const middleware = @import("generate/middleware.zig");
|
const middleware = @import("generate/middleware.zig");
|
||||||
const util = @import("../util.zig");
|
const util = @import("../util.zig");
|
||||||
|
|
||||||
/// Command line options for the `generate` command.
|
/// Command line options for the `generate` command.
|
||||||
pub const Options = struct {
|
pub const Options = struct {
|
||||||
path: ?[]const u8 = null,
|
|
||||||
|
|
||||||
pub const shorthands = .{
|
|
||||||
.p = "path",
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const meta = .{
|
pub const meta = .{
|
||||||
.usage_summary = "[view|middleware] [options]",
|
.usage_summary = "[view|partial|layout|middleware] [options]",
|
||||||
.full_text =
|
.full_text =
|
||||||
\\Generates scaffolding for views, middleware, and other objects in future.
|
\\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(
|
pub fn run(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
options: Options,
|
options: Options,
|
||||||
@ -49,7 +44,7 @@ pub fn run(
|
|||||||
try args.printHelp(Options, "jetzig generate", writer);
|
try args.printHelp(Options, "jetzig generate", writer);
|
||||||
return;
|
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);
|
var sub_args = std.ArrayList([]const u8).init(allocator);
|
||||||
defer sub_args.deinit();
|
defer sub_args.deinit();
|
||||||
|
|
||||||
@ -58,6 +53,8 @@ pub fn run(
|
|||||||
generate_type = .view;
|
generate_type = .view;
|
||||||
} else if (generate_type == null and std.mem.eql(u8, arg, "partial")) {
|
} else if (generate_type == null and std.mem.eql(u8, arg, "partial")) {
|
||||||
generate_type = .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")) {
|
} else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
|
||||||
generate_type = .middleware;
|
generate_type = .middleware;
|
||||||
} else if (generate_type == null) {
|
} else if (generate_type == null) {
|
||||||
@ -72,10 +69,11 @@ pub fn run(
|
|||||||
return switch (capture) {
|
return switch (capture) {
|
||||||
.view => view.run(allocator, cwd, sub_args.items),
|
.view => view.run(allocator, cwd, sub_args.items),
|
||||||
.partial => partial.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),
|
.middleware => middleware.run(allocator, cwd, sub_args.items),
|
||||||
};
|
};
|
||||||
} else {
|
} 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;
|
return error.JetzigCommandError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
cli/commands/generate/layout.zig
Normal file
54
cli/commands/generate/layout.zig
Normal file
@ -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(
|
||||||
|
\\<html>
|
||||||
|
\\ <head></head>
|
||||||
|
\\ <body>
|
||||||
|
\\ <main>{zmpl.content}</main>
|
||||||
|
\\ </body>
|
||||||
|
\\</html>
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
const realpath = try dir.realpathAlloc(allocator, filename);
|
||||||
|
defer allocator.free(realpath);
|
||||||
|
std.debug.print("Generated layout: {s}\n", .{realpath});
|
||||||
|
}
|
17
demo/src/app/views/iguanas.zig
Normal file
17
demo/src/app/views/iguanas.zig
Normal file
@ -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);
|
||||||
|
}
|
3
demo/src/app/views/iguanas/index.zmpl
Normal file
3
demo/src/app/views/iguanas/index.zmpl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<span>Content goes here</span>
|
||||||
|
</div>
|
8
demo/src/app/views/layouts/application.zmpl
Normal file
8
demo/src/app/views/layouts/application.zmpl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>{zmpl.content}</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
demo/src/app/views/layouts/bangbang.zmpl
Normal file
6
demo/src/app/views/layouts/bangbang.zmpl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<main>{zmpl.content}</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -165,6 +165,7 @@ fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !v
|
|||||||
\\ .action = "{s}",
|
\\ .action = "{s}",
|
||||||
\\ .uri_path = "{s}",
|
\\ .uri_path = "{s}",
|
||||||
\\ .template = "{s}",
|
\\ .template = "{s}",
|
||||||
|
\\ .module = @import("{s}"),
|
||||||
\\ .function = @import("{s}").{s},
|
\\ .function = @import("{s}").{s},
|
||||||
\\ .params = {s},
|
\\ .params = {s},
|
||||||
\\ }},
|
\\ }},
|
||||||
@ -196,6 +197,7 @@ fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !v
|
|||||||
uri_path,
|
uri_path,
|
||||||
full_name,
|
full_name,
|
||||||
module_path,
|
module_path,
|
||||||
|
module_path,
|
||||||
route.name,
|
route.name,
|
||||||
params_buf.items,
|
params_buf.items,
|
||||||
});
|
});
|
||||||
|
@ -32,12 +32,18 @@ fn compileStaticRoutes(allocator: std.mem.Allocator) !void {
|
|||||||
comptime var params: [static_params_len][]const u8 = undefined;
|
comptime var params: [static_params_len][]const u8 = undefined;
|
||||||
inline for (static_route.params, 0..) |json, index| params[index] = json;
|
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{
|
const route = jetzig.views.Route{
|
||||||
.name = static_route.name,
|
.name = static_route.name,
|
||||||
.action = @field(jetzig.views.Route.Action, static_route.action),
|
.action = @field(jetzig.views.Route.Action, static_route.action),
|
||||||
.view = static_view,
|
.view = static_view,
|
||||||
.static = true,
|
.static = true,
|
||||||
.uri_path = static_route.uri_path,
|
.uri_path = static_route.uri_path,
|
||||||
|
.layout = layout,
|
||||||
.template = static_route.template,
|
.template = static_route.template,
|
||||||
.json_params = ¶ms,
|
.json_params = ¶ms,
|
||||||
};
|
};
|
||||||
@ -95,6 +101,24 @@ fn writeContent(
|
|||||||
std.debug.print("[jetzig] Compiled static route: {s}\n", .{json_path});
|
std.debug.print("[jetzig] Compiled static route: {s}\n", .{json_path});
|
||||||
|
|
||||||
if (zmpl.find(route.template)) |template| {
|
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(
|
const html_path = try std.mem.concat(
|
||||||
allocator,
|
allocator,
|
||||||
u8,
|
u8,
|
||||||
@ -102,7 +126,7 @@ fn writeContent(
|
|||||||
);
|
);
|
||||||
defer allocator.free(html_path);
|
defer allocator.free(html_path);
|
||||||
const html_file = try dir.createFile(html_path, .{ .truncate = true });
|
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();
|
defer html_file.close();
|
||||||
std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
|
std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
|
||||||
}
|
}
|
||||||
|
@ -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] = .{
|
detected[index] = .{
|
||||||
.name = dynamic_route.name,
|
.name = dynamic_route.name,
|
||||||
.action = @field(views.Route.Action, dynamic_route.action),
|
.action = @field(views.Route.Action, dynamic_route.action),
|
||||||
.view = view,
|
.view = view,
|
||||||
.static = false,
|
.static = false,
|
||||||
.uri_path = dynamic_route.uri_path,
|
.uri_path = dynamic_route.uri_path,
|
||||||
|
.layout = layout,
|
||||||
.template = dynamic_route.template,
|
.template = dynamic_route.template,
|
||||||
.json_params = &.{},
|
.json_params = &.{},
|
||||||
};
|
};
|
||||||
@ -138,12 +144,18 @@ pub fn route(comptime routes: anytype) []views.Route {
|
|||||||
comptime var static_params: [params_size][]const u8 = undefined;
|
comptime var static_params: [params_size][]const u8 = undefined;
|
||||||
inline for (static_route.params, 0..) |json, params_index| static_params[params_index] = json;
|
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] = .{
|
detected[index] = .{
|
||||||
.name = static_route.name,
|
.name = static_route.name,
|
||||||
.action = @field(views.Route.Action, static_route.action),
|
.action = @field(views.Route.Action, static_route.action),
|
||||||
.view = view,
|
.view = view,
|
||||||
.static = true,
|
.static = true,
|
||||||
.uri_path = static_route.uri_path,
|
.uri_path = static_route.uri_path,
|
||||||
|
.layout = layout,
|
||||||
.template = static_route.template,
|
.template = static_route.template,
|
||||||
.json_params = &static_params,
|
.json_params = &static_params,
|
||||||
};
|
};
|
||||||
|
@ -36,6 +36,7 @@ pub fn start(self: Self, comptime_routes: []jetzig.views.Route) !void {
|
|||||||
.static = comptime_route.static,
|
.static = comptime_route.static,
|
||||||
.render = comptime_route.render,
|
.render = comptime_route.render,
|
||||||
.renderStatic = comptime_route.renderStatic,
|
.renderStatic = comptime_route.renderStatic,
|
||||||
|
.layout = comptime_route.layout,
|
||||||
.template = comptime_route.template,
|
.template = comptime_route.template,
|
||||||
.json_params = comptime_route.json_params,
|
.json_params = comptime_route.json_params,
|
||||||
};
|
};
|
||||||
|
@ -210,9 +210,35 @@ fn renderView(
|
|||||||
if (isBadRequest(err)) return try self.renderBadRequest(request);
|
if (isBadRequest(err)) return try self.renderBadRequest(request);
|
||||||
return try self.renderInternalServerError(request, err);
|
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 {
|
fn isBadRequest(err: anyerror) bool {
|
||||||
|
@ -44,6 +44,7 @@ static_view: ?StaticViewType = null,
|
|||||||
static: bool,
|
static: bool,
|
||||||
render: RenderFn = renderFn,
|
render: RenderFn = renderFn,
|
||||||
renderStatic: RenderStaticFn = renderStaticFn,
|
renderStatic: RenderStaticFn = renderStaticFn,
|
||||||
|
layout: ?[]const u8,
|
||||||
template: []const u8,
|
template: []const u8,
|
||||||
json_params: [][]const u8,
|
json_params: [][]const u8,
|
||||||
params: std.ArrayList(*jetzig.data.Data) = undefined,
|
params: std.ArrayList(*jetzig.data.Data) = undefined,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user