256 lines
7.8 KiB
Zig
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 = .{
|
|
.{ "&", "&" },
|
|
.{ "<", "<" },
|
|
.{ ">", ">" },
|
|
};
|
|
|
|
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);
|
|
}
|