diff --git a/build.zig b/build.zig
index 5337a24..e64f999 100644
--- a/build.zig
+++ b/build.zig
@@ -12,11 +12,22 @@ const use_llvm_default = builtin.os.tag != .linux;
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
+ var jetzig_templates_path = std.ArrayList([]const u8).init(b.allocator);
+ try jetzig_templates_path.append("/");
+ var it = std.mem.splitSequence(
+ u8,
+ try b.path("src/jetzig/templates").getPath3(b, null).toString(b.allocator),
+ std.fs.path.sep_str,
+ );
+ while (it.next()) |segment| {
+ try jetzig_templates_path.append(segment);
+ }
const templates_paths = try zmpl_build.templatesPaths(
b.allocator,
&.{
.{ .prefix = "views", .path = &.{ "src", "app", "views" } },
.{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } },
+ .{ .prefix = "jetzig", .path = jetzig_templates_path.items },
},
);
diff --git a/build.zig.zon b/build.zig.zon
index 0061b20..cbefcb8 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -25,8 +25,8 @@
.hash = "jetkv-0.0.0-zCv0fmCGAgCyYqwHjk0P5KrYVRew1MJAtbtAcIO-WPpT",
},
.zmpl = .{
- .url = "https://github.com/jetzig-framework/zmpl/archive/c57fc9b83027e8c1459d9625c3509f59f0fb89f3.tar.gz",
- .hash = "zmpl-0.0.1-SYFGBgdqAwDeA6xm4KAhpKoNrWs5CMQK6x447zhWclCs",
+ .url = "https://github.com/jetzig-framework/zmpl/archive/89ee0ce9b4c96c316cc0575266fb66c864f24a49.tar.gz",
+ .hash = "zmpl-0.0.1-SYFGBtuNAwCj2YbqnoEJt3bk1iFIZjGK6JwMc72toZBR",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/37d7cb9819b804ade5f4b974b82f8dd0622225ed.tar.gz",
diff --git a/demo/src/app/views/inertia/_head.zmpl b/demo/src/app/views/inertia/_head.zmpl
new file mode 100644
index 0000000..783e4ab
--- /dev/null
+++ b/demo/src/app/views/inertia/_head.zmpl
@@ -0,0 +1 @@
+
My Inertia App
diff --git a/demo/src/main.zig b/demo/src/main.zig
index b306e70..8306b6e 100644
--- a/demo/src/main.zig
+++ b/demo/src/main.zig
@@ -14,9 +14,10 @@ pub const jetzig_options = struct {
pub const middleware: []const type = &.{
// jetzig.middleware.AuthMiddleware,
// jetzig.middleware.AntiCsrfMiddleware,
- // jetzig.middleware.HtmxMiddleware,
- // jetzig.middleware.CompressionMiddleware,
- // @import("app/middleware/DemoMiddleware.zig"),
+ jetzig.middleware.HtmxMiddleware,
+ // jetzig.middleware.InertiaMiddleware,
+ // jetzig.middleware.CompressionMiddleware,
+ // @import("app/middleware/DemoMiddleware.zig"),
};
// Maximum bytes to allow in request body.
diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig
index 1f45ec5..b027218 100644
--- a/src/compile_static_routes.zig
+++ b/src/compile_static_routes.zig
@@ -147,7 +147,7 @@ fn renderMarkdown(
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
view.data.content = .{ .data = content };
- return try layout.render(view.data, jetzig.TemplateContext, .{}, .{});
+ return try layout.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{});
} else {
std.debug.print("Unknown layout: {s}\n", .{layout_name});
return content;
@@ -174,6 +174,7 @@ fn renderZmplTemplate(
view.data,
jetzig.TemplateContext,
.{},
+ &.{},
.{ .layout = layout },
);
} else {
@@ -181,7 +182,7 @@ fn renderZmplTemplate(
return try allocator.dupe(u8, "");
}
} else {
- return try template.render(view.data, jetzig.TemplateContext, .{}, .{});
+ return try template.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{});
}
} else return null;
}
diff --git a/src/jetzig/TemplateContext.zig b/src/jetzig/TemplateContext.zig
index 321e527..b413285 100644
--- a/src/jetzig/TemplateContext.zig
+++ b/src/jetzig/TemplateContext.zig
@@ -1,13 +1,18 @@
const std = @import("std");
pub const http = @import("http.zig");
+pub const views = @import("views.zig");
pub const config = @import("config.zig");
/// Context available in every Zmpl template as `context`.
pub const TemplateContext = @This();
request: ?*http.Request = null,
+route: ?views.Route = null,
+/// Return an authenticity token stored in the current request's session. If no token exists,
+/// generate and store before returning.
+/// Use to create a form element which can be verified by `AntiCsrfMiddleware`.
pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
return if (self.request) |request|
try request.authenticityToken()
@@ -15,6 +20,8 @@ pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
null;
}
+/// Generate a hidden form element containing an authenticity token provided by
+/// `authenticityToken`. Use as `{{context.authenticityFormElement()}}` in a Zmpl template.
pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
return if (self.request) |request| blk: {
const token = try request.authenticityToken();
@@ -23,3 +30,10 @@ pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
, .{ config.get([]const u8, "authenticity_token_name"), token });
} else null;
}
+
+pub fn path(self: TemplateContext) ?[]const u8 {
+ return if (self.request) |request|
+ request.path.path
+ else
+ null;
+}
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index 4e31d3e..fd06a76 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -15,6 +15,7 @@ pub const RequestState = enum {
initial, // No processing has taken place
processed, // Request headers have been processed
after_request, // Initial middleware processing
+ after_view, // View returned, response data ready for full response render
rendered, // Rendered by middleware or view
redirected, // Redirected by middleware or view
failed, // Failed by middleware or view
@@ -217,6 +218,20 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
return self.rendered_view.?;
}
+/// Render a response with pre-rendered content. This function can only be called once per
+/// request (repeat calls will trigger an error).
+pub fn renderContent(
+ self: *Request,
+ status_code: jetzig.http.status_codes.StatusCode,
+ content: []const u8,
+) jetzig.views.View {
+ if (self.isRendered()) self.rendered_multiple = true;
+
+ self.state = .rendered;
+ self.rendered_view = .{ .data = self.response_data, .status_code = status_code, .content = content };
+ return self.rendered_view.?;
+}
+
/// Render an error. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
@@ -230,7 +245,7 @@ pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) je
pub inline fn isRendered(self: *const Request) bool {
return switch (self.state) {
.initial, .processed, .after_request, .before_response => false,
- .rendered, .redirected, .failed, .finalized => true,
+ .after_view, .rendered, .redirected, .failed, .finalized => true,
};
}
@@ -306,6 +321,7 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void {
self.response_data,
jetzig.TemplateContext,
.{ .request = self },
+ &.{},
.{},
);
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index 9fd9ce1..f23c6e4 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -152,13 +152,18 @@ pub fn processNextRequest(
try request.process();
+ // Allow middleware to render templates even though we have not mapped a view/action yet.
+ // TODO: We should probably separate the routing and map routes before we invoke middleware.
+ try request.response_data.addConst("jetzig_action", request.response_data.string(""));
+ try request.response_data.addConst("jetzig_view", request.response_data.string(""));
+
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
if (try maybeMiddlewareRender(&request, &response)) {
try self.logger.logRequest(&request);
return;
}
- try self.renderResponse(&request);
+ try self.renderResponse(&request, &middleware_data);
try request.response.headers.append("Content-Type", response.content_type);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
@@ -175,8 +180,7 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.
if (request.redirect_state) |state| {
try request.renderRedirect(state);
} else if (request.rendered_view) |rendered| {
- // TODO: Allow middleware to set content
- request.setResponse(.{ .view = rendered, .content = "" }, .{});
+ request.setResponse(.{ .view = rendered, .content = rendered.content orelse "" }, .{});
}
try request.response.headers.append("Content-Type", response.content_type);
try request.respond();
@@ -184,7 +188,11 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.
} else return false;
}
-fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
+fn renderResponse(
+ self: *Server,
+ request: *jetzig.http.Request,
+ middleware_data: *jetzig.http.middleware.MiddlewareData,
+) !void {
const static_resource = self.matchStaticResource(request) catch |err| {
if (isUnhandledError(err)) return err;
@@ -236,6 +244,34 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
return;
}
}
+
+ // View functions return a `View` to encourage users to return from a view function with
+ // `return request.render(.ok)`, but the actual rendered view is stored in
+ // `request.rendered_view`.
+ _ = route.render(route, request) catch |err| {
+ if (isUnhandledError(err)) return err;
+ const rendered_error = if (isBadRequest(err))
+ try self.renderBadRequest(request)
+ else
+ try self.renderInternalServerError(request, @errorReturnTrace(), err);
+ request.setResponse(rendered_error, .{});
+ return;
+ };
+
+ if (request.rendered_view != null) {
+ try jetzig.http.middleware.afterView(middleware_data, request, route);
+ }
+ }
+
+ if (request.middleware_rendered) |_| {
+ // Request processing ends when a middleware renders or redirects.
+ if (request.redirect_state) |state| {
+ try request.renderRedirect(state);
+ } else if (request.rendered_view) |rendered| {
+ request.setResponse(.{ .view = rendered, .content = rendered.content orelse "" }, .{});
+ }
+ try request.response.headers.append("Content-Type", request.response.content_type);
+ return try request.respond();
}
switch (request.requestFormat()) {
@@ -347,15 +383,6 @@ fn renderView(
request: *jetzig.http.Request,
maybe_template: ?zmpl.Template,
) !RenderedView {
- // View functions return a `View` to encourage users to return from a view function with
- // `return request.render(.ok)`, but the actual rendered view is stored in
- // `request.rendered_view`.
- _ = route.render(route, request) catch |err| {
- if (isUnhandledError(err)) return err;
- if (isBadRequest(err)) return try self.renderBadRequest(request);
- return try self.renderInternalServerError(request, @errorReturnTrace(), err);
- };
-
if (request.state == .failed) {
const view: jetzig.views.View = request.rendered_view orelse .{
.data = request.response_data,
@@ -415,7 +442,7 @@ fn renderTemplateWithLayout(
) ![]const u8 {
try addTemplateConstants(view, route);
- const template_context = jetzig.TemplateContext{ .request = request };
+ const template_context = jetzig.TemplateContext{ .request = request, .route = route };
if (request.getLayout(route)) |layout_name| {
// TODO: Allow user to configure layouts directory other than src/app/views/layouts/
@@ -431,6 +458,7 @@ fn renderTemplateWithLayout(
view.data,
jetzig.TemplateContext,
template_context,
+ &.{},
.{ .layout = layout },
);
} else {
@@ -439,6 +467,7 @@ fn renderTemplateWithLayout(
view.data,
jetzig.TemplateContext,
template_context,
+ &.{},
.{},
);
}
@@ -446,6 +475,7 @@ fn renderTemplateWithLayout(
view.data,
jetzig.TemplateContext,
template_context,
+ &.{},
.{},
);
}
@@ -608,7 +638,8 @@ fn renderErrorView(
.content = try template.render(
request.response_data,
jetzig.TemplateContext,
- .{ .request = request },
+ .{ .request = request, .route = route.* },
+ &.{},
.{},
),
};
diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig
index e0a4a15..cd42b87 100644
--- a/src/jetzig/http/middleware.zig
+++ b/src/jetzig/http/middleware.zig
@@ -94,6 +94,38 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
return middleware_data;
}
+pub fn afterView(middleware_data: *MiddlewareData, request: *jetzig.http.Request, route: jetzig.views.Route) !void {
+ if (request.state != .failed) request.state = .after_view;
+
+ inline for (middlewares, 0..) |middleware, index| {
+ if (comptime !@hasDecl(middleware, "afterView")) continue;
+ if (request.state == .after_view) {
+ if (comptime @hasDecl(middleware, "init")) {
+ const data = middleware_data.get(index).?;
+ try @call(
+ .always_inline,
+ middleware.afterView,
+ .{ @as(*middleware, @ptrCast(@alignCast(data))), request, route },
+ );
+ } else {
+ try @call(
+ .always_inline,
+ middleware.afterView,
+ .{ request, route },
+ );
+ }
+ }
+
+ if (request.state != .after_view) {
+ request.middleware_rendered = .{
+ .name = @typeName(middleware),
+ .action = "afterView",
+ };
+ break;
+ }
+ }
+}
+
pub fn beforeResponse(
middleware_data: *MiddlewareData,
request: *jetzig.http.Request,
diff --git a/src/jetzig/mail/Job.zig b/src/jetzig/mail/Job.zig
index dccf828..657ed07 100644
--- a/src/jetzig/mail/Job.zig
+++ b/src/jetzig/mail/Job.zig
@@ -148,7 +148,7 @@ fn defaultHtml(
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
- try template.render(&data, jetzig.TemplateContext, .{}, .{})
+ try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{})
else
null;
}
@@ -166,7 +166,7 @@ fn defaultText(
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|
- try template.render(&data, jetzig.TemplateContext, .{}, .{})
+ try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{})
else
null;
}
diff --git a/src/jetzig/middleware.zig b/src/jetzig/middleware.zig
index 4be3640..0da095c 100644
--- a/src/jetzig/middleware.zig
+++ b/src/jetzig/middleware.zig
@@ -5,6 +5,7 @@ pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig");
pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig");
pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig");
+pub const InertiaMiddleware = @import("middleware/InertiaMiddleware.zig");
const RouteOptions = struct {
content: ?[]const u8 = null,
diff --git a/src/jetzig/middleware/InertiaMiddleware.zig b/src/jetzig/middleware/InertiaMiddleware.zig
new file mode 100644
index 0000000..4272eb2
--- /dev/null
+++ b/src/jetzig/middleware/InertiaMiddleware.zig
@@ -0,0 +1,6 @@
+const std = @import("std");
+const jetzig = @import("../../jetzig.zig");
+
+// WIP
+
+const InertiaMiddleware = @This();
diff --git a/src/jetzig/templates/inertia.zmpl b/src/jetzig/templates/inertia.zmpl
new file mode 100644
index 0000000..51732d1
--- /dev/null
+++ b/src/jetzig/templates/inertia.zmpl
@@ -0,0 +1,13 @@
+
+
+
+ @partial views:inertia/head
+
+
+
+
+
+
diff --git a/src/jetzig/views/View.zig b/src/jetzig/views/View.zig
index 98beaac..f1fbefd 100644
--- a/src/jetzig/views/View.zig
+++ b/src/jetzig/views/View.zig
@@ -6,6 +6,7 @@ const jetzig = @import("../../jetzig.zig");
data: *jetzig.data.Data,
status_code: jetzig.http.status_codes.StatusCode = .ok,
+content: ?[]const u8 = null,
pub fn deinit(self: Self) void {
_ = self;