const fs = require('fs'); const path = require('path'); const MarkdownIt = require('markdown-it'); const anchor = require('markdown-it-anchor'); const hljs = require('highlight.js'); const filter = require('./doc-filter'); // Extract version from FlexLove.lua function getVersion() { try { const flexlovePath = path.join(__dirname, '..', 'FlexLove.lua'); const content = fs.readFileSync(flexlovePath, 'utf8'); const match = content.match(/flexlove\._VERSION\s*=\s*["']([^"']+)["']/); return match ? match[1] : 'unknown'; } catch (e) { return 'unknown'; } } const VERSION = getVersion(); console.log(`Building docs for FlexLöve v${VERSION}`); const md = new MarkdownIt({ html: true, linkify: true, typographer: true, highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(str, { language: lang }).value; } catch (__) {} } return ''; } }).use(anchor, { permalink: anchor.permalink.headerLink() }); // Read the markdown file let markdownContent = fs.readFileSync(path.join(__dirname, 'doc.md'), 'utf8'); // Filter content based on doc-filter.js configuration function filterMarkdown(content) { const lines = content.split('\n'); const filtered = []; let currentClass = null; let skipUntilNextClass = false; let classContent = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const h1Match = line.match(/^# (.+)$/); if (h1Match) { // New class found - decide if we should keep previous class if (currentClass && !skipUntilNextClass) { filtered.push(...classContent); } currentClass = h1Match[1]; classContent = [line]; // Check if this class should be included if (filter.mode === 'whitelist') { skipUntilNextClass = !filter.include.includes(currentClass); } else { skipUntilNextClass = filter.exclude.includes(currentClass); } } else { classContent.push(line); } } // Don't forget the last class if (currentClass && !skipUntilNextClass) { filtered.push(...classContent); } return filtered.join('\n'); } markdownContent = filterMarkdown(markdownContent); // Sort properties: public first, then internal (prefixed with _) function sortAndInjectWarning(content) { const lines = content.split('\n'); const result = []; let i = 0; while (i < lines.length) { const line = lines[i]; // Check if this is a class heading (h1) if (line.match(/^# .+$/)) { // Found a class, collect all its properties/methods result.push(line); i++; const properties = []; let currentProperty = null; // Collect all properties until next class or end of file while (i < lines.length && !lines[i].match(/^# .+$/)) { const propLine = lines[i]; const h2Match = propLine.match(/^## (.+)$/); if (h2Match) { // Save previous property if exists if (currentProperty) { properties.push(currentProperty); } // Start new property currentProperty = { name: h2Match[1], lines: [propLine], isInternal: h2Match[1].startsWith('_') }; } else if (currentProperty) { // Add line to current property currentProperty.lines.push(propLine); } else { // Line before any property (e.g., class description) result.push(propLine); } i++; } // Save last property if (currentProperty) { properties.push(currentProperty); } // Sort: public first, then internal const publicProps = properties.filter(p => !p.isInternal); const internalProps = properties.filter(p => p.isInternal); // Add public properties publicProps.forEach(prop => { result.push(...prop.lines); }); // Add warning and internal properties if any exist if (internalProps.length > 0) { result.push(''); result.push('---'); result.push(''); result.push('## ⚠️ Internal Properties'); result.push(''); result.push('> **Warning:** The following properties are internal implementation details and should not be accessed directly. They are prefixed with `_` to indicate they are private. Accessing these properties may break in future versions without notice.'); result.push(''); result.push('---'); result.push(''); internalProps.forEach(prop => { result.push(...prop.lines); }); } } else { result.push(line); i++; } } return result.join('\n'); } markdownContent = sortAndInjectWarning(markdownContent); // Parse markdown structure to build navigation const lines = markdownContent.split('\n'); const navigation = []; let currentClass = null; lines.forEach(line => { const h1Match = line.match(/^# (.+)$/); const h2Match = line.match(/^## (.+)$/); if (h1Match) { currentClass = { name: h1Match[1], id: h1Match[1].toLowerCase().replace(/[^a-z0-9]+/g, '-'), members: [] }; navigation.push(currentClass); } else if (h2Match && currentClass) { currentClass.members.push({ name: h2Match[1], id: h2Match[1].toLowerCase().replace(/[^a-z0-9]+/g, '-') }); } }); // Scan for available documentation versions function getAvailableVersions() { const versionsDir = path.join(__dirname, 'versions'); const versions = []; try { if (fs.existsSync(versionsDir)) { const entries = fs.readdirSync(versionsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && entry.name.startsWith('v')) { const apiPath = path.join(versionsDir, entry.name, 'api.html'); if (fs.existsSync(apiPath)) { versions.push(entry.name); } } } } } catch (e) { console.warn('Warning: Could not scan versions directory:', e.message); } // Sort versions (newest first) versions.sort((a, b) => { const parseVersion = (v) => { const parts = v.substring(1).split('.').map(Number); return parts[0] * 10000 + parts[1] * 100 + parts[2]; }; return parseVersion(b) - parseVersion(a); }); return versions; } const availableVersions = getAvailableVersions(); console.log(`Found ${availableVersions.length} archived version(s):`, availableVersions.join(', ')); // Convert markdown to HTML const htmlContent = md.render(markdownContent); // Create HTML template const template = ` FlexLöve v${VERSION} - API Reference
${htmlContent}
↑ Top `; // Write the HTML file fs.writeFileSync(path.join(__dirname, 'api.html'), template, 'utf8'); console.log('✓ Generated api.html');