--- 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 += `${escapeHtml(line.slice(i))}`; 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 ? `${escapeHtml(word)}` : escapeHtml(word); i = end; } return result; }).join('\n'); } // --- Markdown renderer --- function escapeHtml(s: string): string { return s.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, '$1'); text = text.replace(/~~([^~]+)~~/g, '$1'); text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); text = text.replace(/_([^_]+)_/g, '$1'); text = text.replace(/\x00(\d+)\x00/g, (_, i) => `${escapeHtml(codes[+i])}`); return text; } function parseTableCells(line: string, tag: string): string { return line.split('|').slice(1, -1).map(cell => `<${tag}>${inline(cell.trim())}`).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 += `

${inline(line.slice(4))}

`; i++; continue; } if (line.startsWith('## ')) { html += `

${inline(line.slice(3))}

`; i++; continue; } if (line.startsWith('# ')) { html += `

${inline(line.slice(2))}

`; i++; continue; } if (/^---+$|^\*\*\*+$/.test(line.trim())) { html += '
'; i++; continue; } if (line.startsWith('> ')) { html += '
'; while (i < lines.length && lines[i].startsWith('> ')) { html += `

${inline(lines[i].slice(2))}

`; i++; } html += '
'; 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' ? `
${highlightForth(code)}
` : `
${escapeHtml(code)}
`; continue; } if (line.startsWith('|')) { html += ''; html += `${parseTableCells(line, 'th')}`; i++; if (i < lines.length && /^\|[\s:|-]+\|$/.test(lines[i])) i++; while (i < lines.length && lines[i].startsWith('|')) { html += `${parseTableCells(lines[i], 'td')}`; i++; } html += '
'; continue; } if (/^\d+\.\s/.test(line)) { html += '
    '; while (i < lines.length && (/^\d+\.\s/.test(lines[i]) || /^\s+[*-]\s/.test(lines[i]))) { if (/^\d+\.\s/.test(lines[i])) { html += `
  1. ${inline(lines[i].replace(/^\d+\.\s/, ''))}`; i++; if (i < lines.length && /^\s+[*-]\s/.test(lines[i])) { html += ''; } html += '
  2. '; } else { i++; } } html += '
'; continue; } if (/^(\s*)[*-]\s/.test(line)) { let depth = 0; let first = true; html += ''; depth--; } html += ''; 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 += `

${inline(para)}

`; } 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); } } --- Cagire - Documentation
← Back to Cagire
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.
{SECTIONS.map((section, si) => section.topics.map((topic, ti) => (
)) )}