Compare commits
10 commits
fed75e4930
...
7c2f5f39fb
Author | SHA1 | Date | |
---|---|---|---|
7c2f5f39fb | |||
1f0e2b63c6 | |||
68d2981914 | |||
9366be0499 | |||
22365ff977 | |||
9f15da0835 | |||
8a1313eb6e | |||
aecc648fdd | |||
9d27566b66 | |||
07a7386e1d |
18 changed files with 419 additions and 29 deletions
12
Readme.md
12
Readme.md
|
@ -1,3 +1,15 @@
|
||||||
# hornwitser.no
|
# hornwitser.no
|
||||||
|
|
||||||
The code behind hornwitser.no
|
The code behind hornwitser.no
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
For regular development run:
|
||||||
|
- `pnpm watch-ts` to transpile the TypeScript code to build/node and watch for changes.
|
||||||
|
- `pnpm watch-site` to host the site on http://localhost:8080 and reload the server on changes.
|
||||||
|
- `pnpm test` to run the tests and report coverage.
|
||||||
|
|
||||||
|
Other scripts available are:
|
||||||
|
- `pnpm prepare` to transpile the TypeScript code to build/node
|
||||||
|
- `pnpm build` to build the static resources to build/web.
|
||||||
|
- `pnpm serve` to host the website on http://localhost:8080
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
"main": "build/node/index.js",
|
"main": "build/node/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "tsc",
|
"prepare": "tsc",
|
||||||
|
"watch-ts": "tsc --watch",
|
||||||
|
"watch-site": "node --enable-source-maps build/node/cli.js watch",
|
||||||
"build": "node --enable-source-maps build/node/cli.js build",
|
"build": "node --enable-source-maps build/node/cli.js build",
|
||||||
|
"serve": "node --enable-source-maps build/node/cli.js serve",
|
||||||
"test": "node --test --enable-source-maps --experimental-test-coverage"
|
"test": "node --test --enable-source-maps --experimental-test-coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -18,8 +18,9 @@ source.addEventListener("reload", () => location.reload());`;
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>{props.title}</title>
|
<title>{props.title}</title>
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/assets/styles/font.css" />
|
||||||
<script type="module" defer="" src="viewport.js" />
|
<link rel="stylesheet" href="/assets/styles/base.css" />
|
||||||
|
<script type="module" defer="" src="/assets/scripts/viewport.js" />
|
||||||
{ reloadScript }
|
{ reloadScript }
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -20,10 +20,22 @@
|
||||||
- Please stop using framesets. They are horrible UX, mkay?
|
- Please stop using framesets. They are horrible UX, mkay?
|
||||||
- One MB is 1 million bytes.
|
- One MB is 1 million bytes.
|
||||||
- Stop using arbitrary linear scales for wide ranged things like volume. Use dB! For brightness too!
|
- Stop using arbitrary linear scales for wide ranged things like volume. Use dB! For brightness too!
|
||||||
|
- Why your site should have dark mode (and light mode).
|
||||||
|
- Detailed breakdown of the CSS 3D perspective script.
|
||||||
|
- Dark patterns
|
||||||
|
- YouTube's subscriber notification setting requires enabling notifications for the app on mobile to ring the bell.
|
||||||
|
- Discord giving force feeding you free nitro for a week
|
||||||
|
- Android phone asking "want to set firefox as default" but then the "don't ask me again" changes the choice.
|
||||||
|
- Using a hardware passkey on windows with separate device requiring you to click "cancel" to use it.
|
||||||
|
- Every website saying "we value your privacy".
|
||||||
|
- The travel app asking for location permission every time I open it, despite not needing it.
|
||||||
|
- Apps that when you open a link that takes you to the app, don't take you back to where you where when you press back. YouTube and X does this.
|
||||||
|
- Ask before writing a long irrelevant angry rant. You might just have the details wrong.
|
||||||
- Projects
|
- Projects
|
||||||
- Buddhabrot renderer.
|
- Buddhabrot renderer.
|
||||||
- Wooden Drawing Board.
|
- Wooden Drawing Board.
|
||||||
- Blender to CSS export script.
|
- Blender to CSS export script.
|
||||||
|
- Page Flip animation for web like turn.js
|
||||||
- Art
|
- Art
|
||||||
- Flying Hornwitser papercraft.
|
- Flying Hornwitser papercraft.
|
||||||
- Safiria papercraft
|
- Safiria papercraft
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"tags": ["webrings", "history"],
|
"tags": ["webrings", "history"],
|
||||||
"description": "A brief history of Webrings",
|
"description": "A brief history of Webrings",
|
||||||
"url": "https://brisray.com/web/webring-history.htm",
|
"url": "https://brisray.com/web/webring-history.htm",
|
||||||
|
"read": "2025-01-05",
|
||||||
"author": "Ray Thomas",
|
"author": "Ray Thomas",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -214,7 +215,7 @@
|
||||||
"title": "The Small Website Discoverability Crisis",
|
"title": "The Small Website Discoverability Crisis",
|
||||||
"url": "https://www.marginalia.nu/log/19-website-discoverability-crisis/",
|
"url": "https://www.marginalia.nu/log/19-website-discoverability-crisis/",
|
||||||
"tags": ["article", "small web", "discoverability", "bookmarks", "links"],
|
"tags": ["article", "small web", "discoverability", "bookmarks", "links"],
|
||||||
"description": "Victor argues a solution to the small web discoverability problem lies in sharing bookmarks.",
|
"description": "Viktor argues a solution to the small web discoverability problem lies in sharing bookmarks.",
|
||||||
"read": "2025-01-24",
|
"read": "2025-01-24",
|
||||||
"author": "Viktor Lofgren",
|
"author": "Viktor Lofgren",
|
||||||
},
|
},
|
||||||
|
@ -283,6 +284,130 @@
|
||||||
"read": "2025-02-02",
|
"read": "2025-02-02",
|
||||||
"author": "David and Felipe",
|
"author": "David and Felipe",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "IndieWeb",
|
||||||
|
"url": "https://indieweb.org/",
|
||||||
|
"tags": ["resource", "community", "small web"],
|
||||||
|
"description": "A community of independent and personal websites with a focus on owning your content and identity.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "It’s about being, not having",
|
||||||
|
"url": "https://sive.rs/being",
|
||||||
|
"tags": ["article", "life goals", "learning"],
|
||||||
|
"description": "How living is about what you want to be and do, and not what you have in the moment.",
|
||||||
|
"quote": "When you sign up to run a marathon, you don’t want a taxi to take you to the finish line.",
|
||||||
|
"read": "2025-02-03",
|
||||||
|
"author": "Derke Sivers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Low-tech Magazine",
|
||||||
|
"url": "https://solar.lowtechmagazine.com/",
|
||||||
|
"tags": ["magazine", "solar power", "low tech"],
|
||||||
|
"description": "A beautiful magazine on low tech solutions and solar power that practise what it preaches.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Quietism",
|
||||||
|
"url": "https://tommorris.org/posts/2018/quietism/",
|
||||||
|
"tags": ["article", "small web", "maintenance tax"],
|
||||||
|
"description": "A personal website breaking from lack of maintenance, and thoughts about why not to have a website.",
|
||||||
|
"read": "2025-02-03",
|
||||||
|
"author": "Tom Morris",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Hart contracts, not smart contracts",
|
||||||
|
"url": "https://tommorris.org/posts/2020/hart-contracts-not-smart-contracts/",
|
||||||
|
"tags": ["article", "blockchain", "smart contracts", "contract law", "philosophy"],
|
||||||
|
"description": "Why smart contracts cannot fulfil a useful purpose in the wider society.",
|
||||||
|
"read": "2025-02-03",
|
||||||
|
"author": "Tom Morris",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Liber Indigo: The Affordances of Magic",
|
||||||
|
"url": "https://www.youtube.com/playlist?list=PLsfH1Ahi4SzE-QmrsrtyZubGmi66iP45V",
|
||||||
|
"tags": ["video", "philosophy", "materialism"],
|
||||||
|
"description": "How we are shaped by the material and philosophical landscape around us.",
|
||||||
|
"read": "2025-02-12",
|
||||||
|
"author": "Justin C Kirkwood",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The hardest working font in Manhattan",
|
||||||
|
"url": "https://aresluna.org/the-hardest-working-font-in-manhattan/",
|
||||||
|
"tags": ["article", "font", "monoline font", "gorton"],
|
||||||
|
"description": "Deep dive into a surprisingly common routing monoline font that crops up everywhere, but isn't widely known or named.",
|
||||||
|
"read": "2025-02-15",
|
||||||
|
"author": "Marcin Wichary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "It’s a knowledge problem! Or is it?",
|
||||||
|
"url": "https://josvisser.substack.com/p/its-a-knowledge-problem-or-is-it",
|
||||||
|
"tags": ["article", "group behaviour", "management"],
|
||||||
|
"description": "When teams do bad work it likely isn't because they don't know it's bad.",
|
||||||
|
"read": "2025-02-15",
|
||||||
|
"author": "Jos Visser",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "GitHub Actions Feels Bad",
|
||||||
|
"url": "https://www.youtube.com/watch?v=9qljpi5jiMQ",
|
||||||
|
"tags": ["video", "github actions", "ci"],
|
||||||
|
"description": "Critique of GitHub Actions involving a brief history of how it emerged, the shortcomings of it particularly with how it refuses to accept that it's a package manager, and examples of bad design and code in it.",
|
||||||
|
"read": "2025-02-17",
|
||||||
|
"author": "fasterthanlime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Reality has a surprising amount of detail",
|
||||||
|
"url": "http://johnsalvatier.org/blog/2017/reality-has-a-surprising-amount-of-detail",
|
||||||
|
"tags": ["article", "detailed oriented thinking", "reality"],
|
||||||
|
"description": "An observation of the surprising amounts of detail embedded in the structure of reality. These aren't immediately obvious or easy to perceive, they are easy to miss.",
|
||||||
|
"read": "2025-02-21",
|
||||||
|
"author": "John Salvatier",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Media for Thinking the Unthinkable",
|
||||||
|
"url": "https://www.youtube.com/watch?v=oUaOucZRlmE",
|
||||||
|
"tags": ["talk", "ux"],
|
||||||
|
"description": "Demonstration of interfaces that interactively show what system are doing, and how this is vital to understanding those systems.",
|
||||||
|
"read": "2025-02-23",
|
||||||
|
"author": "Bret Victor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Computer Revolution Hasn't Happened Yet - OOPSLA 97 Keynote",
|
||||||
|
"url": "https://www.youtube.com/watch?v=aYT2se94eU0",
|
||||||
|
"tags": ["talk", "programming", "system design"],
|
||||||
|
"description": "Talk about how system design is still something humans are trying to figure out and as a result the systems we have today aren't the revolution that computers promised to be. Especially how the current systems like C++ have optimized themselves into a corner rather than innovate something better.",
|
||||||
|
"read": "2025-03-10",
|
||||||
|
"author": "Alan Kay",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "State of Text Rendering 2024",
|
||||||
|
"url": "https://behdad.org/text2024/",
|
||||||
|
"tags": ["article", "text rendering", "font"],
|
||||||
|
"description": "Overview of the various open source text rendering libraries, tools and capabilities in 2024.",
|
||||||
|
"read": "2025-03-21",
|
||||||
|
"author": "Behdad Esfahbod",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Raster Tragedy at Low-Resolution Revisited: Opportunities and Challenges beyond “Delta-Hinting”",
|
||||||
|
"url": "http://rastertragedy.com/",
|
||||||
|
"tags": ["resource", "text rendering", "font"],
|
||||||
|
"description": "A comprehensive resource on the challenges with rendering fonts to pixels.",
|
||||||
|
"author": "Beat Stamm",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Reflections: The ecosystem is moving",
|
||||||
|
"url": "https://signal.org/blog/the-ecosystem-is-moving/",
|
||||||
|
"tags": ["article", "interoperability", "decentralised systems", "federation", "software ecosystems"],
|
||||||
|
"description": "Reflections on how federated systems get stuck and unable to meaningfully update their protocols, as distributing updates to the installed systems become virtually impossible on internet scale systems. Key examples being the incredibly slow adoption of IPv6 and encryption for email.",
|
||||||
|
"read": "2025-05-10",
|
||||||
|
"author": "moxie0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tech Companies Apparently Do Not Understand Why We Dislike AI",
|
||||||
|
"url": "https://soatok.blog/2025/05/04/tech-companies-apparently-do-not-understand-why-we-dislike-ai/",
|
||||||
|
"tags": ["article", "ai", "ethics"],
|
||||||
|
"description": "The real problem with AI is not the hypothetical doomsday prophesies or the singularity, but rather the real ethical problems with the sort of detrimental societal behaviours it enables and the industries it displaces.",
|
||||||
|
"read": "2025-05-10",
|
||||||
|
"author": "Soatok",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "",
|
"title": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -294,6 +419,7 @@
|
||||||
],
|
],
|
||||||
"to_read": [
|
"to_read": [
|
||||||
"https://bitbashing.io/gc-for-systems-programmers.html",
|
"https://bitbashing.io/gc-for-systems-programmers.html",
|
||||||
|
"https://freetype.org/freetype2/docs/hinting/text-rendering-general.html",
|
||||||
],
|
],
|
||||||
"authors": {
|
"authors": {
|
||||||
"Ray Thomas": {
|
"Ray Thomas": {
|
||||||
|
@ -356,6 +482,39 @@
|
||||||
"David and Felipe": {
|
"David and Felipe": {
|
||||||
"urls": ["https://hallofdreams.org/"],
|
"urls": ["https://hallofdreams.org/"],
|
||||||
},
|
},
|
||||||
|
"Tom Morris": {
|
||||||
|
"urls": ["https://tommorris.org/"],
|
||||||
|
},
|
||||||
|
"Justin C Kirkwood": {
|
||||||
|
"urls": ["https://www.justinckirkwood.net/", "https://www.youtube.com/@liber-indigo"],
|
||||||
|
},
|
||||||
|
"Marcin Wichary": {
|
||||||
|
"urls": ["https://aresluna.org/"],
|
||||||
|
},
|
||||||
|
"Jos Visser": {
|
||||||
|
"urls": ["https://substack.com/@josvisser"],
|
||||||
|
},
|
||||||
|
"fasterthanlime": {
|
||||||
|
"urls": ["https://fasterthanli.me/", "https://www.youtube.com/@fasterthanlime"],
|
||||||
|
},
|
||||||
|
"John Salvatier": {
|
||||||
|
"urls": ["http://johnsalvatier.org/"],
|
||||||
|
},
|
||||||
|
"Bret Victor": {
|
||||||
|
"urls": ["https://worrydream.com/"],
|
||||||
|
},
|
||||||
|
"Behdad Esfahbod": {
|
||||||
|
"urls": ["https://behdad.org/"],
|
||||||
|
},
|
||||||
|
"Beat Stamm": {
|
||||||
|
"urls": ["http://rastertragedy.com/RTRBSt.htm#About"],
|
||||||
|
},
|
||||||
|
"moxie0": {
|
||||||
|
"urls": ["https://github.com/moxie0"],
|
||||||
|
},
|
||||||
|
"Soatok": {
|
||||||
|
"urls": ["https://soatok.blog/"],
|
||||||
|
},
|
||||||
"": {
|
"": {
|
||||||
"urls": [""],
|
"urls": [""],
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface LinkData {
|
||||||
description?: string,
|
description?: string,
|
||||||
read?: string,
|
read?: string,
|
||||||
author?: string,
|
author?: string,
|
||||||
|
quote?: string,
|
||||||
}
|
}
|
||||||
interface Data {
|
interface Data {
|
||||||
links: LinkData[];
|
links: LinkData[];
|
||||||
|
@ -23,13 +24,81 @@ interface Data {
|
||||||
function Link(props: { link: LinkData }) {
|
function Link(props: { link: LinkData }) {
|
||||||
const link = props.link;
|
const link = props.link;
|
||||||
return <>
|
return <>
|
||||||
<a href={link.url}>{link.title}</a>
|
<hgroup>
|
||||||
{" "}
|
<h4>
|
||||||
{link.tags.join(", ")}
|
<a href={link.url}>{link.title}</a>
|
||||||
|
{link.author ? <span class="no-break">– {link.author}</span> : null}
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
{link.tags.join(", ")}
|
||||||
|
{link.read ? <>
|
||||||
|
, read: <time>{link.read}</time>
|
||||||
|
</> : null}
|
||||||
|
</p>
|
||||||
|
</hgroup>
|
||||||
|
{link.quote ? <blockquote>
|
||||||
|
{link.quote}
|
||||||
|
</blockquote> : null}
|
||||||
|
{link.description ? <p>
|
||||||
|
{link.description}
|
||||||
|
</p> : null}
|
||||||
|
{link.altUrls ? <p>
|
||||||
|
Also available at {link.altUrls.map(url => <a href={url}>{url}</a>)}.
|
||||||
|
</p> : null}
|
||||||
|
{link.related ? <p>
|
||||||
|
Related <a href={link.related}>{link.related}</a>.
|
||||||
|
</p> : null}
|
||||||
|
{link.via ? <p>
|
||||||
|
Via <a href={link.via}>{link.via}</a>.
|
||||||
|
</p> : null}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
const data: Data = eval(`(${readFileSync("src/content/links.jsonc", "utf8")})`);
|
const data: Data = eval(`(${readFileSync("src/content/links.jsonc", "utf8")})`);
|
||||||
data.links.pop(); // Remove template at the end
|
data.links.pop(); // Remove template at the end
|
||||||
|
|
||||||
|
function compare(a: string, b: string) {
|
||||||
|
return Number(a > b) - Number(b > a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* groupBy<T, K>(items: Iterable<T>, keyFn: (item: T) => K) {
|
||||||
|
let oldKey: K = Symbol() as any;
|
||||||
|
let group: T[] | undefined = undefined;
|
||||||
|
for (const item of items) {
|
||||||
|
let newKey = keyFn(item);
|
||||||
|
if (!Object.is(newKey, oldKey)) {
|
||||||
|
if (group) {
|
||||||
|
yield [oldKey, group] as [K, T[]];
|
||||||
|
}
|
||||||
|
group = [];
|
||||||
|
oldKey = newKey;
|
||||||
|
}
|
||||||
|
group!.push(item);
|
||||||
|
}
|
||||||
|
if (group) {
|
||||||
|
yield [oldKey, group] as [K, T[]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const byDate = data.links.filter(link => link.read).sort((a, b) => -compare(a.read!, b.read!));
|
||||||
|
const byMonth = groupBy(byDate, link => link.read!.slice(0, 7));
|
||||||
|
const byYearMonth = [...groupBy(byMonth, month => month[0].slice(0, 4))];
|
||||||
|
const other = data.links.filter(link => !link.read);
|
||||||
|
const monthNames = [
|
||||||
|
"", // padding to make mapping start at 1
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
];
|
||||||
|
|
||||||
const title = "Links!";
|
const title = "Links!";
|
||||||
export const links: Page = {
|
export const links: Page = {
|
||||||
title,
|
title,
|
||||||
|
@ -37,8 +106,27 @@ export const links: Page = {
|
||||||
content: <BasePage title={title}>
|
content: <BasePage title={title}>
|
||||||
<main>
|
<main>
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
<ul>
|
<p>
|
||||||
{ data.links.map(link => <li><Link link={link} /></li>)}
|
Here be interesting things I've read, watched, listened to or otherwise found useful as resources over the years. These are ordered by when I read them as that seems most practical to me.
|
||||||
|
</p>
|
||||||
|
{byYearMonth.map(([year, months]) => <section>
|
||||||
|
<h2>{year}</h2>
|
||||||
|
{months.map(([yearMonth, links]) => <section>
|
||||||
|
<h3>{monthNames[Number.parseInt(yearMonth.slice(5))]}</h3>
|
||||||
|
<ul role="list">
|
||||||
|
{links.map(link => <li class="link">
|
||||||
|
<Link link={link} />
|
||||||
|
</li>)}
|
||||||
|
</ul>
|
||||||
|
</section>)}
|
||||||
|
</section>)}
|
||||||
|
<h2>
|
||||||
|
Resources and other things
|
||||||
|
</h2>
|
||||||
|
<ul role="list">
|
||||||
|
{other.map(link => <li class="link">
|
||||||
|
<Link link={link} />
|
||||||
|
</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
|
|
|
@ -19,7 +19,7 @@ const content = <>
|
||||||
<p>
|
<p>
|
||||||
This has some interesting advantages:
|
This has some interesting advantages:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul class="tight">
|
||||||
<li>
|
<li>
|
||||||
HTML content is only generated when it's changed, rather than being redone on every request.
|
HTML content is only generated when it's changed, rather than being redone on every request.
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -25,6 +25,16 @@ suite("function resolveRefs", () => {
|
||||||
assert.equal((el as Element).attributes.get("href"), "../alt/page.html");
|
assert.equal((el as Element).attributes.get("href"), "../alt/page.html");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("script src", () => {
|
||||||
|
const el = resolveRefs(<script src="/assets/script.js"></script>, "/subdir");
|
||||||
|
assert.equal((el as Element).attributes.get("src"), "../assets/script.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("img src", () => {
|
||||||
|
const el = resolveRefs(<img src="/assets/image.png"></img>, "/subdir");
|
||||||
|
assert.equal((el as Element).attributes.get("src"), "../assets/image.png");
|
||||||
|
});
|
||||||
|
|
||||||
test("nested element", () => {
|
test("nested element", () => {
|
||||||
const el = resolveRefs(<div>Content with <a href="/page.html">Link</a></div>, "/");
|
const el = resolveRefs(<div>Content with <a href="/page.html">Link</a></div>, "/");
|
||||||
assert.equal((el.childNodes[1] as Element).attributes.get("href"), "page.html");
|
assert.equal((el.childNodes[1] as Element).attributes.get("href"), "page.html");
|
||||||
|
|
|
@ -8,6 +8,13 @@ function shallowCopyElement(element: Element) {
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedAttributes = new Map([
|
||||||
|
["a", "href"],
|
||||||
|
["img", "src"],
|
||||||
|
["link", "href"],
|
||||||
|
["script", "src"],
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Resolves absolute href attributes in a and link elements the Node tree into relative references from the given directory.
|
Resolves absolute href attributes in a and link elements the Node tree into relative references from the given directory.
|
||||||
|
|
||||||
|
@ -21,20 +28,18 @@ export function resolveRefs(node: Node, dir: string) {
|
||||||
}
|
}
|
||||||
let resolvedNode = node;
|
let resolvedNode = node;
|
||||||
const name = node.name;
|
const name = node.name;
|
||||||
if (
|
let attribute = resolvedAttributes.get(name);
|
||||||
(name === "link" || name === "a")
|
if (attribute && node.attributes.has(attribute)) {
|
||||||
&& node.attributes.has("href")
|
const original = node.attributes.get(attribute)!
|
||||||
) {
|
|
||||||
const original = node.attributes.get("href")!
|
|
||||||
if (/^[a-z][a-z+.-]*:/i.test(original)) {
|
if (/^[a-z][a-z+.-]*:/i.test(original)) {
|
||||||
// Ignore refs that start with a URI scheme.
|
// Ignore refs that start with a URI scheme.
|
||||||
/* node:coverage ignore next 3 */
|
/* node:coverage ignore next 3 */
|
||||||
} else if (!original.startsWith("/")) {
|
} else if (!original.startsWith("/")) {
|
||||||
console.log(`Warning: found relative href to ${original}`);
|
console.log(`Warning: found relative ${attribute} to ${original}`);
|
||||||
} else {
|
} else {
|
||||||
const ref = posix.relative(dir, original);
|
const ref = posix.relative(dir, original);
|
||||||
resolvedNode = shallowCopyElement(node);
|
resolvedNode = shallowCopyElement(node);
|
||||||
resolvedNode.attributes.set("href", ref);
|
resolvedNode.attributes.set(attribute, ref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const resolvedChildren: Node[] = [];
|
const resolvedChildren: Node[] = [];
|
||||||
|
|
BIN
web/assets/fonts/ComicNeue-Bold.woff2
Normal file
BIN
web/assets/fonts/ComicNeue-Bold.woff2
Normal file
Binary file not shown.
BIN
web/assets/fonts/ComicNeue-BoldItalic.woff2
Normal file
BIN
web/assets/fonts/ComicNeue-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
web/assets/fonts/ComicNeue-Italic.woff2
Normal file
BIN
web/assets/fonts/ComicNeue-Italic.woff2
Normal file
Binary file not shown.
BIN
web/assets/fonts/ComicNeue-Light.woff2
Normal file
BIN
web/assets/fonts/ComicNeue-Light.woff2
Normal file
Binary file not shown.
BIN
web/assets/fonts/ComicNeue-LightItalic.woff2
Normal file
BIN
web/assets/fonts/ComicNeue-LightItalic.woff2
Normal file
Binary file not shown.
BIN
web/assets/fonts/ComicNeue-Regular.woff2
Normal file
BIN
web/assets/fonts/ComicNeue-Regular.woff2
Normal file
Binary file not shown.
|
@ -14,6 +14,10 @@ html {
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:is(ul, ol)[role="list"] {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, button, input, label {
|
h1, h2, h3, h4, button, input, label {
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
@ -39,29 +43,43 @@ textarea:not([rows]) {
|
||||||
/* Overall styling */
|
/* Overall styling */
|
||||||
html {
|
html {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
font-family: sans-serif;
|
font-family: "Comic Neue", sans-serif;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
hgroup h1 {
|
hgroup :is(h1, h2, h3, h4) {
|
||||||
margin-bottom: 0.1em;
|
margin-block-start: 0;
|
||||||
|
margin-block-end: 0.1em;
|
||||||
}
|
}
|
||||||
hgroup p {
|
hgroup p {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
h1, h2, h3, h4 {
|
||||||
|
font-family: "Comic Neue", sans-serif;
|
||||||
margin-block-start: 1.25em;
|
margin-block-start: 1.25em;
|
||||||
margin-block-end: 0.5em;
|
margin-block-end: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(p, ol, ul) + :is(p, ol, ul) {
|
:is(hgroup, p, blockquote, ol, ul, li) + :is(hgroup, blockquote, p, ol, ul, li) {
|
||||||
padding-block-start: 1em;
|
margin-block-start: var(--block-space, 1em);
|
||||||
|
}
|
||||||
|
.tight {
|
||||||
|
--block-space: 0.35em;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol, ul {
|
blockquote {
|
||||||
|
padding-inline: 1.25em;
|
||||||
|
}
|
||||||
|
blockquote::before {
|
||||||
|
content: "“";
|
||||||
|
}
|
||||||
|
blockquote::after {
|
||||||
|
content: "”";
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(ol, ul):not([role]) {
|
||||||
padding-inline-start: 1.25em;
|
padding-inline-start: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +98,8 @@ ol, ul {
|
||||||
/* Base Page Layout */
|
/* Base Page Layout */
|
||||||
body {
|
body {
|
||||||
max-width: 50rem;
|
max-width: 50rem;
|
||||||
padding: 0;
|
padding-inline: 0.5em;
|
||||||
margin-block: 0;
|
padding-block-end: 2em;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +112,7 @@ body {
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* index */
|
/* about */
|
||||||
.author {
|
.author {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
|
@ -102,13 +120,24 @@ body {
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
margin-block: 1em;
|
margin-block: 1em;
|
||||||
}
|
}
|
||||||
.author h1 {
|
|
||||||
margin-block-start: 0;
|
|
||||||
}
|
|
||||||
.author p {
|
.author p {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* links */
|
||||||
|
.no-break {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
background-color: color-mix(in oklab, Canvas 90%, white);
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
.link>* {
|
||||||
|
--block-space: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
/* sandbox */
|
/* sandbox */
|
||||||
.sandbox-inset-3d {
|
.sandbox-inset-3d {
|
||||||
contain: paint;
|
contain: paint;
|
71
web/assets/styles/font.css
Normal file
71
web/assets/styles/font.css
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comic Neue';
|
||||||
|
src:
|
||||||
|
local('Comic Neue Light'),
|
||||||
|
local('ComicNeue-Light'),
|
||||||
|
url('/assets/fonts/ComicNeue-Light.woff2') format('woff2')
|
||||||
|
;
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comic Neue';
|
||||||
|
src:
|
||||||
|
local('Comic Neue Italic'),
|
||||||
|
local('ComicNeue-Italic'),
|
||||||
|
url('/assets/fonts/ComicNeue-Italic.woff2') format('woff2')
|
||||||
|
;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comic Neue';
|
||||||
|
src:
|
||||||
|
local('Comic Neue Bold'),
|
||||||
|
local('ComicNeue-Bold'),
|
||||||
|
url('/assets/fonts/ComicNeue-Bold.woff2') format('woff2')
|
||||||
|
;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comic Neue';
|
||||||
|
src:
|
||||||
|
local('Comic Neue Bold Italic'),
|
||||||
|
local('ComicNeue-BoldItalic'),
|
||||||
|
url('/assets/fonts/ComicNeue-BoldItalic.woff2') format('woff2')
|
||||||
|
;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comic Neue';
|
||||||
|
src:
|
||||||
|
local('Comic Neue Light Italic'),
|
||||||
|
local('ComicNeue-LightItalic'),
|
||||||
|
url('/assets/fonts/ComicNeue-LightItalic.woff2') format('woff2')
|
||||||
|
;
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comic Neue';
|
||||||
|
src:
|
||||||
|
local('Comic Neue Regular'),
|
||||||
|
local('ComicNeue-Regular'),
|
||||||
|
url('/assets/fonts/ComicNeue-Regular.woff2') format('woff2')
|
||||||
|
;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue