// Build the static API reference from the canonical Markdown, without runtime dependencies. // Supports the headings, paragraphs, lists, links, rules, and fenced code used by this reference. import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const source = new URL('../docs/API_Documentation.md', import.meta.url); const output = new URL('../meshview/static/api.html', import.meta.url); const markdown = readFileSync(source, 'utf8'); const themeStyles = readFileSync(new URL('../meshview/templates/theme-styles.html', import.meta.url), 'utf8'); const escape = text => text.replace(/[&<>"']/g, char => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[char])); const slug = text => text.toLowerCase().replace(/`/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); function inline(text) { const tokens = /`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*/g; let html = '', cursor = 0; for (const match of text.matchAll(tokens)) { html += escape(text.slice(cursor, match.index)); if (match[1] !== undefined) html += `${escape(match[1])}`; else if (match[2] !== undefined && /^(#|\/|https?:\/\/)/.test(match[3])) { html += `${escape(match[2])}`; } else if (match[4] !== undefined) html += `${escape(match[4])}`; else html += escape(match[0]); cursor = match.index + match[0].length; } return html + escape(text.slice(cursor)); } const endpoints = []; const blocks = []; const lines = markdown.split('\n'); let sectionOpen = false; for (let i = 0; i < lines.length;) { const line = lines[i]; if (!line.trim()) { i++; continue; } if (line.startsWith('```')) { const language = line.slice(3).trim(); const code = []; i++; while (i < lines.length && !lines[i].startsWith('```')) code.push(lines[i++]); if (i === lines.length) throw new Error('Unclosed code fence'); if (language === 'json') JSON.parse(code.join('\n')); blocks.push(`
${escape(code.join('\n'))}
`); i++; continue; } const heading = line.match(/^(#{1,3})\s+(.+)$/); if (heading) { if (sectionOpen) { blocks.push(''); sectionOpen = false; } const level = heading[1].length; const text = heading[2]; if (level === 3 && text.startsWith('GET ')) { const id = slug(text); endpoints.push({text:text.replace(/`/g, ''),id}); blocks.push(`

GET ${inline(text.slice(4))}

`); sectionOpen = true; } else blocks.push(`${inline(text)}`); i++; continue; } if (/^---+$/.test(line)) { blocks.push('
'); i++; continue; } if (/^- /.test(line)) { const items = []; while (i < lines.length && /^- /.test(lines[i])) { let item = lines[i++].slice(2); while (i < lines.length && /^\s+\S/.test(lines[i])) item += ` ${lines[i++].trim()}`; items.push(`
  • ${inline(item)}
  • `); } blocks.push(``); continue; } const paragraph = [line]; i++; while (i < lines.length && lines[i].trim() && !/^(#{1,3} |```|- |---+$)/.test(lines[i])) paragraph.push(lines[i++]); blocks.push(`

    ${inline(paragraph.join(' '))}

    `); } if (sectionOpen) blocks.push('
    '); const index = endpoints.map(({text,id}) => `
  • ${escape(text)}
  • `).join(''); writeFileSync(output, ` Meshview API Reference ${themeStyles}
    MeshviewAPI referenceHome ↗
    Browse all ${endpoints.length} endpoints
    ${blocks.join('\n')}
    `); console.log(`Built ${endpoints.length} endpoint sections: ${fileURLToPath(output)}`); // The legacy chat reference and standalone maps use the same saved preference. for (const filename of ['api-chat.html','kiosk.html','heatmap.html']) { const path = new URL('../meshview/static/' + filename, import.meta.url); let html = readFileSync(path,'utf8'); const themeBlock = '\n' + themeStyles + '\n'; if (!html.includes('/static/theme.js')) html = html.replace('