From 5b8a3ef9badfd6eccd85b38bbae11a9f15d37e2e Mon Sep 17 00:00:00 2001 From: Frank Loesche Date: Wed, 4 Feb 2026 23:52:15 -0500 Subject: [PATCH 1/3] fall back to web when maDisplayTool is not a sibling The generate-arena-config.js script assumes, that maDisplayTools lives in a sibling folder. If this isn't the case, this new version downloads the most recent version from github. Possibly this could be the default? If I run this right now, I get one more arena: G6_3x16_full --- js/arena-configs.js | 15 +- scripts/generate-arena-configs.js | 267 +++++++++++++++++++++++------- 2 files changed, 221 insertions(+), 61 deletions(-) diff --git a/js/arena-configs.js b/js/arena-configs.js index bef5a35..e09d972 100644 --- a/js/arena-configs.js +++ b/js/arena-configs.js @@ -1,7 +1,7 @@ /** * Arena Configurations * Auto-generated from maDisplayTools/configs/arenas/ - * Last updated: 2026-01-30T15:23:37.594Z + * Last updated: 2026-02-05T04:51:33.254Z * * DO NOT EDIT MANUALLY - regenerate with: node scripts/generate-arena-configs.js */ @@ -68,6 +68,19 @@ const STANDARD_CONFIGS = { "angle_offset_deg": -60 } }, + "G6_3x16_full": { + "label": "G6 (3×16) - 360°", + "description": "G6 arena, 3 rows × 16 columns", + "arena": { + "generation": "G6", + "num_rows": 3, + "num_cols": 16, + "columns_installed": null, + "orientation": "normal", + "column_order": "cw", + "angle_offset_deg": 0 + } + }, "G41_2x12_ccw": { "label": "G4.1 CCW (2×12) - 360°", "description": "Standard G4.1 arena, 2 rows x 12 columns, 360 degree coverage, CCW column order", diff --git a/scripts/generate-arena-configs.js b/scripts/generate-arena-configs.js index 3a685a2..32c688e 100644 --- a/scripts/generate-arena-configs.js +++ b/scripts/generate-arena-configs.js @@ -7,13 +7,21 @@ * Usage: * node scripts/generate-arena-configs.js [config-dir] * - * If config-dir is not specified, looks for: - * 1. temp_configs/ (CI/CD fetched configs) - * 2. ../maDisplayTools/configs/arenas/ (local development) + * Config sources (in order of priority): + * 1. Command-line argument path + * 2. temp_configs/ (CI/CD fetched configs) + * 3. ../maDisplayTools/configs/arenas/ (local development) + * 4. GitHub: reiserlab/maDisplayTools feature/g6-tools branch (fallback download) */ const fs = require('fs'); const path = require('path'); +const https = require('https'); + +// GitHub configuration for fallback download +const GITHUB_REPO = 'reiserlab/maDisplayTools'; +const GITHUB_BRANCH = 'feature/g6-tools'; +const GITHUB_CONFIG_PATH = 'configs/arenas'; // Simple YAML parser for our arena config format function parseYAML(yamlText) { @@ -48,7 +56,7 @@ function parseYAML(yamlText) { if (arrayContent.trim() === '') { currentSection[key] = []; } else { - currentSection[key] = arrayContent.split(',').map(v => { + currentSection[key] = arrayContent.split(',').map((v) => { v = v.trim(); const num = parseFloat(v); return isNaN(num) ? v.replace(/^"|"$/g, '') : num; @@ -101,7 +109,7 @@ function generateLabel(parsed) { if (columnsInstalled && Array.isArray(columnsInstalled)) { // columns_installed is always column indices (0-indexed) const installedCols = columnsInstalled.length; - const coverageDeg = Math.round(360 * installedCols / cols); + const coverageDeg = Math.round((360 * installedCols) / cols); coverage = `${coverageDeg}°`; } @@ -111,78 +119,151 @@ function generateLabel(parsed) { return `${gen}${orderSuffix} (${rows}×${cols}) - ${coverage}`; } -// Find config directory -function findConfigDir() { - // Check command line argument - if (process.argv[2]) { - return process.argv[2]; - } +/** + * Make an HTTPS GET request and return the response body + * @param {string} url - URL to fetch + * @returns {Promise} Response body + */ +function httpsGet(url) { + return new Promise((resolve, reject) => { + const options = { + headers: { + 'User-Agent': 'webDisplayTools-config-generator' + } + }; - // Check for CI/CD fetched configs - const ciDir = path.join(process.cwd(), 'temp_configs'); - if (fs.existsSync(ciDir)) { - return ciDir; - } + https + .get(url, options, (res) => { + // Handle redirects + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + httpsGet(res.headers.location).then(resolve).catch(reject); + return; + } - // Check for local maDisplayTools - const localDir = path.join(process.cwd(), '..', 'maDisplayTools', 'configs', 'arenas'); - if (fs.existsSync(localDir)) { - return localDir; - } + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); + return; + } - console.error('Error: Could not find config directory.'); - console.error('Provide path as argument or ensure temp_configs/ or ../maDisplayTools/configs/arenas/ exists.'); - process.exit(1); + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => resolve(data)); + res.on('error', reject); + }) + .on('error', reject); + }); } -// Main -function main() { - const configDir = findConfigDir(); - const outputFile = path.join(process.cwd(), 'js', 'arena-configs.js'); +/** + * Fetch the list of YAML files from GitHub API + * @returns {Promise>} + */ +async function fetchGitHubFileList() { + const apiUrl = `https://api.github.com/repos/${GITHUB_REPO}/contents/${GITHUB_CONFIG_PATH}?ref=${GITHUB_BRANCH}`; + console.log(` Fetching file list from GitHub API...`); + + const response = await httpsGet(apiUrl); + const files = JSON.parse(response); + + // Filter for YAML files only + return files + .filter((f) => f.type === 'file' && (f.name.endsWith('.yaml') || f.name.endsWith('.yml'))) + .map((f) => ({ + name: f.name, + download_url: f.download_url + })); +} - console.log(`Reading configs from: ${configDir}`); +/** + * Download YAML files from GitHub to a temporary directory + * @returns {Promise} Path to temp directory with downloaded files + */ +async function downloadFromGitHub() { + console.log(`Downloading configs from GitHub: ${GITHUB_REPO}@${GITHUB_BRANCH}`); - const configs = {}; - const files = fs.readdirSync(configDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); + // Create temp directory + const tempDir = path.join(process.cwd(), 'temp_configs'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + // Get list of YAML files + const files = await fetchGitHubFileList(); if (files.length === 0) { - console.error('Error: No YAML files found in config directory.'); - process.exit(1); + throw new Error('No YAML files found in GitHub repository'); } + console.log(` Found ${files.length} YAML files`); + + // Download each file for (const file of files) { - const content = fs.readFileSync(path.join(configDir, file), 'utf8'); - const parsed = parseYAML(content); - const name = file.replace(/\.ya?ml$/, ''); + console.log(` Downloading: ${file.name}`); + const content = await httpsGet(file.download_url); + fs.writeFileSync(path.join(tempDir, file.name), content); + } - configs[name] = { - label: generateLabel(parsed), - description: parsed.description || '', - arena: parsed.arena - }; + console.log(` Downloaded to: ${tempDir}`); + return tempDir; +} - console.log(` Parsed: ${name} -> ${configs[name].label}`); +/** + * Find config directory, downloading from GitHub if necessary + * @returns {Promise} Path to config directory + */ +async function findConfigDir() { + // Check command line argument + if (process.argv[2]) { + const argDir = process.argv[2]; + if (fs.existsSync(argDir)) { + return argDir; + } + console.error(`Warning: Specified directory not found: ${argDir}`); } - // Sort configs by generation then by name - const sortedConfigs = {}; - const sortOrder = ['G6', 'G4.1', 'G4', 'G3']; + // Check for CI/CD fetched configs + const ciDir = path.join(process.cwd(), 'temp_configs'); + if (fs.existsSync(ciDir)) { + const yamlFiles = fs + .readdirSync(ciDir) + .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')); + if (yamlFiles.length > 0) { + console.log('Using existing temp_configs/ directory'); + return ciDir; + } + } - Object.keys(configs) - .sort((a, b) => { - const genA = configs[a].arena?.generation || ''; - const genB = configs[b].arena?.generation || ''; - const orderA = sortOrder.indexOf(genA); - const orderB = sortOrder.indexOf(genB); - if (orderA !== orderB) return orderA - orderB; - return a.localeCompare(b); - }) - .forEach(key => { - sortedConfigs[key] = configs[key]; - }); + // Check for local maDisplayTools + const localDir = path.join(process.cwd(), '..', 'maDisplayTools', 'configs', 'arenas'); + if (fs.existsSync(localDir)) { + const yamlFiles = fs + .readdirSync(localDir) + .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')); + if (yamlFiles.length > 0) { + console.log('Using local maDisplayTools configs'); + return localDir; + } + } - // Generate output - const output = `/** + // Fallback: download from GitHub + console.log('Local configs not found, downloading from GitHub...'); + try { + return await downloadFromGitHub(); + } catch (err) { + console.error(`Error downloading from GitHub: ${err.message}`); + console.error('\nCould not find config directory. Options:'); + console.error( + ' 1. Provide path as argument: node scripts/generate-arena-configs.js ' + ); + console.error(' 2. Ensure ../maDisplayTools/configs/arenas/ exists locally'); + console.error(' 3. Check network connection for GitHub download'); + process.exit(1); + } +} + +// Generate the output JavaScript file +function generateOutput(sortedConfigs) { + return `/** * Arena Configurations * Auto-generated from maDisplayTools/configs/arenas/ * Last updated: ${new Date().toISOString()} @@ -253,7 +334,70 @@ function getConfigsByGeneration() { if (typeof module !== 'undefined' && module.exports) { module.exports = { STANDARD_CONFIGS, PANEL_SPECS, getConfig, getConfigsByGeneration }; } + +// Browser global export (for non-module scripts) +if (typeof window !== 'undefined') { + window.STANDARD_CONFIGS = STANDARD_CONFIGS; + window.PANEL_SPECS = PANEL_SPECS; + window.getConfig = getConfig; + window.getConfigsByGeneration = getConfigsByGeneration; +} + +// ES6 module export +export { STANDARD_CONFIGS, PANEL_SPECS, getConfig, getConfigsByGeneration }; `; +} + +// Main +async function main() { + const configDir = await findConfigDir(); + const outputFile = path.join(process.cwd(), 'js', 'arena-configs.js'); + + console.log(`Reading configs from: ${configDir}`); + + const configs = {}; + const files = fs + .readdirSync(configDir) + .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')); + + if (files.length === 0) { + console.error('Error: No YAML files found in config directory.'); + process.exit(1); + } + + for (const file of files) { + const content = fs.readFileSync(path.join(configDir, file), 'utf8'); + const parsed = parseYAML(content); + const name = file.replace(/\.ya?ml$/, ''); + + configs[name] = { + label: generateLabel(parsed), + description: parsed.description || '', + arena: parsed.arena + }; + + console.log(` Parsed: ${name} -> ${configs[name].label}`); + } + + // Sort configs by generation then by name + const sortedConfigs = {}; + const sortOrder = ['G6', 'G4.1', 'G4', 'G3']; + + Object.keys(configs) + .sort((a, b) => { + const genA = configs[a].arena?.generation || ''; + const genB = configs[b].arena?.generation || ''; + const orderA = sortOrder.indexOf(genA); + const orderB = sortOrder.indexOf(genB); + if (orderA !== orderB) return orderA - orderB; + return a.localeCompare(b); + }) + .forEach((key) => { + sortedConfigs[key] = configs[key]; + }); + + // Generate output + const output = generateOutput(sortedConfigs); // Ensure js/ directory exists const jsDir = path.dirname(outputFile); @@ -265,4 +409,7 @@ if (typeof module !== 'undefined' && module.exports) { console.log(`\nGenerated ${outputFile} with ${Object.keys(sortedConfigs).length} configs`); } -main(); +main().catch((err) => { + console.error('Error:', err.message); + process.exit(1); +}); From 3e0b1860eac78f82c5a56783a6bba5cb14223263 Mon Sep 17 00:00:00 2001 From: Frank Loesche Date: Wed, 11 Feb 2026 08:30:36 -0500 Subject: [PATCH 2/3] consider new maDisplayTools file structure --- js/arena-configs.js | 8 +- package.json | 3 +- scripts/generate-arena-configs.js | 298 +++++++++++++++++++++++++++++- 3 files changed, 300 insertions(+), 9 deletions(-) diff --git a/js/arena-configs.js b/js/arena-configs.js index b36538a..a98a9a5 100644 --- a/js/arena-configs.js +++ b/js/arena-configs.js @@ -1,7 +1,7 @@ /** * Arena Configurations * Auto-generated from maDisplayTools/configs/arenas/ - * Last updated: 2026-02-05T04:51:33.254Z + * Last updated: 2026-02-11T13:29:21.223Z * * DO NOT EDIT MANUALLY - regenerate with: node scripts/generate-arena-configs.js */ @@ -199,9 +199,9 @@ const GENERATIONS = { // Arena ID registry — per-generation namespaces (from maDisplayTools/configs/arena_registry/index.yaml) const ARENA_REGISTRY = { - 'G4': { 1: 'G4_4x12', 2: 'G4_3x12of18' }, - 'G4.1': { 1: 'G41_2x12_cw' }, - 'G6': { 1: 'G6_2x10', 2: 'G6_2x8of10', 3: 'G6_3x12of18' } + 'G4': { 1: 'G4_4x12', 2: 'G4_3x12of18' }, + 'G4.1': { 1: 'G41_2x12_cw' }, + 'G6': { 1: 'G6_2x10', 2: 'G6_2x8of10', 3: 'G6_3x12of18' } }; /** diff --git a/package.json b/package.json index d0d0711..40d6b3f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "test": "node tests/validate-arena-calculations.js", "validate": "node tests/validate-arena-calculations.js", "format": "prettier --write \"**/*.js\"", - "format:check": "prettier --check \"**/*.js\"" + "format:check": "prettier --check \"**/*.js\"", + "generate:configs": "node scripts/generate-arena-configs.js" }, "repository": { "type": "git", diff --git a/scripts/generate-arena-configs.js b/scripts/generate-arena-configs.js index 32c688e..0493009 100644 --- a/scripts/generate-arena-configs.js +++ b/scripts/generate-arena-configs.js @@ -22,6 +22,7 @@ const https = require('https'); const GITHUB_REPO = 'reiserlab/maDisplayTools'; const GITHUB_BRANCH = 'feature/g6-tools'; const GITHUB_CONFIG_PATH = 'configs/arenas'; +const GITHUB_REGISTRY_PATH = 'configs/arena_registry'; // Simple YAML parser for our arena config format function parseYAML(yamlText) { @@ -93,6 +94,116 @@ function parseYAML(yamlText) { return config; } +/** + * Parse generations.yaml — maps numeric IDs to generation info + * Format: top-level "generations:" section with numeric keys (0, 1, 2, ...) + * each containing { name, panel_size, deprecated? } + */ +function parseGenerationsYAML(yamlText) { + const generations = {}; + let currentId = null; + + for (const line of yamlText.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + // Match generation ID line (2-space indent, numeric key only) + const idMatch = line.match(/^ (\d+):$/); + if (idMatch) { + currentId = parseInt(idMatch[1]); + generations[currentId] = {}; + continue; + } + + // Non-numeric key at 2-space indent (e.g., " 6-7:") — reset context + if (line.match(/^ \S+:/) && !line.match(/^ /)) { + currentId = null; + continue; + } + + // Match property under a generation (4-space indent) + if (currentId !== null) { + const propMatch = line.match(/^ (\w+):\s*(.+?)(?:\s*#.*)?$/); + if (propMatch) { + const key = propMatch[1]; + let value = propMatch[2].trim(); + + if (value === 'null') value = null; + else if (value === 'true') value = true; + else if (value === 'false') value = false; + else if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1); + else if (!isNaN(parseFloat(value))) value = parseFloat(value); + + generations[currentId][key] = value; + continue; + } + } + + // Non-indented lines reset context (e.g., "generations:" header, "version: 1") + if (!line.startsWith(' ')) { + currentId = null; + } + } + + return generations; +} + +/** + * Parse index.yaml — maps generation keys to arena ID→name mappings + * Format: top-level generation keys (G4, G41, G6) with numeric sub-keys + * Uses generations data to map YAML keys (G41) to canonical names (G4.1) + */ +function parseRegistryIndexYAML(yamlText, generations) { + const registry = {}; + + // Build mapping from YAML-safe keys (G41) to canonical names (G4.1) + const yamlKeyToName = {}; + for (const gen of Object.values(generations)) { + if (gen.name) { + yamlKeyToName[gen.name] = gen.name; + yamlKeyToName[gen.name.replace('.', '')] = gen.name; + } + } + + let currentGen = null; + + for (const line of yamlText.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + // Match generation section header (top-level key, no value) + const genMatch = line.match(/^(\w[\w.]*):$/); + if (genMatch) { + const yamlKey = genMatch[1]; + const canonicalName = yamlKeyToName[yamlKey]; + if (canonicalName) { + currentGen = canonicalName; + registry[currentGen] = {}; + } else { + currentGen = null; + } + continue; + } + + // Match arena ID → name mapping (indented) + if (currentGen) { + const arenaMatch = line.match(/^\s+(\d+):\s*(\S+)/); + if (arenaMatch) { + const arenaId = parseInt(arenaMatch[1]); + const arenaName = arenaMatch[2]; + registry[currentGen][arenaId] = arenaName; + } + } + + // Top-level key:value lines (like "version: 1") reset context + if (!line.startsWith(' ') && !line.match(/^(\w[\w.]*):$/)) { + currentGen = null; + } + } + + return registry; +} + // Generate human-readable label from config function generateLabel(parsed) { if (!parsed.arena) return 'Unknown'; @@ -261,8 +372,115 @@ async function findConfigDir() { } } +/** + * Download registry files (generations.yaml, index.yaml) from GitHub + * @param {string} targetDir - Parent directory to create arena_registry/ in + * @returns {Promise} Path to registry directory + */ +async function downloadRegistryFromGitHub(targetDir) { + const registryDir = path.join(targetDir, 'arena_registry'); + if (!fs.existsSync(registryDir)) { + fs.mkdirSync(registryDir, { recursive: true }); + } + + const baseUrl = `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}/${GITHUB_REGISTRY_PATH}`; + const files = ['generations.yaml', 'index.yaml']; + + for (const file of files) { + console.log(` Downloading: arena_registry/${file}`); + const content = await httpsGet(`${baseUrl}/${file}`); + fs.writeFileSync(path.join(registryDir, file), content); + } + + return registryDir; +} + +/** + * Parse registry files from a directory + * @param {string} dir - Directory containing generations.yaml and index.yaml + * @returns {{generations: Object, arenaRegistry: Object}} + */ +function parseRegistryFiles(dir) { + const genText = fs.readFileSync(path.join(dir, 'generations.yaml'), 'utf8'); + const idxText = fs.readFileSync(path.join(dir, 'index.yaml'), 'utf8'); + + const generations = parseGenerationsYAML(genText); + const arenaRegistry = parseRegistryIndexYAML(idxText, generations); + + return { generations, arenaRegistry }; +} + +/** + * Load registry data (generations + arena index) from local files or GitHub + * @param {string} configDir - The arena configs directory (used to find sibling registry dir) + * @returns {Promise<{generations: Object, arenaRegistry: Object}>} + */ +async function loadRegistryData(configDir) { + // Candidate directories for registry files + const candidates = [ + // Sibling to configDir (local maDisplayTools: configs/arenas/ → configs/arena_registry/) + path.join(configDir, '..', 'arena_registry'), + // Subdirectory of configDir (temp_configs/ → temp_configs/arena_registry/) + path.join(configDir, 'arena_registry'), + // Absolute local path + path.join(process.cwd(), '..', 'maDisplayTools', 'configs', 'arena_registry') + ]; + + for (const dir of candidates) { + const genFile = path.join(dir, 'generations.yaml'); + const idxFile = path.join(dir, 'index.yaml'); + if (fs.existsSync(genFile) && fs.existsSync(idxFile)) { + console.log(`Reading registry from: ${dir}`); + return parseRegistryFiles(dir); + } + } + + // Fallback: download from GitHub + console.log('Registry files not found locally, downloading from GitHub...'); + try { + const registryDir = await downloadRegistryFromGitHub(configDir); + return parseRegistryFiles(registryDir); + } catch (err) { + console.error(`Error downloading registry from GitHub: ${err.message}`); + console.error('\nCould not find registry files (generations.yaml, index.yaml). Options:'); + console.error(' 1. Ensure ../maDisplayTools/configs/arena_registry/ exists locally'); + console.error(' 2. Check network connection for GitHub download'); + process.exit(1); + } +} + +/** + * Format GENERATIONS object as JavaScript source + */ +function formatGenerationsJS(generations) { + const ids = Object.keys(generations).map(Number).sort((a, b) => a - b); + const lines = ids.map((id) => { + const gen = generations[id]; + const props = [`name: '${gen.name}'`, `panel_size: ${gen.panel_size}`]; + if (gen.deprecated) props.push('deprecated: true'); + return ` ${id}: { ${props.join(', ')} }`; + }); + return `{\n${lines.join(',\n')}\n}`; +} + +/** + * Format ARENA_REGISTRY object as JavaScript source + */ +function formatArenaRegistryJS(registry) { + const lines = Object.entries(registry).map(([gen, arenas]) => { + const arenaEntries = Object.entries(arenas) + .sort(([a], [b]) => parseInt(a) - parseInt(b)) + .map(([id, name]) => `${id}: '${name}'`); + const padding = ' '.repeat(Math.max(1, 6 - gen.length)); + return ` '${gen}':${padding}{ ${arenaEntries.join(', ')} }`; + }); + return `{\n${lines.join(',\n')}\n}`; +} + // Generate the output JavaScript file -function generateOutput(sortedConfigs) { +function generateOutput(sortedConfigs, registryData) { + const { generations, arenaRegistry } = registryData; + return `/** * Arena Configurations * Auto-generated from maDisplayTools/configs/arenas/ @@ -273,6 +491,60 @@ function generateOutput(sortedConfigs) { const STANDARD_CONFIGS = ${JSON.stringify(sortedConfigs, null, 2)}; +// Generation ID registry (from maDisplayTools/configs/arena_registry/generations.yaml) +const GENERATIONS = ${formatGenerationsJS(generations)}; + +// Arena ID registry — per-generation namespaces (from maDisplayTools/configs/arena_registry/index.yaml) +const ARENA_REGISTRY = ${formatArenaRegistryJS(arenaRegistry)}; + +/** + * Get generation name from ID + * @param {number} id - Generation ID (0-7) + * @returns {string} Generation name or 'unknown' + */ +function getGenerationName(id) { + return GENERATIONS[id] ? GENERATIONS[id].name : 'unknown'; +} + +/** + * Get generation ID from name + * @param {string} name - Generation name (e.g., 'G6', 'G4.1') + * @returns {number} Generation ID or 0 + */ +function getGenerationId(name) { + for (const [id, gen] of Object.entries(GENERATIONS)) { + if (gen.name === name) return parseInt(id); + } + return 0; +} + +/** + * Get arena config name from generation and arena ID + * @param {string} generation - Generation name (e.g., 'G6', 'G4') + * @param {number} arenaId - Arena ID + * @returns {string|null} Arena config name or null + */ +function getArenaName(generation, arenaId) { + const genRegistry = ARENA_REGISTRY[generation]; + if (!genRegistry) return null; + return genRegistry[arenaId] || null; +} + +/** + * Get arena ID from generation and config name + * @param {string} generation - Generation name (e.g., 'G6', 'G4') + * @param {string} arenaName - Arena config name (e.g., 'G6_2x10') + * @returns {number} Arena ID or 0 + */ +function getArenaId(generation, arenaName) { + const genRegistry = ARENA_REGISTRY[generation]; + if (!genRegistry) return 0; + for (const [id, name] of Object.entries(genRegistry)) { + if (name === arenaName) return parseInt(id); + } + return 0; +} + // Panel specifications by generation const PANEL_SPECS = { 'G3': { @@ -332,19 +604,33 @@ function getConfigsByGeneration() { // Export for both browser and Node.js if (typeof module !== 'undefined' && module.exports) { - module.exports = { STANDARD_CONFIGS, PANEL_SPECS, getConfig, getConfigsByGeneration }; + module.exports = { + STANDARD_CONFIGS, PANEL_SPECS, GENERATIONS, ARENA_REGISTRY, + getConfig, getConfigsByGeneration, + getGenerationName, getGenerationId, getArenaName, getArenaId + }; } // Browser global export (for non-module scripts) if (typeof window !== 'undefined') { window.STANDARD_CONFIGS = STANDARD_CONFIGS; window.PANEL_SPECS = PANEL_SPECS; + window.GENERATIONS = GENERATIONS; + window.ARENA_REGISTRY = ARENA_REGISTRY; window.getConfig = getConfig; window.getConfigsByGeneration = getConfigsByGeneration; + window.getGenerationName = getGenerationName; + window.getGenerationId = getGenerationId; + window.getArenaName = getArenaName; + window.getArenaId = getArenaId; } // ES6 module export -export { STANDARD_CONFIGS, PANEL_SPECS, getConfig, getConfigsByGeneration }; +export { + STANDARD_CONFIGS, PANEL_SPECS, GENERATIONS, ARENA_REGISTRY, + getConfig, getConfigsByGeneration, + getGenerationName, getGenerationId, getArenaName, getArenaId +}; `; } @@ -396,8 +682,12 @@ async function main() { sortedConfigs[key] = configs[key]; }); + // Load registry data (generations + arena index) + const registryData = await loadRegistryData(configDir); + console.log(` Loaded ${Object.keys(registryData.generations).length} generations, ${Object.values(registryData.arenaRegistry).reduce((n, g) => n + Object.keys(g).length, 0)} arena registry entries`); + // Generate output - const output = generateOutput(sortedConfigs); + const output = generateOutput(sortedConfigs, registryData); // Ensure js/ directory exists const jsDir = path.dirname(outputFile); From 002a50f57c6a8421d71540d537fd91ce5dcb5855 Mon Sep 17 00:00:00 2001 From: Frank Loesche Date: Wed, 11 Feb 2026 08:36:20 -0500 Subject: [PATCH 3/3] use js-yaml instead of parsing YAML file ourselves --- js/arena-configs.js | 2 +- package-lock.json | 25 +++- package.json | 1 + scripts/generate-arena-configs.js | 217 +++++------------------------- 4 files changed, 57 insertions(+), 188 deletions(-) diff --git a/js/arena-configs.js b/js/arena-configs.js index a98a9a5..1784c33 100644 --- a/js/arena-configs.js +++ b/js/arena-configs.js @@ -1,7 +1,7 @@ /** * Arena Configurations * Auto-generated from maDisplayTools/configs/arenas/ - * Last updated: 2026-02-11T13:29:21.223Z + * Last updated: 2026-02-11T13:35:39.006Z * * DO NOT EDIT MANUALLY - regenerate with: node scripts/generate-arena-configs.js */ diff --git a/package-lock.json b/package-lock.json index 03ed1b6..1a09f7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,38 @@ { "name": "web-display-tools", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-display-tools", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "devDependencies": { + "js-yaml": "^4.1.1", "prettier": "^3.8.1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", diff --git a/package.json b/package.json index 40d6b3f..d8443c9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "author": "Reiser Lab", "license": "MIT", "devDependencies": { + "js-yaml": "^4.1.1", "prettier": "^3.8.1" } } diff --git a/scripts/generate-arena-configs.js b/scripts/generate-arena-configs.js index 0493009..5e46627 100644 --- a/scripts/generate-arena-configs.js +++ b/scripts/generate-arena-configs.js @@ -17,6 +17,7 @@ const fs = require('fs'); const path = require('path'); const https = require('https'); +const yaml = require('js-yaml'); // GitHub configuration for fallback download const GITHUB_REPO = 'reiserlab/maDisplayTools'; @@ -24,186 +25,6 @@ const GITHUB_BRANCH = 'feature/g6-tools'; const GITHUB_CONFIG_PATH = 'configs/arenas'; const GITHUB_REGISTRY_PATH = 'configs/arena_registry'; -// Simple YAML parser for our arena config format -function parseYAML(yamlText) { - const config = {}; - let currentSection = config; - - const lines = yamlText.split('\n'); - for (const line of lines) { - // Skip comments and empty lines - if (line.trim().startsWith('#') || line.trim() === '') continue; - - // Check for section (arena:) - if (line.match(/^(\w+):$/)) { - const sectionName = line.match(/^(\w+):$/)[1]; - config[sectionName] = {}; - currentSection = config[sectionName]; - continue; - } - - // Check for key: value pairs (indented) - const kvMatch = line.match(/^\s+(\w+):\s*(.+?)(?:\s*#.*)?$/); - if (kvMatch) { - const key = kvMatch[1]; - let value = kvMatch[2].trim(); - - // Handle different value types - if (value === 'null') { - currentSection[key] = null; - } else if (value.startsWith('[') && value.endsWith(']')) { - // Array - const arrayContent = value.slice(1, -1); - if (arrayContent.trim() === '') { - currentSection[key] = []; - } else { - currentSection[key] = arrayContent.split(',').map((v) => { - v = v.trim(); - const num = parseFloat(v); - return isNaN(num) ? v.replace(/^"|"$/g, '') : num; - }); - } - } else if (value.startsWith('"') && value.endsWith('"')) { - // Quoted string - currentSection[key] = value.slice(1, -1); - } else if (!isNaN(parseFloat(value))) { - // Number - currentSection[key] = parseFloat(value); - } else { - // Unquoted string - currentSection[key] = value; - } - continue; - } - - // Check for top-level key: value - const topKvMatch = line.match(/^(\w+):\s*(.+?)(?:\s*#.*)?$/); - if (topKvMatch) { - const key = topKvMatch[1]; - let value = topKvMatch[2].trim(); - - if (value.startsWith('"') && value.endsWith('"')) { - config[key] = value.slice(1, -1); - } else { - config[key] = value; - } - currentSection = config; - } - } - - return config; -} - -/** - * Parse generations.yaml — maps numeric IDs to generation info - * Format: top-level "generations:" section with numeric keys (0, 1, 2, ...) - * each containing { name, panel_size, deprecated? } - */ -function parseGenerationsYAML(yamlText) { - const generations = {}; - let currentId = null; - - for (const line of yamlText.split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - - // Match generation ID line (2-space indent, numeric key only) - const idMatch = line.match(/^ (\d+):$/); - if (idMatch) { - currentId = parseInt(idMatch[1]); - generations[currentId] = {}; - continue; - } - - // Non-numeric key at 2-space indent (e.g., " 6-7:") — reset context - if (line.match(/^ \S+:/) && !line.match(/^ /)) { - currentId = null; - continue; - } - - // Match property under a generation (4-space indent) - if (currentId !== null) { - const propMatch = line.match(/^ (\w+):\s*(.+?)(?:\s*#.*)?$/); - if (propMatch) { - const key = propMatch[1]; - let value = propMatch[2].trim(); - - if (value === 'null') value = null; - else if (value === 'true') value = true; - else if (value === 'false') value = false; - else if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1); - else if (!isNaN(parseFloat(value))) value = parseFloat(value); - - generations[currentId][key] = value; - continue; - } - } - - // Non-indented lines reset context (e.g., "generations:" header, "version: 1") - if (!line.startsWith(' ')) { - currentId = null; - } - } - - return generations; -} - -/** - * Parse index.yaml — maps generation keys to arena ID→name mappings - * Format: top-level generation keys (G4, G41, G6) with numeric sub-keys - * Uses generations data to map YAML keys (G41) to canonical names (G4.1) - */ -function parseRegistryIndexYAML(yamlText, generations) { - const registry = {}; - - // Build mapping from YAML-safe keys (G41) to canonical names (G4.1) - const yamlKeyToName = {}; - for (const gen of Object.values(generations)) { - if (gen.name) { - yamlKeyToName[gen.name] = gen.name; - yamlKeyToName[gen.name.replace('.', '')] = gen.name; - } - } - - let currentGen = null; - - for (const line of yamlText.split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - - // Match generation section header (top-level key, no value) - const genMatch = line.match(/^(\w[\w.]*):$/); - if (genMatch) { - const yamlKey = genMatch[1]; - const canonicalName = yamlKeyToName[yamlKey]; - if (canonicalName) { - currentGen = canonicalName; - registry[currentGen] = {}; - } else { - currentGen = null; - } - continue; - } - - // Match arena ID → name mapping (indented) - if (currentGen) { - const arenaMatch = line.match(/^\s+(\d+):\s*(\S+)/); - if (arenaMatch) { - const arenaId = parseInt(arenaMatch[1]); - const arenaName = arenaMatch[2]; - registry[currentGen][arenaId] = arenaName; - } - } - - // Top-level key:value lines (like "version: 1") reset context - if (!line.startsWith(' ') && !line.match(/^(\w[\w.]*):$/)) { - currentGen = null; - } - } - - return registry; -} - // Generate human-readable label from config function generateLabel(parsed) { if (!parsed.arena) return 'Unknown'; @@ -401,11 +222,37 @@ async function downloadRegistryFromGitHub(targetDir) { * @returns {{generations: Object, arenaRegistry: Object}} */ function parseRegistryFiles(dir) { - const genText = fs.readFileSync(path.join(dir, 'generations.yaml'), 'utf8'); - const idxText = fs.readFileSync(path.join(dir, 'index.yaml'), 'utf8'); + const genData = yaml.load(fs.readFileSync(path.join(dir, 'generations.yaml'), 'utf8')); + const idxData = yaml.load(fs.readFileSync(path.join(dir, 'index.yaml'), 'utf8')); + + // Extract generations: keep only numeric IDs, pick name/panel_size/deprecated + const generations = {}; + for (const [key, value] of Object.entries(genData.generations || {})) { + if (!/^\d+$/.test(key)) continue; // skip range keys like "6-7" + const id = parseInt(key); + generations[id] = { name: value.name, panel_size: value.panel_size ?? null }; + if (value.deprecated) generations[id].deprecated = true; + } - const generations = parseGenerationsYAML(genText); - const arenaRegistry = parseRegistryIndexYAML(idxText, generations); + // Build YAML key → canonical name mapping (e.g., G41 → G4.1) + const yamlKeyToName = {}; + for (const gen of Object.values(generations)) { + if (gen.name) { + yamlKeyToName[gen.name] = gen.name; + yamlKeyToName[gen.name.replace('.', '')] = gen.name; + } + } + + // Extract arena registry: map YAML keys to canonical generation names + const arenaRegistry = {}; + for (const [key, value] of Object.entries(idxData)) { + const canonicalName = yamlKeyToName[key]; + if (!canonicalName || typeof value !== 'object') continue; + arenaRegistry[canonicalName] = {}; + for (const [arenaId, arenaName] of Object.entries(value)) { + arenaRegistry[canonicalName][parseInt(arenaId)] = arenaName; + } + } return { generations, arenaRegistry }; } @@ -653,7 +500,7 @@ async function main() { for (const file of files) { const content = fs.readFileSync(path.join(configDir, file), 'utf8'); - const parsed = parseYAML(content); + const parsed = yaml.load(content); const name = file.replace(/\.ya?ml$/, ''); configs[name] = {