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 three views of a list, named as the contract's `filter` field names them.
2public enum Filter: String, Sendable, CaseIterable {
3 case all, active, completed
4
5 /// A missing or unknown value means `all`.
6 init(field: String?) {
7 self = field.flatMap(Filter.init(rawValue:)) ?? .all
8 }
9
10 var path: String {
11 switch self {
12 case .all: "/"
13 case .active: "/active"
14 case .completed: "/completed"
15 }
16 }
17
18 func includes(_ todo: Todo) -> Bool {
19 switch self {
20 case .all: true
21 case .active: !todo.completed
22 case .completed: todo.completed
23 }
24 }
25}
26
27/// Server-rendered HTML. Everything a visitor typed goes through `escape`.
28///
29/// The markup follows `shared/reference.html`. The controls marked
30/// `class="nojs"` are what a page without JavaScript uses; `app.css` lays the
31/// toggle buttons exactly over the checkboxes they stand in for, and hides
32/// every `nojs` control once the `js` class is on `<html>`.
33enum Page {
34 static let language = "Swift"
35
36 static func document(todos: [Todo], filter: Filter, editing: Int?, version: String) -> String {
37 var html = """
38 <!doctype html>
39 <html lang="en">
40 <head>
41 <meta charset="utf-8">
42 <meta name="viewport" content="width=device-width, initial-scale=1">
43 <title>TodoMVC</title>
44 <script>document.documentElement.className = "js"</script>
45 <link rel="stylesheet" href="/static/base.css">
46 <link rel="stylesheet" href="/static/index.css">
47 <link rel="stylesheet" href="/static/app.css?v=\(escape(version))">
48 <script type="module" src="/static/app.js?v=\(escape(version))"></script>
49 </head>
50 <body>
51 <section class="todoapp">
52 <header class="header">
53 <h1>todos</h1>
54 <form method="post" action="/todos">
55 \(filterField(filter))<input class="new-todo" name="title" placeholder="What needs to be done?" autocomplete="off"\(editing == nil ? " autofocus" : "")>
56 </form>
57 </header>
58
59 """
60 html += body(todos: todos, filter: filter, editing: editing)
61 html += """
62 </section>
63 <footer class="info">
64 <p>Double-click to edit a todo</p>
65 <p>Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> · \(language)</p>
66 <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
67 </footer>
68 </body>
69 </html>
70
71 """
72 return html
73 }
74
75 /// `section.main` and `footer.footer`, or nothing when the list is empty.
76 /// The page's JavaScript swaps this in after each action.
77 static func body(todos: [Todo], filter: Filter, editing: Int?) -> String {
78 guard !todos.isEmpty else { return "" }
79 let left = todos.count { !$0.completed }
80 let done = todos.count - left
81 var html = """
82 <section class="main">
83 <input id="toggle-all" class="toggle-all" type="checkbox"\(left == 0 ? " checked" : "")>
84 <label for="toggle-all">Mark all as complete</label>
85 <form class="inline" method="post" action="/todos/toggle-all">\(filterField(filter))<button class="nojs" data-action="toggle-all">Mark all as complete</button></form>
86 <ul class="todo-list">
87
88 """
89 for todo in todos where filter.includes(todo) {
90 html += item(todo, filter: filter, editing: todo.id == editing)
91 }
92 html += """
93 </ul>
94 </section>
95 <footer class="footer">
96 <span class="todo-count"><strong>\(left)</strong> \(left == 1 ? "item" : "items") left</span>
97 <ul class="filters">
98
99 """
100 for (f, name) in [(Filter.all, "All"), (.active, "Active"), (.completed, "Completed")] {
101 html += "<li><a\(f == filter ? " class=\"selected\"" : "") href=\"\(f.path)\">\(name)</a></li>\n"
102 }
103 html += "</ul>\n"
104 if done > 0 {
105 html += """
106 <form class="inline" method="post" action="/todos/clear-completed">\(filterField(filter))<button class="clear-completed">Clear completed</button></form>
107
108 """
109 }
110 html += "</footer>\n"
111 return html
112 }
113
114 static func item(_ todo: Todo, filter: Filter, editing: Bool) -> String {
115 let id = todo.id
116 let title = escape(todo.title)
117 var classes: [String] = []
118 if todo.completed { classes.append("completed") }
119 if editing { classes.append("editing") }
120 let classAttr = classes.isEmpty ? "" : " class=\"\(classes.joined(separator: " "))\""
121 let editHref = "\(filter.path)?edit=\(id)"
122 return """
123 <li\(classAttr) data-id="\(id)">
124 <form id="toggle-\(id)" method="post" action="/todos/\(id)/toggle" hidden>\(filterField(filter))</form>
125 <form id="destroy-\(id)" method="post" action="/todos/\(id)/destroy" hidden>\(filterField(filter))</form>
126 <div class="view">
127 <input class="toggle" type="checkbox"\(todo.completed ? " checked" : "") aria-label="Toggle">
128 <label>\(title)</label>
129 <button class="nojs" data-action="toggle" form="toggle-\(id)">Toggle</button>
130 <a class="nojs" data-action="edit" href="\(editHref)" title="Edit">✎</a>
131 <button class="destroy" form="destroy-\(id)" aria-label="Delete"></button>
132 </div>
133 <form method="post" action="/todos/\(id)/edit">\(filterField(filter))<input class="edit" name="title" value="\(title)" autocomplete="off"\(editing ? " autofocus" : "")></form>
134 </li>
135
136 """
137 }
138
139 static func filterField(_ filter: Filter) -> String {
140 "<input type=\"hidden\" name=\"filter\" value=\"\(filter.rawValue)\">"
141 }
142
143 /// Escapes text for use in HTML content and in double-quoted attributes.
144 static func escape(_ text: String) -> String {
145 var out = ""
146 out.reserveCapacity(text.utf8.count)
147 for c in text.unicodeScalars {
148 switch c {
149 case "&": out += "&amp;"
150 case "<": out += "&lt;"
151 case ">": out += "&gt;"
152 case "\"": out += "&quot;"
153 case "'": out += "&#39;"
154 default: out.unicodeScalars.append(c)
155 }
156 }
157 return out
158 }
159}
160