Migrate documentation site to Astro

This commit is contained in:
Keisuke Hirata 2026-06-17 00:21:52 +09:00
parent 4020b7c2d5
commit 0e873fbd51
No known key found for this signature in database
19 changed files with 5494 additions and 325 deletions

3
.gitignore vendored
View File

@ -6,3 +6,6 @@
node_modules
/dist
site/decodal-site/dist
# Astro
site/decodal-site/.astro

View File

@ -22,7 +22,7 @@ cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
## Web site and playground
The Svelte documentation site and browser playground are kept in:
The Astro documentation site and browser playground are kept in:
```text
site/decodal-site/
@ -34,8 +34,9 @@ The playground loads `decodal-wasm` and evaluates DCDL entirely in the browser.
Important files:
```text
site/decodal-site/src/App.svelte
site/decodal-site/src/Playground.svelte
site/decodal-site/src/pages/docs/[...slug].astro
site/decodal-site/src/pages/playground.astro
site/decodal-site/src/layouts/ManualLayout.astro
site/decodal-site/src/lib/docs.js
crates/decodal-wasm/src/lib.rs
```

View File

@ -0,0 +1,5 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
});

View File

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Decodal</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,14 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0",
"dev": "astro dev --host 0.0.0.0",
"build": "astro build",
"preview": "astro preview --host 0.0.0.0",
"build:wasm": "wasm-pack build ../../crates/decodal-wasm --target web --out-dir ../../site/decodal-site/src/wasm --release"
},
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"marked": "^12.0.2",
"svelte": "^4.2.18",
"vite": "^5.3.5"
},
"devDependencies": {}
"@astrojs/check": "^0.9.4",
"astro": "^4.16.18",
"marked": "^12.0.2"
}
}

View File

@ -1,52 +0,0 @@
<script>
import { onMount } from 'svelte';
import NavTree from './NavTree.svelte';
import Playground from './Playground.svelte';
import { nav, renderMarkdown } from './lib/docs.js';
let route = parseRoute(location.hash);
onMount(() => {
const onHashChange = () => (route = parseRoute(location.hash));
addEventListener('hashchange', onHashChange);
return () => removeEventListener('hashchange', onHashChange);
});
$: isPlayground = route.kind === 'playground';
$: currentSlug = route.slug ?? 'introduction';
$: html = isPlayground ? '' : renderMarkdown(currentSlug);
function parseRoute(hash) {
const raw = hash.replace(/^#\/?/, '');
if (raw === 'playground') return { kind: 'playground' };
if (raw.startsWith('docs/')) {
return { kind: 'docs', slug: raw.slice('docs/'.length) || 'introduction' };
}
return { kind: 'docs', slug: 'introduction' };
}
</script>
<header class="topbar">
<a class="brand" href="#/docs/introduction">Decodal</a>
<nav class="topnav">
<a href="#/docs/introduction">Docs</a>
<a href="#/playground">Playground</a>
</nav>
</header>
<div class="layout">
<aside class="sidebar">
<a class="sidebar-title" href="#/docs/index">Manual</a>
<NavTree items={nav} active={currentSlug} />
</aside>
<main class:playground={isPlayground}>
{#if isPlayground}
<Playground />
{:else}
<article class="markdown">
{@html html}
</article>
{/if}
</main>
</div>

View File

@ -1,15 +0,0 @@
<script>
export let items = [];
export let active = '';
</script>
<ul class="nav-tree">
{#each items as item}
<li>
<a class:active={active === item.slug} href={`#/docs/${item.slug}`}>{item.title}</a>
{#if item.children}
<svelte:self items={item.children} {active} />
{/if}
</li>
{/each}
</ul>

View File

@ -1,81 +0,0 @@
<script>
import { onMount } from 'svelte';
const starter = `let
Service = {
name = String;
port = Int & > 443 default 8443;
feature.enable = Bool default true;
};
in
Service & {
name = "api";
port = 9443;
}
`;
let source = starter;
let output = '';
let error = '';
let ready = false;
let loading = true;
let evaluate;
onMount(async () => {
try {
const wasm = await import('./wasm/decodal_wasm.js');
await wasm.default();
evaluate = wasm.evaluate;
ready = true;
run();
} catch (err) {
error = `Failed to load WASM playground: ${err.message ?? err}`;
} finally {
loading = false;
}
});
function run() {
if (!evaluate) return;
const result = JSON.parse(evaluate(source));
if (result.ok) {
output = result.output;
error = '';
} else {
output = '';
error = result.error;
}
}
</script>
<section class="playground-page">
<div class="playground-header">
<div>
<h1>Playground</h1>
<p>Evaluate Decodal directly in your browser through WebAssembly.</p>
</div>
<button on:click={run} disabled={!ready}>Run</button>
</div>
{#if loading}
<p class="status">Loading WASM...</p>
{/if}
<div class="playground-grid">
<label class="pane">
<span>Input</span>
<textarea bind:value={source} spellcheck="false" on:keydown={(event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') run();
}} />
</label>
<section class="pane output-pane">
<span>Output</span>
{#if error}
<pre class="error">{error}</pre>
{:else}
<pre>{output}</pre>
{/if}
</section>
</div>
</section>

1
site/decodal-site/src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

View File

@ -0,0 +1,43 @@
---
import { nav } from '../lib/docs.js';
import '../style.css';
const { title = 'Decodal', active = '' } = Astro.props;
function renderNav(items) {
return `<ul class="nav-tree">${items
.map((item) => {
const href = `/docs/${item.slug}/`;
const activeClass = active === item.slug ? ' class="active"' : '';
const children = item.children ? renderNav(item.children) : '';
return `<li><a${activeClass} href="${href}">${item.title}</a>${children}</li>`;
})
.join('')}</ul>`;
}
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<header class="topbar">
<a class="brand" href="/docs/introduction/">Decodal</a>
<nav class="topnav">
<a href="/docs/introduction/">Docs</a>
<a href="/playground/">Playground</a>
</nav>
</header>
<div class="layout">
<aside class="sidebar">
<a class="sidebar-title" href="/docs/">Manual</a>
<nav set:html={renderNav(nav)} />
</aside>
<main class={Astro.props.playground ? 'playground' : ''}>
<slot />
</main>
</div>
</body>
</html>

View File

@ -79,18 +79,18 @@ export const nav = [
{ title: 'Open Issues', slug: 'open-issues' },
];
marked.setOptions({
gfm: true,
mangle: false,
headerIds: true,
});
marked.setOptions({ gfm: true });
export function allDocSlugs() {
return Object.keys(docs).filter((slug) => slug !== 'index');
}
export function renderMarkdown(slug) {
const source = docs[slug] ?? docs.index;
const html = marked.parse(source ?? '# Not found\n');
return html.replace(/href="([^"#][^"]*)\.md(#[^"]*)?"/g, (_all, href, hash = '') => {
const target = normalizeDocLink(slug, href);
return `href="#/docs/${target}${hash}"`;
return `href="/docs/${target}/${hash}"`;
});
}

View File

@ -1,8 +0,0 @@
import App from './App.svelte';
import './style.css';
const app = new App({
target: document.getElementById('app'),
});
export default app;

View File

@ -0,0 +1,17 @@
---
import ManualLayout from '../../layouts/ManualLayout.astro';
import { allDocSlugs, renderMarkdown } from '../../lib/docs.js';
export function getStaticPaths() {
return allDocSlugs().map((slug) => ({
params: { slug },
props: { slug },
}));
}
const { slug } = Astro.props;
const html = renderMarkdown(slug);
---
<ManualLayout title={`Decodal - ${slug}`} active={slug}>
<article class="markdown" set:html={html} />
</ManualLayout>

View File

@ -0,0 +1,3 @@
---
return Astro.redirect('/docs/introduction/');
---

View File

@ -0,0 +1,3 @@
---
return Astro.redirect('/docs/introduction/');
---

View File

@ -0,0 +1,26 @@
---
import ManualLayout from '../layouts/ManualLayout.astro';
---
<ManualLayout title="Decodal Playground" playground>
<section class="playground-page">
<div class="playground-header">
<div>
<h1>Playground</h1>
<p>Evaluate Decodal directly in your browser through WebAssembly.</p>
</div>
<button id="run" disabled>Run</button>
</div>
<p id="status" class="status">Loading WASM...</p>
<div class="playground-grid">
<label class="pane">
<span>Input</span>
<textarea id="source" spellcheck="false"></textarea>
</label>
<section class="pane output-pane">
<span>Output</span>
<pre id="output"></pre>
</section>
</div>
</section>
<script type="module" src="../scripts/playground.js"></script>
</ManualLayout>

View File

@ -0,0 +1,41 @@
import init, { evaluate } from '../wasm/decodal_wasm.js';
const starter = `let
Service = {
name = String;
port = Int & > 443 default 8443;
feature.enable = Bool default true;
};
in
Service & {
name = "api";
port = 9443;
}
`;
const source = document.getElementById('source');
const output = document.getElementById('output');
const run = document.getElementById('run');
const status = document.getElementById('status');
source.value = starter;
function execute() {
const result = JSON.parse(evaluate(source.value));
output.textContent = result.ok ? result.output : result.error;
output.classList.toggle('error', !result.ok);
}
try {
await init();
run.disabled = false;
status.textContent = '';
execute();
} catch (error) {
status.textContent = `Failed to load WASM: ${error?.message ?? error}`;
}
run.addEventListener('click', execute);
source.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') execute();
});

View File

@ -1,14 +0,0 @@
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';
const repoRoot = fileURLToPath(new URL('../..', import.meta.url));
export default defineConfig({
plugins: [svelte()],
server: {
fs: {
allow: [repoRoot],
},
},
});