chore: convert all files to unix-style line ends

This commit is contained in:
clover caruso 2025-08-14 20:37:51 -07:00
parent c9d24a4fdd
commit 3a36e53635
10 changed files with 574 additions and 574 deletions

View file

@ -1,4 +1,4 @@
<div meow=null />
<div>
wait(${null})
</div>
<div meow=null />
<div>
wait(${null})
</div>

View file

@ -1,6 +1,6 @@
import Component from './Component.marko';
<h1>web page</h1>
<if=!false>
<Component=null/>
</>
import Component from './Component.marko';
<h1>web page</h1>
<if=!false>
<Component=null/>
</>

View file

@ -1,339 +1,339 @@
export const blog: BlogMeta = {
title: "Marko is the coziest HTML templating language",
desc: "...todo...",
created: "2025-06-13",
draft: true,
};
export const meta = formatBlogMeta(blob);
export * as layout from "@/blog/layout.tsx";
I've been recently playing around [Marko], and after adding limited support
for it in my website generator, [sitegen], I instantly fell in love with how
minimalistic it is in comparison to JSX, Astro components, and Svelte.
[Marko]: https://next.markojs.com
[sitegen]: https://paperclover.dev/clo/sitegen
## Introduction to Marko
If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving
JavaScript into HTML. Attributes are JavaScript expressions.
```marko
<div>
// `input` is like props, but given in the top-level scope
<time datetime=input.date.toISOString()>
// Interpolation with JS template string syntax
${formatTimeNicely(input.date)}
</time>
<div>
<a href=`/users/${input.user.id}`>${input.user.name}</a>
</div>
// Capital letter variables for imported components
<MarkdownContent message=input.message />
// Components also can be auto-imported by lowercase.
// This will look upwards for a `tags/` folder containing
// "custom-footer.marko", similar to how Node.js finds
// package names in all upwards `node_modules` folders.
<custom-footer />
</div>
// ESM `import` / `export` just work as expected.
// I prefer my imports at the end, to highlight the markup.
import MarkdownContent from "./MarkdownContent.marko";
import { formatTimeNicely } from "../date-helpers.ts";
```
Tags with the `value` attribute have a shorthand, which is used by the built-in
`<if>` for conditional rendering.
```marko
// Sugar for <input value="string" />
<input="string" />
// and it composes amazingly to the 'if' built-in
<if=input.user>
<UserProfile=input.user />
</if>
```
Tags 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/>`.
```
<id/uniqueId />
<input id=uniqueId type="checkbox" name="allow_trans_rights" />
<label for=uniqueId>click me!</>
// ^ oh, you can also omit the
// closing tag name if you want.
```
It's important that I started with the two forms of "Tag I/O": `=` for input
and `/` for output. With those building blocks, we introduce local variables
with `const`
```
<const/rendered = markdownToHtml(input.value) />
// This is how you insert raw HTML to the document
<inline-html=rendered />
// It supports all of the cozy destructuring syntax JS has
<const/{ id, name } = user />
```
Unlike JSX, when you pass content within a tag (`input.content` instead of
JSX's `children`), instead of it being a JSX element, it is actually a
function. This means that the `for` tag can render the content multiple times.
```
<ul>
<for from=1 to=10>
// Renders a new random number for each iteration.
<li>${Math.random()}</li>
</>
</ul>
```
Since `content` is a function, it can take arguments. This is done with `|`
```
<h1>my friends</h1>
<ul>
// I tend to omit the closing tag names for the built-in control
// flow tags, but I keep them for HTML tags. It's kinda like how
// in JavaScript you just write `}` to close your `if`s and loops.
//
// Anyways <for> also has 'of'
<for|item| of=user.friends>
<li class="friend">${item.name}</li>
</>
// They support the same syntax JavaScript function params allows,
// so you can have destructuring here too, and multiple params.
<for|{ name }, index| of=user.friends>
// By the way you can also use emmet-style class and ID shorthands.
<li.friend>My #${index + 1} friend is ${name}</li>
</>
</ul>
```
Instead of named slots, Marko has attribute tags. These are more powerful than
slots since they are functions, and can also act as sugar for more complicated
attributes.
```
<Layout title="Welcome">
<@header variant="big">
<h1>the next big thing</h1>
</@header>
<p>body text...</p>
</Layout>
// The `input` variable inside of <Layout /> is:
//
// {
// title: "Welcome",
// header: {
// content: /* function rendering "<h1>the next big thing</h1>" */,
// variant: "big",
// },
// content: /* function rendering "<p>body text</p>" */
// }
```
This layout could be implemented as such:
```marko
<main>
<if=input.header />
<const/{ ...headerProps, content }=input.header />
<header ...headerProps>
// Instead of assigning to a variable with a capital letter,
// template interpolation works on tag names. This can also
// be a string to render the native HTML tag of that kind.
<${content} />
</header>
<hr />
</>
<${input.content} />
</main>
```
The last syntax feature missing is calling a tag with parameters. That is done
just like a regular function call, with '('.
```
<Something(item, index) />
```
In fact, attributes can just be sugar over this syntax. (this technically isn't
true but it's close enough for the example)
```
<SpecialButton type="submit" class="red" />
// is equal to
<SpecialButton({ type: "submit", class: "red" }) />
```
All of the above is about how Marko's syntax works, and how it performs HTML
generation with components. Marko also allows interactive components, but an
explaination of that is beyond the scope of this page, mostly since I have not
used it. A brief example of it, modified from their documentation.
```marko
// Reactive variables with <let/> just work...
<let/basicCounter=0 />
<button onClick() { basicCounter += 1 }>${basicCounter}</button>
// ...but a counter is boring.
<let/todos=[
{ id: 0, text: "Learn Marko" },
{ id: 1, text: "Make a Website" },
]/>
// 'by' is like React JSX's "key" property, but it's optional.
<ul><for|todo, i| of=todos by=(todo => todo.id)>
<li.todo>
// this variable remains stable even if the list
// re-orders, because 'by' was specified.
<let/done=false/>
<label>
<span>${todo.text}</span>
// ':=' creates a two-way reactive binding,
// (it passes a callback for `checkedChanged`)
<input type="checkbox" checked:=done />
</label>
<button
title="delete"
disabled=!done
onClick() {
todos = todos.toSpliced(i, 1);
}
> &times; </button>
</li>
</></ul>
// Form example
<let/nextId=2/>
<form onSubmit(e) {
e.preventDefault();
todos = todos.concat({
id: nextId++,
// HTMLFormElement exposes all its named input
// elements as extra properties on the object.
text: e.target.text.value,
});
// And you can clear it with 'reset()'
e.target.reset();
}>
// We don't 'onChange' like a React loser. The form
// value can be read in the submit event like normal.
<input name="text" placeholder="Another Item">
<button type="submit">Add</button>
</form>
```
<SectionHeader updated="2025-08-11">Usage on `paperclover.net`</Section>
Using Marko for HTML generation is quite easy. `.marko` files can be compiled
into `.js` using the `@marko/compiler` library.
```ts
const src = fs.readFileSync("page.marko", "utf8");
const compile = marko.compileSync(src, filepath);
fs.writeFileSync("page.js", compile.code);
const page = require("./page.js");
console.info(page);
import * as fs from "node:fs";
import * as marko from "@marko/compiler";
```
To get client side JavaScript, an option can be passed to the Marko compiler to
generate the client side code. While it is a big selling point of Marko, I do
not use any of their client side features, instead deferring to manually-written
frontend scripts. This is because that is how my website has been for years,
statically generated. And for websites like mine that are content focused, this
is the correct way to do things.
Since I have a custom HTML generation library (built on JSX and some React-like
patterns), I have written a simple integration for it to utilize Marko
components, which is loaded by replacing the generated import to `marko/html`,
which lets me overwrite functions like `createTemplate` (to change the signature
of a component), `dynamicTag` (to allow Marko to render non-Marko components),
and `fork` (to enable async integration with the rendering framework). An
additional feature of this is I have a Node.js loader hook to allow importing
these files directly.
```tsx
function Page() {
const q = Question.getByDate(new Date("2025-06-07 12:12 EST"));
return <div>
<h1>example question</h1>
<QuestionRender question={q} />
</div>;
}
// The synchronous render can be used because `Page` and `question.marko`
// do not await any promises (SQLite runs synchronously)
console.info(render.sync(<Page />).text);
import * as render from "#engine/render";
import QuestionRender from "@/q+a/tags/question.marko";
import { Question } from "@/q+a/models/Question.ts";
```
Here is the `question.marko` tag used to render [questions on the clover q+a](/q+a).
```marko
// Renders a `Question` entry including its markdown body.
export interface Input {
question: Question;
admin?: boolean;
}
// 2024-12-31 05:00:00 EST
export const transitionDate = 1735639200000;
<const/{ question, admin } = input />
<const/{ id, date, text } = question/>
<${"e-"}
f=(date > transitionDate ? true : undefined)
id=admin ? `q${id}` : undefined
>
<if=admin>
<a
style="margin-right: 0.5rem"
href=`/admin/q+a/${id}`
>[EDIT]</a>
</>
<a>
<time
datetime=formatQuestionISOTimestamp(date)
>${formatQuestionTimestamp(date)}</time>
</a>
<CloverMarkdown ...{ text } />
</>
// this singleton script will make all the '<time>' tags clickable.
client import "./clickable-links.client.ts";
import type { Question } from "@/q+a/models/Question.ts";
import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
```
The integration is great, `client import` is quite a magical concept, and I've
tuned it to do the expected thing in my framework.
import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts';
export const blog: BlogMeta = {
title: "Marko is the coziest HTML templating language",
desc: "...todo...",
created: "2025-06-13",
draft: true,
};
export const meta = formatBlogMeta(blob);
export * as layout from "@/blog/layout.tsx";
I've been recently playing around [Marko], and after adding limited support
for it in my website generator, [sitegen], I instantly fell in love with how
minimalistic it is in comparison to JSX, Astro components, and Svelte.
[Marko]: https://next.markojs.com
[sitegen]: https://paperclover.dev/clo/sitegen
## Introduction to Marko
If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving
JavaScript into HTML. Attributes are JavaScript expressions.
```marko
<div>
// `input` is like props, but given in the top-level scope
<time datetime=input.date.toISOString()>
// Interpolation with JS template string syntax
${formatTimeNicely(input.date)}
</time>
<div>
<a href=`/users/${input.user.id}`>${input.user.name}</a>
</div>
// Capital letter variables for imported components
<MarkdownContent message=input.message />
// Components also can be auto-imported by lowercase.
// This will look upwards for a `tags/` folder containing
// "custom-footer.marko", similar to how Node.js finds
// package names in all upwards `node_modules` folders.
<custom-footer />
</div>
// ESM `import` / `export` just work as expected.
// I prefer my imports at the end, to highlight the markup.
import MarkdownContent from "./MarkdownContent.marko";
import { formatTimeNicely } from "../date-helpers.ts";
```
Tags with the `value` attribute have a shorthand, which is used by the built-in
`<if>` for conditional rendering.
```marko
// Sugar for <input value="string" />
<input="string" />
// and it composes amazingly to the 'if' built-in
<if=input.user>
<UserProfile=input.user />
</if>
```
Tags 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/>`.
```
<id/uniqueId />
<input id=uniqueId type="checkbox" name="allow_trans_rights" />
<label for=uniqueId>click me!</>
// ^ oh, you can also omit the
// closing tag name if you want.
```
It's important that I started with the two forms of "Tag I/O": `=` for input
and `/` for output. With those building blocks, we introduce local variables
with `const`
```
<const/rendered = markdownToHtml(input.value) />
// This is how you insert raw HTML to the document
<inline-html=rendered />
// It supports all of the cozy destructuring syntax JS has
<const/{ id, name } = user />
```
Unlike JSX, when you pass content within a tag (`input.content` instead of
JSX's `children`), instead of it being a JSX element, it is actually a
function. This means that the `for` tag can render the content multiple times.
```
<ul>
<for from=1 to=10>
// Renders a new random number for each iteration.
<li>${Math.random()}</li>
</>
</ul>
```
Since `content` is a function, it can take arguments. This is done with `|`
```
<h1>my friends</h1>
<ul>
// I tend to omit the closing tag names for the built-in control
// flow tags, but I keep them for HTML tags. It's kinda like how
// in JavaScript you just write `}` to close your `if`s and loops.
//
// Anyways <for> also has 'of'
<for|item| of=user.friends>
<li class="friend">${item.name}</li>
</>
// They support the same syntax JavaScript function params allows,
// so you can have destructuring here too, and multiple params.
<for|{ name }, index| of=user.friends>
// By the way you can also use emmet-style class and ID shorthands.
<li.friend>My #${index + 1} friend is ${name}</li>
</>
</ul>
```
Instead of named slots, Marko has attribute tags. These are more powerful than
slots since they are functions, and can also act as sugar for more complicated
attributes.
```
<Layout title="Welcome">
<@header variant="big">
<h1>the next big thing</h1>
</@header>
<p>body text...</p>
</Layout>
// The `input` variable inside of <Layout /> is:
//
// {
// title: "Welcome",
// header: {
// content: /* function rendering "<h1>the next big thing</h1>" */,
// variant: "big",
// },
// content: /* function rendering "<p>body text</p>" */
// }
```
This layout could be implemented as such:
```marko
<main>
<if=input.header />
<const/{ ...headerProps, content }=input.header />
<header ...headerProps>
// Instead of assigning to a variable with a capital letter,
// template interpolation works on tag names. This can also
// be a string to render the native HTML tag of that kind.
<${content} />
</header>
<hr />
</>
<${input.content} />
</main>
```
The last syntax feature missing is calling a tag with parameters. That is done
just like a regular function call, with '('.
```
<Something(item, index) />
```
In fact, attributes can just be sugar over this syntax. (this technically isn't
true but it's close enough for the example)
```
<SpecialButton type="submit" class="red" />
// is equal to
<SpecialButton({ type: "submit", class: "red" }) />
```
All of the above is about how Marko's syntax works, and how it performs HTML
generation with components. Marko also allows interactive components, but an
explaination of that is beyond the scope of this page, mostly since I have not
used it. A brief example of it, modified from their documentation.
```marko
// Reactive variables with <let/> just work...
<let/basicCounter=0 />
<button onClick() { basicCounter += 1 }>${basicCounter}</button>
// ...but a counter is boring.
<let/todos=[
{ id: 0, text: "Learn Marko" },
{ id: 1, text: "Make a Website" },
]/>
// 'by' is like React JSX's "key" property, but it's optional.
<ul><for|todo, i| of=todos by=(todo => todo.id)>
<li.todo>
// this variable remains stable even if the list
// re-orders, because 'by' was specified.
<let/done=false/>
<label>
<span>${todo.text}</span>
// ':=' creates a two-way reactive binding,
// (it passes a callback for `checkedChanged`)
<input type="checkbox" checked:=done />
</label>
<button
title="delete"
disabled=!done
onClick() {
todos = todos.toSpliced(i, 1);
}
> &times; </button>
</li>
</></ul>
// Form example
<let/nextId=2/>
<form onSubmit(e) {
e.preventDefault();
todos = todos.concat({
id: nextId++,
// HTMLFormElement exposes all its named input
// elements as extra properties on the object.
text: e.target.text.value,
});
// And you can clear it with 'reset()'
e.target.reset();
}>
// We don't 'onChange' like a React loser. The form
// value can be read in the submit event like normal.
<input name="text" placeholder="Another Item">
<button type="submit">Add</button>
</form>
```
<SectionHeader updated="2025-08-11">Usage on `paperclover.net`</Section>
Using Marko for HTML generation is quite easy. `.marko` files can be compiled
into `.js` using the `@marko/compiler` library.
```ts
const src = fs.readFileSync("page.marko", "utf8");
const compile = marko.compileSync(src, filepath);
fs.writeFileSync("page.js", compile.code);
const page = require("./page.js");
console.info(page);
import * as fs from "node:fs";
import * as marko from "@marko/compiler";
```
To get client side JavaScript, an option can be passed to the Marko compiler to
generate the client side code. While it is a big selling point of Marko, I do
not use any of their client side features, instead deferring to manually-written
frontend scripts. This is because that is how my website has been for years,
statically generated. And for websites like mine that are content focused, this
is the correct way to do things.
Since I have a custom HTML generation library (built on JSX and some React-like
patterns), I have written a simple integration for it to utilize Marko
components, which is loaded by replacing the generated import to `marko/html`,
which lets me overwrite functions like `createTemplate` (to change the signature
of a component), `dynamicTag` (to allow Marko to render non-Marko components),
and `fork` (to enable async integration with the rendering framework). An
additional feature of this is I have a Node.js loader hook to allow importing
these files directly.
```tsx
function Page() {
const q = Question.getByDate(new Date("2025-06-07 12:12 EST"));
return <div>
<h1>example question</h1>
<QuestionRender question={q} />
</div>;
}
// The synchronous render can be used because `Page` and `question.marko`
// do not await any promises (SQLite runs synchronously)
console.info(render.sync(<Page />).text);
import * as render from "#engine/render";
import QuestionRender from "@/q+a/tags/question.marko";
import { Question } from "@/q+a/models/Question.ts";
```
Here is the `question.marko` tag used to render [questions on the clover q+a](/q+a).
```marko
// Renders a `Question` entry including its markdown body.
export interface Input {
question: Question;
admin?: boolean;
}
// 2024-12-31 05:00:00 EST
export const transitionDate = 1735639200000;
<const/{ question, admin } = input />
<const/{ id, date, text } = question/>
<${"e-"}
f=(date > transitionDate ? true : undefined)
id=admin ? `q${id}` : undefined
>
<if=admin>
<a
style="margin-right: 0.5rem"
href=`/admin/q+a/${id}`
>[EDIT]</a>
</>
<a>
<time
datetime=formatQuestionISOTimestamp(date)
>${formatQuestionTimestamp(date)}</time>
</a>
<CloverMarkdown ...{ text } />
</>
// this singleton script will make all the '<time>' tags clickable.
client import "./clickable-links.client.ts";
import type { Question } from "@/q+a/models/Question.ts";
import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
```
The integration is great, `client import` is quite a magical concept, and I've
tuned it to do the expected thing in my framework.
import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts';

View file

@ -1,71 +1,71 @@
import "./lofi.css";
export interface Input {
file: MediaFile;
hasCotyledonCookie: boolean;
}
export { meta, theme } from "./clofi.tsx";
<const/{ file: dir, hasCotyledonCookie } = input />
<const/{ path: fullPath, dirname, basename, kind } = dir />
<const/isRoot = fullPath == '/'>
<div#lofi>
<define/ListItem|{ value: file }|>
<const/dir=file.kind === MediaFileKind.directory/>
<const/meta=(
dir
? formatSize(file.size)
: (file.duration ?? 0) > 0
? formatDuration(file.duration!)
: null
)/>
<li class={ dir }>
<a href=`/file${escapeUri(file.path)}` >
<code>${formatDate(file.date)}</>${" "}
${file.basenameWithoutExt}<span class="ext">${file.extension}</>${dir ? '/' : ''}
<if=meta><span class='meta'>(${meta})</></>
</a>
</li>
</define>
<h1>
<if=isRoot>clo's files</>
<else>${fullPath}</>
</h1>
<if=isRoot>
<const/{ readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie) />
<if=readme><ul><ListItem=readme/></ul></>
<for|{key, files, titleColor }| of=sections>
<h2 style={color: titleColor }>${key}</>
<ul>
<for|item| of=files><ListItem=item /></>
</ul>
</>
<if=!hasCotyledonCookie>
<br><br><br><br><br><br>
<p style={ opacity: 0.3 }>
would you like to
<a
href="/file/cotyledon"
style={
color: 'white',
'text-decoration': 'underline',
}
>dive deeper?</a>
</p>
</>
</><else>
<ul>
<li><a href=`/file${escapeUri(path.posix.dirname(fullPath))}`>[up one...]</a></li>
<for|item| of=dir.getChildren()> <ListItem=item /> </>
</ul>
</>
</div>
import * as path from "node:path";
import { escapeUri, formatDuration, formatSize, formatDate } from "@/file-viewer/format.ts";
import { MediaFileKind } from "@/file-viewer/models/MediaFile.ts";
import * as sort from "@/file-viewer/sort.ts";
import "./lofi.css";
export interface Input {
file: MediaFile;
hasCotyledonCookie: boolean;
}
export { meta, theme } from "./clofi.tsx";
<const/{ file: dir, hasCotyledonCookie } = input />
<const/{ path: fullPath, dirname, basename, kind } = dir />
<const/isRoot = fullPath == '/'>
<div#lofi>
<define/ListItem|{ value: file }|>
<const/dir=file.kind === MediaFileKind.directory/>
<const/meta=(
dir
? formatSize(file.size)
: (file.duration ?? 0) > 0
? formatDuration(file.duration!)
: null
)/>
<li class={ dir }>
<a href=`/file${escapeUri(file.path)}` >
<code>${formatDate(file.date)}</>${" "}
${file.basenameWithoutExt}<span class="ext">${file.extension}</>${dir ? '/' : ''}
<if=meta><span class='meta'>(${meta})</></>
</a>
</li>
</define>
<h1>
<if=isRoot>clo's files</>
<else>${fullPath}</>
</h1>
<if=isRoot>
<const/{ readme, sections } = sort.splitRootDirFiles(dir, hasCotyledonCookie) />
<if=readme><ul><ListItem=readme/></ul></>
<for|{key, files, titleColor }| of=sections>
<h2 style={color: titleColor }>${key}</>
<ul>
<for|item| of=files><ListItem=item /></>
</ul>
</>
<if=!hasCotyledonCookie>
<br><br><br><br><br><br>
<p style={ opacity: 0.3 }>
would you like to
<a
href="/file/cotyledon"
style={
color: 'white',
'text-decoration': 'underline',
}
>dive deeper?</a>
</p>
</>
</><else>
<ul>
<li><a href=`/file${escapeUri(path.posix.dirname(fullPath))}`>[up one...]</a></li>
<for|item| of=dir.getChildren()> <ListItem=item /> </>
</ul>
</>
</div>
import * as path from "node:path";
import { escapeUri, formatDuration, formatSize, formatDate } from "@/file-viewer/format.ts";
import { MediaFileKind } from "@/file-viewer/models/MediaFile.ts";
import * as sort from "@/file-viewer/sort.ts";

View file

@ -1,22 +1,22 @@
export interface Input {
stage: number
}
export const meta = { title: 'C O T Y L E D O N' };
export const theme = {
bg: '#ff00ff',
fg: '#000000',
};
<h1>co<br>ty<br>le<br>don</h1>
<if=input.stage==0>
<p>
this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts.
</p><p>
by 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
</p><p>
<sub>(in english: please do not store downloads after you're done viewing them)</sub>
</p>
</>
export interface Input {
stage: number
}
export const meta = { title: 'C O T Y L E D O N' };
export const theme = {
bg: '#ff00ff',
fg: '#000000',
};
<h1>co<br>ty<br>le<br>don</h1>
<if=input.stage==0>
<p>
this place is sacred, but dangerous. i have to keep visitors to an absolute minimum; you'll get dust on all the artifacts.
</p><p>
by 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
</p><p>
<sub>(in english: please do not store downloads after you're done viewing them)</sub>
</p>
</>

View file

@ -1,4 +1,4 @@
# this directory is private, and not checked into the git repository
# instead, it is version-controlled via computer clover's backup system.
*
!.gitignore
# this directory is private, and not checked into the git repository
# instead, it is version-controlled via computer clover's backup system.
*
!.gitignore

View file

@ -1,50 +1,50 @@
import "./resume.css";
export const meta = { title: 'clover\'s resume' };
<main>
<h1>clover's resume</h1>
<div>last updated: 2025</>
<article.job>
<header>
<h2>web/backend engineer</h2>
<em>2025-now</em>
</>
<ul>
<i>(more details added as time goes on...)</i>
</ul>
</>
<article.job>
<header>
<h2>runtime/systems engineer</h2>
<em>2023-2025</em>
<p>developer tools company</p>
</>
<ul>
<li>hardcore engineering, elegant solutions</>
<li>platform compatibility & stability</>
<li>debugging and profiling across platforms</>
</ul>
</>
<article.job>
<header>
<h2>technician</h2>
<em>2023; part time</em>
<p>automotive maintainance company</p>
</>
<ul>
<li>pressed buttons on a computer</>
</ul>
</>
<footer>
<h2>eduation</h2> <em>2004-now</em>
<p>
my life on earth has taught me more than i expected. i <br/>
continue to learn new things daily, as if it was magic.
</p>
</footer>
</main>
import "./resume.css";
export const meta = { title: 'clover\'s resume' };
<main>
<h1>clover's resume</h1>
<div>last updated: 2025</>
<article.job>
<header>
<h2>web/backend engineer</h2>
<em>2025-now</em>
</>
<ul>
<i>(more details added as time goes on...)</i>
</ul>
</>
<article.job>
<header>
<h2>runtime/systems engineer</h2>
<em>2023-2025</em>
<p>developer tools company</p>
</>
<ul>
<li>hardcore engineering, elegant solutions</>
<li>platform compatibility & stability</>
<li>debugging and profiling across platforms</>
</ul>
</>
<article.job>
<header>
<h2>technician</h2>
<em>2023; part time</em>
<p>automotive maintainance company</p>
</>
<ul>
<li>pressed buttons on a computer</>
</ul>
</>
<footer>
<h2>eduation</h2> <em>2004-now</em>
<p>
my life on earth has taught me more than i expected. i <br/>
continue to learn new things daily, as if it was magic.
</p>
</footer>
</main>

View file

@ -1,13 +1,13 @@
<form action="/q+a" method="POST">
<textarea
name="text"
placeholder="ask clover a question..."
required
minlength="1"
maxlength="10000"
/>
<div aria-hidden class="title">ask a question</div>
<button type="submit">send</button>
<div class="disabled-button">send</div>
</form>
<form action="/q+a" method="POST">
<textarea
name="text"
placeholder="ask clover a question..."
required
minlength="1"
maxlength="10000"
/>
<div aria-hidden class="title">ask a question</div>
<button type="submit">send</button>
<div class="disabled-button">send</div>
</form>

View file

@ -1,16 +1,16 @@
export const meta = { title: "embed image" };
export interface Input {
question: Question;
}
<html-style>
main { padding-top: 11px; }
e- { margin: 0!important }
e- > :first-child { margin-top: 0!important }
e- > :last-child { margin-bottom: 0!important }
</html-style>
<main.qa>
<question question=input.question />
</main>
import { Question } from '@/q+a/models/Question.ts';
export const meta = { title: "embed image" };
export interface Input {
question: Question;
}
<html-style>
main { padding-top: 11px; }
e- { margin: 0!important }
e- > :first-child { margin-top: 0!important }
e- > :last-child { margin-bottom: 0!important }
</html-style>
<main.qa>
<question question=input.question />
</main>
import { Question } from '@/q+a/models/Question.ts';

View file

@ -1,49 +1,49 @@
export * as layout from "@/q+a/layout.tsx";
export interface Input {
question: Question;
}
server export function meta({ context, question }) {
const isDiscord = context.get("user-agent")
?.toLowerCase()
.includes("discordbot");
if (question.type === QuestionType.normal) {
return {
title: "question permalink",
openGraph: {
images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],
},
twitter: { card: "summary_large_image" },
themeColor: isDiscord
? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
: undefined,
};
}
return { title: 'question permalink' };
}
<const/{ question }=input/>
<const/{ type }=question/>
<if=type==QuestionType.normal>
<p>this page is a permalink to the following question:</p>
<question ...{question} />
</><else if=type==QuestionType.pending>
<p>
this page is a permalink to a question that
has not yet been answered.
</p>
<p><a href="/q+a">read questions with existing responses</a>.</p>
</><else if=type==QuestionType.reject>
<p>
this page is a permalink to a question, but the question
was deleted instead of answered. maybe it was sent multiple
times, or maybe the question was not a question. who knows.
</p>
<p>sorry, sister</p>
<p><a href="/q+a">all questions</a></p>
</><else>
<p>oh dear, this question is in an invalid state</p>
<pre>${JSON.stringify(question, null, 2)}</pre>
</>
import { Question, QuestionType } from '@/q+a/models/Question.ts';
export * as layout from "@/q+a/layout.tsx";
export interface Input {
question: Question;
}
server export function meta({ context, question }) {
const isDiscord = context.get("user-agent")
?.toLowerCase()
.includes("discordbot");
if (question.type === QuestionType.normal) {
return {
title: "question permalink",
openGraph: {
images: [{ url: `https://paperclover.net/q+a/${question.id}.png` }],
},
twitter: { card: "summary_large_image" },
themeColor: isDiscord
? question.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
: undefined,
};
}
return { title: 'question permalink' };
}
<const/{ question }=input/>
<const/{ type }=question/>
<if=type==QuestionType.normal>
<p>this page is a permalink to the following question:</p>
<question ...{question} />
</><else if=type==QuestionType.pending>
<p>
this page is a permalink to a question that
has not yet been answered.
</p>
<p><a href="/q+a">read questions with existing responses</a>.</p>
</><else if=type==QuestionType.reject>
<p>
this page is a permalink to a question, but the question
was deleted instead of answered. maybe it was sent multiple
times, or maybe the question was not a question. who knows.
</p>
<p>sorry, sister</p>
<p><a href="/q+a">all questions</a></p>
</><else>
<p>oh dear, this question is in an invalid state</p>
<pre>${JSON.stringify(question, null, 2)}</pre>
</>
import { Question, QuestionType } from '@/q+a/models/Question.ts';