authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-08-14 20:37:51-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:46-07:00
log89a4f02a8f324f23275dd26bb4a23988ca07bc59
treee7fea7ba47a8f19e8dbd87a4b519f7e0660d81ab
parent952e415c67491ff213182c226cf0a4efa870b928
signature Commit is signed but in an unrecognized format.

chore: convert all files to unix-style line ends


10 files changed, 574 insertions(+), 574 deletions(-)

framework/test-fixture/Component.marko+4-4
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1<div meow=null /> 1<div meow=null />
2<div> 2<div>
3 wait(${null}) 3 wait(${null})
4</div> 4</div>
framework/test-fixture/UseComponent.marko+6-6
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import Component from './Component.marko'; 1import Component from './Component.marko';
2 2
3<h1>web page</h1> 3<h1>web page</h1>
4<if=!false> 4<if=!false>
5 <Component=null/> 5 <Component=null/>
6</> 6</>
src/blog/pages/webdev/marko-intro.markodown+339-339
...@@ -1,339 +1,339 @@...@@ -1,339 +1,339 @@
1export const blog: BlogMeta = { 1export const blog: BlogMeta = {
2 title: "Marko is the coziest HTML templating language", 2 title: "Marko is the coziest HTML templating language",
3 desc: "...todo...", 3 desc: "...todo...",
4 created: "2025-06-13", 4 created: "2025-06-13",
5 draft: true, 5 draft: true,
6}; 6};
7export const meta = formatBlogMeta(blob); 7export const meta = formatBlogMeta(blob);
8export * as layout from "@/blog/layout.tsx"; 8export * as layout from "@/blog/layout.tsx";
9 9
10I've been recently playing around [Marko], and after adding limited support 10I've been recently playing around [Marko], and after adding limited support
11for it in my website generator, [sitegen], I instantly fell in love with how 11for it in my website generator, [sitegen], I instantly fell in love with how
12minimalistic it is in comparison to JSX, Astro components, and Svelte. 12minimalistic it is in comparison to JSX, Astro components, and Svelte.
13 13
14[Marko]: https://next.markojs.com 14[Marko]: https://next.markojs.com
15[sitegen]: https://paperclover.dev/clo/sitegen 15[sitegen]: https://paperclover.dev/clo/sitegen
16 16
17## Introduction to Marko 17## Introduction to Marko
18 18
19If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving 19If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving
20JavaScript into HTML. Attributes are JavaScript expressions. 20JavaScript into HTML. Attributes are JavaScript expressions.
21 21
22```marko 22```marko
23<div> 23<div>
24 // `input` is like props, but given in the top-level scope 24 // `input` is like props, but given in the top-level scope
25 <time datetime=input.date.toISOString()> 25 <time datetime=input.date.toISOString()>
26 // Interpolation with JS template string syntax 26 // Interpolation with JS template string syntax
27 ${formatTimeNicely(input.date)} 27 ${formatTimeNicely(input.date)}
28 </time> 28 </time>
29 <div> 29 <div>
30 <a href=`/users/${input.user.id}`>${input.user.name}</a> 30 <a href=`/users/${input.user.id}`>${input.user.name}</a>
31 </div> 31 </div>
32 32
33 // Capital letter variables for imported components 33 // Capital letter variables for imported components
34 <MarkdownContent message=input.message /> 34 <MarkdownContent message=input.message />
35 35
36 // Components also can be auto-imported by lowercase. 36 // Components also can be auto-imported by lowercase.
37 // This will look upwards for a `tags/` folder containing 37 // This will look upwards for a `tags/` folder containing
38 // "custom-footer.marko", similar to how Node.js finds 38 // "custom-footer.marko", similar to how Node.js finds
39 // package names in all upwards `node_modules` folders. 39 // package names in all upwards `node_modules` folders.
40 <custom-footer /> 40 <custom-footer />
41</div> 41</div>
42 42
43// ESM `import` / `export` just work as expected. 43// ESM `import` / `export` just work as expected.
44// I prefer my imports at the end, to highlight the markup. 44// I prefer my imports at the end, to highlight the markup.
45import MarkdownContent from "./MarkdownContent.marko"; 45import MarkdownContent from "./MarkdownContent.marko";
46import { formatTimeNicely } from "../date-helpers.ts"; 46import { formatTimeNicely } from "../date-helpers.ts";
47``` 47```
48 48
49Tags with the `value` attribute have a shorthand, which is used by the built-in 49Tags with the `value` attribute have a shorthand, which is used by the built-in
50`<if>` for conditional rendering. 50`<if>` for conditional rendering.
51 51
52```marko 52```marko
53// Sugar for <input value="string" /> 53// Sugar for <input value="string" />
54<input="string" /> 54<input="string" />
55 55
56// and it composes amazingly to the 'if' built-in 56// and it composes amazingly to the 'if' built-in
57<if=input.user> 57<if=input.user>
58 <UserProfile=input.user /> 58 <UserProfile=input.user />
59</if> 59</if>
60``` 60```
61 61
62Tags can also return values into the scope for use in the template using `/`, such as `<id>` for unique ID generation. This is available to components that `<return=output/>`. 62Tags can also return values into the scope for use in the template using `/`, such as `<id>` for unique ID generation. This is available to components that `<return=output/>`.
63 63
64``` 64```
65<id/uniqueId /> 65<id/uniqueId />
66 66
67<input id=uniqueId type="checkbox" name="allow_trans_rights" /> 67<input id=uniqueId type="checkbox" name="allow_trans_rights" />
68<label for=uniqueId>click me!</> 68<label for=uniqueId>click me!</>
69// ^ oh, you can also omit the 69// ^ oh, you can also omit the
70// closing tag name if you want. 70// closing tag name if you want.
71``` 71```
72 72
73It's important that I started with the two forms of "Tag I/O": `=` for input 73It's important that I started with the two forms of "Tag I/O": `=` for input
74and `/` for output. With those building blocks, we introduce local variables 74and `/` for output. With those building blocks, we introduce local variables
75with `const` 75with `const`
76 76
77``` 77```
78<const/rendered = markdownToHtml(input.value) /> 78<const/rendered = markdownToHtml(input.value) />
79 79
80// This is how you insert raw HTML to the document 80// This is how you insert raw HTML to the document
81<inline-html=rendered /> 81<inline-html=rendered />
82 82
83// It supports all of the cozy destructuring syntax JS has 83// It supports all of the cozy destructuring syntax JS has
84<const/{ id, name } = user /> 84<const/{ id, name } = user />
85``` 85```
86 86
87Unlike JSX, when you pass content within a tag (`input.content` instead of 87Unlike JSX, when you pass content within a tag (`input.content` instead of
88JSX's `children`), instead of it being a JSX element, it is actually a 88JSX's `children`), instead of it being a JSX element, it is actually a
89function. This means that the `for` tag can render the content multiple times. 89function. This means that the `for` tag can render the content multiple times.
90 90
91``` 91```
92<ul> 92<ul>
93 <for from=1 to=10> 93 <for from=1 to=10>
94 // Renders a new random number for each iteration. 94 // Renders a new random number for each iteration.
95 <li>${Math.random()}</li> 95 <li>${Math.random()}</li>
96 </> 96 </>
97</ul> 97</ul>
98``` 98```
99 99
100Since `content` is a function, it can take arguments. This is done with `|` 100Since `content` is a function, it can take arguments. This is done with `|`
101 101
102``` 102```
103<h1>my friends</h1> 103<h1>my friends</h1>
104<ul> 104<ul>
105 // I tend to omit the closing tag names for the built-in control 105 // I tend to omit the closing tag names for the built-in control
106 // flow tags, but I keep them for HTML tags. It's kinda like how 106 // flow tags, but I keep them for HTML tags. It's kinda like how
107 // in JavaScript you just write `}` to close your `if`s and loops. 107 // in JavaScript you just write `}` to close your `if`s and loops.
108 // 108 //
109 // Anyways <for> also has 'of' 109 // Anyways <for> also has 'of'
110 <for|item| of=user.friends> 110 <for|item| of=user.friends>
111 <li class="friend">${item.name}</li> 111 <li class="friend">${item.name}</li>
112 </> 112 </>
113 113
114 // They support the same syntax JavaScript function params allows, 114 // They support the same syntax JavaScript function params allows,
115 // so you can have destructuring here too, and multiple params. 115 // so you can have destructuring here too, and multiple params.
116 <for|{ name }, index| of=user.friends> 116 <for|{ name }, index| of=user.friends>
117 // By the way you can also use emmet-style class and ID shorthands. 117 // By the way you can also use emmet-style class and ID shorthands.
118 <li.friend>My #${index + 1} friend is ${name}</li> 118 <li.friend>My #${index + 1} friend is ${name}</li>
119 </> 119 </>
120</ul> 120</ul>
121``` 121```
122 122
123Instead of named slots, Marko has attribute tags. These are more powerful than 123Instead of named slots, Marko has attribute tags. These are more powerful than
124slots since they are functions, and can also act as sugar for more complicated 124slots since they are functions, and can also act as sugar for more complicated
125attributes. 125attributes.
126 126
127``` 127```
128<Layout title="Welcome"> 128<Layout title="Welcome">
129 <@header variant="big"> 129 <@header variant="big">
130 <h1>the next big thing</h1> 130 <h1>the next big thing</h1>
131 </@header> 131 </@header>
132 132
133 <p>body text...</p> 133 <p>body text...</p>
134</Layout> 134</Layout>
135 135
136// The `input` variable inside of <Layout /> is: 136// The `input` variable inside of <Layout /> is:
137// 137//
138// { 138// {
139// title: "Welcome", 139// title: "Welcome",
140// header: { 140// header: {
141// content: /* function rendering "<h1>the next big thing</h1>" */, 141// content: /* function rendering "<h1>the next big thing</h1>" */,
142// variant: "big", 142// variant: "big",
143// }, 143// },
144// content: /* function rendering "<p>body text</p>" */ 144// content: /* function rendering "<p>body text</p>" */
145// } 145// }
146``` 146```
147 147
148This layout could be implemented as such: 148This layout could be implemented as such:
149 149
150```marko 150```marko
151<main> 151<main>
152 <if=input.header /> 152 <if=input.header />
153 <const/{ ...headerProps, content }=input.header /> 153 <const/{ ...headerProps, content }=input.header />
154 <header ...headerProps> 154 <header ...headerProps>
155 // Instead of assigning to a variable with a capital letter, 155 // Instead of assigning to a variable with a capital letter,
156 // template interpolation works on tag names. This can also 156 // template interpolation works on tag names. This can also
157 // be a string to render the native HTML tag of that kind. 157 // be a string to render the native HTML tag of that kind.
158 <${content} /> 158 <${content} />
159 </header> 159 </header>
160 <hr /> 160 <hr />
161 </> 161 </>
162 162
163 <${input.content} /> 163 <${input.content} />
164</main> 164</main>
165``` 165```
166 166
167The last syntax feature missing is calling a tag with parameters. That is done 167The last syntax feature missing is calling a tag with parameters. That is done
168just like a regular function call, with '('. 168just like a regular function call, with '('.
169 169
170``` 170```
171<Something(item, index) /> 171<Something(item, index) />
172``` 172```
173 173
174In fact, attributes can just be sugar over this syntax. (this technically isn't 174In fact, attributes can just be sugar over this syntax. (this technically isn't
175true but it's close enough for the example) 175true but it's close enough for the example)
176 176
177``` 177```
178<SpecialButton type="submit" class="red" /> 178<SpecialButton type="submit" class="red" />
179 179
180// is equal to 180// is equal to
181 181
182<SpecialButton({ type: "submit", class: "red" }) /> 182<SpecialButton({ type: "submit", class: "red" }) />
183``` 183```
184 184
185All of the above is about how Marko's syntax works, and how it performs HTML 185All of the above is about how Marko's syntax works, and how it performs HTML
186generation with components. Marko also allows interactive components, but an 186generation with components. Marko also allows interactive components, but an
187explaination of that is beyond the scope of this page, mostly since I have not 187explaination of that is beyond the scope of this page, mostly since I have not
188used it. A brief example of it, modified from their documentation. 188used it. A brief example of it, modified from their documentation.
189 189
190```marko 190```marko
191// Reactive variables with <let/> just work... 191// Reactive variables with <let/> just work...
192<let/basicCounter=0 /> 192<let/basicCounter=0 />
193<button onClick() { basicCounter += 1 }>${basicCounter}</button> 193<button onClick() { basicCounter += 1 }>${basicCounter}</button>
194// ...but a counter is boring. 194// ...but a counter is boring.
195 195
196<let/todos=[ 196<let/todos=[
197 { id: 0, text: "Learn Marko" }, 197 { id: 0, text: "Learn Marko" },
198 { id: 1, text: "Make a Website" }, 198 { id: 1, text: "Make a Website" },
199]/> 199]/>
200 200
201// 'by' is like React JSX's "key" property, but it's optional. 201// 'by' is like React JSX's "key" property, but it's optional.
202<ul><for|todo, i| of=todos by=(todo => todo.id)> 202<ul><for|todo, i| of=todos by=(todo => todo.id)>
203 <li.todo> 203 <li.todo>
204 // this variable remains stable even if the list 204 // this variable remains stable even if the list
205 // re-orders, because 'by' was specified. 205 // re-orders, because 'by' was specified.
206 <let/done=false/> 206 <let/done=false/>
207 <label> 207 <label>
208 <span>${todo.text}</span> 208 <span>${todo.text}</span>
209 // ':=' creates a two-way reactive binding, 209 // ':=' creates a two-way reactive binding,
210 // (it passes a callback for `checkedChanged`) 210 // (it passes a callback for `checkedChanged`)
211 <input type="checkbox" checked:=done /> 211 <input type="checkbox" checked:=done />
212 </label> 212 </label>
213 <button 213 <button
214 title="delete" 214 title="delete"
215 disabled=!done 215 disabled=!done
216 onClick() { 216 onClick() {
217 todos = todos.toSpliced(i, 1); 217 todos = todos.toSpliced(i, 1);
218 } 218 }
219 > &times; </button> 219 > &times; </button>
220 </li> 220 </li>
221</></ul> 221</></ul>
222 222
223// Form example 223// Form example
224<let/nextId=2/> 224<let/nextId=2/>
225<form onSubmit(e) { 225<form onSubmit(e) {
226 e.preventDefault(); 226 e.preventDefault();
227 todos = todos.concat({ 227 todos = todos.concat({
228 id: nextId++, 228 id: nextId++,
229 // HTMLFormElement exposes all its named input 229 // HTMLFormElement exposes all its named input
230 // elements as extra properties on the object. 230 // elements as extra properties on the object.
231 text: e.target.text.value, 231 text: e.target.text.value,
232 }); 232 });
233 // And you can clear it with 'reset()' 233 // And you can clear it with 'reset()'
234 e.target.reset(); 234 e.target.reset();
235}> 235}>
236 // We don't 'onChange' like a React loser. The form 236 // We don't 'onChange' like a React loser. The form
237 // value can be read in the submit event like normal. 237 // value can be read in the submit event like normal.
238 <input name="text" placeholder="Another Item"> 238 <input name="text" placeholder="Another Item">
239 <button type="submit">Add</button> 239 <button type="submit">Add</button>
240</form> 240</form>
241``` 241```
242 242
243<SectionHeader updated="2025-08-11">Usage on `paperclover.net`</Section> 243<SectionHeader updated="2025-08-11">Usage on `paperclover.net`</Section>
244 244
245Using Marko for HTML generation is quite easy. `.marko` files can be compiled 245Using Marko for HTML generation is quite easy. `.marko` files can be compiled
246into `.js` using the `@marko/compiler` library. 246into `.js` using the `@marko/compiler` library.
247 247
248```ts 248```ts
249const src = fs.readFileSync("page.marko", "utf8"); 249const src = fs.readFileSync("page.marko", "utf8");
250const compile = marko.compileSync(src, filepath); 250const compile = marko.compileSync(src, filepath);
251fs.writeFileSync("page.js", compile.code); 251fs.writeFileSync("page.js", compile.code);
252 252
253const page = require("./page.js"); 253const page = require("./page.js");
254console.info(page); 254console.info(page);
255 255
256import * as fs from "node:fs"; 256import * as fs from "node:fs";
257import * as marko from "@marko/compiler"; 257import * as marko from "@marko/compiler";
258``` 258```
259 259
260To get client side JavaScript, an option can be passed to the Marko compiler to 260To get client side JavaScript, an option can be passed to the Marko compiler to
261generate the client side code. While it is a big selling point of Marko, I do 261generate the client side code. While it is a big selling point of Marko, I do
262not use any of their client side features, instead deferring to manually-written 262not use any of their client side features, instead deferring to manually-written
263frontend scripts. This is because that is how my website has been for years, 263frontend scripts. This is because that is how my website has been for years,
264statically generated. And for websites like mine that are content focused, this 264statically generated. And for websites like mine that are content focused, this
265is the correct way to do things. 265is the correct way to do things.
266 266
267Since I have a custom HTML generation library (built on JSX and some React-like 267Since I have a custom HTML generation library (built on JSX and some React-like
268patterns), I have written a simple integration for it to utilize Marko 268patterns), I have written a simple integration for it to utilize Marko
269components, which is loaded by replacing the generated import to `marko/html`, 269components, which is loaded by replacing the generated import to `marko/html`,
270which lets me overwrite functions like `createTemplate` (to change the signature 270which lets me overwrite functions like `createTemplate` (to change the signature
271of a component), `dynamicTag` (to allow Marko to render non-Marko components), 271of a component), `dynamicTag` (to allow Marko to render non-Marko components),
272and `fork` (to enable async integration with the rendering framework). An 272and `fork` (to enable async integration with the rendering framework). An
273additional feature of this is I have a Node.js loader hook to allow importing 273additional feature of this is I have a Node.js loader hook to allow importing
274these files directly. 274these files directly.
275 275
276```tsx 276```tsx
277function Page() { 277function Page() {
278 const q = Question.getByDate(new Date("2025-06-07 12:12 EST")); 278 const q = Question.getByDate(new Date("2025-06-07 12:12 EST"));
279 return <div> 279 return <div>
280 <h1>example question</h1> 280 <h1>example question</h1>
281 <QuestionRender question={q} /> 281 <QuestionRender question={q} />
282 </div>; 282 </div>;
283} 283}
284 284
285// The synchronous render can be used because `Page` and `question.marko` 285// The synchronous render can be used because `Page` and `question.marko`
286// do not await any promises (SQLite runs synchronously) 286// do not await any promises (SQLite runs synchronously)
287console.info(render.sync(<Page />).text); 287console.info(render.sync(<Page />).text);
288 288
289import * as render from "#engine/render"; 289import * as render from "#engine/render";
290import QuestionRender from "@/q+a/tags/question.marko"; 290import QuestionRender from "@/q+a/tags/question.marko";
291import { Question } from "@/q+a/models/Question.ts"; 291import { Question } from "@/q+a/models/Question.ts";
292``` 292```
293 293
294Here is the `question.marko` tag used to render [questions on the clover q+a](/q+a). 294Here is the `question.marko` tag used to render [questions on the clover q+a](/q+a).
295 295
296```marko 296```marko
297// Renders a `Question` entry including its markdown body. 297// Renders a `Question` entry including its markdown body.
298export interface Input { 298export interface Input {
299 question: Question; 299 question: Question;
300 admin?: boolean; 300 admin?: boolean;
301} 301}
302 302
303// 2024-12-31 05:00:00 EST 303// 2024-12-31 05:00:00 EST
304export const transitionDate = 1735639200000; 304export const transitionDate = 1735639200000;
305 305
306<const/{ question, admin } = input /> 306<const/{ question, admin } = input />
307<const/{ id, date, text } = question/> 307<const/{ id, date, text } = question/>
308 308
309<${"e-"} 309<${"e-"}
310 f=(date > transitionDate ? true : undefined) 310 f=(date > transitionDate ? true : undefined)
311 id=admin ? `q${id}` : undefined 311 id=admin ? `q${id}` : undefined
312> 312>
313 <if=admin> 313 <if=admin>
314 <a 314 <a
315 style="margin-right: 0.5rem" 315 style="margin-right: 0.5rem"
316 href=`/admin/q+a/${id}` 316 href=`/admin/q+a/${id}`
317 >[EDIT]</a> 317 >[EDIT]</a>
318 </> 318 </>
319 <a> 319 <a>
320 <time 320 <time
321 datetime=formatQuestionISOTimestamp(date) 321 datetime=formatQuestionISOTimestamp(date)
322 >${formatQuestionTimestamp(date)}</time> 322 >${formatQuestionTimestamp(date)}</time>
323 </a> 323 </a>
324 324
325 <CloverMarkdown ...{ text } /> 325 <CloverMarkdown ...{ text } />
326</> 326</>
327 327
328// this singleton script will make all the '<time>' tags clickable. 328// this singleton script will make all the '<time>' tags clickable.
329client import "./clickable-links.client.ts"; 329client import "./clickable-links.client.ts";
330 330
331import type { Question } from "@/q+a/models/Question.ts"; 331import type { Question } from "@/q+a/models/Question.ts";
332import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts"; 332import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
333import { CloverMarkdown } from "@/q+a/clover-markdown.tsx"; 333import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
334``` 334```
335 335
336The integration is great, `client import` is quite a magical concept, and I've 336The integration is great, `client import` is quite a magical concept, and I've
337tuned it to do the expected thing in my framework. 337tuned it to do the expected thing in my framework.
338 338
339import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts'; 339import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts';
src/file-viewer/views/lofi.marko+71-71
...@@ -1,71 +1,71 @@...@@ -1,71 +1,71 @@
1import "./lofi.css"; 1import "./lofi.css";
2export interface Input { 2export interface Input {
3 file: MediaFile; 3 file: MediaFile;
4 hasCotyledonCookie: boolean; 4 hasCotyledonCookie: boolean;
5} 5}
6export { meta, theme } from "./clofi.tsx"; 6export { meta, theme } from "./clofi.tsx";
7 7
8<const/{ file: dir, hasCotyledonCookie } = input /> 8<const/{ file: dir, hasCotyledonCookie } = input />
9<const/{ path: fullPath, dirname, basename, kind } = dir /> 9<const/{ path: fullPath, dirname, basename, kind } = dir />
10<const/isRoot = fullPath == '/'> 10<const/isRoot = fullPath == '/'>
11 11
12<div#lofi> 12<div#lofi>
13 13
14<define/ListItem|{ value: file }|> 14<define/ListItem|{ value: file }|>
15 <const/dir=file.kind === MediaFileKind.directory/> 15 <const/dir=file.kind === MediaFileKind.directory/>
16 <const/meta=( 16 <const/meta=(
17 dir 17 dir
18 ? formatSize(file.size) 18 ? formatSize(file.size)
19 : (file.duration ?? 0) > 0 19 : (file.duration ?? 0) > 0
20 ? formatDuration(file.duration!) 20 ? formatDuration(file.duration!)
21 : null 21 : null
22 )/> 22 )/>
23 <li class={ dir }> 23 <li class={ dir }>
24 <a href=`/file${escapeUri(file.path)}` > 24 <a href=`/file${escapeUri(file.path)}` >
25 <code>${formatDate(file.date)}</>${" "} 25 <code>${formatDate(file.date)}</>${" "}
26 ${file.basenameWithoutExt}<span class="ext">${file.extension}</>${dir ? '/' : ''} 26 ${file.basenameWithoutExt}<span class="ext">${file.extension}</>${dir ? '/' : ''}
27 <if=meta><span class='meta'>(${meta})</></> 27 <if=meta><span class='meta'>(${meta})</></>
28 </a> 28 </a>
29 </li> 29 </li>
30</define> 30</define>
31 31
32<h1> 32<h1>
33 <if=isRoot>clo's files</> 33 <if=isRoot>clo's files</>
34 <else>${fullPath}</> 34 <else>${fullPath}</>
35</h1> 35</h1>
36 36
37<if=isRoot> 37<if=isRoot>
38 <const/{ readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie) /> 38 <const/{ readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie) />
39 <if=readme><ul><ListItem=readme/></ul></> 39 <if=readme><ul><ListItem=readme/></ul></>
40 <for|{key, files, titleColor }| of=sections> 40 <for|{key, files, titleColor }| of=sections>
41 <h2 style={color: titleColor }>${key}</> 41 <h2 style={color: titleColor }>${key}</>
42 <ul> 42 <ul>
43 <for|item| of=files><ListItem=item /></> 43 <for|item| of=files><ListItem=item /></>
44 </ul> 44 </ul>
45 </> 45 </>
46 <if=!hasCotyledonCookie> 46 <if=!hasCotyledonCookie>
47 <br><br><br><br><br><br> 47 <br><br><br><br><br><br>
48 <p style={ opacity: 0.3 }> 48 <p style={ opacity: 0.3 }>
49 would you like to 49 would you like to
50 <a 50 <a
51 href="/file/cotyledon" 51 href="/file/cotyledon"
52 style={ 52 style={
53 color: 'white', 53 color: 'white',
54 'text-decoration': 'underline', 54 'text-decoration': 'underline',
55 } 55 }
56 >dive deeper?</a> 56 >dive deeper?</a>
57 </p> 57 </p>
58 </> 58 </>
59</><else> 59</><else>
60 <ul> 60 <ul>
61 <li><a href=`/file${escapeUri(path.posix.dirname(fullPath))}`>[up one...]</a></li> 61 <li><a href=`/file${escapeUri(path.posix.dirname(fullPath))}`>[up one...]</a></li>
62 <for|item| of=dir.getChildren()> <ListItem=item /> </> 62 <for|item| of=dir.getChildren()> <ListItem=item /> </>
63 </ul> 63 </ul>
64</> 64</>
65 65
66</div> 66</div>
67 67
68import * as path from "node:path"; 68import * as path from "node:path";
69import { escapeUri, formatDuration, formatSize, formatDate } from "@/file-viewer/format.ts"; 69import { escapeUri, formatDuration, formatSize, formatDate } from "@/file-viewer/format.ts";
70import { MediaFileKind } from "@/file-viewer/models/MediaFile.ts"; 70import { MediaFileKind } from "@/file-viewer/models/MediaFile.ts";
71import * as sort from "@/file-viewer/sort.ts"; 71import * as sort from "@/file-viewer/sort.ts";
src/file-viewer/views/lofideon.marko+22-22
...@@ -1,22 +1,22 @@...@@ -1,22 +1,22 @@
1export interface Input { 1export interface Input {
2 stage: number 2 stage: number
3} 3}
4export const meta = { title: 'C O T Y L E D O N' }; 4export const meta = { title: 'C O T Y L E D O N' };
5export const theme = { 5export const theme = {
6 bg: '#ff00ff', 6 bg: '#ff00ff',
7 fg: '#000000', 7 fg: '#000000',
8}; 8};
9 9
10<h1>co<br>ty<br>le<br>don</h1> 10<h1>co<br>ty<br>le<br>don</h1>
11 11
12<if=input.stage==0> 12<if=input.stage==0>
13 13
14<p> 14<p>
15this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts. 15this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts.
16</p><p> 16</p><p>
17by entering our museum, you agree not to use your camera. flash off isn't enough; the bits and bytes are alergic even to a camera's sensor 17by entering our museum, you agree not to use your camera. flash off isn't enough; the bits and bytes are alergic even to a camera's sensor
18</p><p> 18</p><p>
19<sub>(in english: please do not store downloads after you're done viewing them)</sub> 19<sub>(in english: please do not store downloads after you're done viewing them)</sub>
20</p> 20</p>
21 21
22</> 22</>
src/friends/.gitignore+4-4
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1# this directory is private, and not checked into the git repository 1# this directory is private, and not checked into the git repository
2# instead, it is version-controlled via computer clover's backup system. 2# instead, it is version-controlled via computer clover's backup system.
3* 3*
4!.gitignore 4!.gitignore
src/pages/resume.marko+50-50
...@@ -1,50 +1,50 @@...@@ -1,50 +1,50 @@
1import "./resume.css"; 1import "./resume.css";
2 2
3export const meta = { title: 'clover\'s resume' }; 3export const meta = { title: 'clover\'s resume' };
4 4
5<main> 5<main>
6<h1>clover's resume</h1> 6<h1>clover's resume</h1>
7<div>last updated: 2025</> 7<div>last updated: 2025</>
8 8
9<article.job> 9<article.job>
10 <header> 10 <header>
11 <h2>web/backend engineer</h2> 11 <h2>web/backend engineer</h2>
12 <em>2025-now</em> 12 <em>2025-now</em>
13 </> 13 </>
14 <ul> 14 <ul>
15 <i>(more details added as time goes on...)</i> 15 <i>(more details added as time goes on...)</i>
16 </ul> 16 </ul>
17</> 17</>
18 18
19<article.job> 19<article.job>
20 <header> 20 <header>
21 <h2>runtime/systems engineer</h2> 21 <h2>runtime/systems engineer</h2>
22 <em>2023-2025</em> 22 <em>2023-2025</em>
23 <p>developer tools company</p> 23 <p>developer tools company</p>
24 </> 24 </>
25 <ul> 25 <ul>
26 <li>hardcore engineering, elegant solutions</> 26 <li>hardcore engineering, elegant solutions</>
27 <li>platform compatibility & stability</> 27 <li>platform compatibility & stability</>
28 <li>debugging and profiling across platforms</> 28 <li>debugging and profiling across platforms</>
29 </ul> 29 </ul>
30</> 30</>
31 31
32<article.job> 32<article.job>
33 <header> 33 <header>
34 <h2>technician</h2> 34 <h2>technician</h2>
35 <em>2023; part time</em> 35 <em>2023; part time</em>
36 <p>automotive maintainance company</p> 36 <p>automotive maintainance company</p>
37 </> 37 </>
38 <ul> 38 <ul>
39 <li>pressed buttons on a computer</> 39 <li>pressed buttons on a computer</>
40 </ul> 40 </ul>
41</> 41</>
42 42
43<footer> 43<footer>
44 <h2>eduation</h2> <em>2004-now</em> 44 <h2>eduation</h2> <em>2004-now</em>
45 <p> 45 <p>
46 my life on earth has taught me more than i expected. i <br/> 46 my life on earth has taught me more than i expected. i <br/>
47 continue to learn new things daily, as if it was magic. 47 continue to learn new things daily, as if it was magic.
48 </p> 48 </p>
49</footer> 49</footer>
50</main> 50</main>
src/q+a/tags/question-form.marko+13-13
...@@ -1,13 +1,13 @@...@@ -1,13 +1,13 @@
1<form action="/q+a" method="POST"> 1<form action="/q+a" method="POST">
2 <textarea 2 <textarea
3 name="text" 3 name="text"
4 placeholder="ask clover a question..." 4 placeholder="ask clover a question..."
5 required 5 required
6 minlength="1" 6 minlength="1"
7 maxlength="10000" 7 maxlength="10000"
8 /> 8 />
9 <div aria-hidden class="title">ask a question</div> 9 <div aria-hidden class="title">ask a question</div>
10 <button type="submit">send</button> 10 <button type="submit">send</button>
11 <div class="disabled-button">send</div> 11 <div class="disabled-button">send</div>
12</form> 12</form>
13 13
src/q+a/views/image-embed.marko+16-16
...@@ -1,16 +1,16 @@...@@ -1,16 +1,16 @@
1export const meta = { title: "embed image" }; 1export const meta = { title: "embed image" };
2export interface Input { 2export interface Input {
3 question: Question; 3 question: Question;
4} 4}
5 5
6<html-style> 6<html-style>
7 main { padding-top: 11px; } 7 main { padding-top: 11px; }
8 e- { margin: 0!important } 8 e- { margin: 0!important }
9 e- > :first-child { margin-top: 0!important } 9 e- > :first-child { margin-top: 0!important }
10 e- > :last-child { margin-bottom: 0!important } 10 e- > :last-child { margin-bottom: 0!important }
11</html-style> 11</html-style>
12<main.qa> 12<main.qa>
13 <question question=input.question /> 13 <question question=input.question />
14</main> 14</main>
15 15
16import { Question } from '@/q+a/models/Question.ts'; 16import { Question } from '@/q+a/models/Question.ts';
src/q+a/views/permalink.marko+49-49
...@@ -1,49 +1,49 @@...@@ -1,49 +1,49 @@
1export * as layout from "@/q+a/layout.tsx"; 1export * as layout from "@/q+a/layout.tsx";
2export interface Input { 2export interface Input {
3 question: Question; 3 question: Question;
4} 4}
5 5
6server export function meta({ context, question }) { 6server export function meta({ context, question }) {
7 const isDiscord = context.get("user-agent") 7 const isDiscord = context.get("user-agent")
8 ?.toLowerCase() 8 ?.toLowerCase()
9 .includes("discordbot"); 9 .includes("discordbot");
10 if (question.type === QuestionType.normal) { 10 if (question.type === QuestionType.normal) {
11 return { 11 return {
12 title: "question permalink", 12 title: "question permalink",
13 openGraph: { 13 openGraph: {
14 images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }], 14 images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],
15 }, 15 },
16 twitter: { card: "summary_large_image" }, 16 twitter: { card: "summary_large_image" },
17 themeColor: isDiscord 17 themeColor: isDiscord
18 ? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71" 18 ? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
19 : undefined, 19 : undefined,
20 }; 20 };
21 } 21 }
22 return { title: 'question permalink' }; 22 return { title: 'question permalink' };
23} 23}
24 24
25<const/{ question }=input/> 25<const/{ question }=input/>
26<const/{ type }=question/> 26<const/{ type }=question/>
27<if=type==QuestionType.normal> 27<if=type==QuestionType.normal>
28 <p>this page is a permalink to the following question:</p> 28 <p>this page is a permalink to the following question:</p>
29 <question ...{question} /> 29 <question ...{question} />
30</><else if=type==QuestionType.pending> 30</><else if=type==QuestionType.pending>
31 <p> 31 <p>
32 this page is a permalink to a question that 32 this page is a permalink to a question that
33 has not yet been answered. 33 has not yet been answered.
34 </p> 34 </p>
35 <p><a href="/q+a">read questions with existing responses</a>.</p> 35 <p><a href="/q+a">read questions with existing responses</a>.</p>
36</><else if=type==QuestionType.reject> 36</><else if=type==QuestionType.reject>
37 <p> 37 <p>
38 this page is a permalink to a question, but the question 38 this page is a permalink to a question, but the question
39 was deleted instead of answered. maybe it was sent multiple 39 was deleted instead of answered. maybe it was sent multiple
40 times, or maybe the question was not a question. who knows. 40 times, or maybe the question was not a question. who knows.
41 </p> 41 </p>
42 <p>sorry, sister</p> 42 <p>sorry, sister</p>
43 <p><a href="/q+a">all questions</a></p> 43 <p><a href="/q+a">all questions</a></p>
44</><else> 44</><else>
45 <p>oh dear, this question is in an invalid state</p> 45 <p>oh dear, this question is in an invalid state</p>
46 <pre>${JSON.stringify(question, null, 2)}</pre> 46 <pre>${JSON.stringify(question, null, 2)}</pre>
47</> 47</>
48 48
49import { Question, QuestionType } from '@/q+a/models/Question.ts'; 49import { Question, QuestionType } from '@/q+a/models/Question.ts';