chore: convert all files to unix-style line ends
This commit is contained in:
parent
c9d24a4fdd
commit
3a36e53635
10 changed files with 574 additions and 574 deletions
|
@ -1,4 +1,4 @@
|
||||||
<div meow=null />
|
<div meow=null />
|
||||||
<div>
|
<div>
|
||||||
wait(${null})
|
wait(${null})
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
> × </button>
|
> × </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';
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
8
src/friends/.gitignore
vendored
8
src/friends/.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in a new issue