Merge pull request #59 from jetzig-framework/zmpl-markdown-mode-formatters

Update Zmpl, adds Markdown mode formatters
This commit is contained in:
bobf 2024-04-27 23:37:05 +01:00 committed by GitHub
commit 0c450ff9b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 127 additions and 102 deletions

View File

@ -45,6 +45,7 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize, .optimize = optimize,
.zmpl_templates_paths = templates_paths, .zmpl_templates_paths = templates_paths,
.zmpl_auto_build = false, .zmpl_auto_build = false,
.zmpl_markdown_fragments = try generateMarkdownFragments(b),
.zmpl_constants = try zmpl_build.addTemplateConstants(b, struct { .zmpl_constants = try zmpl_build.addTemplateConstants(b, struct {
jetzig_view: []const u8, jetzig_view: []const u8,
jetzig_action: []const u8, jetzig_action: []const u8,
@ -55,13 +56,13 @@ pub fn build(b: *std.Build) !void {
const zmpl_module = zmpl_dep.module("zmpl"); const zmpl_module = zmpl_dep.module("zmpl");
const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize }); const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize });
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
// This is the way to make it look nice in the zig build script // This is the way to make it look nice in the zig build script
// If we would do it the other way around, we would have to do // If we would do it the other way around, we would have to do
// b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl"); // b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl");
b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory"); b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory");
b.modules.put("zmd", zmd_dep.module("zmd")) catch @panic("Out of memory");
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
const smtp_client_dep = b.dependency("smtp_client", .{ const smtp_client_dep = b.dependency("smtp_client", .{
.target = target, .target = target,
@ -117,9 +118,11 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
); );
const jetzig_module = jetzig_dep.module("jetzig"); const jetzig_module = jetzig_dep.module("jetzig");
const zmpl_module = jetzig_dep.module("zmpl"); const zmpl_module = jetzig_dep.module("zmpl");
const zmd_module = jetzig_dep.module("zmd");
exe.root_module.addImport("jetzig", jetzig_module); exe.root_module.addImport("jetzig", jetzig_module);
exe.root_module.addImport("zmpl", zmpl_module); exe.root_module.addImport("zmpl", zmpl_module);
exe.root_module.addImport("zmd", zmd_module);
if (b.option(bool, "jetzig_runner", "Used internally by `jetzig server` command.")) |jetzig_runner| { if (b.option(bool, "jetzig_runner", "Used internally by `jetzig server` command.")) |jetzig_runner| {
if (jetzig_runner) { if (jetzig_runner) {
@ -170,7 +173,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
if (!std.mem.eql(u8, ".zig", std.fs.path.extension(entry.path))) continue; if (!std.mem.eql(u8, ".zig", std.fs.path.extension(entry.path))) continue;
const stat = try src_dir.statFile(entry.path); const stat = try src_dir.statFile(entry.path);
const src_data = try src_dir.readFileAlloc(b.allocator, entry.path, stat.size); const src_data = try src_dir.readFileAlloc(b.allocator, entry.path, @intCast(stat.size));
defer b.allocator.free(src_data); defer b.allocator.free(src_data);
const relpath = try std.fs.path.join(b.allocator, &[_][]const u8{ "src", entry.path }); const relpath = try std.fs.path.join(b.allocator, &[_][]const u8{ "src", entry.path });
@ -204,3 +207,45 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
run_static_routes_cmd.expectExitCode(0); run_static_routes_cmd.expectExitCode(0);
exe.step.dependOn(&run_static_routes_cmd.step); exe.step.dependOn(&run_static_routes_cmd.step);
} }
fn generateMarkdownFragments(b: *std.Build) ![]const u8 {
const file = std.fs.cwd().openFile(b.pathJoin(&.{ "src", "main.zig" }), .{}) catch |err| {
switch (err) {
error.FileNotFound => return "",
else => return err,
}
};
const stat = try file.stat();
const source = try file.readToEndAllocOptions(b.allocator, @intCast(stat.size), null, @alignOf(u8), 0);
if (try getMarkdownFragmentsSource(b.allocator, source)) |markdown_fragments_source| {
return try std.fmt.allocPrint(b.allocator,
\\const std = @import("std");
\\const zmd = @import("zmd");
\\
\\{s};
\\
, .{markdown_fragments_source});
} else {
return "";
}
}
fn getMarkdownFragmentsSource(allocator: std.mem.Allocator, source: [:0]const u8) !?[]const u8 {
var ast = try std.zig.Ast.parse(allocator, source, .zig);
defer ast.deinit(allocator);
for (ast.nodes.items(.tag), 0..) |tag, index| {
switch (tag) {
.simple_var_decl => {
const decl = ast.simpleVarDecl(@intCast(index));
const identifier = ast.tokenSlice(decl.ast.mut_token + 1);
if (std.mem.eql(u8, identifier, "markdown_fragments")) {
return ast.getNodeSource(@intCast(index));
}
},
else => continue,
}
}
return null;
}

View File

@ -3,12 +3,12 @@
.version = "0.0.0", .version = "0.0.0",
.dependencies = .{ .dependencies = .{
.zmd = .{ .zmd = .{
.url = "https://github.com/jetzig-framework/zmd/archive/901e4ce55cdbfd82c42cfd4feb3a1682dab4b418.tar.gz", .url = "https://github.com/jetzig-framework/zmd/archive/557ab8a29c0f7b5d096070cde2858cf27da8b0f1.tar.gz",
.hash = "12207d49df326e0c180a90fa65d9993898e0a0ffd8e79616b4b81f81769261858856", .hash = "1220482f07f2bbaef335f20d6890c15a1e14739950b784232bc69182423520e058a5",
}, },
.zmpl = .{ .zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/4511ae706e8679385d38cc1366497082f8f53afb.tar.gz", .url = "https://github.com/jetzig-framework/zmpl/archive/046c05d376a4fe89d86c52596baa18137891cd87.tar.gz",
.hash = "1220d493e6fdfaccbafff41df2b7b407728ed11619bebb198c90dae9420f03a6d29d", .hash = "1220d8890b1161e4356d2c59d4b88280566d55480dae840b6f5dae34bf852bef6b56",
}, },
.args = .{ .args = .{
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz", .url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",

View File

@ -42,19 +42,36 @@ pub fn run(
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();
const map = std.ComptimeStringMap(Generator, .{
.{ "view", .view },
.{ "partial", .partial },
.{ "layout", .layout },
.{ "job", .job },
.{ "mailer", .mailer },
.{ "middleware", .middleware },
.{ "secret", .secret },
});
var available_buf = std.ArrayList([]const u8).init(allocator); var available_buf = std.ArrayList([]const u8).init(allocator);
defer available_buf.deinit(); defer available_buf.deinit();
for (map.kvs) |kv| try available_buf.append(kv.key);
// XXX: 0.12 Compatibility
const map = if (@hasDecl(std, "ComptimeStringMap")) blk: {
const inner_map = std.ComptimeStringMap(Generator, .{
.{ "view", .view },
.{ "partial", .partial },
.{ "layout", .layout },
.{ "job", .job },
.{ "mailer", .mailer },
.{ "middleware", .middleware },
.{ "secret", .secret },
});
for (inner_map.kvs) |kv| try available_buf.append(kv.key);
break :blk inner_map;
} else if (@hasDecl(std, "StaticStringMap")) blk: {
const inner_map = std.StaticStringMap(Generator).initComptime(.{
.{ "view", .view },
.{ "partial", .partial },
.{ "layout", .layout },
.{ "job", .job },
.{ "mailer", .mailer },
.{ "middleware", .middleware },
.{ "secret", .secret },
});
for (inner_map.keys()) |key| try available_buf.append(key);
break :blk inner_map;
} else unreachable;
const available_help = try std.mem.join(allocator, "|", available_buf.items); const available_help = try std.mem.join(allocator, "|", available_buf.items);
defer allocator.free(available_help); defer allocator.free(available_help);

View File

@ -45,7 +45,7 @@ pub fn initDataModule(build: *std.Build) !*std.Build.Module {
const stat = try dir.statFile(path); const stat = try dir.statFile(path);
const encoded = base64Encode( const encoded = base64Encode(
build.allocator, build.allocator,
try dir.readFileAlloc(build.allocator, path, stat.size), try dir.readFileAlloc(build.allocator, path, @intCast(stat.size)),
); );
defer build.allocator.free(encoded); defer build.allocator.free(encoded);

View File

@ -2,11 +2,11 @@
![jetzig logo](https://www.jetzig.dev/jetzig.png) ![jetzig logo](https://www.jetzig.dev/jetzig.png)
_Markdown_ is rendered by _[zmd](https://github.com/jetzig-framework/zmd)_. _Markdown_ is rendered by [zmd](https://github.com/jetzig-framework/zmd).
You can use a `StaticRequest` in your view if you prefer to render your _Markdown_ at build time, or use `Request` in development to render at run time without a server restart. You can use a `StaticRequest` in your view if you prefer to render your _Markdown_ at build time, or use `Request` in development to render at run time without a server restart.
Simply create a `.md` file instead of a `.zmpl` file, e.g. `src/app/views/iguanas/index.md` and _Markdown_ will be rendered. Simply create a `.md.zmpl` file instead of a `.zmpl` file, e.g. `src/app/views/iguanas/index.md.zmpl` and _Markdown_ will be rendered. You can still use _Zmpl_ references, modes, and partials. In fact, a `.md.zmpl` template is simply a regular _Zmpl_ template with the root mode set to `markdown`.
## An _ordered_ list ## An _ordered_ list

View File

@ -37,7 +37,7 @@ const Quote = struct {
fn randomQuote(allocator: std.mem.Allocator) !Quote { fn randomQuote(allocator: std.mem.Allocator) !Quote {
const path = "src/app/config/quotes.json"; const path = "src/app/config/quotes.json";
const stat = try std.fs.cwd().statFile(path); const stat = try std.fs.cwd().statFile(path);
const json = try std.fs.cwd().readFileAlloc(allocator, path, stat.size); const json = try std.fs.cwd().readFileAlloc(allocator, path, @intCast(stat.size));
const quotes = try std.json.parseFromSlice([]Quote, allocator, json, .{}); const quotes = try std.json.parseFromSlice([]Quote, allocator, json, .{});
return quotes.value[std.crypto.random.intRangeLessThan(usize, 0, quotes.value.len)]; return quotes.value[std.crypto.random.intRangeLessThan(usize, 0, quotes.value.len)];
} }

View File

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
pub const jetzig = @import("jetzig"); const jetzig = @import("jetzig");
const zmd = @import("zmd");
pub const routes = @import("routes"); pub const routes = @import("routes");
@ -54,14 +55,14 @@ pub const jetzig_options = struct {
// pub const force_development_email_delivery = false; // pub const force_development_email_delivery = false;
// Set custom fragments for rendering markdown templates. Any values will fall back to // Set custom fragments for rendering markdown templates. Any values will fall back to
// defaults provided by Zmd (https://github.com/bobf/zmd/blob/main/src/zmd/html.zig). // defaults provided by Zmd (https://github.com/jetzig-framework/zmd/blob/main/src/zmd/html.zig).
pub const markdown_fragments = struct { pub const markdown_fragments = struct {
pub const root = .{ pub const root = .{
"<div class='p-5'>", "<div class='p-5'>",
"</div>", "</div>",
}; };
pub const h1 = .{ pub const h1 = .{
"<h1 class='text-2xl mb-3 font-bold'>", "<h1 class='text-2xl mb-3 text-green font-bold'>",
"</h1>", "</h1>",
}; };
pub const h2 = .{ pub const h2 = .{
@ -91,13 +92,13 @@ pub const jetzig_options = struct {
"</ul>", "</ul>",
}; };
pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { pub fn block(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 {
return try std.fmt.allocPrint(allocator, return try std.fmt.allocPrint(allocator,
\\<pre class="w-1/2 font-mono mt-4 ms-3 bg-gray-900 p-2 text-white"><code class="language-{?s}">{s}</code></pre> \\<pre class="w-1/2 font-mono mt-4 ms-3 bg-gray-900 p-2 text-white"><code class="language-{?s}">{s}</code></pre>
, .{ node.meta, node.content }); , .{ node.meta, node.content });
} }
pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 { pub fn link(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 {
return try std.fmt.allocPrint(allocator, return try std.fmt.allocPrint(allocator,
\\<a class="underline decoration-sky-500" href="{0s}" title={1s}>{1s}</a> \\<a class="underline decoration-sky-500" href="{0s}" title={1s}>{1s}</a>
, .{ node.href.?, node.title.? }); , .{ node.href.?, node.title.? });

View File

@ -10,7 +10,7 @@ const JsonMimeType = struct {
pub fn generateMimeModule(build: *std.Build) !*std.Build.Module { pub fn generateMimeModule(build: *std.Build) !*std.Build.Module {
const file = try std.fs.openFileAbsolute(build.pathFromRoot("src/jetzig/http/mime/mimeData.json"), .{}); const file = try std.fs.openFileAbsolute(build.pathFromRoot("src/jetzig/http/mime/mimeData.json"), .{});
const stat = try file.stat(); const stat = try file.stat();
const json = try file.readToEndAlloc(build.allocator, stat.size); const json = try file.readToEndAlloc(build.allocator, @intCast(stat.size));
defer build.allocator.free(json); defer build.allocator.free(json);
const parsed_mime_types = try std.json.parseFromSlice( const parsed_mime_types = try std.json.parseFromSlice(

View File

@ -296,7 +296,7 @@ const RouteSet = struct {
fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet { fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet {
const stat = try dir.statFile(path); const stat = try dir.statFile(path);
const source = try dir.readFileAllocOptions(self.allocator, path, stat.size, null, @alignOf(u8), 0); const source = try dir.readFileAllocOptions(self.allocator, path, @intCast(stat.size), null, @alignOf(u8), 0);
defer self.allocator.free(source); defer self.allocator.free(source);
self.ast = try std.zig.Ast.parse(self.allocator, source, .zig); self.ast = try std.zig.Ast.parse(self.allocator, source, .zig);

View File

@ -143,7 +143,7 @@ fn renderZmplTemplate(
defer allocator.free(prefixed_name); defer allocator.free(prefixed_name);
if (zmpl.findPrefixed("views", prefixed_name)) |layout| { if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
return try template.renderWithLayout(layout, view.data); return try template.renderWithOptions(view.data, .{ .layout = layout });
} else { } else {
std.debug.print("Unknown layout: {s}\n", .{layout_name}); std.debug.print("Unknown layout: {s}\n", .{layout_name});
return try allocator.dupe(u8, ""); return try allocator.dupe(u8, "");

View File

@ -2,6 +2,7 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const zmpl = @import("zmpl"); const zmpl = @import("zmpl");
const zmd = @import("zmd");
pub const ServerOptions = struct { pub const ServerOptions = struct {
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
@ -153,27 +154,22 @@ fn renderHTML(
route: ?*jetzig.views.Route, route: ?*jetzig.views.Route,
) !void { ) !void {
if (route) |matched_route| { if (route) |matched_route| {
const template = zmpl.findPrefixed("views", matched_route.template); if (zmpl.findPrefixed("views", matched_route.template)) |template| {
if (template == null) { const rendered = self.renderView(matched_route, request, template) catch |err| {
request.response_data.noop(bool, false); // FIXME: Weird Zig bug ? Any call here fixes it. if (isUnhandledError(err)) return err;
if (try self.renderMarkdown(request, route)) |rendered_markdown| { const rendered_error = try self.renderInternalServerError(request, err);
return request.setResponse(rendered_markdown, .{}); return request.setResponse(rendered_error, .{});
} };
}
const rendered = self.renderView(matched_route, request, template) catch |err| {
if (isUnhandledError(err)) return err;
const rendered_error = try self.renderInternalServerError(request, err);
return request.setResponse(rendered_error, .{});
};
if (request.status_code != .not_found) {
return request.setResponse(rendered, .{}); return request.setResponse(rendered, .{});
} else {
return request.setResponse(try renderNotFound(request), .{});
} }
}
if (try self.renderMarkdown(request, route)) |rendered| {
return request.setResponse(rendered, .{});
} else { } else {
return request.setResponse(try renderNotFound(request), .{}); if (try self.renderMarkdown(request)) |rendered| {
return request.setResponse(rendered, .{});
} else {
return request.setResponse(try renderNotFound(request), .{});
}
} }
} }
@ -199,57 +195,18 @@ fn renderJSON(
} }
} }
fn renderMarkdown( fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView {
self: *Server, _ = self;
request: *jetzig.http.Request, // No route recognized, but we can still render a static markdown file if it matches the URI:
maybe_route: ?*jetzig.views.Route, if (request.method != .GET) return null;
) !?RenderedView { if (try jetzig.markdown.render(request.allocator, request.path.base_path, null)) |content| {
const route = maybe_route orelse { return .{
// No route recognized, but we can still render a static markdown file if it matches the URI: .view = jetzig.views.View{ .data = request.response_data, .status_code = .ok },
if (request.method != .GET) return null; .content = content,
if (try jetzig.markdown.render(request.allocator, request.path.base_path, null)) |content| { };
return .{ } else {
.view = jetzig.views.View{ .data = request.response_data, .status_code = .ok },
.content = content,
};
} else {
return null;
}
};
const path = try std.mem.join(
request.allocator,
"/",
&[_][]const u8{ route.uri_path, @tagName(route.action) },
);
const markdown_content = try jetzig.markdown.render(request.allocator, path, null) orelse
return null; return null;
var rendered = self.renderView(route, request, null) catch |err| {
if (isUnhandledError(err)) return err;
return try self.renderInternalServerError(request, err);
};
try addTemplateConstants(rendered.view, route);
if (request.getLayout(route)) |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.findPrefixed("views", prefixed_name)) |layout| {
rendered.view.data.content = .{ .data = markdown_content };
rendered.content = try layout.render(rendered.view.data);
} else {
try self.logger.WARN("Unknown layout: {s}", .{layout_name});
rendered.content = markdown_content;
}
} }
return rendered;
} }
pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 }; pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 };
@ -260,7 +217,7 @@ fn renderView(
request: *jetzig.http.Request, request: *jetzig.http.Request,
template: ?zmpl.Template, template: ?zmpl.Template,
) !RenderedView { ) !RenderedView {
// View functions return a `View` to help encourage users to return from a view function with // 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 // `return request.render(.ok)`, but the actual rendered view is stored in
// `request.rendered_view`. // `request.rendered_view`.
_ = route.render(route.*, request) catch |err| { _ = route.render(route.*, request) catch |err| {
@ -307,11 +264,15 @@ fn renderTemplateWithLayout(
if (request.getLayout(route)) |layout_name| { if (request.getLayout(route)) |layout_name| {
// TODO: Allow user to configure layouts directory other than src/app/views/layouts/ // 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 }); const prefixed_name = try std.mem.concat(
self.allocator,
u8,
&[_][]const u8{ "layouts", "/", layout_name },
);
defer self.allocator.free(prefixed_name); defer self.allocator.free(prefixed_name);
if (zmpl.findPrefixed("views", prefixed_name)) |layout| { if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
return try template.renderWithLayout(layout, view.data); return try template.renderWithOptions(view.data, .{ .layout = layout });
} else { } else {
try self.logger.WARN("Unknown layout: {s}", .{layout_name}); try self.logger.WARN("Unknown layout: {s}", .{layout_name});
return try template.render(view.data); return try template.render(view.data);

View File

@ -1,8 +1,9 @@
const std = @import("std"); const std = @import("std");
const Zmd = @import("zmd").Zmd;
const jetzig = @import("../jetzig.zig"); const jetzig = @import("../jetzig.zig");
const Zmd = @import("zmd").Zmd;
pub fn render( pub fn render(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
path: []const u8, path: []const u8,
@ -32,7 +33,7 @@ pub fn render(
else => err, else => err,
}; };
}; };
const markdown_content = std.fs.cwd().readFileAlloc(allocator, full_path, stat.size) catch |err| { const markdown_content = std.fs.cwd().readFileAlloc(allocator, full_path, @intCast(stat.size)) catch |err| {
switch (err) { switch (err) {
error.FileNotFound => return null, error.FileNotFound => return null,
else => return err, else => return err,