From 9d12b5c717e264cfa755d6c3bfeecb7854b688bf Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 27 Apr 2024 22:20:28 +0100 Subject: [PATCH 1/2] Update Zmpl, adds Markdown mode formatters Also adds support for `.md.zmpl` templates - root node is Markdown. --- build.zig | 49 ++++++++- build.zig.zon | 8 +- .../markdown/{index.md => index.md.zmpl} | 4 +- demo/src/app/views/root/index.zmpl | 6 ++ demo/src/main.zig | 11 +- src/compile_static_routes.zig | 2 +- src/jetzig/http/Server.zig | 101 ++++++------------ src/jetzig/markdown.zig | 3 +- 8 files changed, 99 insertions(+), 85 deletions(-) rename demo/src/app/views/markdown/{index.md => index.md.zmpl} (83%) diff --git a/build.zig b/build.zig index bee5b61..18f3429 100644 --- a/build.zig +++ b/build.zig @@ -45,6 +45,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, .zmpl_templates_paths = templates_paths, .zmpl_auto_build = false, + .zmpl_markdown_fragments = try generateMarkdownFragments(b), .zmpl_constants = try zmpl_build.addTemplateConstants(b, struct { jetzig_view: []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 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 // If we would do it the other way around, we would have to do // b.dependency("jetzig",.{}).builder.dependency("zmpl",.{}).module("zmpl"); b.modules.put("zmpl", zmpl_dep.module("zmpl")) catch @panic("Out of memory"); - - const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize }); + b.modules.put("zmd", zmd_dep.module("zmd")) catch @panic("Out of memory"); const smtp_client_dep = b.dependency("smtp_client", .{ .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 zmpl_module = jetzig_dep.module("zmpl"); + const zmd_module = jetzig_dep.module("zmd"); exe.root_module.addImport("jetzig", jetzig_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 (jetzig_runner) { @@ -204,3 +207,45 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn run_static_routes_cmd.expectExitCode(0); 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, 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; +} diff --git a/build.zig.zon b/build.zig.zon index 4bcec26..ebf6b63 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,12 +3,12 @@ .version = "0.0.0", .dependencies = .{ .zmd = .{ - .url = "https://github.com/jetzig-framework/zmd/archive/901e4ce55cdbfd82c42cfd4feb3a1682dab4b418.tar.gz", - .hash = "12207d49df326e0c180a90fa65d9993898e0a0ffd8e79616b4b81f81769261858856", + .url = "https://github.com/jetzig-framework/zmd/archive/45a9ff0e3a55b4758163abad5a601bd3ef8127a8.tar.gz", + .hash = "122067a3107499e4b561a883a9f1e9f39662c4b87249a2e4e630279bd91ab7840230", }, .zmpl = .{ - .url = "https://github.com/jetzig-framework/zmpl/archive/4511ae706e8679385d38cc1366497082f8f53afb.tar.gz", - .hash = "1220d493e6fdfaccbafff41df2b7b407728ed11619bebb198c90dae9420f03a6d29d", + .url = "https://github.com/jetzig-framework/zmpl/archive/9712b85e61f33879f388d5e1829856e326c53822.tar.gz", + .hash = "1220b185cf8316ae5ad6dd7d45bea278575391986b7d8233a9d9b2c342e339d65ac0", }, .args = .{ .url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz", diff --git a/demo/src/app/views/markdown/index.md b/demo/src/app/views/markdown/index.md.zmpl similarity index 83% rename from demo/src/app/views/markdown/index.md rename to demo/src/app/views/markdown/index.md.zmpl index 1ab7e18..5efae1d 100644 --- a/demo/src/app/views/markdown/index.md +++ b/demo/src/app/views/markdown/index.md.zmpl @@ -2,11 +2,11 @@ ![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. -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 diff --git a/demo/src/app/views/root/index.zmpl b/demo/src/app/views/root/index.zmpl index 1fb1e42..f5fc30b 100644 --- a/demo/src/app/views/root/index.zmpl +++ b/demo/src/app/views/root/index.zmpl @@ -16,3 +16,9 @@
Take a look at the /demo/src/app/ directory to see how this application works.
Visit jetzig.dev to get started.
+ +@markdown { + # Hello + + [foobar](https://www.google.com) +} diff --git a/demo/src/main.zig b/demo/src/main.zig index 943e849..702b519 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); -pub const jetzig = @import("jetzig"); +const jetzig = @import("jetzig"); +const zmd = @import("zmd"); pub const routes = @import("routes"); @@ -54,14 +55,14 @@ pub const jetzig_options = struct { // pub const force_development_email_delivery = false; // 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 root = .{ "
", "
", }; pub const h1 = .{ - "

", + "

", "

", }; pub const h2 = .{ @@ -91,13 +92,13 @@ pub const jetzig_options = struct { "", }; - 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, \\
{s}
, .{ 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, \\{1s} , .{ node.href.?, node.title.? }); diff --git a/src/compile_static_routes.zig b/src/compile_static_routes.zig index 99e7c86..70197c0 100644 --- a/src/compile_static_routes.zig +++ b/src/compile_static_routes.zig @@ -143,7 +143,7 @@ fn renderZmplTemplate( defer allocator.free(prefixed_name); if (zmpl.findPrefixed("views", prefixed_name)) |layout| { - return try template.renderWithLayout(layout, view.data); + return try template.renderWithOptions(view.data, .{ .layout = layout }); } else { std.debug.print("Unknown layout: {s}\n", .{layout_name}); return try allocator.dupe(u8, ""); diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index c0c979d..1c98d19 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -2,6 +2,7 @@ const std = @import("std"); const jetzig = @import("../../jetzig.zig"); const zmpl = @import("zmpl"); +const zmd = @import("zmd"); pub const ServerOptions = struct { logger: jetzig.loggers.Logger, @@ -153,27 +154,22 @@ fn renderHTML( route: ?*jetzig.views.Route, ) !void { if (route) |matched_route| { - const template = zmpl.findPrefixed("views", matched_route.template); - if (template == null) { - request.response_data.noop(bool, false); // FIXME: Weird Zig bug ? Any call here fixes it. - if (try self.renderMarkdown(request, route)) |rendered_markdown| { - return request.setResponse(rendered_markdown, .{}); - } - } - 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) { + if (zmpl.findPrefixed("views", matched_route.template)) |template| { + 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, .{}); + }; return request.setResponse(rendered, .{}); + } else { + return request.setResponse(try renderNotFound(request), .{}); } - } - - if (try self.renderMarkdown(request, route)) |rendered| { - return request.setResponse(rendered, .{}); } 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( - self: *Server, - request: *jetzig.http.Request, - maybe_route: ?*jetzig.views.Route, -) !?RenderedView { - const route = maybe_route orelse { - // No route recognized, but we can still render a static markdown file if it matches the URI: - if (request.method != .GET) return null; - if (try jetzig.markdown.render(request.allocator, request.path.base_path, null)) |content| { - return .{ - .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 +fn renderMarkdown(self: *Server, request: *jetzig.http.Request) !?RenderedView { + _ = self; + // No route recognized, but we can still render a static markdown file if it matches the URI: + if (request.method != .GET) return null; + if (try jetzig.markdown.render(request.allocator, request.path.base_path, null)) |content| { + return .{ + .view = jetzig.views.View{ .data = request.response_data, .status_code = .ok }, + .content = content, + }; + } else { 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 }; @@ -260,7 +217,7 @@ fn renderView( request: *jetzig.http.Request, template: ?zmpl.Template, ) !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 // `request.rendered_view`. _ = route.render(route.*, request) catch |err| { @@ -307,11 +264,15 @@ fn renderTemplateWithLayout( 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 }); + 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| { - return try template.renderWithLayout(layout, view.data); + return try template.renderWithOptions(view.data, .{ .layout = layout }); } else { try self.logger.WARN("Unknown layout: {s}", .{layout_name}); return try template.render(view.data); diff --git a/src/jetzig/markdown.zig b/src/jetzig/markdown.zig index 1e92410..d02ac86 100644 --- a/src/jetzig/markdown.zig +++ b/src/jetzig/markdown.zig @@ -1,8 +1,9 @@ const std = @import("std"); +const Zmd = @import("zmd").Zmd; + const jetzig = @import("../jetzig.zig"); -const Zmd = @import("zmd").Zmd; pub fn render( allocator: std.mem.Allocator, path: []const u8, From 6b940e8a53e138849218d5703935e5b0a2d6c1ae Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 27 Apr 2024 22:46:22 +0100 Subject: [PATCH 2/2] 0.13 support Add support for 0.13. For now, we can be compatible with both 0.12 and 0.13. We will drop 0.12 after a while. --- build.zig | 4 +-- build.zig.zon | 8 +++--- cli/commands/generate.zig | 39 +++++++++++++++++++++--------- cli/compile.zig | 2 +- demo/src/app/views/quotes.zig | 2 +- demo/src/app/views/root/index.zmpl | 6 ----- src/GenerateMimeTypes.zig | 2 +- src/Routes.zig | 2 +- src/jetzig/markdown.zig | 2 +- 9 files changed, 39 insertions(+), 28 deletions(-) diff --git a/build.zig b/build.zig index 18f3429..19456fa 100644 --- a/build.zig +++ b/build.zig @@ -173,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; 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); const relpath = try std.fs.path.join(b.allocator, &[_][]const u8{ "src", entry.path }); @@ -216,7 +216,7 @@ fn generateMarkdownFragments(b: *std.Build) ![]const u8 { } }; const stat = try file.stat(); - const source = try file.readToEndAllocOptions(b.allocator, stat.size, null, @alignOf(u8), 0); + 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"); diff --git a/build.zig.zon b/build.zig.zon index ebf6b63..2754e5e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,12 +3,12 @@ .version = "0.0.0", .dependencies = .{ .zmd = .{ - .url = "https://github.com/jetzig-framework/zmd/archive/45a9ff0e3a55b4758163abad5a601bd3ef8127a8.tar.gz", - .hash = "122067a3107499e4b561a883a9f1e9f39662c4b87249a2e4e630279bd91ab7840230", + .url = "https://github.com/jetzig-framework/zmd/archive/557ab8a29c0f7b5d096070cde2858cf27da8b0f1.tar.gz", + .hash = "1220482f07f2bbaef335f20d6890c15a1e14739950b784232bc69182423520e058a5", }, .zmpl = .{ - .url = "https://github.com/jetzig-framework/zmpl/archive/9712b85e61f33879f388d5e1829856e326c53822.tar.gz", - .hash = "1220b185cf8316ae5ad6dd7d45bea278575391986b7d8233a9d9b2c342e339d65ac0", + .url = "https://github.com/jetzig-framework/zmpl/archive/046c05d376a4fe89d86c52596baa18137891cd87.tar.gz", + .hash = "1220d8890b1161e4356d2c59d4b88280566d55480dae840b6f5dae34bf852bef6b56", }, .args = .{ .url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz", diff --git a/cli/commands/generate.zig b/cli/commands/generate.zig index 059599d..fd7259f 100644 --- a/cli/commands/generate.zig +++ b/cli/commands/generate.zig @@ -42,19 +42,36 @@ pub fn run( var sub_args = std.ArrayList([]const u8).init(allocator); 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); 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); defer allocator.free(available_help); diff --git a/cli/compile.zig b/cli/compile.zig index b1bb2ef..1875896 100644 --- a/cli/compile.zig +++ b/cli/compile.zig @@ -45,7 +45,7 @@ pub fn initDataModule(build: *std.Build) !*std.Build.Module { const stat = try dir.statFile(path); const encoded = base64Encode( build.allocator, - try dir.readFileAlloc(build.allocator, path, stat.size), + try dir.readFileAlloc(build.allocator, path, @intCast(stat.size)), ); defer build.allocator.free(encoded); diff --git a/demo/src/app/views/quotes.zig b/demo/src/app/views/quotes.zig index 3626913..3ee13f6 100644 --- a/demo/src/app/views/quotes.zig +++ b/demo/src/app/views/quotes.zig @@ -37,7 +37,7 @@ const Quote = struct { fn randomQuote(allocator: std.mem.Allocator) !Quote { const path = "src/app/config/quotes.json"; 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, .{}); return quotes.value[std.crypto.random.intRangeLessThan(usize, 0, quotes.value.len)]; } diff --git a/demo/src/app/views/root/index.zmpl b/demo/src/app/views/root/index.zmpl index f5fc30b..1fb1e42 100644 --- a/demo/src/app/views/root/index.zmpl +++ b/demo/src/app/views/root/index.zmpl @@ -16,9 +16,3 @@
Take a look at the /demo/src/app/ directory to see how this application works.
Visit jetzig.dev to get started.
- -@markdown { - # Hello - - [foobar](https://www.google.com) -} diff --git a/src/GenerateMimeTypes.zig b/src/GenerateMimeTypes.zig index e1353e7..e908f1a 100644 --- a/src/GenerateMimeTypes.zig +++ b/src/GenerateMimeTypes.zig @@ -10,7 +10,7 @@ const JsonMimeType = struct { pub fn generateMimeModule(build: *std.Build) !*std.Build.Module { const file = try std.fs.openFileAbsolute(build.pathFromRoot("src/jetzig/http/mime/mimeData.json"), .{}); 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); const parsed_mime_types = try std.json.parseFromSlice( diff --git a/src/Routes.zig b/src/Routes.zig index 3540911..10a5b2e 100644 --- a/src/Routes.zig +++ b/src/Routes.zig @@ -296,7 +296,7 @@ const RouteSet = struct { fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !RouteSet { 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); self.ast = try std.zig.Ast.parse(self.allocator, source, .zig); diff --git a/src/jetzig/markdown.zig b/src/jetzig/markdown.zig index d02ac86..397d943 100644 --- a/src/jetzig/markdown.zig +++ b/src/jetzig/markdown.zig @@ -33,7 +33,7 @@ pub fn render( 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) { error.FileNotFound => return null, else => return err,