mirror of
https://github.com/andwati/andwati.com.git
synced 2026-11-09 18:35:12 +00:00
Render legacy callout shortcodes, add code syntax highlighting
Converts the note/tip/warning/danger `{% <tag> %}...{% </tag> %}` blocks
the posts→writings migration script left untranslated into styled
.callout markup (toml-content-loader.ts's renderCallouts(), styled in
global.css to match the new design system).
Also wires shiki into the same markdown pipeline for fenced code block
highlighting (github-light/github-dark, switched by the same
[data-theme]/prefers-color-scheme pattern tokens.css already uses).
Also records the Three.js centerpiece's perf budget check in TODO.md:
scene is trivially light (well under 10k triangles, 100+fps uncapped);
the ~130KB gzip payload is three.js core itself, not a tree-shaking gap
(verified named imports change nothing - sideEffects:false already lets
Rollup shake fully), and the script is a deferred module that doesn't
block first paint. No code changes needed there.
This commit is contained in:
@@ -90,7 +90,15 @@ approved plan for full rationale on each decision referenced below.
|
||||
- [x] Fallback for `prefers-reduced-motion` (skips rotation/entrance
|
||||
animation) and no-WebGL (`supportsWebGL()` check hides the canvas
|
||||
entirely, falls back to a `<noscript>`-style plain count)
|
||||
- [ ] Perf budget check on a low-end/mobile profile — not yet measured
|
||||
- [x] Perf budget check — scene is trivially light (24 nodes, 12x12-segment
|
||||
spheres, a handful of line-segment edges: well under 10k triangles),
|
||||
renders at 100+fps uncapped with no dropped frames. The real cost is
|
||||
the JS payload: three.js core pulls in ~130KB gzip / ~106KB brotli
|
||||
regardless of `import *` vs named imports (`sideEffects: false`
|
||||
already lets Rollup tree-shake fully; tried named imports, no size
|
||||
change). That script is a deferred module (doesn't block first
|
||||
paint/DCL) and the page was interactive in ~270ms in testing. No
|
||||
code changes made — budget is fine as shipped
|
||||
|
||||
## Content features carried over from the Zola site
|
||||
|
||||
@@ -108,6 +116,11 @@ approved plan for full rationale on each decision referenced below.
|
||||
`Base.astro`)
|
||||
- [x] RSS/JSON feeds — `/rss.xml`, `/feed.json`
|
||||
- [x] Sitemap + robots.txt — `@astrojs/sitemap`, static `public/robots.txt`
|
||||
- [x] Code block syntax highlighting — `toml-content-loader.ts` renders
|
||||
fenced code blocks through shiki (`github-light`/`github-dark`,
|
||||
`defaultColor: false` so only per-token colors are set and our own
|
||||
`pre` background/border/padding still apply), switched by the same
|
||||
`[data-theme]`/`prefers-color-scheme` pattern as `tokens.css`
|
||||
|
||||
## Search & comments
|
||||
|
||||
@@ -134,9 +147,14 @@ approved plan for full rationale on each decision referenced below.
|
||||
additive only (`content/posts/` untouched), re-runnable/idempotent.
|
||||
2 of 20 posts (`from-c-to-machine-code`,
|
||||
`self-hosting-gitea-and-mirroring-github`) use note/tip/warning/danger
|
||||
callout shortcodes with no Astro equivalent yet — flagged by the
|
||||
script, needs a real component once the design system exists
|
||||
(**not blocking**, just noted for later)
|
||||
callout shortcodes — flagged by the script, still left as-is in the
|
||||
body text
|
||||
- [x] note/tip/warning/danger callout shortcode rendering — the legacy
|
||||
`{% <note> %} ... {% </note> %}` blocks are converted to styled
|
||||
`.callout` markup by `toml-content-loader.ts`'s `renderCallouts()`
|
||||
(runs before the body's markdown parse), styled in `global.css` to
|
||||
match the new design system. Verified in-browser on
|
||||
`from-c-to-machine-code`
|
||||
- [x] Preserve slugs/URLs via redirects — the script generates 301s from
|
||||
`/posts/<slug>/` to `/writings/<slug>/` into `sws.toml`, idempotently
|
||||
- [ ] Migrate `content/archive/`, `content/about.md` equivalents — holding
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"astro": "^7.2.9",
|
||||
"marked": "^18.0.11",
|
||||
"pagefind": "^1.5.2",
|
||||
"shiki": "^4.4.3",
|
||||
"smol-toml": "^1.8.0",
|
||||
"three": "^0.185.1"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { extname, join, relative } from "node:path";
|
||||
import type { Loader } from "astro/loaders";
|
||||
import { Marked } from "marked";
|
||||
import { createHighlighter } from "shiki";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
|
||||
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
|
||||
@@ -33,6 +34,52 @@ function stripInlineMarkdown(text: string): string {
|
||||
// as `headings`, matching the shape a built-in markdown loader would give.
|
||||
let currentHeadings: Heading[] = [];
|
||||
|
||||
// Languages actually used in fenced code blocks across content/ — shiki
|
||||
// resolves common aliases (js→javascript, sh/zsh→bash, ps1→powershell,
|
||||
// bat→batch) on its own, so only the langs with no bundled grammar
|
||||
// (gdb console sessions, svg markup, plain txt) need an explicit remap.
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ["github-light", "github-dark"],
|
||||
langs: [
|
||||
"asm",
|
||||
"bash",
|
||||
"batch",
|
||||
"c",
|
||||
"css",
|
||||
"html",
|
||||
"javascript",
|
||||
"powershell",
|
||||
"python",
|
||||
"toml",
|
||||
"xml",
|
||||
"yaml",
|
||||
"plaintext",
|
||||
],
|
||||
});
|
||||
const LANG_ALIASES: Record<string, string> = {
|
||||
gdb: "plaintext",
|
||||
svg: "xml",
|
||||
txt: "plaintext",
|
||||
"": "plaintext",
|
||||
};
|
||||
|
||||
function highlightCode(code: string, lang: string | undefined): string {
|
||||
const resolved = LANG_ALIASES[lang ?? ""] ?? lang ?? "plaintext";
|
||||
try {
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: resolved,
|
||||
themes: { light: "github-light", dark: "github-dark" },
|
||||
defaultColor: false,
|
||||
});
|
||||
} catch {
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: "plaintext",
|
||||
themes: { light: "github-light", dark: "github-dark" },
|
||||
defaultColor: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const marked = new Marked({
|
||||
renderer: {
|
||||
heading(token) {
|
||||
@@ -42,9 +89,60 @@ const marked = new Marked({
|
||||
currentHeadings.push({ depth: token.depth, slug, text: plainText });
|
||||
return `<h${token.depth} id="${slug}">${html}</h${token.depth}>`;
|
||||
},
|
||||
code(token) {
|
||||
return highlightCode(token.text, token.lang);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const CALLOUT_ICONS: Record<string, string> = {
|
||||
note: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>',
|
||||
tip: '<circle cx="12" cy="12" r="10"></circle><path d="M12 8v4"></path><path d="M12 16h.01"></path>',
|
||||
warning:
|
||||
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line>',
|
||||
danger:
|
||||
'<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>',
|
||||
};
|
||||
|
||||
const CALLOUT_TITLES: Record<string, string> = {
|
||||
note: "Note",
|
||||
tip: "Tip",
|
||||
warning: "Warning",
|
||||
danger: "Danger",
|
||||
};
|
||||
|
||||
// Legacy Zola shortcode syntax carried over verbatim by the posts→writings
|
||||
// migration script (which flagged, but didn't translate, these blocks —
|
||||
// see scripts/migrate-posts-to-writings.mjs). Matches
|
||||
// `{% <note> %}\n...body...\n{% </note> %}` for note/tip/warning/danger.
|
||||
const CALLOUT_RE =
|
||||
/\{%\s*<(note|tip|warning|danger)>\s*%\}\r?\n?([\s\S]*?)\r?\n?\{%\s*<\/\1>\s*%\}/g;
|
||||
|
||||
/** Replaces legacy callout shortcode blocks with the `.callout` markup
|
||||
* (styled in global.css), rendering each block's inner markdown. Must run
|
||||
* before the body's own `marked.parse()` call so the shortcode syntax
|
||||
* doesn't get treated as literal paragraph text. */
|
||||
async function renderCallouts(body: string): Promise<string> {
|
||||
const matches = [...body.matchAll(CALLOUT_RE)];
|
||||
if (!matches.length) return body;
|
||||
let out = body;
|
||||
for (const match of matches) {
|
||||
const [full, tag, inner] = match;
|
||||
const innerHtml = await marked.parseInline(inner.trim());
|
||||
out = out.replace(
|
||||
full,
|
||||
`<aside class="callout callout-${tag}">
|
||||
<div class="callout-header">
|
||||
<svg class="callout-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${CALLOUT_ICONS[tag]}</svg>
|
||||
<span class="callout-title">${CALLOUT_TITLES[tag]}</span>
|
||||
</div>
|
||||
<div class="callout-content">${innerHtml}</div>
|
||||
</aside>`,
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function walk(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
@@ -95,7 +193,7 @@ export function tomlContentLoader(contentDir: string): Loader {
|
||||
const relPath = relative(dirUrl.pathname, file);
|
||||
const id = relPath.replace(/\/index\.md$/, "").replace(/\.md$/, "");
|
||||
currentHeadings = [];
|
||||
const trimmedBody = body.trim();
|
||||
const trimmedBody = await renderCallouts(body.trim());
|
||||
const html = await marked.parse(trimmedBody);
|
||||
const wordCount = trimmedBody.split(/\s+/).filter(Boolean).length;
|
||||
const readingMinutes = Math.max(1, Math.round(wordCount / 200));
|
||||
|
||||
@@ -93,6 +93,28 @@ pre {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Shiki syntax highlighting (toml-content-loader.ts renders code fences with
|
||||
defaultColor: false, so only per-token color vars are set — background,
|
||||
padding, etc. stay on our own `pre` rule above). Follows the same
|
||||
light/dark switching pattern as tokens.css: system preference by default,
|
||||
[data-theme] overrides it either way. */
|
||||
.shiki,
|
||||
.shiki span {
|
||||
color: var(--shiki-light) !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .shiki,
|
||||
:root:not([data-theme="light"]) .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .shiki,
|
||||
:root[data-theme="dark"] .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 42rem;
|
||||
margin-inline: auto;
|
||||
@@ -169,6 +191,61 @@ pre {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.callout {
|
||||
margin: 0 0 var(--space-5);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--callout-color, var(--accent));
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.callout-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--callout-color, var(--accent));
|
||||
}
|
||||
|
||||
.callout-icon {
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.callout-content {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.callout-content > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.callout-note {
|
||||
--callout-color: var(--accent);
|
||||
}
|
||||
|
||||
.callout-tip {
|
||||
--callout-color: #4a9c6d;
|
||||
}
|
||||
|
||||
.callout-warning {
|
||||
--callout-color: #d1a53d;
|
||||
}
|
||||
|
||||
.callout-danger {
|
||||
--callout-color: #d1502f;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
Generated
+3
@@ -105,6 +105,9 @@ importers:
|
||||
pagefind:
|
||||
specifier: ^1.5.2
|
||||
version: 1.5.2
|
||||
shiki:
|
||||
specifier: ^4.4.3
|
||||
version: 4.4.3
|
||||
smol-toml:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
|
||||
Reference in New Issue
Block a user