diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..33b9477
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -0,0 +1,46 @@
+name: Deploy website
+
+on:
+ push:
+ branches:
+ - master
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out source
+ uses: actions/checkout@v4
+
+ - name: Install Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install --no-progress
+
+ - name: Build site
+ run: bun run build
+
+ - name: Configure SSH
+ env:
+ DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
+ DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
+ run: |
+ install -d -m 700 "$HOME/.ssh"
+ printf '%s\n' "$DEPLOY_SSH_KEY" > "$HOME/.ssh/deploy_key"
+ chmod 600 "$HOME/.ssh/deploy_key"
+ printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
+
+ - name: Deploy dist/
+ env:
+ DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+ DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+ DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+ run: |
+ rsync -az --delete \
+ -e "ssh -i $HOME/.ssh/deploy_key -o IdentitiesOnly=yes" \
+ dist/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/"
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 0000000..0e7e01f
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,7 @@
+{
+ "printWidth": 120,
+ "semi": false,
+ "useTabs": false,
+ "singleQuote": true,
+ "ignorePatterns": ["**/*.md"]
+}
diff --git a/check.js b/check.js
index 28a1e7f..30b9ea7 100644
--- a/check.js
+++ b/check.js
@@ -1,67 +1,79 @@
-import { readdir, readFile, stat } from 'node:fs/promises';
-import { join } from 'node:path';
+import { readdir, readFile, stat } from 'node:fs/promises'
+import { join } from 'node:path'
-const generatedRoot = '.generated';
+const generatedRoot = '.generated'
const walk = async (directory) => {
- const files = [];
+ 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);
+ const path = join(directory, entry.name)
+ if (entry.isDirectory()) files.push(...(await walk(path)))
+ else files.push(path)
}
- return files;
-};
+ return files
+}
const exists = async (path) => {
try {
- await stat(path);
- return true;
+ await stat(path)
+ return true
} catch {
- return false;
+ 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' };
+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(' element`);
+ 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(' element`)
for (const [, fragment] of html.matchAll(/href="#([^"]+)"/g)) {
- if (!ids.includes(fragment)) errors.push(`${file}: missing fragment target #${fragment}`);
+ 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}`);
+ 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 (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;
+ console.error(errors.join('\n'))
+ process.exitCode = 1
} else {
- console.log(`Checked ${htmlFiles.length} generated pages.`);
+ console.log(`Checked ${htmlFiles.length} generated pages.`)
}
diff --git a/clean.js b/clean.js
index 13c071a..b433cfb 100644
--- a/clean.js
+++ b/clean.js
@@ -1,6 +1,3 @@
-import { rm } from 'node:fs/promises';
+import { rm } from 'node:fs/promises'
-await Promise.all([
- rm('.generated', { recursive: true, force: true }),
- rm('dist', { recursive: true, force: true }),
-]);
+await Promise.all([rm('.generated', { recursive: true, force: true }), rm('dist', { recursive: true, force: true })])
diff --git a/external.json b/external.json
index 8ce844c..5fd17bd 100644
--- a/external.json
+++ b/external.json
@@ -1,4 +1,4 @@
{
- "Git": "https://git.paulw.xyz/xyz",
- "Twitter/X": "https://x.com/paulw_xyz"
+ "Git": "https://git.paulw.xyz/xyz",
+ "Twitter/X": "https://x.com/paulw_xyz"
}
diff --git a/generator.js b/generator.js
index 65eca3d..9011239 100644
--- a/generator.js
+++ b/generator.js
@@ -1,16 +1,18 @@
-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';
+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 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',
@@ -18,213 +20,269 @@ const languageLabels = {
lua: 'Lua',
sh: 'Shell',
bash: 'Shell',
-};
+}
const slug = (value) => {
- let result = '';
- let dash = false;
+ 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;
+ 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();
+ 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();
+ 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 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 = '') => `${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 pageShell = (title, body, path = '/', navTitle = '') =>
+ `${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]);
+ 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 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;
+ 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 ''; }
-};
+ try {
+ return (await stat(file)).mtime.toISOString()
+ } catch {
+ return ''
+ }
+}
const readPages = async (dir) => {
- const entries = await readdir(dir, { withFileTypes: true });
+ 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);
-};
+ .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;
+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),
- };
+ 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;
+ return undefined
+ },
+ renderer({ tokens }) {
+ sidenoteNumber += 1
+ const id = `sidenote-${sidenoteNumber}`
+ const content = this.parser.parseInline(tokens)
+ return `${content}`
+ },
},
- renderer({ tokens }) {
- sidenoteNumber += 1;
- const id = `sidenote-${sidenoteNumber}`;
- const content = this.parser.parseInline(tokens);
- return `${content}`;
- },
- }],
+ ],
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 `
\n`;
+ 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 `\n`
},
heading({ tokens, depth }) {
- const text = this.parser.parseInline(tokens);
- const id = nextHeadingId(tokens);
- return `${text}\n`;
+ const text = this.parser.parseInline(tokens)
+ const id = nextHeadingId(tokens)
+ return `${text}\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 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 = '';
+ 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; }
+ const end = text.indexOf(']', index + 2)
+ if (end >= 0) {
+ index = end + 1
+ continue
+ }
}
- result += text[index++];
+ 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');
+ 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;
+ nextHeadingId = createSlugger()
+ sidenoteNumber = 0
+ const html = marked.parse(withoutTitle(stripHeadingSidenotes(source)))
+ let result = ''
+ let cursor = 0
while (cursor < html.length) {
- const start = html.indexOf('', start);
- if (end < 0) { result += html.slice(cursor); break; }
- result += html.slice(cursor, start);
- result += `${html.slice(start, end + 8)}
`;
- cursor = end + 8;
+ const start = html.indexOf('', start)
+ if (end < 0) {
+ result += html.slice(cursor)
+ break
+ }
+ result += html.slice(cursor, start)
+ result += `${html.slice(start, end + 8)}
`
+ cursor = end + 8
}
- return result;
-};
+ 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();
+ 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);
+ 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) => `${items.map(({ heading, children }) => {
- const id = headingId(heading.tokens);
- const nested = children.length > 0 ? renderItems(children) : '';
- return `- ${escapeHtml(headingLabel(heading.tokens))}${nested}
`;
- }).join('')}
`;
- return renderItems(root.children);
-};
+ const renderItems = (items) =>
+ `${items
+ .map(({ heading, children }) => {
+ const id = headingId(heading.tokens)
+ const nested = children.length > 0 ? renderItems(children) : ''
+ return `- ${escapeHtml(headingLabel(heading.tokens))}${nested}
`
+ })
+ .join('')}
`
+ 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 directory = join(output, path)
+ await mkdir(directory, { recursive: true })
+ await writeFile(join(directory, 'index.html'), html)
+}
const titleHeading = (title, className = '') => `
`;
+ `
-const renderHome = (externalLinks, sectionLinks, recentLinks) => pageShell('', `
+const renderHome = (externalLinks, sectionLinks, recentLinks) =>
+ pageShell(
+ '',
+ `
${titleHeading(siteName)}
${sectionLinks}${externalLinks}
@@ -232,97 +290,131 @@ const renderHome = (externalLinks, sectionLinks, recentLinks) => pageShell('', `
Recent
${recentLinks || '- Nothing to show yet.
'}
- `);
+ `,
+ )
const renderNotesTable = (pages, directory) => {
- const rows = pages.map((page) => `
+ const rows = pages
+ .map(
+ (page) => `
| ${escapeHtml(page.title)} |
${page.date ? `` : ''} |
-
`).join('');
- return `${escapeHtml(directory)} pages| Page | Last updated |
${rows}
`;
-};
+ `,
+ )
+ .join('')
+ return `${escapeHtml(directory)} pages${rows}
`
+}
const renderPostsList = (pages, directory) => {
- const items = pages.map((page) => `
+ const items = pages
+ .map(
+ (page) => `
${escapeHtml(page.title)}
${page.date ? `` : ''}
- `).join('');
- return ``;
-};
+ `,
+ )
+ .join('')
+ return ``
+}
const renderEmptySection = () => `
-
crickets
+
**crickets**
Nothing to list...
Go Home
-
`;
+ `
+
+const renderNotFound = () =>
+ pageShell(
+ 'Page not found',
+ `${titleHeading('404: Page Not Found')}Uh oh! The page you are looking for does not exist...
Go HomeMore on 404`,
+ '/',
+ ' ... ??? / 404: Page Not Found',
+ )
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);
+ 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)}`,
`/${directory}`,
title,
- );
-};
+ )
+}
const renderArticle = (directory, page, navigationPath = `/${directory}/${page.name}`) => {
const updated = page.date
? ``
- : '';
- const heading = titleHeading(page.title, `${directory}-heading`);
- const tableOfContents = toc(page.source);
+ : ''
+ const heading = titleHeading(page.title, `${directory}-heading`)
+ const tableOfContents = toc(page.source)
const contents = tableOfContents
? `Contents
${tableOfContents}
`
- : '';
- const article = `${renderMarkdown(page.source)}`;
- const body = `${heading}${updated}${contents}${article}`;
+ : ''
+ const article = `${renderMarkdown(page.source)}`
+ const body = `${heading}${updated}${contents}${article}`
- return pageShell(page.title, body, navigationPath, page.title);
-};
+ 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)])));
+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]) => `${escapeHtml(text)}`)
+ .join('')
+
+const sectionLinks = markdownDirs
+ .map((dir) => `${dir[0].toUpperCase() + dir.slice(1)}`)
+ .join('')
-const external = JSON.parse(await readFile('external.json', 'utf8'));
-const externalLinks = Object.entries(external).map(([text, href]) => `${escapeHtml(text)}`).join('');
-const sectionLinks = markdownDirs.map((dir) => `${dir[0].toUpperCase() + dir.slice(1)}`).join('');
const standaloneLinks = standalonePages
.filter((page) => page.path !== 'licenses')
.map((page) => `${page.title}`)
- .join('');
-const recentPages = markdownDirs.flatMap((dir) => sections[dir].map((page) => ({ ...page, dir })))
+ .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) => `${page.date ? `` : ''}${page.dir}${escapeHtml(page.title)}`).join('');
-await writeFile(join(output, 'index.html'), renderHome(externalLinks, sectionLinks + standaloneLinks, recentLinks));
+ .slice(0, 5)
+
+const recentLinks = recentPages
+ .map(
+ (page) =>
+ `${page.date ? `` : ''}${page.dir}${escapeHtml(page.title)}`,
+ )
+ .join('')
+await writeFile(join(output, 'index.html'), renderHome(externalLinks, sectionLinks + standaloneLinks, recentLinks))
+await writeFile(join(output, '404.html'), renderNotFound())
for (const dir of markdownDirs) {
- const pages = sections[dir];
- await writePage(dir, renderSection(dir, pages));
+ const pages = sections[dir]
+ await writePage(dir, renderSection(dir, pages))
for (const page of pages) {
- await writePage(join(dir, page.name), renderArticle(dir, page));
+ await writePage(join(dir, page.name), renderArticle(dir, page))
}
}
for (const page of standalonePages) {
- const source = await readFile(page.source, 'utf8');
+ 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, '/'));
+ }
+ await writePage(page.path, renderArticle(page.path, generatedPage, '/'))
}
diff --git a/html.js b/html.js
index 59a4f9e..3ea3a7f 100644
--- a/html.js
+++ b/html.js
@@ -1,24 +1,45 @@
-const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
+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 raw = (html) => ({ type: 'raw', html })
export const h = (tag, attributes = {}, children = []) => ({
- type: 'element', tag, attributes, children: Array.isArray(children) ? children : [children],
-});
+ type: 'element',
+ tag,
+ attributes,
+ children: Array.isArray(children) ? children : [children],
+})
-export const escapeHtml = (value) => String(value)
- .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
- .replaceAll('"', '"').replaceAll("'", ''');
+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;
+ 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}>`;
-};
+ .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}>`
+}
diff --git a/package.json b/package.json
index e800a5b..48c60b6 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,6 @@
"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",
@@ -20,5 +19,6 @@
},
"devDependencies": {
"vite": "^7.0.0"
- }
+ },
+ "packageManager": "bun@1.3.8"
}
diff --git a/src/global.css b/src/global.css
index cf1e5fc..eff39a7 100644
--- a/src/global.css
+++ b/src/global.css
@@ -1,22 +1,23 @@
: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;
+ --main-background-color: #0d1117;
+ --main-border-color: #555555;
+ --secondary-border-color: #30363d;
+ --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;
+ box-sizing: border-box;
}
article,
@@ -29,30 +30,30 @@ hgroup,
main,
nav,
section {
- display: block;
+ 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;
+ 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;
+[tabindex='-1']:focus {
+ outline: 0 !important;
}
hr {
- box-sizing: content-box;
- height: 0;
- overflow: visible;
+ box-sizing: content-box;
+ height: 0;
+ overflow: visible;
}
h1,
@@ -61,323 +62,305 @@ h3,
h4,
h5,
h6 {
- margin-top: 0;
- margin-bottom: 0.5rem;
+ margin-top: 0;
+ margin-bottom: 0.5rem;
}
h1,
.h1 {
- font-size: 2.5rem;
+ font-size: 2.5rem;
}
h2,
.h2 {
- font-size: 2rem;
+ font-size: 2rem;
}
h3,
.h3 {
- font-size: 1.75rem;
+ font-size: 1.75rem;
}
h4,
.h4 {
- font-size: 1.5rem;
+ font-size: 1.5rem;
}
h5,
.h5 {
- font-size: 1.25rem;
+ font-size: 1.25rem;
}
h6,
.h6 {
- font-size: 1rem;
+ font-size: 1rem;
}
p {
- margin-top: 0;
- margin-bottom: 1rem;
+ margin-top: 0;
+ margin-bottom: 1rem;
}
a {
- color: var(--link-color);
- text-decoration: underline;
- background-color: transparent;
- outline: none;
+ color: var(--link-color);
+ text-decoration: underline;
+ background-color: transparent;
+ outline: none;
}
a:hover {
- text-decoration: underline;
+ text-decoration: underline;
}
a:focus {
- text-decoration: underline dotted;
+ text-decoration: underline dotted;
}
a:focus-visible,
summary:focus-visible,
.sidenote-number:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: 2px;
+ 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;
+ 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;
+ margin: 0.5rem;
}
pre {
- width: 100%;
- max-width: 100%;
- overflow-x: auto;
+ 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;
+ 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;
+ font-family: var(--monospace-font);
+ font-size: 0.9rem;
}
table {
- margin: 1rem auto;
- width: 100%;
- border-collapse: collapse;
+ width: max-content;
+ margin: 1rem 0;
+ border-collapse: collapse;
+}
+
+table th,
+table td {
+ border: 0;
+}
+
+table th + th,
+table td + td {
+ border-left: 1px solid var(--secondary-border-color);
+}
+
+table tbody tr:nth-child(even) {
+ background-color: var(--table-even-color);
+}
+
+table tbody tr:nth-child(odd) {
+ background-color: var(--table-odd-color);
}
.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);
+ width: 100%;
+ border: 1px solid var(--main-border-color);
}
-.notes-table thead + tbody tr {
- border-radius: 0;
+.notes-table th + th,
+.notes-table td + td,
+.posts-table th + th,
+.posts-table td + td {
+ border-left: 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);
+ background: var(--secondary-green);
}
.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);
+ transition: 100ms ease-in-out all;
}
.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;
+ width: 75%;
+ font-size: 1.1rem;
}
.notes-table tbody tr td:last-child {
- color: #bbbbbb;
- font-family: var(--monospace-font);
- font-size: 0.75rem;
- text-align: right;
- white-space: nowrap;
+ 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);
+ margin: 1rem 0;
+ padding-left: 1.5rem;
+ font-family: var(--serif-font);
}
.posts-list li {
- padding: 0.2rem 0;
+ padding: 0.2rem 0;
}
.posts-list time {
- margin-left: 1rem;
- font-size: 0.85rem;
- font-style: italic;
+ margin-left: 1rem;
+ font-size: 0.85rem;
+ font-style: italic;
}
.recent ul {
- position: relative;
- margin: 0;
- padding-left: 0;
- list-style: none;
+ 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);
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 7.75rem;
+ border-left: 1px solid var(--secondary-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;
+ 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;
+ 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;
+ 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;
+ 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;
+ opacity: 1;
}
table thead tr th,
table tbody tr td {
- padding: .25rem 0.75rem;
+ padding: 0.25rem 0.75rem;
}
.table-last-updated {
- font-style: italic;
- margin-right: 0;
+ font-style: italic;
+ margin-right: 0;
}
ul li {
- list-style-type: square;
+ list-style-type: square;
}
.container {
- margin: 0 0.5rem;
- position: relative;
+ margin: 0 0.5rem;
+ position: relative;
}
@media screen and (min-width: 818px) {
- .container {
- max-width: 818px;
- margin: 0 auto;
- position: relative;
- }
+ .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;
+ display: block;
+ padding: 0.5rem;
+ max-width: 100%;
+ margin: 1rem 0.25rem;
+ border-radius: 1rem;
}
.toc {
- font-size: 0.9rem;
- border-radius: 0;
+ font-size: 0.9rem;
+ border-radius: 0;
}
.toc summary {
- cursor: pointer;
- font-size: 1.1rem;
- font-style: normal;
- font-weight: bold;
+ 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;
+ 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;
+ font-family: var(--serif-font);
+ font-size: 1.25rem;
+ line-height: 1.8;
}
.posts-page p {
- margin-bottom: 1.25rem;
+ margin-bottom: 1.25rem;
}
.article-page.posts-page h2,
@@ -385,16 +368,16 @@ ul li {
.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;
+ 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;
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
}
.article-page h2,
@@ -402,9 +385,9 @@ ul li {
.article-page h4,
.article-page h5,
.article-page h6 {
- font-family: var(--sans-serif-font);
- line-height: 1.25;
- margin-top: 2rem;
+ font-family: var(--sans-serif-font);
+ line-height: 1.25;
+ margin-top: 2rem;
}
.posts-page h2::before,
@@ -412,12 +395,12 @@ ul li {
.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;
+ content: '';
+ position: absolute;
+ right: 100%;
+ color: var(--link-color);
+ opacity: 0;
+ transition: 100ms ease-in-out opacity;
}
.posts-page h2:hover::before,
@@ -425,413 +408,411 @@ ul li {
.posts-page h4:hover::before,
.posts-page h5:hover::before,
.posts-page h6:hover::before {
- content: '§';
- opacity: 1;
+ content: '§';
+ opacity: 1;
}
.article-page .toc {
- font-family: var(--sans-serif-font);
- font-size: 0.9rem;
- line-height: 1.4;
+ font-family: var(--sans-serif-font);
+ font-size: 0.9rem;
+ line-height: 1.4;
}
.toc details {
- padding: 0.5rem 0.75rem;
- border-radius: 0;
+ padding: 0.5rem 0.75rem;
+ border-radius: 0;
}
.toc > details > ol {
- margin: 0.75rem 0 0;
- padding-left: 1.5rem;
- list-style-type: decimal;
+ 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;
+ margin-top: 0;
+ padding-left: 0.75rem;
+ list-style-type: lower-alpha;
}
.toc ol ol ol {
- list-style-type: lower-roman;
+ list-style-type: lower-roman;
}
.toc li {
- margin: 0.25rem 0;
- padding-left: 0.15rem;
+ margin: 0.25rem 0;
+ padding-left: 0.15rem;
}
.toc a {
- text-decoration: none;
+ text-decoration: none;
}
.toc a:hover {
- text-decoration: underline;
+ 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;
+ 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;
+ margin: 0;
+ padding: 1rem;
+ border: 1px solid var(--main-border-color);
+ border-top: 0;
}
.article-page .code-block {
- margin: 1.5rem 0;
+ 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;
+ 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.8rem;
+ 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;
+ align-self: stretch;
+ min-width: 5.5rem;
+ padding: 0.25rem 0.75rem;
+ border: 0;
+ border-left: 1px solid var(--secondary-border-color);
+ color: #ffffff;
+ background: transparent;
+ font: inherit;
+ cursor: pointer;
}
.copy-code:hover {
- background-color: var(--tertiary-green);
- color: #cccccc;
+ background-color: var(--tertiary-green);
+ color: #cccccc;
}
.copy-code:focus-visible {
- outline: 2px solid var(--link-color);
- outline-offset: -3px;
+ outline: 2px solid var(--link-color);
+ outline-offset: -3px;
}
.article-page pre.line-numbers {
- padding-left: 3.8em;
+ padding-left: 3.8em;
}
.article-page pre > code {
- display: block;
- overflow: visible;
- padding: 0;
- background: transparent;
+ display: block;
+ overflow: visible;
+ padding: 0;
+ background: transparent;
}
.article-page hr {
- margin: 2rem 0;
- border: 0;
- border-top: 1px solid var(--main-border-color);
+ 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;
+ width: max-content;
+ border: 1px solid var(--main-border-color);
+ white-space: nowrap;
}
.table-scroll {
- width: 100%;
- overflow-x: auto;
+ width: 100%;
+ max-width: 100%;
+ overflow-x: auto;
}
.article-page table thead {
- background-color: var(--secondary-green);
+ 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);
+ padding: 0.4rem 0.75rem;
+ text-align: left;
}
.margin-toggle {
- display: none;
+ display: none;
}
.sidenote {
- float: none;
- width: 18rem;
- margin-right: 0;
- margin-top: 0.25rem;
- color: #bbbbbb;
- font-size: 0.90rem;
- line-height: 1.4;
+ float: none;
+ width: 18rem;
+ margin-right: 0;
+ margin-top: 0.25rem;
+ color: #bbbbbb;
+ font-size: 0.9rem;
+ line-height: 1.4;
}
.sidenote::before {
- content: counter(sidenote-counter) '. ';
- color: var(--link-color);
- font-weight: bold;
+ 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;
+ 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;
+ display: inline-block;
+ min-width: 0.8rem;
}
.article-page {
- counter-reset: sidenote-counter;
+ 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 {
+ 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::before {
+ content: none;
+ }
- .sidenote-toggle:checked + .margin-toggle + .sidenote {
- display: block !important;
- }
+ .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-toggle:focus-visible + .sidenote-number {
+ outline: 2px solid var(--link-color);
+ outline-offset: 2px;
+ }
- .sidenote-number {
- cursor: pointer;
- }
+ .sidenote-number {
+ cursor: pointer;
+ }
}
@media screen and (min-width: 1400px) {
- .article-page .toc + hr {
- display: none;
- }
+ .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 {
+ 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::-webkit-scrollbar {
+ display: none;
+ }
- .article-page .toc summary {
- cursor: default;
- list-style: none;
- pointer-events: none;
- }
+ .article-page .toc summary {
+ cursor: default;
+ list-style: none;
+ pointer-events: none;
+ }
- .article-page .toc summary::-webkit-details-marker {
- display: 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;
- }
+ .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;
+ 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);
+ 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);
+ 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;
+ box-shadow: 0 0 0 2px #ffffff;
}
.button.blue {
- background: var(--primary-blue);
+ background: var(--primary-blue);
}
.button.blue:hover {
- background: var(--secondary-blue);
- box-shadow: 0 0 0 1px var(--secondary-blue);
+ 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);
+ background: var(--tertiary-blue);
+ box-shadow: 0 0 0 1px var(--tertiary-green);
}
.button.blue:focus {
- box-shadow: 0 0 0 2px #ffffff;
+ box-shadow: 0 0 0 2px #ffffff;
}
.text.center {
- text-align: 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;
+ content: '';
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ margin-left: 0.3rem;
+ vertical-align: -.175rem;
+ 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;
+ display: inline-block;
+ transition: 50ms ease-in-out all;
}
.button.link:hover::after {
- transform: translateX(0.25rem) scale(1.3);
+ transform: translateX(0.25rem) scale(1.3);
+}
+
+.button.link.extern::after {
+ transform: rotateZ(-45deg);
}
.button.link.extern:hover::after {
- transform: rotateZ(-45deg) scale(1.5);
+ transform: rotateZ(0) translateX(0.25rem) scale(1.3);
}
.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;
+ content: '';
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ margin-right: 0.3rem;
+ vertical-align: -0.175em;
+ 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);
+ transform: translateX(-0.2rem) rotate(180deg) scale(1.3);
}
.button.link.back::after {
- content: '';
- display: none;
+ content: '';
+ display: none;
}
.page-heading {
- text-align: center;
- margin: auto;
- background-color: var(--main-background-color);
+ text-align: center;
+ margin: auto;
+ background-color: var(--main-background-color);
}
.page-heading .title {
- border-bottom: 1px solid #FFFFFF;
- max-width: 95%;
- margin: auto;
+ 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;
+ 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;
+ 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;
- }
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }
}
diff --git a/src/main.js b/src/main.js
index de5d341..3a472b4 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,22 +1,24 @@
-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';
+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 relativeDateFormatter = new Intl.RelativeTimeFormat(undefined, {
+ numeric: 'auto',
+})
const relativeTimeUnits = [
[31536000, 'year'],
[2592000, 'month'],
@@ -24,66 +26,75 @@ const relativeTimeUnits = [
[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.classList.add('language-plain')
}
- code.closest('pre').classList.add('line-numbers');
-});
-Prism.highlightAll();
+ code.closest('pre').classList.add('line-numbers')
+})
+Prism.highlightAll()
+
+const sidebarTocMedia = window.matchMedia('(min-width: 1400px)')
+const syncSidebarToc = () => {
+ if (!sidebarTocMedia.matches) return
+ document.querySelectorAll('.article-page .toc details').forEach((details) => {
+ details.open = true
+ })
+}
+
+syncSidebarToc()
+sidebarTocMedia.addEventListener('change', syncSidebarToc)
const copyText = async (text) => {
if (navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(text);
- return;
+ 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');
-};
+ 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;
+ const code = button.closest('.code-block')?.querySelector('code')
+ if (!code) return
try {
- await copyText(code.textContent);
- button.textContent = 'Copied';
+ await copyText(code.textContent)
+ button.textContent = 'Copied'
} catch {
- button.textContent = 'Copy failed';
+ button.textContent = 'Copy failed'
}
window.setTimeout(() => {
- button.textContent = 'Copy';
- }, 1500);
- });
-});
+ button.textContent = 'Copy'
+ }, 1500)
+ })
+})
document.querySelectorAll('[data-value]').forEach((element) => {
- const date = new Date(element.dataset.value);
- if (Number.isNaN(date.valueOf())) return;
+ 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;
+ 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;
-});
+ const formatted = absoluteDateFormatter.format(date)
+ element.textContent = element.classList.contains('last-updated') ? `Last updated: ${formatted}` : formatted
+})
diff --git a/vite.config.js b/vite.config.js
index 992ee00..fc3feaa 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,48 +1,48 @@
-import fs from 'node:fs';
-import { spawn } from 'node:child_process';
-import path from 'node:path';
-import { defineConfig } from 'vite';
+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 = {};
+ 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));
+ 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);
+ const relativePath = path.relative('.generated', filePath)
+ const key = relativePath.endsWith('/index.html') ? relativePath.slice(0, -'/index.html'.length) : relativePath
+ inputs[key] = path.resolve(filePath)
+ } else if (entry.name === '404.html' && directory === '.generated') {
+ inputs['404'] = path.resolve(filePath)
}
}
- return inputs;
+ return inputs
}
function contentGenerator() {
- let timer;
- let generator;
- let rerun = false;
+ let timer
+ let generator
+ let rerun = false
const generate = (server) => {
if (generator) {
- rerun = true;
- return;
+ rerun = true
+ return
}
- generator = spawn(process.execPath, ['generator.js'], { stdio: 'inherit' });
+ generator = spawn(process.execPath, ['generator.js'], { stdio: 'inherit' })
generator.on('error', (error) => {
- console.error(`Content generation failed: ${error.message}`);
- });
+ console.error(`Content generation failed: ${error.message}`)
+ })
generator.on('close', (code) => {
- generator = undefined;
- if (code === 0) server.ws.send({ type: 'full-reload' });
+ generator = undefined
+ if (code === 0) server.ws.send({ type: 'full-reload' })
if (rerun) {
- rerun = false;
- generate(server);
+ rerun = false
+ generate(server)
}
- });
- };
+ })
+ }
return {
name: 'content-generator',
@@ -55,22 +55,25 @@ function contentGenerator() {
path.resolve('external.json'),
path.resolve('generator.js'),
path.resolve('html.js'),
- ];
- server.watcher.add(sources);
+ ]
+ 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);
+ 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 === path.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);
- });
+ generate(server)
+ }, 100)
+ })
},
- };
+ }
}
export default defineConfig({
@@ -94,4 +97,4 @@ export default defineConfig({
input: generatedPages('.generated'),
},
},
-});
+})