UNPKG

python2igcse

Version:

Convert Python code to IGCSE Pseudocode format

385 lines 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseEmitter = void 0; const emitter_1 = require("../types/emitter"); /** * Base class for emitters * Provides IR to text conversion functionality */ 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 empty 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) { for (const child of node.children) { this.emitNode(child); } } /** * Format text */ formatText(text) { let formatted = text; // Convert operators (Python → IGCSE) formatted = this.convertOperators(formatted); // Capitalize keywords if (this.context.formatter.uppercaseKeywords) { formatted = this.uppercaseKeywords(formatted); } // Space around operators if (this.context.formatter.spaceAroundOperators) { formatted = this.addSpaceAroundOperators(formatted); } // Space after comma (excluding inside string literals) if (this.context.formatter.spaceAfterComma) { formatted = this.addSpaceAfterCommaOutsideStrings(formatted); } return formatted; } /** * Capitalize keywords */ uppercaseKeywords(text) { const keywords = [ 'IF', 'THEN', 'ELSE', 'ENDIF', 'FOR', 'TO', 'STEP', 'NEXT', 'WHILE', 'ENDWHILE', 'REPEAT', 'UNTIL', 'PROCEDURE', 'ENDPROCEDURE', 'FUNCTION', 'ENDFUNCTION', 'RETURN', 'INPUT', 'OUTPUT', 'CASE', 'OF', 'OTHERWISE', 'ENDCASE', 'TYPE', 'ENDTYPE', 'CLASS', 'ENDCLASS', 'DECLARE', 'CONSTANT', 'ARRAY', 'RECORD', 'AND', 'OR', 'NOT', 'MOD', 'DIV', ]; // Protect string literals const stringLiterals = []; let result = text.replace(/"([^"]*)"/g, (_, content) => { const placeholder = `__STRING_${stringLiterals.length}__`; stringLiterals.push(content); return `"${placeholder}"`; }); for (const keyword of keywords) { const regex = new RegExp(`\\b${keyword.toLowerCase()}\\b`, 'gi'); result = result.replace(regex, keyword); } // Restore string literals result = result.replace(/"__STRING_(\d+)__"/g, (_, index) => { return `"${stringLiterals[parseInt(index)]}"`; }); 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}__`; }); // Protect string literals const stringLiterals = []; result = result.replace(/"([^"]*)"/g, (_, content) => { const placeholder = `__STRING_${stringLiterals.length}__`; stringLiterals.push(content); return `"${placeholder}"`; }); // 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 result = result.replace(/\band\b/gi, 'AND'); result = result.replace(/\bor\b/gi, 'OR'); result = result.replace(/\bnot\b/gi, 'NOT'); // Convert string concatenation (convert + to & on 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 & return line.replace(/\s*\+\s*/g, ' & '); } return line; }) .join('\n'); // Convert arithmetic operators (only // that are not comments) result = result.replace(/\s*%\s*/g, ' MOD '); result = result.replace(/\s*\/\/\s*/g, ' DIV '); // Convert input() function (special handling for assignment statements) result = result.replace(/(\w+)\s*←\s*input\(\)/g, 'INPUT $1'); result = result.replace(/(\w+)\s*←\s*input\(([^)]+)\)/g, 'OUTPUT $2\nINPUT $1'); // Convert regular input() function result = result.replace(/\binput\(\)/g, 'INPUT'); result = result.replace(/\binput\(([^)]+)\)/g, 'INPUT($1)'); // Restore string literals result = result.replace(/"__STRING_(\d+)__"/g, (_, index) => { return `"${stringLiterals[parseInt(index)]}"`; }); // Restore comments (convert # to //, leave // as is) commentMatches.forEach((comment, index) => { const convertedComment = comment.startsWith('#') ? comment.replace(/^#/, '//') : comment; result = result.replace(`__COMMENT_${index}__`, convertedComment); }); return result; } /** * Add space around operators */ addSpaceAroundOperators(text) { const operators = ['←', '=', '≠', '<', '>', '≤', '≥', '+', '-', '*', '/', 'MOD', 'DIV']; let result = text; for (const op of operators) { // Don't process if space already exists const regex = new RegExp(`(?<!\\s)${this.escapeRegex(op)}(?!\\s)`, 'g'); result = result.replace(regex, ` ${op} `); } // Remove duplicate spaces result = result.replace(/\s+/g, ' '); return result; } /** * Escape for regular expressions */ escapeRegex(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Add space after commas outside string literals */ addSpaceAfterCommaOutsideStrings(text) { let result = ''; let inString = false; let stringChar = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; const prevChar = i > 0 ? text[i - 1] : ''; // Detect string start/end if ((char === '"' || char === "'") && prevChar !== '\\') { if (!inString) { inString = true; stringChar = char; } else if (char === stringChar) { inString = false; stringChar = ''; } } result += char; // Add space after commas outside strings if (!inString && char === ',' && i + 1 < text.length && text[i + 1] !== ' ') { result += ' '; } } 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), 0), }, success: this.context.errors.length === 0, emitTime, output: code, }; } /** * 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 options */ 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