mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Implement Markdown
Create `.md` files instead of `.zmpl` files to render static markdown content.
This commit is contained in:
parent
8d0a3f40d0
commit
0e29934718
@ -44,6 +44,8 @@ pub fn build(b: *std.Build) !void {
|
||||
// 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 });
|
||||
|
||||
const ZmplBuild = @import("zmpl").ZmplBuild;
|
||||
const ZmplTemplate = @import("zmpl").Template;
|
||||
var zmpl_build = ZmplBuild.init(b, lib, template_path);
|
||||
@ -60,6 +62,7 @@ pub fn build(b: *std.Build) !void {
|
||||
lib.root_module.addImport("zmpl", zmpl_module);
|
||||
jetzig_module.addImport("zmpl", zmpl_module);
|
||||
jetzig_module.addImport("args", zig_args_dep.module("args"));
|
||||
jetzig_module.addImport("zmd", zmd_dep.module("zmd"));
|
||||
|
||||
const main_tests = b.addTest(.{
|
||||
.root_source_file = .{ .path = "src/tests.zig" },
|
||||
@ -148,6 +151,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
exe_static_routes.root_module.addImport("routes", routes_module);
|
||||
exe_static_routes.root_module.addImport("jetzig", jetzig_module);
|
||||
exe_static_routes.root_module.addImport("zmpl", zmpl_module);
|
||||
exe_static_routes.root_module.addImport("jetzig_app", &exe.root_module);
|
||||
|
||||
const run_static_routes_cmd = b.addRunArtifact(exe_static_routes);
|
||||
exe.step.dependOn(&run_static_routes_cmd.step);
|
||||
|
@ -2,6 +2,10 @@
|
||||
.name = "jetzig",
|
||||
.version = "0.0.0",
|
||||
.dependencies = .{
|
||||
.zmd = .{
|
||||
.url = "https://github.com/jetzig-framework/zmd/archive/1a526ca1cf577789ca15ca1b71d3e943b81fb801.tar.gz",
|
||||
.hash = "1220bfc5c29bc930b5a524c210712ef65c6cde6770450899bef01164a3089e6707fa",
|
||||
},
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/9e2df115c9f17e92fb60a4d09bf55ea48d0388b0.tar.gz",
|
||||
.hash = "1220820b7f5f3e01b7dc976d32cf9ff65d44dee2642533f4b8104e19a824e802d7e1",
|
||||
|
3
demo/public/prism.css
Normal file
3
demo/public/prism.css
Normal file
@ -0,0 +1,3 @@
|
||||
/* PrismJS 1.29.0
|
||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+zig&plugins=file-highlight */
|
||||
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
10
demo/public/prism.js
Normal file
10
demo/public/prism.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,12 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="/prism.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>{zmpl.content}</main>
|
||||
<script src="/prism.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
9
demo/src/app/views/markdown.zig
Normal file
9
demo/src/app/views/markdown.zig
Normal file
@ -0,0 +1,9 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const layout = "application";
|
||||
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
_ = data;
|
||||
return request.render(.ok);
|
||||
}
|
66
demo/src/app/views/markdown/index.md
Normal file
66
demo/src/app/views/markdown/index.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Markdown Example
|
||||
|
||||

|
||||
|
||||
_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.
|
||||
|
||||
## An _ordered_ list
|
||||
|
||||
1. List item with a [link](https://ziglang.org/)
|
||||
1. List item with some **bold** and _italic_ text
|
||||
1. List item 3
|
||||
|
||||
## An _unordered_ list
|
||||
|
||||
* List item 1
|
||||
* List item 2
|
||||
* List item 3
|
||||
|
||||
## Define your own formatters in `src/main.zig`
|
||||
|
||||
```zig
|
||||
pub const jetzig_options = struct {
|
||||
pub const markdown_fragments = struct {
|
||||
pub const root = .{
|
||||
"<div class='p-5'>",
|
||||
"</div>",
|
||||
};
|
||||
pub const h1 = .{
|
||||
"<h1 class='text-2xl mb-3 font-bold'>",
|
||||
"</h1>",
|
||||
};
|
||||
pub const h2 = .{
|
||||
"<h2 class='text-xl mb-3 font-bold'>",
|
||||
"</h2>",
|
||||
};
|
||||
pub const h3 = .{
|
||||
"<h3 class='text-lg mb-3 font-bold'>",
|
||||
"</h3>",
|
||||
};
|
||||
pub const paragraph = .{
|
||||
"<p class='p-3'>",
|
||||
"</p>",
|
||||
};
|
||||
pub const code = .{
|
||||
"<span class='font-mono bg-gray-900 p-2 text-white'>",
|
||||
"</span>",
|
||||
};
|
||||
|
||||
pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 {
|
||||
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>{s}</code></pre>
|
||||
, .{try jetzig.zmd.html.escape(allocator, node.content)});
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 {
|
||||
return try std.fmt.allocPrint(allocator,
|
||||
\\<a class="underline decoration-sky-500" href="{0s}" title={1s}>{1s}</a>
|
||||
, .{ node.href.?, node.title.? });
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const jetzig = @import("jetzig");
|
||||
|
||||
pub const routes = @import("routes").routes;
|
||||
|
||||
// Override default settings in `jetzig.config` here:
|
||||
@ -30,6 +31,57 @@ pub const jetzig_options = struct {
|
||||
|
||||
// HTTP buffer. Must be large enough to store all headers. This should typically not be modified.
|
||||
// pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
|
||||
|
||||
// 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).
|
||||
pub const markdown_fragments = struct {
|
||||
pub const root = .{
|
||||
"<div class='p-5'>",
|
||||
"</div>",
|
||||
};
|
||||
pub const h1 = .{
|
||||
"<h1 class='text-2xl mb-3 font-bold'>",
|
||||
"</h1>",
|
||||
};
|
||||
pub const h2 = .{
|
||||
"<h2 class='text-xl mb-3 font-bold'>",
|
||||
"</h2>",
|
||||
};
|
||||
pub const h3 = .{
|
||||
"<h3 class='text-lg mb-3 font-bold'>",
|
||||
"</h3>",
|
||||
};
|
||||
pub const paragraph = .{
|
||||
"<p class='p-3'>",
|
||||
"</p>",
|
||||
};
|
||||
pub const code = .{
|
||||
"<span class='font-mono bg-gray-900 p-2 text-white'>",
|
||||
"</span>",
|
||||
};
|
||||
|
||||
pub const unordered_list = .{
|
||||
"<ul class='list-disc ms-8 leading-8'>",
|
||||
"</ul>",
|
||||
};
|
||||
|
||||
pub const ordered_list = .{
|
||||
"<ul class='list-decimal ms-8 leading-8'>",
|
||||
"</ul>",
|
||||
};
|
||||
|
||||
pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 {
|
||||
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>
|
||||
, .{ node.meta, node.content });
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 {
|
||||
return try std.fmt.allocPrint(allocator,
|
||||
\\<a class="underline decoration-sky-500" href="{0s}" title={1s}>{1s}</a>
|
||||
, .{ node.href.?, node.title.? });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
const routes = @import("routes").routes;
|
||||
const zmpl = @import("zmpl");
|
||||
const jetzig_options = @import("jetzig_app").jetzig_options;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
@ -101,10 +102,60 @@ 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);
|
||||
const html_content = try renderZmplTemplate(allocator, route, view) orelse
|
||||
try renderMarkdown(allocator, route, view) orelse
|
||||
null;
|
||||
const html_path = try std.mem.concat(
|
||||
allocator,
|
||||
u8,
|
||||
&[_][]const u8{ route.name, index_suffix, ".html" },
|
||||
);
|
||||
if (html_content) |content| {
|
||||
defer allocator.free(html_path);
|
||||
const html_file = try dir.createFile(html_path, .{ .truncate = true });
|
||||
try html_file.writeAll(content);
|
||||
defer html_file.close();
|
||||
allocator.free(content);
|
||||
std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
|
||||
}
|
||||
}
|
||||
|
||||
fn renderMarkdown(
|
||||
allocator: std.mem.Allocator,
|
||||
route: jetzig.views.Route,
|
||||
view: jetzig.views.View,
|
||||
) !?[]const u8 {
|
||||
const fragments = if (@hasDecl(jetzig_options, "markdown_fragments"))
|
||||
jetzig_options.markdown_fragments
|
||||
else
|
||||
null;
|
||||
const content = try jetzig.markdown.render(allocator, &route, fragments) orelse return null;
|
||||
|
||||
if (route.layout) |layout_name| {
|
||||
try view.data.addConst("jetzig_view", view.data.string(route.name));
|
||||
try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action)));
|
||||
|
||||
// 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);
|
||||
defer allocator.free(prefixed_name);
|
||||
|
||||
if (zmpl.find(prefixed_name)) |layout| {
|
||||
view.data.content = .{ .data = content };
|
||||
return try layout.render(view.data);
|
||||
} else {
|
||||
std.debug.print("Unknown layout: {s}\n", .{layout_name});
|
||||
return content;
|
||||
}
|
||||
} else return null;
|
||||
}
|
||||
|
||||
fn renderZmplTemplate(
|
||||
allocator: std.mem.Allocator,
|
||||
route: jetzig.views.Route,
|
||||
view: jetzig.views.View,
|
||||
) !?[]const u8 {
|
||||
if (zmpl.find(route.template)) |template| {
|
||||
try view.data.addConst("jetzig_view", view.data.string(route.name));
|
||||
try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action)));
|
||||
|
||||
@ -114,24 +165,13 @@ fn writeContent(
|
||||
defer allocator.free(prefixed_name);
|
||||
|
||||
if (zmpl.find(prefixed_name)) |layout| {
|
||||
content = try template.renderWithLayout(layout, view.data);
|
||||
return try template.renderWithLayout(layout, view.data);
|
||||
} else {
|
||||
std.debug.print("Unknown layout: {s}\n", .{layout_name});
|
||||
content = try allocator.dupe(u8, "");
|
||||
return try allocator.dupe(u8, "");
|
||||
}
|
||||
} else {
|
||||
content = try template.render(view.data);
|
||||
return try template.render(view.data);
|
||||
}
|
||||
|
||||
const html_path = try std.mem.concat(
|
||||
allocator,
|
||||
u8,
|
||||
&[_][]const u8{ route.name, index_suffix, ".html" },
|
||||
);
|
||||
defer allocator.free(html_path);
|
||||
const html_file = try dir.createFile(html_path, .{ .truncate = true });
|
||||
try html_file.writeAll(content);
|
||||
defer html_file.close();
|
||||
std.debug.print("[jetzig] Compiled static route: {s}\n", .{html_path});
|
||||
}
|
||||
} else return null;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const zmpl = @import("zmpl").zmpl;
|
||||
pub const zmd = @import("zmd").zmd;
|
||||
|
||||
pub const http = @import("jetzig/http.zig");
|
||||
pub const loggers = @import("jetzig/loggers.zig");
|
||||
@ -10,6 +11,7 @@ pub const colors = @import("jetzig/colors.zig");
|
||||
pub const middleware = @import("jetzig/middleware.zig");
|
||||
pub const util = @import("jetzig/util.zig");
|
||||
pub const types = @import("jetzig/types.zig");
|
||||
pub const markdown = @import("jetzig/markdown.zig");
|
||||
|
||||
/// The primary interface for a Jetzig application. Create an `App` in your application's
|
||||
/// `src/main.zig` and call `start` to launch the application.
|
||||
@ -65,6 +67,9 @@ pub const config = struct {
|
||||
/// modified.
|
||||
pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
|
||||
|
||||
/// A struct of fragments to use when rendering Markdown templates.
|
||||
pub const markdown_fragments = zmd.html.DefaultFragments;
|
||||
|
||||
/// Reconciles a configuration value from user-defined values and defaults provided by Jetzig.
|
||||
pub fn get(T: type, comptime key: []const u8) T {
|
||||
const self = @This();
|
||||
|
@ -108,11 +108,7 @@ fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
|
||||
if (isUnhandledError(err)) return err;
|
||||
|
||||
const rendered = try self.renderInternalServerError(request, err);
|
||||
|
||||
request.response.content = rendered.content;
|
||||
request.response.status_code = rendered.view.status_code;
|
||||
request.response.content_type = "text/html";
|
||||
|
||||
setResponse(request, rendered, .{});
|
||||
return;
|
||||
};
|
||||
|
||||
@ -130,6 +126,16 @@ fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn setResponse(
|
||||
request: *jetzig.http.Request,
|
||||
rendered_view: RenderedView,
|
||||
options: struct { content_type: []const u8 = "text/html" },
|
||||
) void {
|
||||
request.response.content = rendered_view.content;
|
||||
request.response.status_code = rendered_view.view.status_code;
|
||||
request.response.content_type = options.content_type;
|
||||
}
|
||||
|
||||
fn renderStatic(resource: StaticResource, response: *jetzig.http.Response) !void {
|
||||
response.status_code = .ok;
|
||||
response.content = resource.content;
|
||||
@ -146,12 +152,38 @@ fn renderHTML(
|
||||
const rendered = self.renderView(matched_route, request, template) catch |err| {
|
||||
if (isUnhandledError(err)) return err;
|
||||
const rendered_error = try self.renderInternalServerError(request, err);
|
||||
request.response.content = rendered_error.content;
|
||||
request.response.status_code = rendered_error.view.status_code;
|
||||
request.response.content_type = "text/html";
|
||||
setResponse(request, rendered_error, .{});
|
||||
return;
|
||||
};
|
||||
request.response.content = rendered.content;
|
||||
setResponse(request, rendered, .{});
|
||||
return;
|
||||
} else if (try jetzig.markdown.render(request.allocator, matched_route, null)) |markdown_content| {
|
||||
const rendered = self.renderView(matched_route, request, null) catch |err| {
|
||||
if (isUnhandledError(err)) return err;
|
||||
const rendered_error = try self.renderInternalServerError(request, err);
|
||||
setResponse(request, rendered_error, .{});
|
||||
return;
|
||||
};
|
||||
|
||||
try addTemplateConstants(rendered.view, matched_route);
|
||||
|
||||
if (request.getLayout(matched_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.manifest.find(prefixed_name)) |layout| {
|
||||
rendered.view.data.content = .{ .data = markdown_content };
|
||||
request.response.content = try layout.render(rendered.view.data);
|
||||
} else {
|
||||
try self.logger.WARN("Unknown layout: {s}", .{layout_name});
|
||||
request.response.content = markdown_content;
|
||||
}
|
||||
}
|
||||
request.response.status_code = rendered.view.status_code;
|
||||
request.response.content_type = "text/html";
|
||||
return;
|
||||
@ -234,8 +266,7 @@ fn renderTemplateWithLayout(
|
||||
view: jetzig.views.View,
|
||||
route: *jetzig.views.Route,
|
||||
) ![]const u8 {
|
||||
try view.data.addConst("jetzig_view", view.data.string(route.view_name));
|
||||
try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action)));
|
||||
try addTemplateConstants(view, route);
|
||||
|
||||
if (request.getLayout(route)) |layout_name| {
|
||||
// TODO: Allow user to configure layouts directory other than src/app/views/layouts/
|
||||
@ -251,6 +282,11 @@ fn renderTemplateWithLayout(
|
||||
} else return try template.render(view.data);
|
||||
}
|
||||
|
||||
fn addTemplateConstants(view: jetzig.views.View, route: *const jetzig.views.Route) !void {
|
||||
try view.data.addConst("jetzig_view", view.data.string(route.view_name));
|
||||
try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action)));
|
||||
}
|
||||
|
||||
fn isBadRequest(err: anyerror) bool {
|
||||
return switch (err) {
|
||||
error.JetzigBodyParseError, error.JetzigQueryParseError => true,
|
||||
|
43
src/jetzig/markdown.zig
Normal file
43
src/jetzig/markdown.zig
Normal file
@ -0,0 +1,43 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
const Zmd = @import("zmd").Zmd;
|
||||
pub fn render(
|
||||
allocator: std.mem.Allocator,
|
||||
route: *const jetzig.views.Route,
|
||||
custom_fragments: ?type,
|
||||
) !?[]const u8 {
|
||||
const fragments = custom_fragments orelse jetzig.config.get(type, "markdown_fragments");
|
||||
|
||||
var path_buf = std.ArrayList([]const u8).init(allocator);
|
||||
defer path_buf.deinit();
|
||||
|
||||
try path_buf.appendSlice(&[_][]const u8{ "src", "app", "views" });
|
||||
|
||||
var it = std.mem.splitScalar(u8, route.uri_path, '/');
|
||||
while (it.next()) |segment| {
|
||||
try path_buf.append(segment);
|
||||
}
|
||||
try path_buf.append(@tagName(route.action));
|
||||
|
||||
const base_path = try std.fs.path.join(allocator, path_buf.items);
|
||||
defer allocator.free(base_path);
|
||||
|
||||
const full_path = try std.mem.concat(allocator, u8, &[_][]const u8{ base_path, ".md" });
|
||||
defer allocator.free(full_path);
|
||||
|
||||
const stat = try std.fs.cwd().statFile(full_path);
|
||||
const markdown_content = std.fs.cwd().readFileAlloc(allocator, full_path, stat.size) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return null,
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
var zmd = Zmd.init(allocator);
|
||||
defer zmd.deinit();
|
||||
|
||||
try zmd.parse(markdown_content);
|
||||
return try zmd.toHtml(fragments);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user