UNPKG

miijs

Version:

The most complete and easy to use Mii library available.

291 lines (243 loc) 8.79 kB
// Generates detailed JSDoc types for decoded Mii fields based on the format JSON import { writeFileSync } from 'fs'; import { formats, mappings } from './formats'; /** * Analyzes a field definition to determine its JavaScript type */ function getFieldType(field) { if (field.bool) return 'boolean'; if (field.text) return 'string'; if (field.hex) return 'string'; if (field.decoder) return '*'; // Custom decoder could return anything if (field.len && !field.word) return 'number'; return '*'; } /** * Generates documentation for a field, combining info from multiple formats */ function getFieldDoc(fields, formatNames) { const docs = []; const ranges = new Map(); const maxValues = new Map(); // Collect all ranges and max values with their format names fields.forEach((field, idx) => { const formatName = formatNames[idx]; if (field.min !== undefined && field.max !== undefined) { const key = `${field.min}-${field.max}`; if (!ranges.has(key)) ranges.set(key, []); ranges.get(key).push(formatName); } else if (field.max !== undefined) { if (!maxValues.has(field.max)) maxValues.set(field.max, []); maxValues.get(field.max).push(formatName); } }); // Add range documentation if (ranges.size > 0) { if (ranges.size === 1) { docs.push(`Range: ${Array.from(ranges.keys())[0]}`); } else { const rangeStrs = Array.from(ranges.entries()).map(([range, fmts]) => `${range} (${fmts.join(', ')})` ); docs.push(rangeStrs.join('; ')); } } // Add max documentation if (maxValues.size > 0 && ranges.size === 0) { if (maxValues.size === 1) { docs.push(`Max: ${Array.from(maxValues.keys())[0]}`); } else { const maxStrs = Array.from(maxValues.entries()).map(([max, fmts]) => fmts.length > 2 ? `Max: ${max}` : `Max: ${max} (${fmts.join(', ')})` ); docs.push(maxStrs.join('; ')); } } // Add other field attributes (check first field for common attributes) const firstField = fields[0]; if (firstField.bool) { docs.push('Boolean value'); } if (firstField.hex) { docs.push('Hex string'); } if (firstField.text) { docs.push(`Text field (${firstField.text} encoding)`); } return docs.length > 0 ? ` - ${docs.join('; ')}` : ''; } /** * Collects all field information across formats */ function collectFieldInfo() { const fieldInfo = new Map(); for (const formatKey in formats) { const format = formats[formatKey]; if (!format.struct) continue; for (const field of format.struct) { if (field.word || !field.name) continue; const mapping = mappings[field.name]; if (!mapping || mapping === 'SKIP') continue; if (!fieldInfo.has(mapping)) { fieldInfo.set(mapping, { types: new Set(), fields: [], formatNames: [], formatCount: 0 }); } const info = fieldInfo.get(mapping); info.types.add(getFieldType(field)); info.fields.push(field); info.formatNames.push(formatKey); info.formatCount++; } } return fieldInfo; } /** * Builds nested object structure from dot-notation paths */ function buildNestedStructure(fieldInfo, totalFormats) { const root = {}; for (const [path, info] of fieldInfo.entries()) { const parts = path.split('.'); let current = root; for (let i = 0; i < parts.length; i++) { const part = parts[i]; const isLast = i === parts.length - 1; if (isLast) { current[part] = { ...info, optional: info.formatCount < totalFormats, isLeaf: true }; } else { if (!current[part]) { current[part] = { isLeaf: false }; } current = current[part]; } } } return root; } /** * Helper to check if any field in a tree is optional */ function hasOptionalInTree(node) { for (const key in node) { if (key === 'isLeaf') continue; const child = node[key]; if (child.optional) return true; if (!child.isLeaf && hasOptionalInTree(child)) return true; } return false; } /** * Capitalize first letter */ function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Collects all nested typedefs that need to be generated */ function collectNestedTypedefs(structure, path = 'Mii', typedefs = new Map()) { const keys = Object.keys(structure).filter(k => k !== 'isLeaf').sort(); for (const key of keys) { const value = structure[key]; if (!value.isLeaf) { // This is a nested object - needs its own typedef const typedefName = `${path}${capitalize(key)}`; const fullPath = path === 'Mii' ? key : `${path.replace(/^Mii/, '')}.${key}`; typedefs.set(typedefName, { name: typedefName, path: fullPath, structure: value, hasOptionalChildren: hasOptionalInTree(value) }); // Recursively collect nested typedefs collectNestedTypedefs(value, typedefName, typedefs); } } return typedefs; } /** * Generates a single typedef for a structure */ function generateSingleTypedef(name, structure, nestedTypedefs) { const lines = []; const keys = Object.keys(structure).filter(k => k !== 'isLeaf').sort(); lines.push('/**'); lines.push(` * @typedef {Object} ${name}`); for (const key of keys) { const value = structure[key]; if (value.isLeaf) { // Leaf node - actual field const types = Array.from(value.types); const typeStr = types.length === 1 ? types[0] : `(${types.join('|')})`; const optional = value.optional ? '?' : ''; const docStr = getFieldDoc(value.fields, value.formatNames); lines.push(` * @property {${typeStr}} ${key}${optional}${docStr}`); } else { // Branch node - reference to nested typedef const nestedTypedefName = `${name}${capitalize(key)}`; const optional = hasOptionalInTree(value) ? '?' : ''; lines.push(` * @property {${nestedTypedefName}} ${key}${optional}`); } } lines.push(' */'); return lines.join('\n'); } /** * Main generation function */ function generateTypedef() { const fieldInfo = collectFieldInfo(); const totalFormats = Object.keys(formats).filter(k => formats[k].struct).length; const structure = buildNestedStructure(fieldInfo, totalFormats); // Collect all nested typedefs const nestedTypedefs = collectNestedTypedefs(structure); // Generate all nested typedefs first (in dependency order - deepest first) const typedefArray = Array.from(nestedTypedefs.values()); typedefArray.sort((a, b) => { // Sort by depth (deeper first) then alphabetically const depthA = (a.name.match(/[A-Z]/g) || []).length; const depthB = (b.name.match(/[A-Z]/g) || []).length; if (depthB !== depthA) return depthB - depthA; return a.name.localeCompare(b.name); }); const nestedTypedefStrings = typedefArray.map(td => generateSingleTypedef(td.name, td.structure, nestedTypedefs) ); // Generate main typedef const mainTypedef = generateSingleTypedef('Mii', structure, nestedTypedefs); const output = [ '// Auto-generated JSDoc type definitions for Mii data structures', '// Generated: ' + new Date().toISOString(), '', '// Nested type definitions', ...nestedTypedefStrings, '', '// Main Mii type', mainTypedef, '', 'module.exports = {};' ].join('\n'); writeFileSync('mii-jsdoc.js', output); console.log('✓ Generated mii-jsdoc.js'); console.log(` - ${fieldInfo.size} unique properties`); console.log(` - ${typedefArray.length + 1} type definitions`); console.log(` - Analyzed ${totalFormats} format structures`); } // Run if called directly if (require.main === module) { try { generateTypedef(); } catch (error) { console.error('Error generating typedef:', error); process.exit(1); } } export default { generateTypedef };