Markdown
spider.zmd turns Markdown into HTML. It is part of Spider: nothing to install, nothing to turn on. This site is written with it, and so are the posts of Posts, the example app in examples/posts (source).
Text someone typed, on a page
A post's body is Markdown in the database. The handler turns it into HTML and gives it to the template:
pub fn show(c: *spider.Ctx) !Response {
const post = (try policy.load(c)) orelse return error.NotFound;
return c.view("posts/show", .{
.title = post.title,
.post = try present.card(c.arena, post),
// The same rule the routes use decides whether the buttons show.
.can_edit = try Posts.can(c, .update, &post),
// The body is Markdown. Spider's renderer escapes what the author
// typed, so its HTML can go into the page as it is.
.body = spider.RawHtml{ .html = try spider.zmd.parse(c.arena, post.body, .{}) },
}, .{});
}
A template escapes every value it prints, so HTML in a value would show as text. spider.RawHtml says that this one value is HTML already and goes in as it is. That is safe here for one reason: spider.zmd.parse escapes everything the author typed. Never wrap in RawHtml text that did not go through it.
parse(allocator, text, formatters) gives back a fragment, to put inside a page. With c.arena there is nothing to free.
What comes out
test "markdown: what an author types, and the HTML that comes out" {
const text =
\\## A heading
\\
\\Some **bold**, *italic* and `code`, and a [link](https://example.org).
\\
\\- one
\\- two
\\
\\1. first
\\2. second
\\
\\> Someone said this.
\\
\\
;
const html = try spider.zmd.parse(std.testing.allocator, text, .{});
defer std.testing.allocator.free(html);
try std.testing.expectEqualStrings(
\\<h2>A heading</h2>
\\<p>Some <b>bold</b>, <i>italic</i> and <code>code</code>, and a <a href="https://example.org">link</a>.</p>
\\<ul>
\\ <li>one</li>
\\ <li>two</li>
\\</ul>
\\<ol>
\\ <li>first</li>
\\ <li>second</li>
\\</ol>
\\<blockquote>Someone said this.</blockquote>
\\<p><img src="/img/diagram.png" title="A diagram"></p>
\\
, html);
}
A block of code gets a bar with its language, and classes to style it by:
test "markdown: a block of code keeps its language and escapes its text" {
const text =
\\
\\const smaller = 1 < 2;
\\;
const html = try spider.zmd.parse(std.testing.allocator, text, .{});
defer std.testing.allocator.free(html);
try std.testing.expectEqualStrings(
\\<div class="code-block">
\\ <div class="code-block-bar"><span class="code-block-lang">zig</span></div>
\\ <pre><code>const smaller = 1 < 2;</code></pre>
\\</div>
\\
, html);
}
What it does not read yet: tables, horizontal rules (---), strikethrough, and a link whose text is code. They come out as the text that was typed.
What an author cannot do
HTML in the text is shown, not run. A link is kept only when its address is safe to follow: one that would run a script loses its link.
test "markdown: HTML an author types is shown, not run; a link that would run a script is dropped" {
const text = "<script>alert(1)</script> and [a trap](javascript:alert(1))";
const html = try spider.zmd.parse(std.testing.allocator, text, .{});
defer std.testing.allocator.free(html);
try std.testing.expect(std.mem.indexOf(u8, html, "<script>") == null);
try std.testing.expect(std.mem.indexOf(u8, html, "<script>alert(1)</script>") != null);
try std.testing.expect(std.mem.indexOf(u8, html, "javascript:") == null);
try std.testing.expect(std.mem.indexOf(u8, html, "<a ") == null);
}
Your own HTML for an element
The third argument says how each kind of element is written. .{} is the default; name a field to replace one. A formatter gets the element (node.content is its inner HTML, already escaped) and returns its HTML:
fn titled(allocator: std.mem.Allocator, node: spider.zmd.Node) std.mem.Allocator.Error![]const u8 {
return std.fmt.allocPrint(allocator, "<h2 class=\"title\">{s}</h2>\n", .{node.content});
}
test "markdown: a formatter of the app's own writes one kind of element" {
const html = try spider.zmd.parse(std.testing.allocator, "## Prices", .{ .h2 = titled });
defer std.testing.allocator.free(html);
try std.testing.expectEqualStrings("<h2 class=\"title\">Prices</h2>\n", html);
}
This site gives its headings an id that way, so a page can be linked to at one of them:
/// Headings with an `id` made from their text: `<h3 id="ctx-json">`.
/// tools/api_reference.zig makes the same id for the links it writes.
const headings = struct {
fn h2(allocator: std.mem.Allocator, node: spider.zmd.Node) std.mem.Allocator.Error![]const u8 {
return heading(allocator, "h2", node.content);
}
fn h3(allocator: std.mem.Allocator, node: spider.zmd.Node) std.mem.Allocator.Error![]const u8 {
return heading(allocator, "h3", node.content);
}
fn h4(allocator: std.mem.Allocator, node: spider.zmd.Node) std.mem.Allocator.Error![]const u8 {
return heading(allocator, "h4", node.content);
}
fn heading(allocator: std.mem.Allocator, comptime tag: []const u8, content: []const u8) ![]const u8 {
const id = try headingId(allocator, content);
defer allocator.free(id);
return std.fmt.allocPrint(allocator, "<" ++ tag ++ " id=\"{s}\">{s}</" ++ tag ++ ">\n", .{ id, content });
}
};
A formatter that prints node.href, node.title or node.meta has to escape them itself: those are what the author typed.
A whole document
parseFull wraps the result in a complete HTML document, for a file that is opened by itself:
test "markdown: parseFull gives a whole document" {
const html = try spider.zmd.parseFull(std.testing.allocator, "# Notes", .{});
defer std.testing.allocator.free(html);
try std.testing.expect(std.mem.startsWith(u8, html, "<!DOCTYPE html>"));
try std.testing.expect(std.mem.indexOf(u8, html, "<h1>Notes</h1>") != null);
}