Feat: documentation, UI/UX
This commit is contained in:
338
website/src/pages/docs.astro
Normal file
338
website/src/pages/docs.astro
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
import fs from 'node:fs';
|
||||
|
||||
// --- Forth syntax highlighter ---
|
||||
|
||||
const EMIT = new Set(['.', '.!']);
|
||||
const SOUNDS = new Set([
|
||||
'sound', 's', 'saw', 'sine', 'kick', 'hat', 'snare', 'modal', 'noise',
|
||||
'square', 'tri', 'pulse', 'clap', 'rim', 'crash', 'fm', 'sample', 'plaits',
|
||||
'analog', 'waveshaping', 'granular', 'string', 'chord', 'speech', 'sub',
|
||||
'super', 'wt', 'input', 'hh',
|
||||
]);
|
||||
const PARAMS = new Set([
|
||||
'freq', 'note', 'gain', 'decay', 'attack', 'release', 'lpf', 'hpf', 'bpf',
|
||||
'verb', 'delay', 'pan', 'orbit', 'harmonics', 'distort', 'speed', 'voice',
|
||||
'dur', 'sustain', 'delaytime', 'delayfb', 'chorus', 'phaser', 'flanger',
|
||||
'crush', 'fold', 'wrap', 'resonance', 'begin', 'end', 'velocity', 'chan',
|
||||
'dev', 'ccnum', 'ccout', 'bend', 'pressure', 'program', 'tilt', 'slope',
|
||||
'sub_gain', 'sub_oct', 'feedback', 'depth', 'sweep', 'comb', 'damping',
|
||||
'detune', 'timbre', 'morph', 'color', 'model', 'cutoff',
|
||||
]);
|
||||
const STACK = new Set([
|
||||
'dup', 'drop', 'swap', 'over', 'rot', 'nip', 'tuck', '2dup', '2drop',
|
||||
]);
|
||||
const PROB = new Set([
|
||||
'chance', 'prob', 'choose', 'wchoose', 'cycle', 'pcycle', 'bounce',
|
||||
'rand', 'exprand', 'logrand', 'coin', 'seed', 'often', 'sometimes',
|
||||
'rarely', 'always', 'never', 'almostAlways', 'almostNever', 'every',
|
||||
'except', 'bjork', 'pbjork', 'shuffle',
|
||||
]);
|
||||
const KEYWORDS = new Set([
|
||||
':', ';', 'if', 'then', 'else', 'do', 'loop', '?', '!?', 'ifelse',
|
||||
'select', 'apply', 'times', 'forget', 'all', 'noall', 'clear', 'm.',
|
||||
]);
|
||||
const CONTEXT = new Set([
|
||||
'step', 'beat', 'pattern', 'tempo', 'phase', 'runs', 'iter', 'fill', 'stepdur',
|
||||
]);
|
||||
const NOTE_RE = /^[a-g][#sb]?\d+$/i;
|
||||
const NUM_RE = /^-?\d+(\.\d+)?$/;
|
||||
|
||||
function classifyToken(word: string): string | null {
|
||||
if (EMIT.has(word)) return 'f-emit';
|
||||
if (SOUNDS.has(word)) return 'f-snd';
|
||||
if (PARAMS.has(word)) return 'f-par';
|
||||
if (STACK.has(word)) return 'f-stack';
|
||||
if (PROB.has(word)) return 'f-prob';
|
||||
if (KEYWORDS.has(word)) return 'f-kw';
|
||||
if (CONTEXT.has(word)) return 'f-ctx';
|
||||
if (NOTE_RE.test(word)) return 'f-note';
|
||||
if (NUM_RE.test(word)) return 'f-num';
|
||||
if (word.length > 1 && (word[0] === '@' || word[0] === '!' || word[0] === ',')) return 'f-var';
|
||||
return null;
|
||||
}
|
||||
|
||||
function highlightForth(code: string): string {
|
||||
return code.split('\n').map(line => {
|
||||
let result = '';
|
||||
let i = 0;
|
||||
while (i < line.length) {
|
||||
if (line[i] === ' ' || line[i] === '\t') { result += line[i]; i++; continue; }
|
||||
if (line[i] === ';' && line[i + 1] === ';') {
|
||||
result += `<span class="f-com">${escapeHtml(line.slice(i))}</span>`;
|
||||
break;
|
||||
}
|
||||
if (line[i] === '"') {
|
||||
let end = line.indexOf('"', i + 1);
|
||||
if (end === -1) end = line.length - 1;
|
||||
result += escapeHtml(line.slice(i, end + 1));
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
if (line[i] === '(' || line[i] === ')') { result += line[i]; i++; continue; }
|
||||
let end = i;
|
||||
while (end < line.length && line[end] !== ' ' && line[end] !== '\t') end++;
|
||||
const word = line.slice(i, end);
|
||||
const cls = classifyToken(word);
|
||||
result += cls ? `<span class="${cls}">${escapeHtml(word)}</span>` : escapeHtml(word);
|
||||
i = end;
|
||||
}
|
||||
return result;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// --- Markdown renderer ---
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function inline(text: string): string {
|
||||
const codes: string[] = [];
|
||||
text = text.replace(/`([^`]+)`/g, (_, code) => { codes.push(code); return `\x00${codes.length - 1}\x00`; });
|
||||
text = escapeHtml(text);
|
||||
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
text = text.replace(/_([^_]+)_/g, '<em>$1</em>');
|
||||
text = text.replace(/\x00(\d+)\x00/g, (_, i) => `<code>${escapeHtml(codes[+i])}</code>`);
|
||||
return text;
|
||||
}
|
||||
|
||||
function parseTableCells(line: string, tag: string): string {
|
||||
return line.split('|').slice(1, -1).map(cell => `<${tag}>${inline(cell.trim())}</${tag}>`).join('');
|
||||
}
|
||||
|
||||
function renderMarkdown(md: string): string {
|
||||
const lines = md.split('\n');
|
||||
let html = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
if (line.trim() === '') { i++; continue; }
|
||||
|
||||
if (line.startsWith('### ')) { html += `<h3>${inline(line.slice(4))}</h3>`; i++; continue; }
|
||||
if (line.startsWith('## ')) { html += `<h2>${inline(line.slice(3))}</h2>`; i++; continue; }
|
||||
if (line.startsWith('# ')) { html += `<h1>${inline(line.slice(2))}</h1>`; i++; continue; }
|
||||
|
||||
if (/^---+$|^\*\*\*+$/.test(line.trim())) { html += '<hr>'; i++; continue; }
|
||||
|
||||
if (line.startsWith('> ')) {
|
||||
html += '<blockquote>';
|
||||
while (i < lines.length && lines[i].startsWith('> ')) {
|
||||
html += `<p>${inline(lines[i].slice(2))}</p>`;
|
||||
i++;
|
||||
}
|
||||
html += '</blockquote>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('```')) {
|
||||
const lang = line.slice(3).trim();
|
||||
i++;
|
||||
let code = '';
|
||||
while (i < lines.length && !lines[i].startsWith('```')) {
|
||||
code += (code ? '\n' : '') + lines[i];
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
html += lang === 'forth'
|
||||
? `<pre><code>${highlightForth(code)}</code></pre>`
|
||||
: `<pre><code>${escapeHtml(code)}</code></pre>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('|')) {
|
||||
html += '<table>';
|
||||
html += `<tr>${parseTableCells(line, 'th')}</tr>`;
|
||||
i++;
|
||||
if (i < lines.length && /^\|[\s:|-]+\|$/.test(lines[i])) i++;
|
||||
while (i < lines.length && lines[i].startsWith('|')) {
|
||||
html += `<tr>${parseTableCells(lines[i], 'td')}</tr>`;
|
||||
i++;
|
||||
}
|
||||
html += '</table>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\d+\.\s/.test(line)) {
|
||||
html += '<ol>';
|
||||
while (i < lines.length && (/^\d+\.\s/.test(lines[i]) || /^\s+[*-]\s/.test(lines[i]))) {
|
||||
if (/^\d+\.\s/.test(lines[i])) {
|
||||
html += `<li>${inline(lines[i].replace(/^\d+\.\s/, ''))}`;
|
||||
i++;
|
||||
if (i < lines.length && /^\s+[*-]\s/.test(lines[i])) {
|
||||
html += '<ul>';
|
||||
while (i < lines.length && /^\s+[*-]\s/.test(lines[i])) {
|
||||
html += `<li>${inline(lines[i].replace(/^\s*[*-]\s/, ''))}</li>`;
|
||||
i++;
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
html += '</li>';
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
html += '</ol>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^(\s*)[*-]\s/.test(line)) {
|
||||
let depth = 0;
|
||||
let first = true;
|
||||
html += '<ul>';
|
||||
while (i < lines.length && /^(\s*)[*-]\s/.test(lines[i])) {
|
||||
const indent = lines[i].match(/^(\s*)/)![1].length;
|
||||
const level = Math.floor(indent / 2);
|
||||
if (level > depth) {
|
||||
while (level > depth) { html += '<ul>'; depth++; }
|
||||
} else if (level < depth) {
|
||||
html += '</li>';
|
||||
while (level < depth) { html += '</ul></li>'; depth--; }
|
||||
} else if (!first) {
|
||||
html += '</li>';
|
||||
}
|
||||
html += `<li>${inline(lines[i].replace(/^\s*[*-]\s/, ''))}`;
|
||||
first = false;
|
||||
i++;
|
||||
}
|
||||
while (depth > 0) { html += '</li></ul>'; depth--; }
|
||||
html += '</li></ul>';
|
||||
continue;
|
||||
}
|
||||
|
||||
let para = '';
|
||||
while (i < lines.length && lines[i].trim() !== ''
|
||||
&& !lines[i].startsWith('#') && !lines[i].startsWith('```')
|
||||
&& !lines[i].startsWith('|') && !/^(\s*)[*-]\s/.test(lines[i])
|
||||
&& !/^\d+\.\s/.test(lines[i])) {
|
||||
para += (para ? ' ' : '') + lines[i];
|
||||
i++;
|
||||
}
|
||||
if (para) html += `<p>${inline(para)}</p>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// --- Section/topic manifest — mirrors src/model/docs.rs (keep in sync!) ---
|
||||
|
||||
const SECTIONS = [
|
||||
{
|
||||
title: "Getting Started",
|
||||
topics: [
|
||||
{ title: "Welcome", file: "welcome.md" },
|
||||
{ title: "Navigation", file: "getting-started/navigation.md" },
|
||||
{ title: "The Big Picture", file: "getting-started/big_picture.md" },
|
||||
{ title: "Banks & Patterns", file: "getting-started/banks_patterns.md" },
|
||||
{ title: "Stage / Commit", file: "getting-started/staging.md" },
|
||||
{ title: "Using the Sequencer", file: "getting-started/grid.md" },
|
||||
{ title: "Editing a Step", file: "getting-started/editing.md" },
|
||||
{ title: "The Audio Engine", file: "getting-started/engine.md" },
|
||||
{ title: "Options", file: "getting-started/options.md" },
|
||||
{ title: "Saving & Loading", file: "getting-started/saving.md" },
|
||||
{ title: "The Sample Browser", file: "getting-started/samples.md" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Cagire's Forth",
|
||||
topics: [
|
||||
{ title: "About Forth", file: "forth/about_forth.md" },
|
||||
{ title: "The Dictionary", file: "forth/dictionary.md" },
|
||||
{ title: "The Stack", file: "forth/stack.md" },
|
||||
{ title: "Creating Words", file: "forth/definitions.md" },
|
||||
{ title: "Control Flow", file: "forth/control_flow.md" },
|
||||
{ title: "The Prelude", file: "forth/prelude.md" },
|
||||
{ title: "Cagire vs Classic", file: "forth/oddities.md" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Building sounds",
|
||||
topics: [
|
||||
{ title: "Introduction", file: "engine/intro.md" },
|
||||
{ title: "Settings", file: "engine/settings.md" },
|
||||
{ title: "Sources", file: "engine/sources.md" },
|
||||
{ title: "Samples", file: "engine/samples.md" },
|
||||
{ title: "Wavetables", file: "engine/wavetable.md" },
|
||||
{ title: "Filters", file: "engine/filters.md" },
|
||||
{ title: "Modulation", file: "engine/modulation.md" },
|
||||
{ title: "Distortion", file: "engine/distortion.md" },
|
||||
{ title: "Space & Time", file: "engine/space.md" },
|
||||
{ title: "Audio-Rate Mod", file: "engine/audio_modulation.md" },
|
||||
{ title: "Words & Sounds", file: "engine/words.md" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Branching out",
|
||||
topics: [
|
||||
{ title: "Introduction", file: "midi/intro.md" },
|
||||
{ title: "MIDI Output", file: "midi/output.md" },
|
||||
{ title: "MIDI Input", file: "midi/input.md" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tutorials",
|
||||
topics: [
|
||||
{ title: "Randomness", file: "tutorials/randomness.md" },
|
||||
{ title: "Notes & Harmony", file: "tutorials/harmony.md" },
|
||||
{ title: "Generators", file: "tutorials/generators.md" },
|
||||
{ title: "Timing with at", file: "tutorials/at.md" },
|
||||
{ title: "Using Variables", file: "tutorials/variables.md" },
|
||||
{ title: "Recording", file: "tutorials/recording.md" },
|
||||
{ title: "Soundfonts", file: "tutorials/soundfont.md" },
|
||||
{ title: "Sharing", file: "tutorials/sharing.md" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Read all doc files at build time
|
||||
for (const section of SECTIONS) {
|
||||
for (const topic of section.topics) {
|
||||
const raw = fs.readFileSync(`../docs/${topic.file}`, 'utf-8');
|
||||
(topic as any).html = renderMarkdown(raw);
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cagire - Documentation</title>
|
||||
<meta name="description" content="Cagire documentation — Forth-based live coding sequencer">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<link rel="stylesheet" href="/docs.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="docs-topbar">
|
||||
<a class="docs-back" href="/">← Back to Cagire</a>
|
||||
<div class="docs-notice" id="docs-notice">
|
||||
<span>You are reading Cagire's built-in documentation. Here it is static, but inside the app the examples are runnable and the docs are interactive.</span>
|
||||
<button onclick="dismissNotice()">Got it</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="docs-layout">
|
||||
<nav class="docs-sidebar">
|
||||
{SECTIONS.map((section, si) => (
|
||||
<Fragment>
|
||||
<h3>{section.title}</h3>
|
||||
{section.topics.map((topic, ti) => (
|
||||
<button data-s={si} data-t={ti}>{topic.title}</button>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</nav>
|
||||
<main class="docs-content">
|
||||
{SECTIONS.map((section, si) =>
|
||||
section.topics.map((topic, ti) => (
|
||||
<article id={`topic-s${si}t${ti}`} set:html={(topic as any).html} />
|
||||
))
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
<script is:inline src="/docs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -94,6 +94,9 @@ const DL = 'https://dlcagire.raphaelforment.fr';
|
||||
<p class="note">Source code and issue tracker on <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a>. You can also compile the software yourself from source!</p>
|
||||
|
||||
|
||||
<h2>Documentation</h2>
|
||||
<p>Cagire ships with built-in interactive documentation — browse it inside the app with runnable examples, or <a href="/docs">read the static version here</a>.</p>
|
||||
|
||||
<h2>About</h2>
|
||||
<p>Cagire is a step sequencer where each step contains a Forth script instead of typical note data. When the sequencer reaches a step, it runs the associated script. Scripts can produce sound, trigger samples, apply effects, or do nothing at all. You are free to define what your scripts will do. Cagire includes a built-in audio engine called <a href="https://doux.livecoding.fr">Doux</a>. No external software is needed to make sound. It comes with oscillators, sample players, filters, reverb, delay, distortion, and more.</p>
|
||||
|
||||
@@ -125,7 +128,7 @@ const DL = 'https://dlcagire.raphaelforment.fr';
|
||||
<video src="/mono_cagire.mp4" autoplay muted loop playsinline></video>
|
||||
|
||||
<p class="colophon">
|
||||
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · AGPL-3.0 </p>
|
||||
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · <a href="/docs">Docs</a> · AGPL-3.0 </p>
|
||||
|
||||
<script is:inline src="/script.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user