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 meow=null />
<div> <div>
wait(${null}) wait(${null})
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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