carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1//! The TodoMVC page, written straight to a `std.Io.Writer`.
2//!
3//! The markup follows TodoMVC's own template (shared/reference.html). Each
4//! action is a real form, so the page works with JavaScript off; the extra
5//! no-JS controls carry a `nojs-` class that static/app.css hides once the
6//! script has marked the page with `html.js`.
7
8const std = @import("std");
9const Writer = std.Io.Writer;
10const Todo = @import("db.zig").Todo;
11
12pub const Filter = enum {
13 all,
14 active,
15 completed,
16
17 pub fn fromField(value: []const u8) Filter {
18 return std.meta.stringToEnum(Filter, value) orelse .all;
19 }
20
21 pub fn fromPath(p: []const u8) ?Filter {
22 if (std.mem.eql(u8, p, "/")) return .all;
23 if (std.mem.eql(u8, p, "/active")) return .active;
24 if (std.mem.eql(u8, p, "/completed")) return .completed;
25 return null;
26 }
27
28 pub fn path(f: Filter) []const u8 {
29 return switch (f) {
30 .all => "/",
31 .active => "/active",
32 .completed => "/completed",
33 };
34 }
35
36 fn shows(f: Filter, t: Todo) bool {
37 return switch (f) {
38 .all => true,
39 .active => !t.completed,
40 .completed => t.completed,
41 };
42 }
43};
44
45pub const Page = struct {
46 todos: []const Todo,
47 filter: Filter,
48 /// The item whose editor is open, from `?edit=<id>`.
49 editing: ?i64 = null,
50};
51
52pub fn render(w: *Writer, page: Page) Writer.Error!void {
53 var completed: usize = 0;
54 for (page.todos) |t| completed += @intFromBool(t.completed);
55 const left = page.todos.len - completed;
56 const empty = page.todos.len == 0;
57 const filter = @tagName(page.filter);
58
59 try w.writeAll(
60 \\<!doctype html>
61 \\<html lang="en">
62 \\<head>
63 \\<meta charset="utf-8">
64 \\<meta name="viewport" content="width=device-width, initial-scale=1">
65 \\<title>TodoMVC</title>
66 \\<link rel="stylesheet" href="/static/base.css">
67 \\<link rel="stylesheet" href="/static/index.css">
68 \\<link rel="stylesheet" href="/static/app.css">
69 \\<script>document.documentElement.classList.add("js")</script>
70 \\<script type="module" src="/static/app.js"></script>
71 \\</head>
72 \\<body>
73 \\<section class="todoapp">
74 \\<header class="header">
75 \\<h1>todos</h1>
76 \\<form method="post" action="/todos">
77 \\
78 );
79 try hiddenFilter(w, filter);
80 try w.print(
81 \\<input class="new-todo" name="title" placeholder="What needs to be done?" autocomplete="off"{s}>
82 \\</form>
83 \\</header>
84 \\<section class="main"{s}>
85 \\<form method="post" action="/todos/toggle-all">
86 \\
87 , .{
88 if (page.editing == null) " autofocus" else "",
89 if (empty) " hidden" else "",
90 });
91 try hiddenFilter(w, filter);
92 try w.print(
93 \\<input id="toggle-all" class="toggle-all" type="checkbox"{s}>
94 \\<label for="toggle-all">Mark all as complete</label>
95 \\<button class="nojs-toggle-all" data-action="toggle-all">Mark all as complete</button>
96 \\</form>
97 \\<ul class="todo-list">
98 \\
99 , .{if (!empty and left == 0) " checked" else ""});
100
101 for (page.todos) |t| {
102 if (page.filter.shows(t)) try item(w, t, page.filter, page.editing == t.id);
103 }
104
105 try w.print(
106 \\</ul>
107 \\</section>
108 \\<footer class="footer"{s}>
109 \\<span class="todo-count"><strong>{d}</strong> {s} left</span>
110 \\<ul class="filters">
111 \\
112 , .{ if (empty) " hidden" else "", left, if (left == 1) "item" else "items" });
113 inline for (.{ .{ Filter.all, "All" }, .{ Filter.active, "Active" }, .{ Filter.completed, "Completed" } }) |link| {
114 try w.print("<li><a{s} href=\"{s}\">{s}</a></li>\n", .{
115 if (page.filter == link[0]) " class=\"selected\"" else "",
116 link[0].path(),
117 link[1],
118 });
119 }
120 try w.writeAll("</ul>\n");
121 if (completed > 0) {
122 try w.writeAll("<form method=\"post\" action=\"/todos/clear-completed\">\n");
123 try hiddenFilter(w, filter);
124 try w.writeAll("<button class=\"clear-completed\">Clear completed</button>\n</form>\n");
125 }
126 try w.writeAll(
127 \\</footer>
128 \\</section>
129 \\<footer class="info">
130 \\<p>Double-click to edit a todo</p>
131 \\<p>Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> · Zig</p>
132 \\<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
133 \\</footer>
134 \\</body>
135 \\</html>
136 \\
137 );
138}
139
140fn item(w: *Writer, t: Todo, filter: Filter, editing: bool) Writer.Error!void {
141 const f = @tagName(filter);
142 const classes: []const u8 = if (t.completed and editing)
143 " class=\"completed editing\""
144 else if (t.completed)
145 " class=\"completed\""
146 else if (editing)
147 " class=\"editing\""
148 else
149 "";
150 try w.print("<li data-id=\"{d}\"{s}>\n<div class=\"view\">\n", .{ t.id, classes });
151 try w.print("<input class=\"toggle\" type=\"checkbox\"{s}>\n<label>", .{if (t.completed) " checked" else ""});
152 try escape(w, t.title);
153 try w.print("</label>\n<form method=\"post\" action=\"/todos/{d}/toggle\">\n", .{t.id});
154 try hiddenFilter(w, f);
155 try w.print(
156 \\<button class="nojs-toggle" data-action="toggle">{s}</button>
157 \\</form>
158 \\<a class="nojs-edit" data-action="edit" href="{s}?edit={d}">Edit</a>
159 \\<form method="post" action="/todos/{d}/destroy">
160 \\
161 , .{ if (t.completed) "Mark as active" else "Mark as complete", filter.path(), t.id, t.id });
162 try hiddenFilter(w, f);
163 try w.print(
164 \\<button class="destroy" aria-label="Delete"></button>
165 \\</form>
166 \\</div>
167 \\<form method="post" action="/todos/{d}/edit">
168 \\
169 , .{t.id});
170 try hiddenFilter(w, f);
171 try w.writeAll("<input class=\"edit\" name=\"title\" value=\"");
172 try escape(w, t.title);
173 try w.print("\"{s}>\n</form>\n</li>\n", .{if (editing) " autofocus" else ""});
174}
175
176fn hiddenFilter(w: *Writer, filter: []const u8) Writer.Error!void {
177 try w.print("<input type=\"hidden\" name=\"filter\" value=\"{s}\">\n", .{filter});
178}
179
180/// Escapes text for an HTML text node or a double-quoted attribute.
181pub fn escape(w: *Writer, s: []const u8) Writer.Error!void {
182 var start: usize = 0;
183 for (s, 0..) |ch, i| {
184 const entity: []const u8 = switch (ch) {
185 '&' => "&amp;",
186 '<' => "&lt;",
187 '>' => "&gt;",
188 '"' => "&quot;",
189 '\'' => "&#39;",
190 else => continue,
191 };
192 try w.writeAll(s[start..i]);
193 try w.writeAll(entity);
194 start = i + 1;
195 }
196 try w.writeAll(s[start..]);
197}
198
199test escape {
200 var buf: [128]u8 = undefined;
201 var w: Writer = .fixed(&buf);
202 try escape(&w, "<img src=x onerror=\"alert('1')\"> & naïve 🚀");
203 try std.testing.expectEqualStrings(
204 "&lt;img src=x onerror=&quot;alert(&#39;1&#39;)&quot;&gt; &amp; naïve 🚀",
205 w.buffered(),
206 );
207}
208
209fn renderToString(gpa: std.mem.Allocator, page: Page) ![]u8 {
210 var out: Writer.Allocating = .init(gpa);
211 defer out.deinit();
212 try render(&out.writer, page);
213 return out.toOwnedSlice();
214}
215
216test "empty list hides main and footer" {
217 const html = try renderToString(std.testing.allocator, .{ .todos = &.{}, .filter = .all });
218 defer std.testing.allocator.free(html);
219 try std.testing.expect(std.mem.indexOf(u8, html, "<section class=\"main\" hidden>") != null);
220 try std.testing.expect(std.mem.indexOf(u8, html, "<footer class=\"footer\" hidden>") != null);
221 try std.testing.expect(std.mem.indexOf(u8, html, "clear-completed\"") == null);
222}
223
224test "filters, counter and editing" {
225 const todos = [_]Todo{
226 .{ .id = 1, .title = "Buy milk", .completed = true },
227 .{ .id = 2, .title = "Walk <the> dog", .completed = false },
228 };
229 const html = try renderToString(std.testing.allocator, .{ .todos = &todos, .filter = .active, .editing = 2 });
230 defer std.testing.allocator.free(html);
231 try std.testing.expect(std.mem.indexOf(u8, html, "<strong>1</strong> item left") != null);
232 try std.testing.expect(std.mem.indexOf(u8, html, "data-id=\"1\"") == null);
233 try std.testing.expect(std.mem.indexOf(u8, html, "<li data-id=\"2\" class=\"editing\">") != null);
234 try std.testing.expect(std.mem.indexOf(u8, html, "<label>Walk &lt;the&gt; dog</label>") != null);
235 try std.testing.expect(std.mem.indexOf(u8, html, "<a class=\"selected\" href=\"/active\">") != null);
236 try std.testing.expect(std.mem.indexOf(u8, html, "href=\"/active?edit=2\"") != null);
237 try std.testing.expect(std.mem.indexOf(u8, html, "<button class=\"clear-completed\">") != null);
238 try std.testing.expect(std.mem.indexOf(u8, html, "class=\"new-todo\" name=\"title\" placeholder=\"What needs to be done?\" autocomplete=\"off\">") != null);
239}
240