2025-05-06 21:11:52 -05:00

256 lines
7.8 KiB
Zig

const std = @import("std");
const jetzig = @import("jetzig");
const Zmd = @import("zmd").Zmd;
const tokens = @import("zmd").tokens;
const Node = @import("zmd").Node;
pub const layout = "panel";
/// Default fragments. Pass this to `Zmd.toHtml` or provide your own.
/// Formatters can be functions receiving an allocator, the current node, and the rendered
/// content, or a 2-element tuple containing the open and close for each node.
pub const Fragments = struct {
pub fn root(allocator: std.mem.Allocator, node: Node) ![]const u8 {
return try std.fmt.allocPrint(allocator, "{s}", .{node.content});
}
pub fn block(allocator: std.mem.Allocator, node: Node) ![]const u8 {
const style = "font-family: Monospace;";
return if (node.meta) |meta|
std.fmt.allocPrint(allocator,
\\<pre class="language-{s}" style="{s}"><code>{s}</code></pre>
, .{ meta, style, node.content })
else
std.fmt.allocPrint(allocator,
\\<pre style="{s}"><code>{s}</code></pre>
, .{ style, node.content });
}
pub fn link(allocator: std.mem.Allocator, node: Node) ![]const u8 {
return std.fmt.allocPrint(allocator,
\\<a href="{s}">{s}</a>
, .{ node.href.?, node.title.? });
}
pub fn image(allocator: std.mem.Allocator, node: Node) ![]const u8 {
return std.fmt.allocPrint(allocator,
\\<img src="{s}" title="{s}" />
, .{ node.href.?, node.title.? });
}
pub const h1 = .{ "<h1>", "</h1>\n" };
pub const h2 = .{ "<h2>", "</h2>\n" };
pub const h3 = .{ "<h3>", "</h3>\n" };
pub const h4 = .{ "<h4>", "</h4>\n" };
pub const h5 = .{ "<h5>", "</h5>\n" };
pub const h6 = .{ "<h6>", "</h6>\n" };
pub const bold = .{ "<b>", "</b>" };
pub const italic = .{ "<i>", "</i>" };
pub const unordered_list = .{ "<ul>", "</ul>" };
pub const ordered_list = .{ "<ol>", "</ol>" };
pub const list_item = .{ "<li>", "</li>" };
pub const code = .{ "<span style=\"font-family: Monospace\">", "</span>" };
pub const paragraph = .{ "\n<p>", "</p>\n" };
pub const default = .{ "", "" };
};
/// Escape HTML entities.
pub fn escape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
const replacements = .{
.{ "&", "&amp;" },
.{ "<", "&lt;" },
.{ ">", "&gt;" },
};
var output = input;
inline for (replacements) |replacement| {
output = try std.mem.replaceOwned(u8, allocator, output, replacement[0], replacement[1]);
}
return output;
}
pub fn index(request: *jetzig.Request) !jetzig.View {
const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc});
const blogs = try request.repo.all(query);
var root = try request.data(.object);
try root.put("blogs", blogs);
const cookies = try request.cookies();
const allowed = blk: {
const session = cookies.get("session") orelse break :blk false;
const session_query = jetzig.database.Query(.Session)
.findBy(.{ .session_id = session.value });
_ = request.repo.execute(session_query) catch break :blk false;
break :blk true;
};
try root.put("allowed", allowed);
return request.render(.ok);
}
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
const query = jetzig.database.Query(.Blog).find(id);
const blog = try request.repo.execute(query) orelse return request.fail(.not_found);
const Blog = struct {
id: i32,
title: []const u8,
content: ?[]const u8,
created_at: jetzig.jetquery.DateTime,
updated_at: jetzig.jetquery.DateTime,
};
var zmd = Zmd.init(request.allocator);
defer zmd.deinit();
try zmd.parse(blog.content orelse {
return request.fail(.unprocessable_entity);
});
const blob = try zmd.toHtml(Fragments);
const eb = Blog{
.id = blog.id,
.title = blog.title,
.content = blob,
.created_at = blog.created_at,
.updated_at = blog.updated_at,
};
var root = try request.data(.object);
try root.put("blog", eb);
const cookies = try request.cookies();
const allowed = blk: {
const session = cookies.get("session") orelse break :blk false;
const session_query = jetzig.database.Query(.Session)
.findBy(.{ .session_id = session.value });
_ = request.repo.execute(session_query) catch break :blk false;
break :blk true;
};
try root.put("allowed", allowed);
try request.process();
return request.render(.ok);
}
pub fn new(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
const cookies = try request.cookies();
const allowed = blk: {
const session = cookies.get("session") orelse break :blk false;
const session_query = jetzig.database.Query(.Session)
.findBy(.{ .session_id = session.value });
_ = request.repo.execute(session_query) catch break :blk false;
break :blk true;
};
const root = try data.object();
try root.put("allowed", allowed);
return request.render(.ok);
}
pub fn post(request: *jetzig.Request) !jetzig.View {
// check "session" cookie for authentication
const cookies = try request.cookies();
const session = cookies.get("session") orelse {
return request.fail(.not_found);
};
// check if session is valid
const query = jetzig.database.Query(.Session)
.findBy(.{ .session_id = session.value });
if (try request.repo.execute(query)) |_| {
const params = try request.params();
const title = params.getT(.string, "title") orelse {
return request.fail(.unprocessable_entity);
};
const content = params.getT(.string, "content") orelse {
return request.fail(.unprocessable_entity);
};
const preview = params.getT(.string, "preview") orelse {
return request.fail(.unprocessable_entity);
};
try request.repo.insert(.Blog, .{
.title = title,
.blob = preview,
.content = content,
});
return request.redirect("/blogs", .moved_permanently);
} else {
return request.fail(.not_found);
}
}
pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
// check "session" cookie for authentication
const cookies = try request.cookies();
const session = cookies.get("session") orelse {
return request.fail(.not_found);
};
// check if session is valid
const query = jetzig.database.Query(.Session)
.findBy(.{ .session_id = session.value });
if (try request.repo.execute(query)) |_| {
const delete_query = jetzig.database.Query(.Blog)
.find(try std.fmt.parseInt(i32, id, 10));
if (try request.repo.execute(delete_query)) |blog_post| {
try request.repo.delete(blog_post);
}
return request.redirect("/blogs", .moved_permanently);
} else {
return request.fail(.not_found);
}
}
test "index" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/blogs", .{});
try response.expectStatus(.ok);
}
test "get" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/blogs/example-id", .{});
try response.expectStatus(.ok);
}
test "new" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/blogs/new", .{});
try response.expectStatus(.ok);
}
test "post" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.POST, "/blogs", .{});
try response.expectStatus(.created);
}