Merge pull request #23 from jetzig-framework/layouts

Implement layouts
This commit is contained in:
bobf 2024-03-14 19:50:02 +00:00 committed by GitHub
commit df97210586
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 167 additions and 15 deletions

View File

@ -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",

View File

@ -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;
}
}

View 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});
}

View 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);
}

View File

@ -0,0 +1,3 @@
<div>
<span>Content goes here</span>
</div>

View File

@ -0,0 +1,8 @@
<html>
<head>
</head>
<body>
<main>{zmpl.content}</main>
</body>
</html>

View File

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<main>{zmpl.content}</main>
</body>
</html>

View File

@ -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,
});

View File

@ -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 = &params,
};
@ -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});
}

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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 {

View File

@ -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,