UNPKG

python2igcse

Version:

Convert Python code to IGCSE Pseudocode format

322 lines 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseEmitter = void 0; const emitter_1 = require("../types/emitter"); /** * Base class for emitters * Provides functionality for converting IR to text */ class BaseEmitter { constructor(options = {}) { this.startTime = 0; this.options = { ...(0, emitter_1.getDefaultEmitterOptions)(), ...options }; this.context = this.createInitialContext(); } /** * Create initial context */ createInitialContext() { return { indent: (0, emitter_1.createIndentInfo)(0, this.options.indentChar, this.options.indentSize), output: [], currentLine: 1, errors: [], warnings: [], formatter: (0, emitter_1.getDefaultFormatterConfig)() }; } /** * Add error */ addError(message, type, node) { const error = (0, emitter_1.createEmitError)(message, type, node); this.context.errors.push(error); if (this.options.includeDebugInfo) { console.error(`Emit Error: ${message}`); } } /** * Add warning */ addWarning(message, type, node) { const warning = (0, emitter_1.createEmitWarning)(message, type, node); this.context.warnings.push(warning); if (this.options.includeDebugInfo) { console.warn(`Emit Warning: ${message}`); } } /** * Increase indent level */ increaseIndent() { this.context.indent = (0, emitter_1.createIndentInfo)(this.context.indent.level + 1, this.options.indentChar, this.options.indentSize); } /** * Decrease indent level */ decreaseIndent() { if (this.context.indent.level > 0) { this.context.indent = (0, emitter_1.createIndentInfo)(this.context.indent.level - 1, this.options.indentChar, this.options.indentSize); } } /** * Output line */ emitLine(text, indent = true) { const indentedText = indent ? this.context.indent.string + text : text; // Line length check if (this.options.maxLineLength && indentedText.length > this.options.maxLineLength) { this.addWarning(`Line exceeds maximum length (${indentedText.length} > ${this.options.maxLineLength})`, 'long_line'); } // Output with line numbers if (this.options.includeLineNumbers) { const lineNumber = this.context.currentLine.toString().padStart(3, ' '); this.context.output.push(`${lineNumber}: ${indentedText}`); } else { this.context.output.push(indentedText); } this.context.currentLine++; } /** * Output blank line */ emitBlankLine() { this.context.output.push(''); this.context.currentLine++; } /** * Output comment */ emitComment(text) { if (this.options.includeComments) { this.emitLine(text); } } /** * Process child nodes */ emitChildren(node) { if (node.children && Array.isArray(node.children)) { for (const child of node.children) { this.emitNode(child); } } } /** * Format text (Python → IGCSE) */ formatText(text) { let result = text; // Convert operators result = this.convertOperators(result); // Uppercase keywords result = this.uppercaseKeywords(result); return result; } /** * Uppercase keywords (only outside string literals) */ uppercaseKeywords(text) { let result = text; // Temporarily protect string literals const stringLiterals = []; result = result.replace(/(["'])((?:\\.|(?!\1)[^\\])*)\1/g, (match) => { const index = stringLiterals.length; stringLiterals.push(match); return `__STRING_${index}__`; }); // Uppercase IGCSE keywords (only outside string literals) const keywords = [ 'if', 'then', 'else', 'endif', 'elseif', 'for', 'to', 'step', 'next', 'while', 'endwhile', 'repeat', 'until', 'do', 'procedure', 'endprocedure', 'function', 'endfunction', 'return', 'declare', 'constant', 'array', 'of', 'type', 'input', 'output', 'read', 'write', 'and', 'or', 'not', 'true', 'false', 'null', 'case', 'of', 'otherwise', 'endcase', 'class', 'endclass', 'new', 'super', 'this', 'public', 'private', 'inherits' ]; keywords.forEach(keyword => { const regex = new RegExp(`\\b${keyword}\\b`, 'gi'); result = result.replace(regex, keyword.toUpperCase()); }); // Restore string literals stringLiterals.forEach((literal, index) => { result = result.replace(`__STRING_${index}__`, literal); }); return result; } /** * Convert operators (Python → IGCSE) */ convertOperators(text) { let result = text; // Protect comment sections (temporarily replace Python # comments and IGCSE // comments) const commentMatches = []; result = result.replace(/(#.*$|\/\/.*$)/gm, (match) => { const index = commentMatches.length; commentMatches.push(match); return `__COMMENT_${index}__`; }); // Temporarily protect string literals const stringLiterals = []; result = result.replace(/(["'])((?:\\.|(?!\1)[^\\])*)\1/g, (match) => { const index = stringLiterals.length; stringLiterals.push(match); return `__STRING_${index}__`; }); // Convert comparison operators (process before assignment operators) result = result.replace(/!=/g, '≠'); result = result.replace(/<=/g, '≤'); result = result.replace(/>=/g, '≥'); result = result.replace(/==/g, '='); // Convert assignment operators (only = that are not comparison operators) // Convert variable = format from line start to ← result = result.replace(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*/gm, '$1$2 ← '); // Convert logical operators (using word boundaries) result = result.replace(/\band\b/g, ' AND '); result = result.replace(/\bor\b/g, ' OR '); result = result.replace(/\bnot\b/g, 'NOT '); // Restore string literals stringLiterals.forEach((literal, index) => { result = result.replace(`__STRING_${index}__`, literal); }); // Convert string concatenation (convert + to & in lines containing string literals) const lines = result.split('\n'); result = lines.map(line => { // Check if line contains string literals (parts enclosed in " or ') if (/["']/.test(line)) { // Convert + for string concatenation to & (also adjust spacing) // But exclude addition between numbers return line.replace(/\s*\+\s*/g, (match, offset, string) => { // Check characters before and after + const before = string.substring(0, offset).trim(); const after = string.substring(offset + match.length).trim(); // Get word immediately before + const beforeMatch = before.match(/([a-zA-Z_][a-zA-Z0-9_]*|\d+(?:\.\d+)?)$/); const beforeToken = beforeMatch ? beforeMatch[1] : ''; // Get word immediately after + const afterMatch = after.match(/^([a-zA-Z_][a-zA-Z0-9_]*|\d+(?:\.\d+)?)/); const afterToken = afterMatch ? afterMatch[1] : ''; // Check if it's addition between numeric literals const beforeIsNumber = /^\d+(\.\d+)?$/.test(beforeToken); const afterIsNumber = /^\d+(\.\d+)?$/.test(afterToken); // Check if it's addition between variable and number (like y + 1) const beforeIsVar = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(beforeToken); const afterIsVar = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(afterToken); // Keep + for addition between numbers, variables and numbers, or variables if ((beforeIsNumber && afterIsNumber) || (beforeIsVar && afterIsNumber) || (beforeIsNumber && afterIsVar) || (beforeIsVar && afterIsVar)) { return ' + '; } // Convert to & for everything else (string concatenation) return ' & '; }); } return line; }).join('\n'); // Remove extra spaces (fix duplicate spaces around operators) result = result.replace(/\s{2,}/g, ' '); // Restore comments (convert Python # comments to IGCSE // comments) commentMatches.forEach((comment, index) => { let convertedComment = comment; // Convert Python # comments to IGCSE // comments if (comment.startsWith('#')) { convertedComment = comment.replace(/^#/, '//'); } result = result.replace(`__COMMENT_${index}__`, convertedComment); }); return result; } /** * Create emit result */ createEmitResult() { const endTime = Date.now(); const emitTime = endTime - this.startTime; const code = this.context.output.join(this.options.lineEnding); const linesGenerated = this.context.output.length; const charactersGenerated = code.length; return { code, errors: [...this.context.errors], warnings: [...this.context.warnings], stats: { linesGenerated, lineCount: linesGenerated, // Alias for testing charactersGenerated, characterCount: charactersGenerated, // Alias for testing nodesProcessed: 0, // Set during implementation emitTime, processingTime: emitTime, // Alias for testing maxNestingDepth: this.context.indent.level, maxLineLength: Math.max(...this.context.output.map(line => line.length)) } }; } /** * Record emit start time */ startEmitting() { this.startTime = Date.now(); } /** * Output debug information */ debug(_message) { // Debug logging disabled } /** * Reset context */ resetContext() { this.context = this.createInitialContext(); } /** * Wrap long lines */ wrapLongLine(text, maxLength) { const limit = maxLength || this.options.maxLineLength || 80; if (text.length <= limit) { return [text]; } const words = text.split(' '); const lines = []; let currentLine = ''; for (const word of words) { if (currentLine.length + word.length + 1 <= limit) { currentLine += (currentLine ? ' ' : '') + word; } else { if (currentLine) { lines.push(currentLine); } currentLine = word; } } if (currentLine) { lines.push(currentLine); } return lines; } /** * Update settings */ updateOptions(options) { this.options = { ...this.options, ...options }; } /** * Update formatter configuration */ updateFormatterConfig(config) { this.context.formatter = { ...this.context.formatter, ...config }; } } exports.BaseEmitter = BaseEmitter; //# sourceMappingURL=base-emitter.js.map