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