Rewrite site. Again. With JS and Vite
This commit is contained in:
+5
-2
@@ -1,6 +1,9 @@
|
||||
.DS_Store
|
||||
**/.*.md
|
||||
.env
|
||||
build/
|
||||
.generated/
|
||||
.bun-tmp/
|
||||
dist/
|
||||
node_modules/
|
||||
*.pdb
|
||||
*.exe
|
||||
*.exe
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
# PaulW.XYZ
|
||||
|
||||
Paul's personal website.
|
||||
|
||||
## Third-Party Licenses
|
||||
Any trademarks listed on this site are the property of their respective owners. This site neither endorses nor is affiliated with any of the owners. Any source code available on any of the pages is without warranty of any kind and the use of such is at your own risk.
|
||||
|
||||
### Fonts
|
||||
Any trademarks listed on this site are the property of their respective owners.
|
||||
This site neither endorses nor is affiliated with any of the owners.
|
||||
Any source code available on any of the pages is without warranty of any kind.
|
||||
Use it at your own risk.
|
||||
|
||||
[Hack](https://github.com/source-foundry/Hack)
|
||||
- © 2018 Source Foundry Authors
|
||||
- MIT License
|
||||
Direct dependency notices are documented in
|
||||
[the license page](/licenses/).
|
||||
|
||||
[Cantarell](https://github.com/davelab6/cantarell)
|
||||
- © 2009-2010, [Understanding Limited](mailto:dave@understandinglimited.com)
|
||||
- Open Font License, Version 1.1
|
||||
## Development
|
||||
|
||||
[EB Garamond](https://github.com/georgd/EB-Garamond)
|
||||
- © 2010-2013 [Georg Duffner](http://www.georgduffner.at)
|
||||
- Open Font License, Version 1.1
|
||||
The source for this site is hosted at <https://git.paulw.xyz/paul/www>.
|
||||
|
||||
```sh
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Run `bun run check` to validate generated pages and `bun run build` to create
|
||||
the production site in `dist/`.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Third-Party Licenses
|
||||
|
||||
This project uses the following direct dependencies:
|
||||
|
||||
## Cardo
|
||||
|
||||
- Package: `@fontsource/cardo`
|
||||
- License: SIL Open Font License, Version 1.1
|
||||
- <https://scholarsfonts.net/cardofnt.html>
|
||||
- <https://github.com/fontsource/font-files/tree/main/fonts/google/cardo>
|
||||
|
||||
## Inter
|
||||
|
||||
- Package: `@fontsource/inter`
|
||||
- License: SIL Open Font License, Version 1.1
|
||||
- Font copyright: Rasmus Andersson
|
||||
- <https://github.com/rsms/inter>
|
||||
- <https://github.com/fontsource/font-files/tree/main/fonts/other/inter>
|
||||
|
||||
## Iosevka
|
||||
|
||||
- Package: `@fontsource/iosevka`
|
||||
- License: SIL Open Font License, Version 1.1
|
||||
- Sources:
|
||||
- <https://github.com/be5invis/iosevka>
|
||||
- <https://github.com/fontsource/font-files/tree/main/fonts/other/iosevka>
|
||||
|
||||
## Marked
|
||||
|
||||
- Package: `marked`
|
||||
- License: MIT
|
||||
- Copyright: MarkedJS; Christopher Jeffrey
|
||||
- <https://github.com/markedjs/marked>
|
||||
|
||||
## PrismJS
|
||||
|
||||
- Package: `prismjs`
|
||||
- License: MIT
|
||||
- <https://github.com/PrismJS/prism>
|
||||
|
||||
## Vite
|
||||
|
||||
- Package: `vite`
|
||||
- License: MIT
|
||||
- Copyright: VoidZero Inc. and Vite contributors
|
||||
- <https://github.com/vitejs/vite>
|
||||
|
||||
The package-manager lockfile records transitive dependencies. Their license
|
||||
notices remain part of the installed dependency trees.
|
||||
@@ -0,0 +1,67 @@
|
||||
import { readdir, readFile, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const generatedRoot = '.generated';
|
||||
|
||||
const walk = async (directory) => {
|
||||
const files = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...await walk(path));
|
||||
else files.push(path);
|
||||
}
|
||||
return files;
|
||||
};
|
||||
|
||||
const exists = async (path) => {
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const errors = [];
|
||||
const htmlFiles = (await walk(generatedRoot)).filter((file) => file.endsWith('.html'));
|
||||
const clientSource = await readFile('src/main.js', 'utf8');
|
||||
const builtInPrismLanguages = new Set(['markup', 'html', 'xml', 'svg', 'css', 'clike', 'javascript', 'js', 'plain', 'plaintext', 'text', 'txt']);
|
||||
const prismLanguageAliases = { sh: 'bash', shell: 'bash' };
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const html = await readFile(file, 'utf8');
|
||||
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
|
||||
const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index);
|
||||
if (duplicateIds.length > 0) errors.push(`${file}: duplicate IDs: ${[...new Set(duplicateIds)].join(', ')}`);
|
||||
if (!html.includes('<meta name="viewport"')) errors.push(`${file}: missing viewport metadata`);
|
||||
if (html.includes('<heading')) errors.push(`${file}: contains a nonstandard <heading> element`);
|
||||
|
||||
for (const [, fragment] of html.matchAll(/href="#([^"]+)"/g)) {
|
||||
if (!ids.includes(fragment)) errors.push(`${file}: missing fragment target #${fragment}`);
|
||||
}
|
||||
|
||||
for (const [, href] of html.matchAll(/href="(\/[^"]*)"/g)) {
|
||||
if (href === '/') continue;
|
||||
const pathname = href.split(/[?#]/, 1)[0];
|
||||
let target;
|
||||
if (pathname.startsWith('/src/')) target = `.${pathname}`;
|
||||
else if (pathname.startsWith('/assets/') || pathname === '/favicon.ico') target = `public${pathname}`;
|
||||
else target = pathname.endsWith('/')
|
||||
? join(generatedRoot, pathname, 'index.html')
|
||||
: join(generatedRoot, pathname);
|
||||
if (!await exists(target)) errors.push(`${file}: broken internal link ${href}`);
|
||||
}
|
||||
|
||||
for (const [, language] of html.matchAll(/class="language-([\w-]+)"/g)) {
|
||||
if (builtInPrismLanguages.has(language)) continue;
|
||||
const component = prismLanguageAliases[language] ?? language;
|
||||
if (!clientSource.includes(`prism-${component}`)) errors.push(`${file}: Prism grammar is not imported for ${language}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join('\n'));
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Checked ${htmlFiles.length} generated pages.`);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { rm } from 'node:fs/promises';
|
||||
|
||||
await Promise.all([
|
||||
rm('.generated', { recursive: true, force: true }),
|
||||
rm('dist', { recursive: true, force: true }),
|
||||
]);
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { escapeHtml, h, raw, render } from './html.js';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const siteName = 'PaulW.XYZ';
|
||||
const output = '.generated';
|
||||
const markdownDirs = ['notes', 'posts'];
|
||||
const standalonePages = [
|
||||
{ source: 'README.md', path: 'about', title: 'About' },
|
||||
{ source: 'THIRD_PARTY_LICENSES.md', path: 'licenses', title: 'Licenses' },
|
||||
];
|
||||
const languageLabels = {
|
||||
ini: 'INI',
|
||||
js: 'JavaScript',
|
||||
javascript: 'JavaScript',
|
||||
lua: 'Lua',
|
||||
sh: 'Shell',
|
||||
bash: 'Shell',
|
||||
};
|
||||
|
||||
const slug = (value) => {
|
||||
let result = '';
|
||||
let dash = false;
|
||||
for (const character of String(value).toLowerCase()) {
|
||||
const valid = (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9');
|
||||
if (valid) { if (dash && result) result += '-'; result += character; dash = false; }
|
||||
else if (result) dash = true;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const headingLabel = (tokens) => tokens.map((token) => {
|
||||
if (token.type === 'image') return token.text ?? '';
|
||||
if (token.tokens) return headingLabel(token.tokens);
|
||||
return token.text ?? token.raw ?? '';
|
||||
}).join('').trim();
|
||||
|
||||
const createSlugger = () => {
|
||||
const counts = new Map();
|
||||
return (tokens) => {
|
||||
const base = slug(headingLabel(tokens)) || 'section';
|
||||
const count = counts.get(base) ?? 0;
|
||||
counts.set(base, count + 1);
|
||||
return count === 0 ? base : `${base}-${count + 1}`;
|
||||
};
|
||||
};
|
||||
|
||||
const pageShell = (title, body, path = '/', navTitle = '') => `<!DOCTYPE html>${render(h('html', { lang: 'en-US' }, [
|
||||
h('head', {}, [
|
||||
h('meta', { charset: 'utf-8' }),
|
||||
h('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }),
|
||||
h('title', {}, [title ? `${title} | ${siteName}` : siteName]),
|
||||
h('link', { rel: 'icon', href: '/favicon.ico' }),
|
||||
h('link', { rel: 'stylesheet', href: '/src/global.css' }),
|
||||
h('script', { type: 'module', src: '/src/main.js' }),
|
||||
]),
|
||||
h('body', {}, [raw(body), h('nav', { 'aria-label': 'Breadcrumb' }, [raw(nav(path, navTitle))])]),
|
||||
]))}`;
|
||||
|
||||
const nav = (path, currentTitle = '') => {
|
||||
if (path === '/') {
|
||||
if (!currentTitle) return render(h('span', {}, [siteName]));
|
||||
return render([h('a', { href: '/' }, [siteName]), ' / ', currentTitle]);
|
||||
}
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
const children = [h('a', { href: '/' }, [siteName])];
|
||||
const sectionTitle = { notes: 'Notes', posts: 'Posts', about: 'About', licenses: 'Licenses' }[parts[0]] ?? parts[0];
|
||||
if (parts.length >= 1) children.push(' / ', parts.length > 1 ? h('a', { href: `/${parts[0]}/` }, [sectionTitle]) : sectionTitle);
|
||||
if (parts.length > 1) children.push(' / ', currentTitle);
|
||||
return render(children);
|
||||
};
|
||||
|
||||
const dateFor = async (file) => {
|
||||
try {
|
||||
const gitDate = execFileSync('git', ['log', '-1', '--date=format-local:%Y-%m-%dT%H:%M:%SZ', '--format=%ad', '--', file], { env: { ...process.env, TZ: 'UTC' } }).toString().trim();
|
||||
if (gitDate) return gitDate;
|
||||
} catch {}
|
||||
try { return (await stat(file)).mtime.toISOString(); }
|
||||
catch { return ''; }
|
||||
};
|
||||
|
||||
const readPages = async (dir) => {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
const markdownEntries = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md') && !entry.name.startsWith('.'))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const pages = await Promise.all(markdownEntries.map(async (entry) => {
|
||||
const file = join(dir, entry.name);
|
||||
const source = await readFile(file, 'utf8');
|
||||
const titleLine = source.split(/\r?\n/).find((line) => line.startsWith('# '));
|
||||
if (!titleLine) return undefined;
|
||||
return {
|
||||
name: entry.name.slice(0, -3),
|
||||
title: stripSidenotes(titleLine.slice(2).trim()),
|
||||
file,
|
||||
source,
|
||||
date: await dateFor(file),
|
||||
};
|
||||
}));
|
||||
return pages.filter(Boolean);
|
||||
};
|
||||
|
||||
let nextHeadingId = createSlugger();
|
||||
let sidenoteNumber = 0;
|
||||
|
||||
marked.use({
|
||||
extensions: [{
|
||||
name: 'sidenote',
|
||||
level: 'inline',
|
||||
start: (source) => source.indexOf('^['),
|
||||
tokenizer(source) {
|
||||
if (!source.startsWith('^[')) return undefined;
|
||||
let depth = 1;
|
||||
for (let index = 2; index < source.length; index += 1) {
|
||||
if (source[index] === '\\') {
|
||||
index += 1;
|
||||
} else if (source[index] === '[') {
|
||||
depth += 1;
|
||||
} else if (source[index] === ']') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
const text = source.slice(2, index);
|
||||
return {
|
||||
type: 'sidenote',
|
||||
raw: source.slice(0, index + 1),
|
||||
tokens: this.lexer.inlineTokens(text),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
renderer({ tokens }) {
|
||||
sidenoteNumber += 1;
|
||||
const id = `sidenote-${sidenoteNumber}`;
|
||||
const content = this.parser.parseInline(tokens);
|
||||
return `<input type="checkbox" id="${id}" class="sidenote-toggle visually-hidden"><label for="${id}" class="margin-toggle sidenote-number"><span class="visually-hidden">Toggle sidenote ${sidenoteNumber}</span></label><span class="sidenote">${content}</span>`;
|
||||
},
|
||||
}],
|
||||
renderer: {
|
||||
code({ text, lang, escaped }) {
|
||||
const language = lang?.trim().split(/\s+/, 1)[0].toLowerCase() || 'plain';
|
||||
const label = languageLabels[language]
|
||||
?? language.charAt(0).toUpperCase() + language.slice(1);
|
||||
const source = escaped ? text : escapeHtml(text.replace(/\n$/, ''));
|
||||
return `<div class="code-block"><div class="code-block-header"><span>${escapeHtml(label)}</span><button type="button" class="copy-code" aria-label="Copy ${escapeHtml(label)} code" aria-live="polite">Copy</button></div><pre><code class="language-${escapeHtml(language)}">${source}\n</code></pre></div>\n`;
|
||||
},
|
||||
heading({ tokens, depth }) {
|
||||
const text = this.parser.parseInline(tokens);
|
||||
const id = nextHeadingId(tokens);
|
||||
return `<h${depth} id="${id}">${text}</h${depth}>\n`;
|
||||
},
|
||||
},
|
||||
});
|
||||
const withoutTitle = (source) => {
|
||||
const lines = source.split('\n');
|
||||
const index = lines.findIndex((line) => line.startsWith('# '));
|
||||
if (index >= 0) lines.splice(index, 1);
|
||||
return lines.join('\n');
|
||||
};
|
||||
const stripSidenotes = (text) => {
|
||||
let result = '';
|
||||
for (let index = 0; index < text.length;) {
|
||||
if (text[index] === '^' && text[index + 1] === '[') {
|
||||
const end = text.indexOf(']', index + 2);
|
||||
if (end >= 0) { index = end + 1; continue; }
|
||||
}
|
||||
result += text[index++];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const stripHeadingSidenotes = (source) => source.split('\n').map((line) => {
|
||||
const content = line.trimStart();
|
||||
return content.startsWith('# ') || content.startsWith('## ') || content.startsWith('### ') || content.startsWith('#### ') || content.startsWith('##### ') || content.startsWith('###### ')
|
||||
? line.slice(0, line.length - content.length) + stripSidenotes(content)
|
||||
: line;
|
||||
}).join('\n');
|
||||
const renderMarkdown = (source) => {
|
||||
nextHeadingId = createSlugger();
|
||||
sidenoteNumber = 0;
|
||||
const html = marked.parse(withoutTitle(stripHeadingSidenotes(source)));
|
||||
let result = '';
|
||||
let cursor = 0;
|
||||
while (cursor < html.length) {
|
||||
const start = html.indexOf('<table', cursor);
|
||||
if (start < 0) { result += html.slice(cursor); break; }
|
||||
const end = html.indexOf('</table>', start);
|
||||
if (end < 0) { result += html.slice(cursor); break; }
|
||||
result += html.slice(cursor, start);
|
||||
result += `<div class="table-scroll">${html.slice(start, end + 8)}</div>`;
|
||||
cursor = end + 8;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const toc = (source) => {
|
||||
const headings = marked.lexer(withoutTitle(stripHeadingSidenotes(source))).filter((token) => token.type === 'heading');
|
||||
if (headings.length === 0) return '';
|
||||
const root = { depth: 0, children: [] };
|
||||
const stack = [root];
|
||||
const headingId = createSlugger();
|
||||
for (const heading of headings) {
|
||||
while (stack.length > 1 && stack.at(-1).depth >= heading.depth) stack.pop();
|
||||
const node = { depth: heading.depth, heading, children: [] };
|
||||
stack.at(-1).children.push(node);
|
||||
stack.push(node);
|
||||
}
|
||||
const renderItems = (items) => `<ol>${items.map(({ heading, children }) => {
|
||||
const id = headingId(heading.tokens);
|
||||
const nested = children.length > 0 ? renderItems(children) : '';
|
||||
return `<li><a href="#${id}">${escapeHtml(headingLabel(heading.tokens))}</a>${nested}</li>`;
|
||||
}).join('')}</ol>`;
|
||||
return renderItems(root.children);
|
||||
};
|
||||
|
||||
const writePage = async (path, html) => {
|
||||
const directory = join(output, path);
|
||||
await mkdir(directory, { recursive: true });
|
||||
await writeFile(join(directory, 'index.html'), html);
|
||||
};
|
||||
|
||||
const titleHeading = (title, className = '') => `
|
||||
<header class="page-heading${className ? ` ${className}` : ''}">
|
||||
<h1 class="title">${escapeHtml(title)}</h1>
|
||||
</header>`;
|
||||
|
||||
const renderHome = (externalLinks, sectionLinks, recentLinks) => pageShell('', `
|
||||
${titleHeading(siteName)}
|
||||
<main class="container">
|
||||
<section class="block">${sectionLinks}${externalLinks}</section>
|
||||
<section class="block recent">
|
||||
<h2>Recent</h2>
|
||||
<ul>${recentLinks || '<li>Nothing to show yet.</li>'}</ul>
|
||||
</section>
|
||||
</main>`);
|
||||
|
||||
const renderNotesTable = (pages, directory) => {
|
||||
const rows = pages.map((page) => `
|
||||
<tr>
|
||||
<td><a href="/${directory}/${page.name}/">${escapeHtml(page.title)}</a></td>
|
||||
<td>${page.date ? `<time class="table-last-updated" datetime="${escapeHtml(page.date)}" data-value="${escapeHtml(page.date)}">${escapeHtml(page.date)}</time>` : ''}</td>
|
||||
</tr>`).join('');
|
||||
return `<table class="${directory}-table"><caption class="visually-hidden">${escapeHtml(directory)} pages</caption><thead><tr><th scope="col">Page</th><th scope="col">Last updated</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
};
|
||||
|
||||
const renderPostsList = (pages, directory) => {
|
||||
const items = pages.map((page) => `
|
||||
<li>
|
||||
<a href="/${directory}/${page.name}/">${escapeHtml(page.title)}</a>
|
||||
${page.date ? `<time datetime="${escapeHtml(page.date)}" data-value="${escapeHtml(page.date)}">${escapeHtml(page.date)}</time>` : ''}
|
||||
</li>`).join('');
|
||||
return `<ul class="posts-list">${items}</ul>`;
|
||||
};
|
||||
|
||||
const renderEmptySection = () => `
|
||||
<div class="text center">
|
||||
<p><strong>crickets</strong></p>
|
||||
<p>Nothing to list...</p>
|
||||
<a class="link button back" href="/">Go Home</a>
|
||||
</div>`;
|
||||
|
||||
const renderSection = (directory, pages) => {
|
||||
const title = directory[0].toUpperCase() + directory.slice(1);
|
||||
const listing = pages.length === 0
|
||||
? renderEmptySection()
|
||||
: directory === 'posts'
|
||||
? renderPostsList(pages, directory)
|
||||
: renderNotesTable(pages, directory);
|
||||
|
||||
return pageShell(
|
||||
title,
|
||||
`${titleHeading(title)}<main class="container"><section class="block">${listing}</section></main>`,
|
||||
`/${directory}`,
|
||||
title,
|
||||
);
|
||||
};
|
||||
|
||||
const renderArticle = (directory, page, navigationPath = `/${directory}/${page.name}`) => {
|
||||
const updated = page.date
|
||||
? `<time class="last-updated" datetime="${escapeHtml(page.date)}" data-value="${escapeHtml(page.date)}">Last updated: ${escapeHtml(page.date)}</time>`
|
||||
: '';
|
||||
const heading = titleHeading(page.title, `${directory}-heading`);
|
||||
const tableOfContents = toc(page.source);
|
||||
const contents = tableOfContents
|
||||
? `<section class="block toc"><details open><summary>Contents</summary>${tableOfContents}</details></section><hr>`
|
||||
: '';
|
||||
const article = `<section class="block">${renderMarkdown(page.source)}</section>`;
|
||||
const body = `${heading}<main class="article-page ${directory}-page container">${updated}${contents}${article}</main>`;
|
||||
|
||||
return pageShell(page.title, body, navigationPath, page.title);
|
||||
};
|
||||
|
||||
if (process.argv.includes('--clean')) await rm(output, { recursive: true, force: true });
|
||||
await mkdir(output, { recursive: true });
|
||||
const sections = Object.fromEntries(await Promise.all(markdownDirs.map(async (dir) => [dir, await readPages(dir)])));
|
||||
|
||||
const external = JSON.parse(await readFile('external.json', 'utf8'));
|
||||
const externalLinks = Object.entries(external).map(([text, href]) => `<a class="button blue link extern" href="${escapeHtml(href)}">${escapeHtml(text)}</a>`).join('');
|
||||
const sectionLinks = markdownDirs.map((dir) => `<a class="button link" href="/${dir}/">${dir[0].toUpperCase() + dir.slice(1)}</a>`).join('');
|
||||
const standaloneLinks = standalonePages
|
||||
.filter((page) => page.path !== 'licenses')
|
||||
.map((page) => `<a class="button link" href="/${page.path}/">${page.title}</a>`)
|
||||
.join('');
|
||||
const recentPages = markdownDirs.flatMap((dir) => sections[dir].map((page) => ({ ...page, dir })))
|
||||
.sort((a, b) => b.date.localeCompare(a.date))
|
||||
.slice(0, 5);
|
||||
const recentLinks = recentPages.map((page) => `<li>${page.date ? `<time class="recent-date" datetime="${escapeHtml(page.date)}" data-value="${escapeHtml(page.date)}" data-tooltip="${escapeHtml(page.date)}">${escapeHtml(page.date)}</time>` : ''}<span class="recent-gap" aria-hidden="true"></span><span class="recent-type">${page.dir}</span><a href="/${page.dir}/${page.name}/">${escapeHtml(page.title)}</a></li>`).join('');
|
||||
await writeFile(join(output, 'index.html'), renderHome(externalLinks, sectionLinks + standaloneLinks, recentLinks));
|
||||
|
||||
for (const dir of markdownDirs) {
|
||||
const pages = sections[dir];
|
||||
await writePage(dir, renderSection(dir, pages));
|
||||
|
||||
for (const page of pages) {
|
||||
await writePage(join(dir, page.name), renderArticle(dir, page));
|
||||
}
|
||||
}
|
||||
|
||||
for (const page of standalonePages) {
|
||||
const source = await readFile(page.source, 'utf8');
|
||||
const generatedPage = {
|
||||
name: page.path,
|
||||
title: page.title,
|
||||
source,
|
||||
date: await dateFor(page.source),
|
||||
};
|
||||
await writePage(page.path, renderArticle(page.path, generatedPage, '/'));
|
||||
}
|
||||
-838
@@ -1,838 +0,0 @@
|
||||
package www
|
||||
|
||||
import "core:sort"
|
||||
import "base:runtime"
|
||||
import "core:bufio"
|
||||
import "core:c"
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import md "vendor:commonmark"
|
||||
|
||||
SITE_NAME :: "PaulW.XYZ"
|
||||
|
||||
COMMON_STYLESHEETS :: []string {
|
||||
"/assets/global.css",
|
||||
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/themes/prism-tomorrow.min.css",
|
||||
}
|
||||
COMMON_SCRIPTS :: []string {
|
||||
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/components/prism-core.min.js",
|
||||
"https://cdn.jsdelivr.net/npm/prismjs@1.30.0/plugins/autoloader/prism-autoloader.min.js",
|
||||
}
|
||||
|
||||
OUTPUT_ROOT_NAME :: "build"
|
||||
|
||||
markdown_paths :: []string{"notes", "posts"}
|
||||
|
||||
main :: proc() {
|
||||
create_output_directories(markdown_paths)
|
||||
copy_public_assets()
|
||||
|
||||
home_page := generate_sitemap()
|
||||
dump_page(home_page)
|
||||
|
||||
sb := strings.builder_make()
|
||||
|
||||
process_markdown(&sb, home_page)
|
||||
generate_home(&sb, home_page)
|
||||
}
|
||||
|
||||
Page :: struct {
|
||||
title: string,
|
||||
pages: map[string]Page,
|
||||
}
|
||||
|
||||
add_page :: proc(parent: ^Page, key: string, value: Page) {
|
||||
parent.pages[key] = value
|
||||
}
|
||||
|
||||
dump_page :: proc(page: Page, indent_level := 0) {
|
||||
for i in 0 ..< indent_level {
|
||||
fmt.print("\t")
|
||||
}
|
||||
fmt.println(page.title)
|
||||
for name, page in page.pages {
|
||||
for i in 0 ..= indent_level {
|
||||
fmt.print("\t")
|
||||
}
|
||||
fmt.println("/", name)
|
||||
dump_page(page, indent_level + 1)
|
||||
}
|
||||
}
|
||||
|
||||
generate_sitemap :: proc() -> Page {
|
||||
home_page: Page = {
|
||||
title = SITE_NAME,
|
||||
pages = make(map[string]Page),
|
||||
}
|
||||
|
||||
process_markdown_parent(&home_page, "notes", "Notes")
|
||||
process_markdown_parent(&home_page, "posts", "Posts")
|
||||
|
||||
return home_page
|
||||
}
|
||||
|
||||
process_markdown_parent :: proc(parent: ^Page, dir: string, title: string) {
|
||||
page := Page {
|
||||
title = title,
|
||||
pages = make(map[string]Page),
|
||||
}
|
||||
|
||||
files, os_err := os.read_directory_by_path(dir, 0, context.temp_allocator)
|
||||
if os_err != nil {
|
||||
fmt.eprintfln("failed to read files from directory '%s' (%s)", dir, os_err)
|
||||
return
|
||||
}
|
||||
for file_info in files {
|
||||
if len(file_info.name) < 1 || file_info.name[0] == '.' do continue
|
||||
|
||||
file, os_err := os.open(file_info.fullpath, {.Read})
|
||||
if os_err != nil {
|
||||
panic("something funny happened lol; TODO of course!!!")
|
||||
}
|
||||
defer os.close(file)
|
||||
|
||||
reader: bufio.Reader
|
||||
bufio.reader_init(&reader, os.to_reader(file))
|
||||
defer bufio.reader_destroy(&reader)
|
||||
|
||||
line := bufio.reader_read_string(&reader, '\n', context.temp_allocator) or_continue
|
||||
trimmed := strings.trim(line, "\n\r \t")
|
||||
|
||||
if len(trimmed) < 5 || trimmed[0] != '#' || trimmed[1] != ' ' do continue
|
||||
|
||||
title := strings.substring_from(trimmed, 2) or_continue
|
||||
|
||||
ext := os.ext(file_info.name)
|
||||
if ext != ".md" do continue
|
||||
output_name := strings.trim_suffix(file_info.name, ext)
|
||||
|
||||
md_page := Page {
|
||||
title = strings.clone(title),
|
||||
}
|
||||
add_page(&page, output_name, md_page)
|
||||
|
||||
}
|
||||
add_page(parent, dir, page)
|
||||
}
|
||||
|
||||
copy_public_assets :: proc() {
|
||||
err := os.copy_directory_all(OUTPUT_ROOT_NAME, "public")
|
||||
if err != nil {
|
||||
fmt.eprintln("error copying public assets", err)
|
||||
}
|
||||
}
|
||||
|
||||
create_output_directories :: proc(dir_names: []string) {
|
||||
os_err := os.make_directory(OUTPUT_ROOT_NAME)
|
||||
if os_err != nil && os_err != .Exist {
|
||||
fmt.eprintfln("cannot create output root directory '%s': %s", OUTPUT_ROOT_NAME, os_err)
|
||||
return
|
||||
}
|
||||
|
||||
for dir_name in dir_names {
|
||||
dir_path, alloc_err := os.join_path({OUTPUT_ROOT_NAME, dir_name}, context.temp_allocator)
|
||||
if alloc_err != nil {
|
||||
fmt.eprintln("allocator error:", alloc_err)
|
||||
continue
|
||||
}
|
||||
|
||||
os_err = os.make_directory(dir_path)
|
||||
if os_err != nil && os_err != .Exist {
|
||||
fmt.eprintln("failed to create directory:", dir_path)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html_escape :: proc(sb: ^strings.Builder, html: string) {
|
||||
strings.write_string(sb, html)
|
||||
}
|
||||
|
||||
render_html :: proc(sb: ^strings.Builder, root: ^md.Node) {
|
||||
iter := md.iter_new(root)
|
||||
defer md.iter_free(iter)
|
||||
should_write_slug: i32 = 0
|
||||
parent_is_li := false
|
||||
|
||||
for md.iter_next(iter) != .Done {
|
||||
node := md.iter_get_node(iter)
|
||||
event := md.iter_get_event_type(iter)
|
||||
|
||||
#partial switch node.type {
|
||||
case .Heading:
|
||||
level := md.node_get_heading_level(node)
|
||||
|
||||
if event == .Enter {
|
||||
should_write_slug = level
|
||||
} else {
|
||||
fmt.sbprintf(sb, "</h%d>\n", level)
|
||||
}
|
||||
|
||||
case .Paragraph:
|
||||
if parent_is_li {
|
||||
if event != .Enter do parent_is_li = false
|
||||
} else {
|
||||
if event == .Enter {
|
||||
strings.write_string(sb, "<p>")
|
||||
} else if event == .Exit {
|
||||
strings.write_string(sb, "</p>\n")
|
||||
}
|
||||
}
|
||||
|
||||
case .Emph:
|
||||
if event == .Enter {
|
||||
strings.write_string(sb, "<em>")
|
||||
} else {
|
||||
strings.write_string(sb, "</em>")
|
||||
}
|
||||
|
||||
case .Strong:
|
||||
if event == .Enter {
|
||||
strings.write_string(sb, "<strong>")
|
||||
} else {
|
||||
strings.write_string(sb, "</strong>")
|
||||
}
|
||||
|
||||
case .Code:
|
||||
strings.write_string(sb, "<code>")
|
||||
lit := string(md.node_get_literal(node))
|
||||
html_escape(sb, lit)
|
||||
strings.write_string(sb, "</code>")
|
||||
|
||||
case .Text:
|
||||
lit := string(md.node_get_literal(node))
|
||||
if should_write_slug > 0 {
|
||||
fmt.sbprintf(sb, "<h%d id='", should_write_slug)
|
||||
sb_write_slug(sb, lit)
|
||||
strings.write_string(sb, "'>")
|
||||
should_write_slug = 0
|
||||
}
|
||||
html_escape(sb, lit)
|
||||
|
||||
case .Soft_Break:
|
||||
strings.write_byte(sb, ' ')
|
||||
|
||||
case .Line_Break:
|
||||
strings.write_string(sb, "<br />\n")
|
||||
|
||||
case .Code_Block:
|
||||
fence_info := string(md.node_get_fence_info(node))
|
||||
i := strings.index_any(fence_info, " \t\r\n")
|
||||
|
||||
lang := fence_info
|
||||
if i != -1 {
|
||||
lang = fence_info[:i]
|
||||
}
|
||||
|
||||
strings.write_string(sb, "<pre class='language-")
|
||||
strings.write_string(sb, lang)
|
||||
strings.write_string(sb, "'><code>")
|
||||
html_escape(sb, string(md.node_get_literal(node)))
|
||||
strings.write_string(sb, "</code></pre>\n")
|
||||
|
||||
case .HTML_Inline:
|
||||
if event == .Enter {
|
||||
strings.write_string(sb, string(md.node_get_literal(node)))
|
||||
}
|
||||
|
||||
case .HTML_Block:
|
||||
if event == .Enter {
|
||||
strings.write_string(sb, string(md.node_get_literal(node)))
|
||||
}
|
||||
|
||||
case .Thematic_Break:
|
||||
if event == .Enter {
|
||||
strings.write_string(sb, "<hr />\n")
|
||||
}
|
||||
|
||||
case .List:
|
||||
if event == .Enter {
|
||||
if md.node_get_list_type(node) == .Bullet {
|
||||
strings.write_string(sb, "<ul>\n")
|
||||
} else {
|
||||
strings.write_string(sb, "<ol>\n")
|
||||
}
|
||||
} else {
|
||||
if md.node_get_list_type(node) == .Bullet {
|
||||
strings.write_string(sb, "</ul>\n")
|
||||
} else {
|
||||
strings.write_string(sb, "</ol>\n")
|
||||
}
|
||||
}
|
||||
|
||||
case .Item:
|
||||
if event == .Enter {
|
||||
parent_is_li = true
|
||||
strings.write_string(sb, "<li>")
|
||||
} else {
|
||||
strings.write_string(sb, "</li>\n")
|
||||
}
|
||||
|
||||
case .Link:
|
||||
if event == .Enter {
|
||||
fmt.sbprintf(sb,
|
||||
"<a href=\"%s\">",
|
||||
string(md.node_get_url(node)))
|
||||
} else {
|
||||
strings.write_string(sb, "</a>")
|
||||
}
|
||||
|
||||
case .Image:
|
||||
if event == .Enter {
|
||||
fmt.sbprintf(sb,
|
||||
"<img src=\"%s\" alt=\"",
|
||||
string(md.node_get_url(node)))
|
||||
} else {
|
||||
strings.write_string(sb, "\" />")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove_title :: proc(root: ^md.Node) {
|
||||
heading_state: enum {
|
||||
None,
|
||||
Entered,
|
||||
//Copied,
|
||||
Exited,
|
||||
} = .None
|
||||
//title: string
|
||||
iter := md.iter_new(root)
|
||||
defer md.iter_free(iter)
|
||||
for md.iter_next(iter) != .Done {
|
||||
event_type := md.iter_get_event_type(iter)
|
||||
node := md.iter_get_node(iter)
|
||||
|
||||
#partial switch node.type {
|
||||
case .Heading:
|
||||
level := md.node_get_heading_level(node)
|
||||
if level == 1 && heading_state != .Exited {
|
||||
if event_type == .Enter {
|
||||
heading_state = .Entered
|
||||
} else if event_type == .Exit {
|
||||
heading_state = .Exited
|
||||
md.node_unlink(node)
|
||||
md.node_free(node)
|
||||
}
|
||||
}
|
||||
// case .Text:
|
||||
// if heading_state == .Entered {
|
||||
// heading_state = .Copied
|
||||
// title = strings.clone_from_cstring(md.node_get_literal(node))
|
||||
// }
|
||||
}
|
||||
}
|
||||
//return title
|
||||
}
|
||||
|
||||
sb_indent :: proc(sb: ^strings.Builder, level: i32) {
|
||||
for i in 0..<level {
|
||||
strings.write_string(sb, " ")
|
||||
}
|
||||
}
|
||||
|
||||
sb_write_slug :: proc(sb: ^strings.Builder, text: string) {
|
||||
wrote_any := false
|
||||
pending_dash := false
|
||||
|
||||
for c in text {
|
||||
switch {
|
||||
case c >= 'A' && c <= 'Z':
|
||||
if pending_dash {
|
||||
strings.write_byte(sb, '-')
|
||||
pending_dash = false
|
||||
}
|
||||
strings.write_byte(sb, byte(c + ('a' - 'A')))
|
||||
wrote_any = true
|
||||
|
||||
case (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'):
|
||||
if pending_dash {
|
||||
strings.write_byte(sb, '-')
|
||||
pending_dash = false
|
||||
}
|
||||
strings.write_byte(sb, byte(c))
|
||||
wrote_any = true
|
||||
|
||||
case:
|
||||
if wrote_any {
|
||||
pending_dash = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_toc :: proc(sb: ^strings.Builder, root: ^md.Node) {
|
||||
entered_heading := false
|
||||
started := false
|
||||
iter := md.iter_new(root)
|
||||
defer md.iter_free(iter)
|
||||
current_level: i32 = 0
|
||||
for md.iter_next(iter) != .Done {
|
||||
event_type := md.iter_get_event_type(iter)
|
||||
node := md.iter_get_node(iter)
|
||||
|
||||
#partial switch node.type {
|
||||
case .Heading:
|
||||
level := md.node_get_heading_level(node)
|
||||
event_type := md.iter_get_event_type(iter)
|
||||
|
||||
if event_type == .Enter {
|
||||
entered_heading = true
|
||||
if !started {
|
||||
for i in 1..<level do strings.write_string(sb, "<ol>")
|
||||
started = true
|
||||
} else {
|
||||
for i in current_level..<level do strings.write_string(sb, "<ol>")
|
||||
if current_level > level do strings.write_string(sb, "</li>")
|
||||
for i in level..<current_level do strings.write_string(sb, "</ol></li>")
|
||||
if current_level == level do strings.write_string(sb, "</li>")
|
||||
}
|
||||
|
||||
strings.write_string(sb, "<li>")
|
||||
current_level = level
|
||||
}
|
||||
case .Text:
|
||||
if entered_heading {
|
||||
entered_heading = false
|
||||
text := string(md.node_get_literal(node))
|
||||
strings.write_string(sb, "<a href='#")
|
||||
sb_write_slug(sb, text)
|
||||
strings.write_string(sb, "'>")
|
||||
strings.write_string(sb, text)
|
||||
strings.write_string(sb, "</a>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if started {
|
||||
strings.write_string(sb, "</li>")
|
||||
for i in 1..<current_level do strings.write_string(sb, "</ol>")
|
||||
}
|
||||
}
|
||||
|
||||
// must free
|
||||
parse_json :: proc(file_path: string) -> json.Value {
|
||||
json_file_content, os_err := os.read_entire_file_from_path(file_path, context.temp_allocator)
|
||||
if os_err != nil {
|
||||
fmt.eprintfln("failed to read files from directory '%s' (%s)", file_path, os_err)
|
||||
return nil
|
||||
}
|
||||
json_value, json_err := json.parse(
|
||||
json_file_content,
|
||||
json.DEFAULT_SPECIFICATION,
|
||||
true,
|
||||
context.allocator,
|
||||
)
|
||||
if json_err != .None {
|
||||
fmt.eprintfln("error parsing json file '%s' (%s)", file_path, json_err)
|
||||
return nil
|
||||
}
|
||||
return json_value
|
||||
}
|
||||
|
||||
generate_home :: proc(sb: ^strings.Builder, site: Page) {
|
||||
strings.builder_reset(sb)
|
||||
EXTERNAL_JSON_FILE :: "external.json"
|
||||
|
||||
external_json := parse_json(EXTERNAL_JSON_FILE)
|
||||
defer json.destroy_value(external_json)
|
||||
|
||||
output_file_path, alloc_err := os.join_path(
|
||||
{OUTPUT_ROOT_NAME, "index.html"},
|
||||
context.temp_allocator,
|
||||
)
|
||||
if alloc_err != nil {
|
||||
fmt.eprintln("allocator error:", alloc_err)
|
||||
return
|
||||
}
|
||||
|
||||
output_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc})
|
||||
if os_err2 != nil {
|
||||
fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2)
|
||||
return
|
||||
}
|
||||
defer os.close(output_file)
|
||||
|
||||
{
|
||||
html_begin(sb)
|
||||
defer html_end(sb, {})
|
||||
|
||||
heading_title(sb, SITE_NAME)
|
||||
|
||||
container_begin(sb)
|
||||
defer container_end(sb)
|
||||
|
||||
{
|
||||
block_begin(sb)
|
||||
defer block_end(sb)
|
||||
|
||||
obj := external_json.(json.Object)
|
||||
for text, href in obj {
|
||||
fmt.sbprintf(
|
||||
sb,
|
||||
"<a class='button blue link extern' href='%s'>%s</a>",
|
||||
href.(json.String),
|
||||
text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
block_begin(sb)
|
||||
defer block_end(sb)
|
||||
|
||||
fmt.sbprintf(sb, "<h2>%s</h2>", "Navigation")
|
||||
|
||||
for name, page in site.pages {
|
||||
fmt.sbprintf(sb, "<a class='button green link' href='%s'>%s</a>", name, page.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.write_string(output_file, strings.to_string(sb^))
|
||||
}
|
||||
|
||||
set_pages_to_process :: proc(page: Page, pages: ^map[string]^Page, parent_path := "") {
|
||||
for name, &page in page.pages {
|
||||
current_path := strings.trim(fmt.tprintf("%s/%s", parent_path, name), "/")
|
||||
for path in markdown_paths {
|
||||
if strings.trim(path, "/") == current_path do map_insert(pages, path, &page)
|
||||
}
|
||||
set_pages_to_process(page, pages, current_path)
|
||||
}
|
||||
}
|
||||
|
||||
process_markdown :: proc(sb: ^strings.Builder, site: Page) {
|
||||
pages_to_process := make(map[string]^Page)
|
||||
defer delete(pages_to_process)
|
||||
set_pages_to_process(site, &pages_to_process)
|
||||
|
||||
for markdown_dir, page in pages_to_process {
|
||||
markdown_dir_toc := make([dynamic][3]string) // path, title, date
|
||||
defer delete(markdown_dir_toc)
|
||||
defer create_toc_page(page.title, markdown_dir, &markdown_dir_toc, site)
|
||||
|
||||
for md_name, md_page in page.pages {
|
||||
current_path := fmt.tprintf("/%s/%s", markdown_dir, md_name)
|
||||
|
||||
input_file_name := fmt.tprint(md_name, ".md", sep = "")
|
||||
input_file_path, alloc_err := os.join_path(
|
||||
{markdown_dir, input_file_name},
|
||||
context.temp_allocator,
|
||||
)
|
||||
|
||||
git_state, git_out, git_err, os_err := os.process_exec(
|
||||
os.Process_Desc {
|
||||
command = {
|
||||
"git",
|
||||
"log",
|
||||
"-1",
|
||||
"--date=format-local:%Y-%m-%dT%H:%M:%SZ",
|
||||
"--format=%ad",
|
||||
"--",
|
||||
input_file_path,
|
||||
},
|
||||
env = {"TZ=UTC"},
|
||||
},
|
||||
context.temp_allocator,
|
||||
)
|
||||
if os_err != nil {
|
||||
fmt.eprintln("failed to start git process", os_err, git_err)
|
||||
return
|
||||
}
|
||||
date := strings.trim(string(git_out), " \t\n\r")
|
||||
|
||||
// @leak
|
||||
append(&markdown_dir_toc, [3]string{strings.clone(current_path), md_page.title, date})
|
||||
|
||||
file, os_err1 := os.read_entire_file(input_file_path, context.temp_allocator)
|
||||
|
||||
strings.builder_reset(sb)
|
||||
cmark_root := md.parse_document(
|
||||
cast([^]u8)raw_data(file),
|
||||
len(file),
|
||||
{.Validate_UTF8, .Unsafe},
|
||||
)
|
||||
remove_title(cmark_root)
|
||||
|
||||
|
||||
dir, alloc_err3 := os.join_path(
|
||||
{OUTPUT_ROOT_NAME, markdown_dir, md_name},
|
||||
context.temp_allocator
|
||||
)
|
||||
if alloc_err3 != nil {
|
||||
fmt.println("allocator error:", alloc_err3)
|
||||
continue
|
||||
}
|
||||
os.make_directory(dir)
|
||||
|
||||
output_file_path, alloc_err2 := os.join_path(
|
||||
{OUTPUT_ROOT_NAME, markdown_dir, md_name, "index.html"},
|
||||
context.temp_allocator,
|
||||
)
|
||||
|
||||
if alloc_err2 != nil {
|
||||
fmt.println("allocator error:", alloc_err3)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
out_file, os_err2 := os.open(output_file_path, {.Write, .Create, .Trunc})
|
||||
if os_err2 != nil {
|
||||
fmt.eprintfln("failed to open output file '%s' (%s)", output_file_path, os_err2)
|
||||
continue
|
||||
}
|
||||
defer os.close(out_file)
|
||||
|
||||
{
|
||||
html_begin(sb, md_page.title)
|
||||
defer html_end(sb, site, current_path)
|
||||
|
||||
heading_title(sb, md_page.title)
|
||||
|
||||
|
||||
container_begin(sb)
|
||||
defer container_end(sb)
|
||||
|
||||
last_updated_date(sb, date)
|
||||
|
||||
{
|
||||
block_begin(sb)
|
||||
defer block_end(sb)
|
||||
|
||||
strings.write_string(sb, "<h2>Contents</h2>")
|
||||
|
||||
generate_toc(sb, cmark_root)
|
||||
}
|
||||
strings.write_string(sb, "<hr>")
|
||||
{
|
||||
block_begin(sb)
|
||||
defer block_end(sb)
|
||||
|
||||
render_html(sb, cmark_root)
|
||||
}
|
||||
}
|
||||
|
||||
write_len: int
|
||||
write_len, os_err = os.write_strings(out_file, strings.to_string(sb^))
|
||||
if os_err != nil {
|
||||
fmt.eprintfln("failed to write to output file '%s' (%s)", output_file_path, os_err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
create_toc_page :: proc(title, toc_path: string, toc_info: ^[dynamic][3]string, site: Page) {
|
||||
_sb: strings.Builder = strings.builder_make()
|
||||
sb := &_sb
|
||||
defer strings.builder_destroy(sb)
|
||||
toc_file_path, alloc_err := os.join_path(
|
||||
{OUTPUT_ROOT_NAME, toc_path, "index.html"},
|
||||
context.temp_allocator,
|
||||
)
|
||||
if alloc_err != nil {
|
||||
fmt.eprintln("allocator error:", alloc_err)
|
||||
return
|
||||
}
|
||||
|
||||
sort_toc :: proc(a: [3]string, b: [3]string) -> int {
|
||||
return -strings.compare(a[2], b[2])
|
||||
}
|
||||
|
||||
sort.quick_sort_proc(toc_info[:], sort_toc)
|
||||
|
||||
fmt.println(toc_file_path)
|
||||
|
||||
{
|
||||
html_begin(sb, title)
|
||||
defer html_end(sb, site, toc_path)
|
||||
|
||||
heading_title(sb, title)
|
||||
|
||||
container_begin(sb)
|
||||
defer container_end(sb)
|
||||
|
||||
block_begin(sb)
|
||||
defer block_end(sb)
|
||||
|
||||
if len(toc_info) > 0 {
|
||||
table_begin(sb)
|
||||
defer table_end(sb)
|
||||
|
||||
for item in toc_info {
|
||||
tr_begin(sb)
|
||||
fmt.sbprintf(
|
||||
sb,
|
||||
"<td><a href='%s'>%s</a></td><td><span class='table-last-updated' data-value='%s'>%s</span></td>",
|
||||
item[0],
|
||||
item[1],
|
||||
item[2],
|
||||
item[2],
|
||||
)
|
||||
tr_end(sb)
|
||||
}
|
||||
|
||||
fmt.sbprint(sb,
|
||||
`<style>
|
||||
.table-last-updated {font-style: italic;margin-right:0}
|
||||
</style>
|
||||
<script>
|
||||
document.querySelectorAll('.table-last-updated')
|
||||
.forEach(i => {
|
||||
const date = new Date(i.attributes['data-value'].value);
|
||||
i.innerText = new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)
|
||||
})
|
||||
</script>`
|
||||
)
|
||||
} else {
|
||||
fmt.sbprint(
|
||||
sb,
|
||||
"<div class='text center'>",
|
||||
"<p>**crickets**</p>",
|
||||
"<p>Nothing to list...</p>",
|
||||
"<a class='link button green back' href='/'>Go Home</a>",
|
||||
"</div>",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
out_file, os_err2 := os.open(toc_file_path, {.Write, .Create, .Trunc})
|
||||
if os_err2 != nil {
|
||||
fmt.eprintfln("failed to open output file '%s' (%s)", toc_file_path, os_err2)
|
||||
return
|
||||
}
|
||||
defer os.close(out_file)
|
||||
write_len, os_err := os.write_strings(out_file, strings.to_string(_sb))
|
||||
if os_err != nil {
|
||||
fmt.eprintfln("failed to write to output file '%s' (%s)", toc_file_path, os_err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
last_updated_date :: proc(sb: ^strings.Builder, date: string) {
|
||||
strings.write_string(sb, "<span class='last-updated'>Last updated: ")
|
||||
strings.write_string(sb, date)
|
||||
strings.write_string(sb, "</span>")
|
||||
|
||||
strings.write_string(sb, "<script>const date = new Date('")
|
||||
strings.write_string(sb, date)
|
||||
strings.write_string(sb, "'); document.querySelector('.last-updated').innerText = 'Last updated: ' + new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short'}).format(date)</script>")
|
||||
}
|
||||
|
||||
nav :: proc(sb: ^strings.Builder, current_path: string, site: Page) {
|
||||
fmt.sbprint(sb, "<nav>")
|
||||
current_page := site
|
||||
if current_path == "/" {
|
||||
fmt.sbprint(sb, SITE_NAME)
|
||||
} else {
|
||||
fmt.sbprintf(sb, "<a href='%s'>%s</a>", "/", SITE_NAME)
|
||||
res, ok := strings.split(current_path, "/", context.temp_allocator)
|
||||
|
||||
temp_sb := strings.builder_make(context.temp_allocator)
|
||||
for i in 1 ..< (len(res) - 1) {
|
||||
current_page = current_page.pages[res[i]]
|
||||
fmt.sbprintf(&temp_sb, "/%s", res[i])
|
||||
fmt.sbprintf(
|
||||
sb,
|
||||
" / <a href='%s'>%s</a>",
|
||||
strings.to_string(temp_sb),
|
||||
current_page.title,
|
||||
)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
fmt.sbprint(sb, " /", current_page.pages[res[len(res) - 1]].title)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.sbprint(sb, "</nav>")
|
||||
}
|
||||
|
||||
has_called_html_begin := 0
|
||||
html_begin :: proc(sb: ^strings.Builder, title: string = "") {
|
||||
assert(has_called_html_begin >= 0)
|
||||
has_called_html_begin += 1
|
||||
fmt.sbprint(sb, "<!DOCTYPE html>")
|
||||
fmt.sbprint(sb, "<html lang='en-US'>")
|
||||
fmt.sbprint(sb, "<head>")
|
||||
fmt.sbprint(sb, "<meta charset='utf-8'>")
|
||||
if title == "" do fmt.sbprintf(sb, "<title>%s</title>", SITE_NAME)
|
||||
else do fmt.sbprintf(sb, "<title>%s | %s</title>", title, SITE_NAME)
|
||||
for s in COMMON_STYLESHEETS {
|
||||
fmt.sbprintf(sb, "<link rel='stylesheet' href='%s' />", s)
|
||||
}
|
||||
for s in COMMON_SCRIPTS {
|
||||
fmt.sbprintf(sb, "<script type='text/javascript' src='%s'></script>", s)
|
||||
}
|
||||
fmt.sbprint(sb, "</head>")
|
||||
fmt.sbprint(sb, "<body>")
|
||||
}
|
||||
|
||||
html_end :: proc(sb: ^strings.Builder, site: Page, current_path := "/") {
|
||||
assert(has_called_html_begin > 0)
|
||||
has_called_html_begin -= 1
|
||||
nav(sb, current_path, site)
|
||||
fmt.sbprint(sb, "</body>")
|
||||
fmt.sbprint(sb, "</html>")
|
||||
}
|
||||
|
||||
has_called_container_begin := 0
|
||||
container_begin :: proc(sb: ^strings.Builder, element := "main") {
|
||||
assert(has_called_container_begin >= 0)
|
||||
has_called_container_begin += 1
|
||||
fmt.sbprintf(sb, "<%s class='container'>", element)
|
||||
}
|
||||
|
||||
container_end :: proc(sb: ^strings.Builder, element := "main") {
|
||||
assert(has_called_container_begin > 0)
|
||||
has_called_container_begin -= 1
|
||||
fmt.sbprintf(sb, "</%s>", element)
|
||||
}
|
||||
|
||||
has_called_block_begin := 0
|
||||
block_begin :: proc(sb: ^strings.Builder, element := "section") {
|
||||
assert(has_called_block_begin >= 0)
|
||||
has_called_block_begin += 1
|
||||
fmt.sbprintf(sb, "<%s class='block'>", element)
|
||||
}
|
||||
|
||||
block_end :: proc(sb: ^strings.Builder, element := "section") {
|
||||
assert(has_called_block_begin > 0)
|
||||
has_called_block_begin -= 1
|
||||
fmt.sbprintf(sb, "</%s>", element)
|
||||
}
|
||||
|
||||
heading_title :: proc(sb: ^strings.Builder, title: string) {
|
||||
fmt.sbprintf(sb, "<heading class='container'><h1 class='title'>%s</h1></heading>", title)
|
||||
}
|
||||
|
||||
has_called_table_begin := 0
|
||||
table_begin :: proc(sb: ^strings.Builder) {
|
||||
assert(has_called_table_begin >= 0)
|
||||
has_called_table_begin += 1
|
||||
fmt.sbprint(sb, "<table>")
|
||||
}
|
||||
|
||||
table_end :: proc(sb: ^strings.Builder) {
|
||||
assert(has_called_table_begin > 0)
|
||||
has_called_table_begin -= 1
|
||||
fmt.sbprintf(sb, "</table>")
|
||||
}
|
||||
|
||||
|
||||
has_called_tr_begin := 0
|
||||
tr_begin :: proc(sb: ^strings.Builder) {
|
||||
assert(has_called_tr_begin >= 0)
|
||||
has_called_tr_begin += 1
|
||||
fmt.sbprint(sb, "<tr>")
|
||||
}
|
||||
|
||||
tr_end :: proc(sb: ^strings.Builder) {
|
||||
assert(has_called_tr_begin > 0)
|
||||
has_called_tr_begin -= 1
|
||||
fmt.sbprintf(sb, "</tr>")
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
|
||||
|
||||
export const raw = (html) => ({ type: 'raw', html });
|
||||
export const h = (tag, attributes = {}, children = []) => ({
|
||||
type: 'element', tag, attributes, children: Array.isArray(children) ? children : [children],
|
||||
});
|
||||
|
||||
export const escapeHtml = (value) => String(value)
|
||||
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||||
.replaceAll('"', '"').replaceAll("'", ''');
|
||||
|
||||
export const render = (node) => {
|
||||
if (node == null || node === false) return '';
|
||||
if (Array.isArray(node)) return node.map(render).join('');
|
||||
if (typeof node === 'string' || typeof node === 'number') return escapeHtml(node);
|
||||
if (node.type === 'raw') return node.html;
|
||||
const attributes = Object.entries(node.attributes)
|
||||
.filter(([, value]) => value !== false && value != null)
|
||||
.map(([name, value]) => value === true ? name : `${name}="${escapeHtml(value)}"`)
|
||||
.join(' ');
|
||||
const opening = attributes ? `<${node.tag} ${attributes}>` : `<${node.tag}>`;
|
||||
if (voidElements.has(node.tag)) return opening;
|
||||
return `${opening}${node.children.map(render).join('')}</${node.tag}>`;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
# Web Browsers
|
||||
|
||||
Extensions/Plugins I Use on All Supported Browsers:
|
||||
- uBlock Origin
|
||||
- Decentraleyes
|
||||
@@ -8,11 +9,13 @@ Extensions/Plugins I Use on All Supported Browsers:
|
||||
- Greasemonkey/Tampermonkey
|
||||
- Indie Wiki Buddy
|
||||
- SingleFile
|
||||
- Yet Another Flags
|
||||
|
||||
Product/Service-specific Extensions:
|
||||
- SteamDB
|
||||
- Return YouTube Dislike
|
||||
- SponsorBlock
|
||||
- Enhancer for YouTube™
|
||||
|
||||
## Chromium
|
||||
- [Chromium Source Docs](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/README.md)
|
||||
|
||||
+3
-6
@@ -1,12 +1,9 @@
|
||||
# Lua Programming Language
|
||||
<!-- TODO ## Lua 5.4 C API-->
|
||||
|
||||
<!-- TODO ## Lua 5.5 C API-->
|
||||
|
||||
## Lua 5.4 Bytecode
|
||||
|
||||
> These are **unstable** and may differ in different versions of the language.
|
||||
> They are not part of the language specification but an implementation detail, which in this case is the reference implementation.
|
||||
|
||||
> The reference implementation used to have a stack based but now uses a register based VM similar to how modern real computer architectures.
|
||||
^[These are **unstable** and may differ in different versions of the language. They are not part of the language specification but an implementation detail, which in this case is the reference implementation.] ^[The reference implementation used to have a stack based but now uses a register based VM similar to how modern real computer architectures.]
|
||||
|
||||
The instructions are 32 bits wide; every instruction has an opcode that takes up 7 bits, which leaves out 25 bits for the addresses and values.
|
||||
|
||||
|
||||
+4
-2
@@ -31,6 +31,7 @@ WinCDEmu is a lightweight, open-source disc emulator that supports mounting CUE,
|
||||
- [Portable Version](https://wincdemu.sysprogs.org/portable/)
|
||||
|
||||
### Master Control Panel / God Mode
|
||||
|
||||
(Misnomer; you probably won't use this either)
|
||||
|
||||
Shows a list of all the available settings on Windows in a single view.
|
||||
@@ -40,8 +41,9 @@ Open it by exceuting the following command or saving it as a shortcut: `explorer
|
||||
## MacOS
|
||||
|
||||
### Clipboard Management
|
||||
|
||||
- [maccy](https://maccy.app/)
|
||||
- not sure why macOs doesn't have a native clipboard manager like Windows and KDE
|
||||
- not sure why macOS doesn't have a native clipboard manager like Windows and KDE
|
||||
|
||||
### Terminal Emulator
|
||||
|
||||
@@ -51,7 +53,7 @@ Open it by exceuting the following command or saving it as a shortcut: `explorer
|
||||
### Package Manager
|
||||
|
||||
- [HomeBrew](https://brew.sh)
|
||||
- package manager everyone uses but it is noticeably slow
|
||||
- package manager everyone uses
|
||||
|
||||
### Video Players
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "paulw-xyz",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.8",
|
||||
"scripts": {
|
||||
"generate": "node generator.js",
|
||||
"check": "node generator.js --clean && node check.js",
|
||||
"clean": "node clean.js",
|
||||
"dev": "node generator.js && vite",
|
||||
"build": "node generator.js --clean && node check.js && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/cardo": "^5.3.0",
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@fontsource/iosevka": "^5.3.0",
|
||||
"marked": "^16.0.0",
|
||||
"prismjs": "^1.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,93 +0,0 @@
|
||||
Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,73 +0,0 @@
|
||||
EB Garamond Variable Font
|
||||
=========================
|
||||
|
||||
This download contains EB Garamond as both variable fonts and static fonts.
|
||||
|
||||
EB Garamond is a variable font with this axis:
|
||||
wght
|
||||
|
||||
This means all the styles are contained in these files:
|
||||
EBGaramond-VariableFont_wght.ttf
|
||||
EBGaramond-Italic-VariableFont_wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for EB Garamond:
|
||||
static/EBGaramond-Regular.ttf
|
||||
static/EBGaramond-Medium.ttf
|
||||
static/EBGaramond-SemiBold.ttf
|
||||
static/EBGaramond-Bold.ttf
|
||||
static/EBGaramond-ExtraBold.ttf
|
||||
static/EBGaramond-Italic.ttf
|
||||
static/EBGaramond-MediumItalic.ttf
|
||||
static/EBGaramond-SemiBoldItalic.ttf
|
||||
static/EBGaramond-BoldItalic.ttf
|
||||
static/EBGaramond-ExtraBoldItalic.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (OFL.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them freely in your products & projects - print or digital,
|
||||
commercial or otherwise.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,45 +0,0 @@
|
||||
The work in the Hack project is Copyright 2018 Source Foundry Authors and licensed under the MIT License
|
||||
|
||||
The work in the DejaVu project was committed to the public domain.
|
||||
|
||||
Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera"
|
||||
|
||||
### MIT License
|
||||
|
||||
Copyright (c) 2018 Source Foundry Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
### BITSTREAM VERA LICENSE
|
||||
|
||||
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces.
|
||||
|
||||
The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera".
|
||||
|
||||
This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names.
|
||||
|
||||
The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself.
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,453 +0,0 @@
|
||||
:root {
|
||||
--main-background-color: #0d1117;
|
||||
--main-border-color: #555555;
|
||||
--link-color: #009dff;
|
||||
--primary-green: #099945;
|
||||
--secondary-green: #1a3a15;
|
||||
--tertiary-green: #0f200c;
|
||||
--primary-blue: #0a82b1;
|
||||
--secondary-blue: #05455f;
|
||||
--tertiary-blue: #05232f;
|
||||
--table-odd-color: rgba(255, 255, 255, 0.05);
|
||||
--table-even-color: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cantarell';
|
||||
src: url('/assets/fonts/Cantarell/Cantarell-Regular.otf') format('opentype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cantarell';
|
||||
src: url('/assets/fonts/Cantarell/Cantarell-Thin.otf') format('opentype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cantarell';
|
||||
src: url('/assets/fonts/Cantarell/Cantarell-Light.otf') format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cantarell';
|
||||
src: url('/assets/fonts/Cantarell/Cantarell-Bold.otf') format('opentype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cantarell';
|
||||
src: url('/assets/fonts/Cantarell/Cantarell-ExtraBold.otf') format('opentype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'EB Garamond';
|
||||
src: url('/assets/fonts/EB_Garamond/static/EBGaramond-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'EB Garamond';
|
||||
src: url('/assets/fonts/EB_Garamond/static/EBGaramond-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'EB Garamond';
|
||||
src: url('/assets/fonts/EB_Garamond/static/EBGaramond-ExtraBold.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('/assets/fonts/Hack/hack-regular-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-regular-subset.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('/assets/fonts/Hack/hack-bold-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-bold-subset.woff') format('woff');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('/assets/fonts/Hack/hack-italic-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-italic-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url('/assets/fonts/Hack/hack-bolditalic-subset.woff2') format('woff2'), url('/assets/fonts/Hack/hack-bolditalic-subset.woff') format('woff');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
article,
|
||||
aside,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Cantarell', 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', 'Helvetica Neue', 'Helvetica', Arial, sans-serif;
|
||||
margin: 0 0 44px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #ffffff;
|
||||
text-align: left;
|
||||
height: 100%;
|
||||
background-color: var(--main-background-color);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
.h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
h2,
|
||||
.h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h3,
|
||||
.h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
.h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h5,
|
||||
.h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6,
|
||||
.h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
text-decoration: underline;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.1rem 0.5rem;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
pre,
|
||||
kbd,
|
||||
code {
|
||||
font-family: 'Hack', 'Source Code Pro', Consolas, monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 1rem auto;
|
||||
width:100%;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
table thead+tbody tr {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background: var(--secondary-green);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
table tbody,
|
||||
table tr:last-of-type
|
||||
{
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
table tbody tr:nth-of-type(2n) {
|
||||
background-color: var(--table-even-color);
|
||||
}
|
||||
|
||||
table tbody tr:nth-of-type(2n+1) {
|
||||
background-color: var(--table-odd-color);
|
||||
}
|
||||
|
||||
table thead tr th,
|
||||
table tbody tr td {
|
||||
padding: .25rem 0.75rem;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
.lambda-logo {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 818px) {
|
||||
.container {
|
||||
max-width: 818px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
max-width: 100%;
|
||||
margin: 1rem 0.25rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.2rem 1rem;
|
||||
margin: 0.3rem 0.3rem;
|
||||
color: #ffffff;
|
||||
background: var(--primary-green);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
transition: 50ms ease-in-out all;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
text-decoration: none;
|
||||
background: var(--secondary-green);
|
||||
box-shadow: 0 0 0 1px var(--secondary-green);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 0 1px var(--tertiary-green);
|
||||
transform: scale(0.98);
|
||||
color: #cccccc;
|
||||
background: var(--tertiary-green);
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
box-shadow: 0 0 0 2px #ffffff;
|
||||
}
|
||||
|
||||
.button.blue {
|
||||
background: var(--primary-blue);
|
||||
}
|
||||
|
||||
.button.blue:hover {
|
||||
background: var(--secondary-blue);
|
||||
box-shadow: 0 0 0 1px var(--secondary-blue);
|
||||
}
|
||||
|
||||
.button.blue:active {
|
||||
background: var(--tertiary-blue);
|
||||
box-shadow: 0 0 0 1px var(--tertiary-green);
|
||||
}
|
||||
|
||||
.button.blue:focus {
|
||||
box-shadow: 0 0 0 2px #ffffff;
|
||||
}
|
||||
|
||||
.text.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.none.display {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button.link::after {
|
||||
content: ' \2192';
|
||||
display: inline-block;
|
||||
transition: 50ms ease-in-out all;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
.button.link:hover {
|
||||
display: inline-block;
|
||||
transition: 50ms ease-in-out all;
|
||||
}
|
||||
|
||||
.button.link:hover::after {
|
||||
transform: translateX(0.25rem) scale(1.3);
|
||||
}
|
||||
|
||||
.button.link.extern:hover::after {
|
||||
transform: rotateZ(-45deg) scale(1.5);
|
||||
}
|
||||
|
||||
.button.link.back::before {
|
||||
content: ' \2190';
|
||||
display: inline-block;
|
||||
transition: 100ms ease-in-out all;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.button.link:hover::before {
|
||||
transform: translateX(-0.2rem) scale(1.3);
|
||||
}
|
||||
|
||||
.button.link.back::after {
|
||||
content: '';
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sans.serif {
|
||||
font-family: 'Cantarell', 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', 'Helvetica Neue', 'Helvetica', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.serif {
|
||||
font-family: 'EB Garamond', 'Garamond', 'Times New Roman', Times, serif;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: 'Hack', 'Source Code Pro', Consolas, monospace;
|
||||
}
|
||||
|
||||
.license {
|
||||
background-color: #222222;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
heading.container {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
background-color: var(--main-background-color);
|
||||
}
|
||||
|
||||
heading .title {
|
||||
border-bottom: 1px solid #FFFFFF;
|
||||
max-width: 95%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
text-align: right;
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-size: 1rem;
|
||||
margin: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
margin: 0;
|
||||
height: 2.5rem;
|
||||
max-height: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(to bottom right, #1a3a15, #09351b) no-repeat center center fixed;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
@font-face {
|
||||
font-family: 'Cardo';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('@fontsource/cardo/files/cardo-latin-400-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cardo';
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('@fontsource/cardo/files/cardo-latin-400-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cardo';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 700;
|
||||
src: url('@fontsource/cardo/files/cardo-latin-700-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100;
|
||||
src: url('@fontsource/inter/files/inter-latin-100-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 300;
|
||||
src: url('@fontsource/inter/files/inter-latin-300-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('@fontsource/inter/files/inter-latin-400-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 700;
|
||||
src: url('@fontsource/inter/files/inter-latin-700-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 800;
|
||||
src: url('@fontsource/inter/files/inter-latin-800-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('@fontsource/iosevka/files/iosevka-latin-400-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka';
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('@fontsource/iosevka/files/iosevka-latin-400-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 700;
|
||||
src: url('@fontsource/iosevka/files/iosevka-latin-700-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka';
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 700;
|
||||
src: url('@fontsource/iosevka/files/iosevka-latin-700-italic.woff2') format('woff2');
|
||||
}
|
||||
+837
@@ -0,0 +1,837 @@
|
||||
:root {
|
||||
--main-background-color: #0d1117;
|
||||
--main-border-color: #555555;
|
||||
--link-color: #009dff;
|
||||
--primary-green: #099945;
|
||||
--secondary-green: #1a3a15;
|
||||
--tertiary-green: #0f200c;
|
||||
--primary-blue: #0a82b1;
|
||||
--secondary-blue: #05455f;
|
||||
--tertiary-blue: #05232f;
|
||||
--table-odd-color: rgba(255, 255, 255, 0.05);
|
||||
--table-even-color: rgba(255, 255, 255, 0.025);
|
||||
--serif-font: 'Cardo', Georgia, 'Times New Roman', Times, serif;
|
||||
--sans-serif-font: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
--monospace-font: 'Iosevka', 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
article,
|
||||
aside,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--sans-serif-font);
|
||||
margin: 0 0 44px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #ffffff;
|
||||
text-align: left;
|
||||
height: 100%;
|
||||
background-color: var(--main-background-color);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
.h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
h2,
|
||||
.h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h3,
|
||||
.h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
.h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h5,
|
||||
.h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6,
|
||||
.h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
text-decoration: underline;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
summary:focus-visible,
|
||||
.sidenote-number:focus-visible {
|
||||
outline: 2px solid var(--link-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.1rem 0.5rem;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
pre,
|
||||
kbd,
|
||||
code {
|
||||
font-family: var(--monospace-font);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 1rem auto;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.notes-table {
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-spacing: 0;
|
||||
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.notes-table thead + tbody tr {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.notes-table thead {
|
||||
background: var(--secondary-green);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.notes-table tbody,
|
||||
.notes-table tr:last-of-type {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.notes-table tbody tr:nth-of-type(2n) {
|
||||
background-color: var(--table-even-color);
|
||||
}
|
||||
|
||||
.notes-table tbody tr:nth-of-type(2n+1) {
|
||||
background-color: var(--table-odd-color);
|
||||
}
|
||||
|
||||
.notes-table tbody tr {
|
||||
border-left: 3px solid transparent;
|
||||
transition: 100ms ease-in-out all;
|
||||
}
|
||||
|
||||
.notes-table tbody tr:hover {
|
||||
background: var(--tertiary-blue);
|
||||
border-left-color: var(--link-color);
|
||||
transform: translateX(0.15rem);
|
||||
}
|
||||
|
||||
.notes-table tbody tr td:first-child {
|
||||
width: 75%;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.notes-table tbody tr td:first-child a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notes-table tbody tr td:first-child a::after {
|
||||
content: ' →';
|
||||
opacity: 0;
|
||||
transition: 100ms ease-in-out all;
|
||||
}
|
||||
|
||||
.notes-table tbody tr:hover td:first-child a::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notes-table tbody tr td:last-child {
|
||||
color: #bbbbbb;
|
||||
font-family: var(--monospace-font);
|
||||
font-size: 0.75rem;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.posts-list {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
font-family: var(--serif-font);
|
||||
}
|
||||
|
||||
.posts-list li {
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.posts-list time {
|
||||
margin-left: 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.recent ul {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.recent ul::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 7.75rem;
|
||||
border-left: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.recent li {
|
||||
display: grid;
|
||||
grid-template-columns: 7rem 1px max-content minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
align-items: baseline;
|
||||
margin: 0.35rem 0;
|
||||
}
|
||||
|
||||
.recent-type {
|
||||
display: inline-block;
|
||||
padding: 0.05rem 0.35rem;
|
||||
border: 1px solid var(--main-border-color);
|
||||
color: #bbbbbb;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.recent-date {
|
||||
position: relative;
|
||||
color: #bbbbbb;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.recent-date::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 0.4rem);
|
||||
width: max-content;
|
||||
max-width: 18rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
color: #ffffff;
|
||||
background-color: var(--main-background-color);
|
||||
border: 1px solid var(--main-border-color);
|
||||
font-size: 0.75rem;
|
||||
font-style: normal;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity 75ms ease-in;
|
||||
}
|
||||
|
||||
.recent-date:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
table thead tr th,
|
||||
table tbody tr td {
|
||||
padding: .25rem 0.75rem;
|
||||
}
|
||||
|
||||
.table-last-updated {
|
||||
font-style: italic;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 818px) {
|
||||
.container {
|
||||
max-width: 818px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
max-width: 100%;
|
||||
margin: 1rem 0.25rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.toc {
|
||||
font-size: 0.9rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.toc summary {
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.article-page {
|
||||
max-width: 52rem;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.posts-page {
|
||||
font-family: var(--serif-font);
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.posts-page p {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.article-page.posts-page h2,
|
||||
.article-page.posts-page h3,
|
||||
.article-page.posts-page h4,
|
||||
.article-page.posts-page h5,
|
||||
.article-page.posts-page h6 {
|
||||
font-family: var(--serif-font);
|
||||
font-variant-caps: small-caps;
|
||||
font-feature-settings: 'smcp';
|
||||
text-transform: capitalize;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.article-page .block {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.article-page h2,
|
||||
.article-page h3,
|
||||
.article-page h4,
|
||||
.article-page h5,
|
||||
.article-page h6 {
|
||||
font-family: var(--sans-serif-font);
|
||||
line-height: 1.25;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.posts-page h2::before,
|
||||
.posts-page h3::before,
|
||||
.posts-page h4::before,
|
||||
.posts-page h5::before,
|
||||
.posts-page h6::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
color: var(--link-color);
|
||||
opacity: 0;
|
||||
transition: 100ms ease-in-out opacity;
|
||||
}
|
||||
|
||||
.posts-page h2:hover::before,
|
||||
.posts-page h3:hover::before,
|
||||
.posts-page h4:hover::before,
|
||||
.posts-page h5:hover::before,
|
||||
.posts-page h6:hover::before {
|
||||
content: '§';
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.article-page .toc {
|
||||
font-family: var(--sans-serif-font);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.toc details {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.toc > details > ol {
|
||||
margin: 0.75rem 0 0;
|
||||
padding-left: 1.5rem;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.toc ol ol {
|
||||
margin-top: 0;
|
||||
padding-left: 0.75rem;
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.toc ol ol ol {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
margin: 0.25rem 0;
|
||||
padding-left: 0.15rem;
|
||||
}
|
||||
|
||||
.toc a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.article-page blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
border-left: 3px solid var(--main-border-color);
|
||||
color: #cccccc;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.article-page pre {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.article-page .code-block {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 2.25rem;
|
||||
padding-left: 1rem;
|
||||
border: 1px solid var(--main-border-color);
|
||||
background-color: var(--secondary-green);
|
||||
font-family: var(--sans-serif-font);
|
||||
font-size: 0.80rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.copy-code {
|
||||
align-self: stretch;
|
||||
min-width: 5.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border: 0;
|
||||
border-left: 1px solid var(--main-border-color);
|
||||
color: #ffffff;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copy-code:hover {
|
||||
background-color: var(--tertiary-green);
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.copy-code:focus-visible {
|
||||
outline: 2px solid var(--link-color);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.article-page pre.line-numbers {
|
||||
padding-left: 3.8em;
|
||||
}
|
||||
|
||||
.article-page pre > code {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.article-page hr {
|
||||
margin: 2rem 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.article-page table {
|
||||
display: table;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 0.25rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.article-page table thead {
|
||||
background-color: var(--secondary-green);
|
||||
}
|
||||
|
||||
.article-page table th,
|
||||
.article-page table td {
|
||||
padding: 0.4rem 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.article-page table tbody tr:nth-child(even) {
|
||||
background-color: var(--table-even-color);
|
||||
}
|
||||
|
||||
.article-page table tbody tr:nth-child(odd) {
|
||||
background-color: var(--table-odd-color);
|
||||
}
|
||||
|
||||
.margin-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidenote {
|
||||
float: none;
|
||||
width: 18rem;
|
||||
margin-right: 0;
|
||||
margin-top: 0.25rem;
|
||||
color: #bbbbbb;
|
||||
font-size: 0.90rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sidenote::before {
|
||||
content: counter(sidenote-counter) '. ';
|
||||
color: var(--link-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidenote-number::after {
|
||||
content: counter(sidenote-counter);
|
||||
position: relative;
|
||||
top: -0.35rem;
|
||||
margin-left: 0.15rem;
|
||||
color: var(--link-color);
|
||||
font-size: 0.7em;
|
||||
font-weight: bold;
|
||||
counter-increment: sidenote-counter;
|
||||
}
|
||||
|
||||
.sidenote-number {
|
||||
display: inline-block;
|
||||
min-width: 0.8rem;
|
||||
}
|
||||
|
||||
.article-page {
|
||||
counter-reset: sidenote-counter;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1399px) {
|
||||
.sidenote {
|
||||
display: none;
|
||||
float: none;
|
||||
width: auto;
|
||||
margin: 0.75rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
border-left: 3px solid var(--main-border-color);
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.sidenote::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.sidenote-toggle:checked + .margin-toggle + .sidenote {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.sidenote-toggle:focus-visible + .sidenote-number {
|
||||
outline: 2px solid var(--link-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sidenote-number {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1400px) {
|
||||
.article-page .toc + hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article-page .toc {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
height: calc(100vh - 4rem);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
float: left;
|
||||
width: 16rem;
|
||||
margin-left: -18rem;
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 4rem;
|
||||
padding-right: 1rem;
|
||||
border-right: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.article-page .toc::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article-page .toc summary {
|
||||
cursor: default;
|
||||
list-style: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.article-page .toc summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article-page .sidenote {
|
||||
position: static;
|
||||
float: right;
|
||||
clear: right;
|
||||
width: 16rem;
|
||||
margin: 0.25rem -18rem 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.2rem 1rem;
|
||||
margin: 0.3rem 0.3rem;
|
||||
color: #ffffff;
|
||||
background: var(--primary-green);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
transition: 50ms ease-in-out all;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
text-decoration: none;
|
||||
background: var(--secondary-green);
|
||||
box-shadow: 0 0 0 1px var(--secondary-green);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0 0 1px var(--tertiary-green);
|
||||
transform: scale(0.98);
|
||||
color: #cccccc;
|
||||
background: var(--tertiary-green);
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
box-shadow: 0 0 0 2px #ffffff;
|
||||
}
|
||||
|
||||
.button.blue {
|
||||
background: var(--primary-blue);
|
||||
}
|
||||
|
||||
.button.blue:hover {
|
||||
background: var(--secondary-blue);
|
||||
box-shadow: 0 0 0 1px var(--secondary-blue);
|
||||
}
|
||||
|
||||
.button.blue:active {
|
||||
background: var(--tertiary-blue);
|
||||
box-shadow: 0 0 0 1px var(--tertiary-green);
|
||||
}
|
||||
|
||||
.button.blue:focus {
|
||||
box-shadow: 0 0 0 2px #ffffff;
|
||||
}
|
||||
|
||||
.text.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button.link::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-left: 0.3rem;
|
||||
vertical-align: -0.125em;
|
||||
background-color: currentColor;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14m-6-6 6 6-6 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14m-6-6 6 6-6 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
transition: transform 50ms ease-in-out;
|
||||
}
|
||||
|
||||
.button.link:hover {
|
||||
display: inline-block;
|
||||
transition: 50ms ease-in-out all;
|
||||
}
|
||||
|
||||
.button.link:hover::after {
|
||||
transform: translateX(0.25rem) scale(1.3);
|
||||
}
|
||||
|
||||
.button.link.extern:hover::after {
|
||||
transform: rotateZ(-45deg) scale(1.5);
|
||||
}
|
||||
|
||||
.button.link.back::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 0.3rem;
|
||||
vertical-align: -0.125em;
|
||||
background-color: currentColor;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14m-6-6 6 6-6 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14m-6-6 6 6-6 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
transform: rotate(180deg);
|
||||
transition: transform 100ms ease-in-out;
|
||||
}
|
||||
|
||||
.button.link:hover::before {
|
||||
transform: translateX(-0.2rem) rotate(180deg) scale(1.3);
|
||||
}
|
||||
|
||||
.button.link.back::after {
|
||||
content: '';
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
background-color: var(--main-background-color);
|
||||
}
|
||||
|
||||
.page-heading .title {
|
||||
border-bottom: 1px solid #FFFFFF;
|
||||
max-width: 95%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
text-align: right;
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-size: 1rem;
|
||||
margin: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
margin: 0;
|
||||
height: 2.5rem;
|
||||
max-height: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
background: #09351b no-repeat center center fixed;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import Prism from 'prismjs';
|
||||
import './fonts.css';
|
||||
import 'prismjs/components/prism-bash';
|
||||
import 'prismjs/components/prism-ini';
|
||||
import 'prismjs/components/prism-lua';
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers.js';
|
||||
import 'prismjs/themes/prism-tomorrow.css';
|
||||
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
|
||||
import './prism.css';
|
||||
|
||||
const absoluteDateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
const tooltipDateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long',
|
||||
});
|
||||
const relativeDateFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
||||
const relativeTimeUnits = [
|
||||
[31536000, 'year'],
|
||||
[2592000, 'month'],
|
||||
[604800, 'week'],
|
||||
[86400, 'day'],
|
||||
[3600, 'hour'],
|
||||
[60, 'minute'],
|
||||
];
|
||||
|
||||
document.querySelectorAll('pre > code').forEach((code) => {
|
||||
if (![...code.classList].some((name) => name.startsWith('language-'))) {
|
||||
code.classList.add('language-plain');
|
||||
}
|
||||
code.closest('pre').classList.add('line-numbers');
|
||||
});
|
||||
Prism.highlightAll();
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.append(textarea);
|
||||
textarea.select();
|
||||
const copied = document.execCommand('copy');
|
||||
textarea.remove();
|
||||
if (!copied) throw new Error('Copy command failed');
|
||||
};
|
||||
|
||||
document.querySelectorAll('.copy-code').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const code = button.closest('.code-block')?.querySelector('code');
|
||||
if (!code) return;
|
||||
|
||||
try {
|
||||
await copyText(code.textContent);
|
||||
button.textContent = 'Copied';
|
||||
} catch {
|
||||
button.textContent = 'Copy failed';
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
button.textContent = 'Copy';
|
||||
}, 1500);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-value]').forEach((element) => {
|
||||
const date = new Date(element.dataset.value);
|
||||
if (Number.isNaN(date.valueOf())) return;
|
||||
|
||||
if (element.classList.contains('recent-date')) {
|
||||
element.dataset.tooltip = tooltipDateFormatter.format(date);
|
||||
const seconds = Math.round((date.valueOf() - Date.now()) / 1000);
|
||||
const unit = relativeTimeUnits.find(([size]) => Math.abs(seconds) >= size) ?? [1, 'second'];
|
||||
element.textContent = relativeDateFormatter.format(Math.round(seconds / unit[0]), unit[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
const formatted = absoluteDateFormatter.format(date);
|
||||
element.textContent = element.classList.contains('last-updated')
|
||||
? `Last updated: ${formatted}`
|
||||
: formatted;
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
.article-page .code-block pre[class*='language-'] {
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.article-page .code-block code[class*='language-'] {
|
||||
font-family: 'Iosevka', 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import fs from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
function generatedPages(directory) {
|
||||
const inputs = {};
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const filePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) Object.assign(inputs, generatedPages(filePath));
|
||||
else if (entry.name === 'index.html') {
|
||||
const relativePath = path.relative('.generated', filePath);
|
||||
const key = relativePath.endsWith('/index.html')
|
||||
? relativePath.slice(0, -'/index.html'.length)
|
||||
: relativePath;
|
||||
inputs[key] = path.resolve(filePath);
|
||||
}
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function contentGenerator() {
|
||||
let timer;
|
||||
let generator;
|
||||
let rerun = false;
|
||||
|
||||
const generate = (server) => {
|
||||
if (generator) {
|
||||
rerun = true;
|
||||
return;
|
||||
}
|
||||
|
||||
generator = spawn(process.execPath, ['generator.js'], { stdio: 'inherit' });
|
||||
generator.on('error', (error) => {
|
||||
console.error(`Content generation failed: ${error.message}`);
|
||||
});
|
||||
generator.on('close', (code) => {
|
||||
generator = undefined;
|
||||
if (code === 0) server.ws.send({ type: 'full-reload' });
|
||||
if (rerun) {
|
||||
rerun = false;
|
||||
generate(server);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'content-generator',
|
||||
configureServer(server) {
|
||||
const sources = [
|
||||
path.resolve('notes'),
|
||||
path.resolve('posts'),
|
||||
path.resolve('README.md'),
|
||||
path.resolve('THIRD_PARTY_LICENSES.md'),
|
||||
path.resolve('external.json'),
|
||||
path.resolve('generator.js'),
|
||||
path.resolve('html.js'),
|
||||
];
|
||||
server.watcher.add(sources);
|
||||
server.watcher.on('all', (event, file) => {
|
||||
if (!['add', 'change', 'unlink'].includes(event)) return;
|
||||
const isMarkdown = file.endsWith('.md') && (file.startsWith(`${path.resolve('notes')}/`) || file.startsWith(`${path.resolve('posts')}/`));
|
||||
const isExternal = file === path.resolve('external.json');
|
||||
const isStandaloneMarkdown = file === resolve('README.md') || file === path.resolve('THIRD_PARTY_LICENSES.md');
|
||||
const isGenerator = file === path.resolve('generator.js') || file === path.resolve('html.js');
|
||||
if (!isMarkdown && !isStandaloneMarkdown && !isExternal && !isGenerator) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
generate(server);
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [contentGenerator()],
|
||||
root: '.generated',
|
||||
publicDir: '../public',
|
||||
resolve: {
|
||||
alias: {
|
||||
'/src': path.resolve('src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/.generated/**'],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve('dist'),
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: generatedPages('.generated'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user