snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
491 lines • 22.7 kB
JavaScript
"use strict";
/**
* Phase 4: Extended Output Formats
*
* Additional output formats for documentation, analysis, and integration:
* - HTML with hyperlinked cross-references
* - JSON export for external tools
* - XML output format
* - CSV export for spreadsheet analysis
* - Markdown documentation
* - GraphViz call graph generation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BassFormatter = exports.WLADXFormatter = exports.CA65Formatter = exports.ExtendedOutputFormatterFactory = exports.MarkdownFormatter = exports.CSVFormatter = exports.XMLFormatter = exports.JSONFormatter = exports.HTMLFormatter = void 0;
const output_formatters_1 = require("./output-formatters");
class HTMLFormatter extends output_formatters_1.OutputFormatter {
getName() {
return 'HTML';
}
getFileExtension() {
return '.html';
}
format(lines) {
const output = [];
// HTML Document structure
output.push('<!DOCTYPE html>');
output.push('<html lang="en">');
output.push('<head>');
output.push('<meta charset="UTF-8">');
output.push('<meta name="viewport" content="width=device-width, initial-scale=1.0">');
output.push(`<title>SNES Disassembly - ${this.rom.header.title}</title>`);
output.push('<style>');
output.push(this.getCSS());
output.push('</style>');
output.push('</head>');
output.push('<body>');
// Header section
output.push('<div class="header">');
output.push(`<h1>SNES ROM Disassembly: ${this.rom.header.title}</h1>`);
output.push('<div class="rom-info">');
output.push(`<span class="info-item">Map Mode: ${this.rom.cartridgeInfo.type}</span>`);
output.push(`<span class="info-item">ROM Size: ${(this.rom.cartridgeInfo.romSize / 1024).toFixed(0)} KB</span>`);
output.push(`<span class="info-item">Reset Vector: ${this.formatAddress(this.rom.header.nativeVectors.reset)}</span>`);
if (this.rom.cartridgeInfo.specialChip) {
output.push(`<span class="info-item">Special Chip: ${this.rom.cartridgeInfo.specialChip}</span>`);
}
output.push('</div>');
output.push('</div>');
// Navigation sidebar
if (this.options.includeSymbols && this.symbols.size > 0) {
output.push('<div class="sidebar">');
output.push('<h3>Symbols</h3>');
output.push('<div class="symbol-list">');
const sortedSymbols = Array.from(this.symbols.entries()).sort((a, b) => a[0] - b[0]);
for (const [address, symbol] of sortedSymbols) {
const cssClass = symbol.type.toLowerCase();
output.push(`<a href="#addr_${address.toString(16)}" class="symbol ${cssClass}">`);
output.push(`${symbol.name} (${this.formatAddress(address)})</a>`);
}
output.push('</div>');
output.push('</div>');
}
// Main disassembly content
output.push('<div class="main-content">');
output.push('<div class="disassembly">');
for (const line of lines) {
output.push(this.formatHTMLLine(line));
}
output.push('</div>');
output.push('</div>');
// Cross-references section
if (this.options.includeCrossReferences && this.crossRefs.length > 0) {
output.push('<div class="cross-references">');
output.push('<h3>Cross References</h3>');
output.push('<table class="xref-table">');
output.push('<thead><tr><th>From</th><th>To</th><th>Type</th><th>Instruction</th></tr></thead>');
output.push('<tbody>');
for (const ref of this.crossRefs) {
output.push('<tr>');
output.push(`<td><a href="#addr_${ref.fromAddress.toString(16)}">${this.formatAddress(ref.fromAddress)}</a></td>`);
output.push(`<td><a href="#addr_${ref.toAddress.toString(16)}">${this.formatAddress(ref.toAddress)}</a></td>`);
output.push(`<td>${ref.type}</td>`);
output.push(`<td>${ref.instruction || ''}</td>`);
output.push('</tr>');
}
output.push('</tbody>');
output.push('</table>');
output.push('</div>');
}
output.push('</body>');
output.push('</html>');
return output.join('\n');
}
getCSS() {
return `
body { font-family: 'Courier New', monospace; margin: 0; padding: 0; background: #f5f5f5; }
.header { background: #2c3e50; color: white; padding: 20px; border-bottom: 3px solid #3498db; }
.header h1 { margin: 0; font-size: 24px; }
.rom-info { margin-top: 10px; }
.info-item { display: inline-block; background: #34495e; padding: 5px 10px; margin-right: 10px; border-radius: 3px; }
.sidebar { position: fixed; left: 0; top: 120px; width: 250px; height: calc(100vh - 120px); overflow-y: auto; background: white; border-right: 1px solid #ddd; padding: 15px; }
.main-content { margin-left: 280px; padding: 20px; }
.symbol-list { max-height: 400px; overflow-y: auto; }
.symbol { display: block; padding: 3px 5px; text-decoration: none; color: #2c3e50; border-radius: 2px; margin: 1px 0; }
.symbol:hover { background: #ecf0f1; }
.symbol.code { border-left: 3px solid #3498db; }
.symbol.data { border-left: 3px solid #e74c3c; }
.symbol.vector { border-left: 3px solid #f39c12; }
.symbol.register { border-left: 3px solid #9b59b6; }
.disassembly { background: white; border: 1px solid #ddd; border-radius: 5px; }
.line { padding: 2px 10px; border-bottom: 1px solid #f8f9fa; font-size: 14px; }
.line:hover { background: #f8f9fa; }
.line.has-label { border-top: 2px solid #3498db; margin-top: 10px; }
.address { color: #7f8c8d; font-weight: bold; }
.bytes { color: #95a5a6; }
.mnemonic { color: #2c3e50; font-weight: bold; }
.operand { color: #8e44ad; }
.operand.symbol { color: #27ae60; font-weight: bold; }
.comment { color: #27ae60; font-style: italic; }
.label { font-weight: bold; color: #c0392b; margin-right: 10px; }
.cross-references { margin-top: 30px; background: white; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
.xref-table { width: 100%; border-collapse: collapse; }
.xref-table th, .xref-table td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
.xref-table th { background: #ecf0f1; font-weight: bold; }
.xref-table a { color: #3498db; text-decoration: none; }
.xref-table a:hover { text-decoration: underline; }
`;
}
formatHTMLLine(line) {
const addressId = `addr_${line.address.toString(16)}`;
const hasLabel = !!line.label;
let output = `<div class="line${hasLabel ? ' has-label' : ''}" id="${addressId}">`;
// Add label if present
if (line.label) {
output += `<span class="label">${line.label}:</span>`;
}
// Format address
output += `<span class="address">${this.formatAddress(line.address)}:</span> `;
// Format bytes if requested
if (this.options.includeBytes) {
output += `<span class="bytes">${this.formatBytes(line.bytes).padEnd(12)}</span> `;
}
// Format instruction
output += `<span class="mnemonic">${line.instruction.mnemonic.toUpperCase()}</span>`;
// Format operand
const operandStr = this.formatHTMLOperand(line);
if (operandStr) {
output += ` ${operandStr}`;
}
// Add comment
if (this.options.includeComments && line.comment) {
output += ` <span class="comment">; ${line.comment}</span>`;
}
output += '</div>';
return output;
}
formatHTMLOperand(line) {
if (line.operand === undefined) {
return '';
}
const symbolName = this.getSymbolName(line.operand);
if (symbolName) {
const targetId = `addr_${line.operand.toString(16)}`;
return `<a href="#${targetId}" class="operand symbol">${symbolName}</a>`;
}
const formattedOperand = this.formatOperand(line);
return `<span class="operand">${formattedOperand}</span>`;
}
}
exports.HTMLFormatter = HTMLFormatter;
class JSONFormatter extends output_formatters_1.OutputFormatter {
getName() {
return 'JSON';
}
getFileExtension() {
return '.json';
}
format(lines) {
const data = {
metadata: {
title: this.rom.header.title,
cartridgeType: this.rom.cartridgeInfo.type,
romSize: this.rom.cartridgeInfo.romSize,
resetVector: this.rom.header.nativeVectors.reset,
specialChip: this.rom.cartridgeInfo.specialChip,
generatedBy: 'SNES Disassembler',
generatedAt: new Date().toISOString(),
format: 'JSON'
},
symbols: this.serializeSymbols(),
crossReferences: this.crossRefs,
disassembly: lines.map(line => ({
address: line.address,
addressHex: line.address.toString(16).toUpperCase().padStart(6, '0'),
bytes: line.bytes,
bytesHex: line.bytes.map(b => b.toString(16).toUpperCase().padStart(2, '0')),
instruction: {
opcode: line.instruction.opcode,
mnemonic: line.instruction.mnemonic,
addressingMode: line.instruction.addressingMode,
bytes: line.instruction.bytes,
cycles: line.instruction.cycles
},
operand: line.operand,
operandHex: line.operand?.toString(16).toUpperCase().padStart(line.operand <= 0xFF ? 2 : line.operand <= 0xFFFF ? 4 : 6, '0'),
label: line.label,
comment: line.comment
}))
};
return JSON.stringify(data, null, 2);
}
serializeSymbols() {
return Array.from(this.symbols.entries()).map(([address, symbol]) => ({
address,
addressHex: address.toString(16).toUpperCase().padStart(6, '0'),
name: symbol.name,
type: symbol.type,
scope: symbol.scope,
size: symbol.size,
description: symbol.description,
references: symbol.references
}));
}
}
exports.JSONFormatter = JSONFormatter;
class XMLFormatter extends output_formatters_1.OutputFormatter {
getName() {
return 'XML';
}
getFileExtension() {
return '.xml';
}
format(lines) {
const output = [];
output.push('<?xml version="1.0" encoding="UTF-8"?>');
output.push('<snes-disassembly>');
// Metadata
output.push(' <metadata>');
output.push(` <title>${this.escapeXML(this.rom.header.title)}</title>`);
output.push(` <cartridge-type>${this.rom.cartridgeInfo.type}</cartridge-type>`);
output.push(` <rom-size>${this.rom.cartridgeInfo.romSize}</rom-size>`);
output.push(` <reset-vector>0x${this.rom.header.nativeVectors.reset.toString(16).toUpperCase()}</reset-vector>`);
if (this.rom.cartridgeInfo.specialChip) {
output.push(` <special-chip>${this.rom.cartridgeInfo.specialChip}</special-chip>`);
}
output.push(' <generated-by>SNES Disassembler</generated-by>');
output.push(` <generated-at>${new Date().toISOString()}</generated-at>`);
output.push(' </metadata>');
// Symbols
if (this.symbols.size > 0) {
output.push(' <symbols>');
for (const [address, symbol] of this.symbols) {
output.push(` <symbol address="0x${address.toString(16).toUpperCase().padStart(6, '0')}">`);
output.push(` <name>${this.escapeXML(symbol.name)}</name>`);
output.push(` <type>${symbol.type}</type>`);
output.push(` <scope>${symbol.scope}</scope>`);
if (symbol.size !== undefined) {
output.push(` <size>${symbol.size}</size>`);
}
if (symbol.description) {
output.push(` <description>${this.escapeXML(symbol.description)}</description>`);
}
output.push(' </symbol>');
}
output.push(' </symbols>');
}
// Cross References
if (this.crossRefs.length > 0) {
output.push(' <cross-references>');
for (const ref of this.crossRefs) {
output.push(` <reference from="0x${ref.fromAddress.toString(16).toUpperCase()}" to="0x${ref.toAddress.toString(16).toUpperCase()}" type="${ref.type}">`);
if (ref.instruction) {
output.push(` <instruction>${this.escapeXML(ref.instruction)}</instruction>`);
}
output.push(' </reference>');
}
output.push(' </cross-references>');
}
// Disassembly
output.push(' <disassembly>');
for (const line of lines) {
output.push(` <line address="0x${line.address.toString(16).toUpperCase().padStart(6, '0')}">`);
if (line.label) {
output.push(` <label>${this.escapeXML(line.label)}</label>`);
}
output.push(' <bytes>');
for (const byte of line.bytes) {
output.push(` <byte>0x${byte.toString(16).toUpperCase().padStart(2, '0')}</byte>`);
}
output.push(' </bytes>');
output.push(' <instruction>');
output.push(` <opcode>0x${line.instruction.opcode.toString(16).toUpperCase().padStart(2, '0')}</opcode>`);
output.push(` <mnemonic>${line.instruction.mnemonic}</mnemonic>`);
output.push(` <addressing-mode>${line.instruction.addressingMode}</addressing-mode>`);
output.push(` <byte-count>${line.instruction.bytes}</byte-count>`);
const cycles = typeof line.instruction.cycles === 'number' ?
line.instruction.cycles : line.instruction.cycles.base;
output.push(` <cycles>${cycles}</cycles>`);
output.push(' </instruction>');
if (line.operand !== undefined) {
output.push(` <operand value="0x${line.operand.toString(16).toUpperCase()}">${this.formatOperand(line)}</operand>`);
}
if (line.comment) {
output.push(` <comment>${this.escapeXML(line.comment)}</comment>`);
}
output.push(' </line>');
}
output.push(' </disassembly>');
output.push('</snes-disassembly>');
return output.join('\n');
}
escapeXML(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}
exports.XMLFormatter = XMLFormatter;
class CSVFormatter extends output_formatters_1.OutputFormatter {
getName() {
return 'CSV';
}
getFileExtension() {
return '.csv';
}
format(lines) {
const output = [];
// CSV Header
const headers = [
'Address',
'AddressHex',
'Bytes',
'BytesHex',
'Opcode',
'OpcodeHex',
'Mnemonic',
'AddressingMode',
'InstructionBytes',
'Cycles',
'Operand',
'OperandHex',
'OperandFormatted',
'Label',
'Comment'
];
output.push(headers.join(','));
// CSV Data
for (const line of lines) {
const row = [
line.address.toString(),
`"${line.address.toString(16).toUpperCase().padStart(6, '0')}"`,
`"${line.bytes.join(' ')}"`,
`"${line.bytes.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ')}"`,
line.instruction.opcode.toString(),
`"${line.instruction.opcode.toString(16).toUpperCase().padStart(2, '0')}"`,
`"${line.instruction.mnemonic}"`,
`"${line.instruction.addressingMode}"`,
line.instruction.bytes.toString(),
typeof line.instruction.cycles === 'number' ?
line.instruction.cycles.toString() :
line.instruction.cycles.base.toString(),
line.operand?.toString() || '',
line.operand ? `"${line.operand.toString(16).toUpperCase()}"` : '""',
`"${this.formatOperand(line)}"`,
line.label ? `"${line.label}"` : '""',
line.comment ? `"${line.comment.replace(/"/g, '""')}"` : '""'
];
output.push(row.join(','));
}
return output.join('\n');
}
}
exports.CSVFormatter = CSVFormatter;
class MarkdownFormatter extends output_formatters_1.OutputFormatter {
getName() {
return 'Markdown';
}
getFileExtension() {
return '.md';
}
format(lines) {
const output = [];
// Markdown Header
output.push(`# SNES ROM Disassembly: ${this.rom.header.title}`);
output.push('');
output.push('## ROM Information');
output.push('');
output.push(`- **Title:** ${this.rom.header.title}`);
output.push(`- **Cartridge Type:** ${this.rom.cartridgeInfo.type}`);
output.push(`- **ROM Size:** ${(this.rom.cartridgeInfo.romSize / 1024).toFixed(0)} KB`);
output.push(`- **Reset Vector:** ${this.formatAddress(this.rom.header.nativeVectors.reset)}`);
if (this.rom.cartridgeInfo.specialChip) {
output.push(`- **Special Chip:** ${this.rom.cartridgeInfo.specialChip}`);
}
output.push(`- **Generated:** ${new Date().toISOString()}`);
output.push('');
// Symbols table
if (this.options.includeSymbols && this.symbols.size > 0) {
output.push('## Symbols');
output.push('');
output.push('| Address | Name | Type | Scope | Description |');
output.push('|---------|------|------|-------|-------------|');
const sortedSymbols = Array.from(this.symbols.entries()).sort((a, b) => a[0] - b[0]);
for (const [address, symbol] of sortedSymbols) {
const desc = symbol.description || '';
output.push(`| ${this.formatAddress(address)} | \`${symbol.name}\` | ${symbol.type} | ${symbol.scope} | ${desc} |`);
}
output.push('');
}
// Disassembly
output.push('## Disassembly');
output.push('');
output.push('```assembly');
for (const line of lines) {
let lineStr = '';
// Add label if present
if (line.label) {
output.push(`${line.label}:`);
}
// Format instruction line
lineStr += `${this.formatAddress(line.address)}: `;
if (this.options.includeBytes) {
lineStr += `${this.formatBytes(line.bytes).padEnd(12)} `;
}
lineStr += `${line.instruction.mnemonic.toUpperCase().padEnd(4)}`;
const operandStr = this.formatOperand(line);
if (operandStr) {
lineStr += ` ${operandStr}`;
}
if (this.options.includeComments && line.comment) {
lineStr = lineStr.padEnd(40) + ` ; ${line.comment}`;
}
output.push(lineStr);
}
output.push('```');
output.push('');
// Cross-references
if (this.options.includeCrossReferences && this.crossRefs.length > 0) {
output.push('## Cross References');
output.push('');
output.push('| From | To | Type | Instruction |');
output.push('|------|----|----|-------------|');
for (const ref of this.crossRefs) {
output.push(`| ${this.formatAddress(ref.fromAddress)} | ${this.formatAddress(ref.toAddress)} | ${ref.type} | ${ref.instruction || ''} |`);
}
output.push('');
}
return output.join('\n');
}
}
exports.MarkdownFormatter = MarkdownFormatter;
// Update the factory to include new formats
class ExtendedOutputFormatterFactory {
static create(format, rom, symbols, crossRefs, options) {
switch (format.toLowerCase()) {
case 'ca65':
return new output_formatters_1.CA65Formatter(rom, symbols, crossRefs, options);
case 'wla-dx':
case 'wladx':
return new output_formatters_1.WLADXFormatter(rom, symbols, crossRefs, options);
case 'bass':
return new output_formatters_1.BassFormatter(rom, symbols, crossRefs, options);
case 'html':
return new HTMLFormatter(rom, symbols, crossRefs, options);
case 'json':
return new JSONFormatter(rom, symbols, crossRefs, options);
case 'xml':
return new XMLFormatter(rom, symbols, crossRefs, options);
case 'csv':
return new CSVFormatter(rom, symbols, crossRefs, options);
case 'markdown':
case 'md':
return new MarkdownFormatter(rom, symbols, crossRefs, options);
default:
throw new Error(`Unsupported output format: ${format}`);
}
}
static getSupportedFormats() {
return ['ca65', 'wla-dx', 'bass', 'html', 'json', 'xml', 'csv', 'markdown'];
}
}
exports.ExtendedOutputFormatterFactory = ExtendedOutputFormatterFactory;
// Re-export classes from output-formatters.ts for convenience
var output_formatters_2 = require("./output-formatters");
Object.defineProperty(exports, "CA65Formatter", { enumerable: true, get: function () { return output_formatters_2.CA65Formatter; } });
Object.defineProperty(exports, "WLADXFormatter", { enumerable: true, get: function () { return output_formatters_2.WLADXFormatter; } });
Object.defineProperty(exports, "BassFormatter", { enumerable: true, get: function () { return output_formatters_2.BassFormatter; } });
//# sourceMappingURL=output-formats-extended.js.map