Rewrite site. Again. With JS and Vite

This commit is contained in:
2026-08-11 02:35:44 -04:00
parent a433a534a7
commit 7e9e744f0d
46 changed files with 1657 additions and 1523 deletions
+5 -2
View File
@@ -1,6 +1,9 @@
.DS_Store .DS_Store
**/.*.md **/.*.md
.env .env
build/ .generated/
.bun-tmp/
dist/
node_modules/
*.pdb *.pdb
*.exe *.exe
+18 -11
View File
@@ -1,18 +1,25 @@
# PaulW.XYZ # PaulW.XYZ
Paul's personal website.
## Third-Party Licenses ## 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) Direct dependency notices are documented in
- © 2018 Source Foundry Authors [the license page](/licenses/).
- MIT License
[Cantarell](https://github.com/davelab6/cantarell) ## Development
- © 2009-2010, [Understanding Limited](mailto:dave@understandinglimited.com)
- Open Font License, Version 1.1
[EB Garamond](https://github.com/georgd/EB-Garamond) The source for this site is hosted at <https://git.paulw.xyz/paul/www>.
- &copy; 2010-2013 [Georg Duffner](http://www.georgduffner.at)
- Open Font License, Version 1.1 ```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/`.
+49
View File
@@ -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.
+67
View File
@@ -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.`);
}
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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>")
}
+24
View File
@@ -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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
.replaceAll('"', '&quot;').replaceAll("'", '&#39;');
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}>`;
};
+3
View File
@@ -1,4 +1,5 @@
# Web Browsers # Web Browsers
Extensions/Plugins I Use on All Supported Browsers: Extensions/Plugins I Use on All Supported Browsers:
- uBlock Origin - uBlock Origin
- Decentraleyes - Decentraleyes
@@ -8,11 +9,13 @@ Extensions/Plugins I Use on All Supported Browsers:
- Greasemonkey/Tampermonkey - Greasemonkey/Tampermonkey
- Indie Wiki Buddy - Indie Wiki Buddy
- SingleFile - SingleFile
- Yet Another Flags
Product/Service-specific Extensions: Product/Service-specific Extensions:
- SteamDB - SteamDB
- Return YouTube Dislike - Return YouTube Dislike
- SponsorBlock - SponsorBlock
- Enhancer for YouTube™
## Chromium ## Chromium
- [Chromium Source Docs](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/README.md) - [Chromium Source Docs](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/README.md)
+3 -6
View File
@@ -1,12 +1,9 @@
# Lua Programming Language # Lua Programming Language
<!-- TODO ## Lua 5.4 C API-->
<!-- TODO ## Lua 5.5 C API-->
## Lua 5.4 Bytecode ## 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. 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
View File
@@ -31,6 +31,7 @@ WinCDEmu is a lightweight, open-source disc emulator that supports mounting CUE,
- [Portable Version](https://wincdemu.sysprogs.org/portable/) - [Portable Version](https://wincdemu.sysprogs.org/portable/)
### Master Control Panel / God Mode ### Master Control Panel / God Mode
(Misnomer; you probably won't use this either) (Misnomer; you probably won't use this either)
Shows a list of all the available settings on Windows in a single view. 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 ## MacOS
### Clipboard Management ### Clipboard Management
- [maccy](https://maccy.app/) - [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 ### Terminal Emulator
@@ -51,7 +53,7 @@ Open it by exceuting the following command or saving it as a shortcut: `explorer
### Package Manager ### Package Manager
- [HomeBrew](https://brew.sh) - [HomeBrew](https://brew.sh)
- package manager everyone uses but it is noticeably slow - package manager everyone uses
### Video Players ### Video Players
+24
View File
@@ -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.
-93
View File
@@ -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 arent 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.
-45
View File
@@ -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.
-453
View File
@@ -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;
}
+95
View File
@@ -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
View File
@@ -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
View File
@@ -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;
});
+8
View File
@@ -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;
}
+97
View File
@@ -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'),
},
},
});