//! The TodoMVC page, written straight to a `std.Io.Writer`. //! //! The markup follows TodoMVC's own template (shared/reference.html). Each //! action is a real form, so the page works with JavaScript off; the extra //! no-JS controls carry a `nojs-` class that static/app.css hides once the //! script has marked the page with `html.js`. const std = @import("std"); const Writer = std.Io.Writer; const Todo = @import("db.zig").Todo; pub const Filter = enum { all, active, completed, pub fn fromField(value: []const u8) Filter { return std.meta.stringToEnum(Filter, value) orelse .all; } pub fn fromPath(p: []const u8) ?Filter { if (std.mem.eql(u8, p, "/")) return .all; if (std.mem.eql(u8, p, "/active")) return .active; if (std.mem.eql(u8, p, "/completed")) return .completed; return null; } pub fn path(f: Filter) []const u8 { return switch (f) { .all => "/", .active => "/active", .completed => "/completed", }; } fn shows(f: Filter, t: Todo) bool { return switch (f) { .all => true, .active => !t.completed, .completed => t.completed, }; } }; pub const Page = struct { todos: []const Todo, filter: Filter, /// The item whose editor is open, from `?edit=`. editing: ?i64 = null, }; pub fn render(w: *Writer, page: Page) Writer.Error!void { var completed: usize = 0; for (page.todos) |t| completed += @intFromBool(t.completed); const left = page.todos.len - completed; const empty = page.todos.len == 0; const filter = @tagName(page.filter); try w.writeAll( \\ \\ \\ \\ \\ \\TodoMVC \\ \\ \\ \\ \\ \\ \\ \\
\\
\\

todos

\\
\\ ); try hiddenFilter(w, filter); try w.print( \\ \\
\\
\\
\\
\\ , .{ if (page.editing == null) " autofocus" else "", if (empty) " hidden" else "", }); try hiddenFilter(w, filter); try w.print( \\ \\ \\ \\
\\
    \\ , .{if (!empty and left == 0) " checked" else ""}); for (page.todos) |t| { if (page.filter.shows(t)) try item(w, t, page.filter, page.editing == t.id); } try w.print( \\
\\
\\
\\{d} {s} left \\
    \\ , .{ if (empty) " hidden" else "", left, if (left == 1) "item" else "items" }); inline for (.{ .{ Filter.all, "All" }, .{ Filter.active, "Active" }, .{ Filter.completed, "Completed" } }) |link| { try w.print("
  • {s}
  • \n", .{ if (page.filter == link[0]) " class=\"selected\"" else "", link[0].path(), link[1], }); } try w.writeAll("
\n"); if (completed > 0) { try w.writeAll("
\n"); try hiddenFilter(w, filter); try w.writeAll("\n
\n"); } try w.writeAll( \\
\\
\\ \\ \\ \\ ); } fn item(w: *Writer, t: Todo, filter: Filter, editing: bool) Writer.Error!void { const f = @tagName(filter); const classes: []const u8 = if (t.completed and editing) " class=\"completed editing\"" else if (t.completed) " class=\"completed\"" else if (editing) " class=\"editing\"" else ""; try w.print("
  • \n
    \n", .{ t.id, classes }); try w.print("\n\n
    \n", .{t.id}); try hiddenFilter(w, f); try w.print( \\ \\
    \\Edit \\
    \\ , .{ if (t.completed) "Mark as active" else "Mark as complete", filter.path(), t.id, t.id }); try hiddenFilter(w, f); try w.print( \\ \\
    \\
    \\
    \\ , .{t.id}); try hiddenFilter(w, f); try w.writeAll("\n
    \n
  • \n", .{if (editing) " autofocus" else ""}); } fn hiddenFilter(w: *Writer, filter: []const u8) Writer.Error!void { try w.print("\n", .{filter}); } /// Escapes text for an HTML text node or a double-quoted attribute. pub fn escape(w: *Writer, s: []const u8) Writer.Error!void { var start: usize = 0; for (s, 0..) |ch, i| { const entity: []const u8 = switch (ch) { '&' => "&", '<' => "<", '>' => ">", '"' => """, '\'' => "'", else => continue, }; try w.writeAll(s[start..i]); try w.writeAll(entity); start = i + 1; } try w.writeAll(s[start..]); } test escape { var buf: [128]u8 = undefined; var w: Writer = .fixed(&buf); try escape(&w, " & naïve 🚀"); try std.testing.expectEqualStrings( "<img src=x onerror="alert('1')"> & naïve 🚀", w.buffered(), ); } fn renderToString(gpa: std.mem.Allocator, page: Page) ![]u8 { var out: Writer.Allocating = .init(gpa); defer out.deinit(); try render(&out.writer, page); return out.toOwnedSlice(); } test "empty list hides main and footer" { const html = try renderToString(std.testing.allocator, .{ .todos = &.{}, .filter = .all }); defer std.testing.allocator.free(html); try std.testing.expect(std.mem.indexOf(u8, html, "