diff --git a/Makefile b/Makefile index 46b82626..f13ca6ce 100644 --- a/Makefile +++ b/Makefile @@ -83,6 +83,7 @@ help: @echo " status - Show git status for repo and submodules" @echo " clean - Clean build artifacts" @echo " check-deps - Check dependency status" + @echo " validate-js - Validate JavaScript syntax in templates" @echo "" @echo "Documentation:" @echo " update-digest - Update ProjectStatusDigest.md (requires Claude Code)" @@ -1340,3 +1341,13 @@ cost-help: @echo "šŸ’° Currency: Costs calculated in USD and EUR" @echo "šŸ¤– Model: Default claude-sonnet-4 pricing" +# JavaScript validation for edit mode templates +validate-js: $(VENV)/bin/activate + @echo "šŸ” Validating JavaScript syntax in templates..." + @if command -v node >/dev/null 2>&1; then \ + $(PYTHON) tools/validate_js_syntax.py; \ + else \ + echo "āš ļø Node.js not available - skipping JavaScript validation"; \ + echo " Install Node.js to enable JavaScript syntax checking"; \ + fi + diff --git a/markitect/document_manager.py b/markitect/document_manager.py index c847bdc5..40626078 100644 --- a/markitect/document_manager.py +++ b/markitect/document_manager.py @@ -659,18 +659,115 @@ class DocumentManager: // Define editor class first (if in edit mode) {editor_scripts if edit_mode else ''} - // Error reporting utility - function reportEditModeError(errorMsg, technicalDetails) {{ + // Enhanced error reporting utility + function reportEditModeError(errorMsg, technicalDetails, errorType = 'error') {{ const statusDiv = document.getElementById('markitect-status'); const errorDiv = document.getElementById('error-details'); const errorText = document.getElementById('error-text'); const statusMsg = document.getElementById('status-message'); const browserInfo = document.getElementById('browser-info'); - if (statusMsg) statusMsg.textContent = 'Edit mode unavailable - content displayed in read-only mode'; + // Log to console for debugging + console.error('[MarkiTect Edit Mode Error]', errorMsg, technicalDetails); + + // Create error report object + const errorReport = {{ + timestamp: new Date().toISOString(), + error: errorMsg, + details: technicalDetails, + type: errorType, + userAgent: navigator.userAgent, + url: window.location.href, + markdownContent: typeof markdownContent !== 'undefined' ? markdownContent.length + ' chars' : 'unavailable' + }}; + + // Store error for potential reporting + if (!window.markitectErrors) window.markitectErrors = []; + window.markitectErrors.push(errorReport); + + // Update UI + if (statusMsg) {{ + const statusText = errorType === 'warning' + ? 'Edit mode partially available - some features may not work' + : 'Edit mode unavailable - content displayed in read-only mode'; + statusMsg.textContent = statusText; + }} + if (errorDiv) errorDiv.style.display = 'block'; - if (errorText) errorText.textContent = errorMsg + (technicalDetails ? ' (' + technicalDetails + ')' : ''); + if (errorText) {{ + const fullError = errorMsg + (technicalDetails ? ' (' + technicalDetails + ')' : ''); + errorText.textContent = fullError; + }} if (browserInfo) browserInfo.textContent = navigator.userAgent.split(' ').slice(-2).join(' '); + + // Auto-hide warnings after 10 seconds + if (errorType === 'warning' && errorDiv) {{ + setTimeout(() => {{ + errorDiv.style.display = 'none'; + }}, 10000); + }} + }} + + // Enhanced error recovery utility + function attemptErrorRecovery(error, context) {{ + console.warn('[MarkiTect] Attempting error recovery for:', context, error); + + try {{ + // Try to ensure content is still visible + const contentDiv = document.getElementById('markdown-content'); + if (contentDiv && !contentDiv.innerHTML.trim()) {{ + // Fallback content rendering + const fallbackHtml = markdownContent + .replace(/^# (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/\\n\\n/g, '

') + .replace(/\\n/g, '
'); + contentDiv.innerHTML = '
' + fallbackHtml + '
'; + + reportEditModeError('Recovered with fallback rendering', 'Edit features disabled', 'warning'); + return true; + }} + }} catch (recoveryError) {{ + console.error('[MarkiTect] Recovery failed:', recoveryError); + }} + + return false; + }} + + // Validation utility for edit mode state + function validateEditModeState() {{ + const issues = []; + + // Check required elements + if (!document.getElementById('markdown-content')) {{ + issues.push('Missing markdown-content container'); + }} + + if (!document.getElementById('markitect-status')) {{ + issues.push('Missing status display'); + }} + + // Check JavaScript dependencies + if (typeof marked === 'undefined') {{ + issues.push('marked.js library not available'); + }} + + if (typeof MARKITECT_EDIT_MODE === 'undefined') {{ + issues.push('Edit mode configuration missing'); + }} + + // Check for MarkitectEditor + if (typeof MarkitectEditor === 'undefined') {{ + issues.push('MarkitectEditor class not defined'); + }} + + if (issues.length > 0) {{ + console.warn('[MarkiTect] Edit mode validation issues:', issues); + reportEditModeError('Edit mode validation failed', issues.join(', '), 'warning'); + return false; + }} + + return true; }} // Status update utility @@ -718,19 +815,44 @@ class DocumentManager: }} // Step 2: Try to enhance with edit capabilities (if in edit mode) - {'''if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) { + {'''if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) {{ updateStatus("Initializing edit capabilities..."); - try { + + // Validate edit mode prerequisites + if (!validateEditModeState()) {{ + if (!attemptErrorRecovery('validation failed', 'edit mode prerequisites')) {{ + return; // Stop here if recovery fails + }} + }} + + try {{ updateStatus("Creating editor instance..."); markitectEditor = new MarkitectEditor(); updateStatus("āœ“ Edit mode active - click any section to edit"); console.log("āœ“ Edit mode initialized successfully"); - } catch (error) { + + // Final validation check + setTimeout(() => {{ + const sections = document.querySelectorAll('.markitect-section-editable'); + if (sections.length === 0) {{ + reportEditModeError('No editable sections found', 'Content may not be compatible with edit mode', 'warning'); + }} else {{ + console.log(`[MarkiTect] Found ${{sections.length}} editable sections`); + }} + }}, 1000); + + }} catch (error) {{ updateStatus("Edit mode failed to initialize", true); - reportEditModeError("Edit mode initialization failed", error.message); console.error("Edit mode error:", error); - } - }''' if edit_mode else ''} + + // Try error recovery + if (attemptErrorRecovery(error, 'editor initialization')) {{ + reportEditModeError("Edit mode partially recovered", error.message, 'warning'); + }} else {{ + reportEditModeError("Edit mode initialization failed", error.message); + }} + }} + }}''' if edit_mode else ''} }}); // Handle CDN loading errors diff --git a/tests/test_issue_144_edit_mode_regression.py b/tests/test_issue_144_edit_mode_regression.py new file mode 100644 index 00000000..d8757d9a --- /dev/null +++ b/tests/test_issue_144_edit_mode_regression.py @@ -0,0 +1,348 @@ +""" +Test suite for md-render --edit functionality to prevent regression. + +This test suite specifically targets the critical JavaScript syntax errors +that were causing edit mode to fail completely, ensuring they never happen again. +""" + +import tempfile +import pytest +from pathlib import Path +import re +import subprocess + + +class TestEditModeRegression: + """Tests to prevent regression of the md-render --edit functionality.""" + + def test_edit_mode_generates_valid_javascript(self): + """Test that edit mode generates syntactically valid JavaScript.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + # Test markdown content + test_content = "# Test Header\n\nThis is a test paragraph.\n\n## Section 2\n\nAnother paragraph." + + # Generate HTML with edit mode + html_content = doc_manager._generate_html_template( + title="Test Document", + markdown_content=test_content, + edit_mode=True, + editor_theme='github', + keyboard_shortcuts=True + ) + + # Extract JavaScript from HTML + js_match = re.search(r'', html_content, re.DOTALL) + assert js_match, "No JavaScript found in edit mode HTML" + + js_content = js_match.group(1) + + # Write to temp file and validate syntax with Node.js + with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f: + f.write(js_content) + temp_js_path = f.name + + try: + # Use Node.js to check JavaScript syntax + result = subprocess.run( + ['node', '-c', temp_js_path], + capture_output=True, + text=True + ) + + assert result.returncode == 0, f"JavaScript syntax error: {result.stderr}" + + finally: + Path(temp_js_path).unlink() + + def test_edit_mode_contains_required_functions(self): + """Test that edit mode HTML contains all required JavaScript functions.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test", + edit_mode=True + ) + + # Check for critical functions that must be present + required_functions = [ + 'MarkitectEditor', + 'updateStatus', + 'reportEditModeError', + 'makeContentEditable', + 'handleSectionClick', + 'editSection' + ] + + for func_name in required_functions: + assert func_name in html_content, f"Required function '{func_name}' not found in edit mode HTML" + + def test_edit_mode_no_broken_string_literals(self): + """Test that there are no broken string literals in the generated JavaScript.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test", + edit_mode=True + ) + + # Extract JavaScript + js_match = re.search(r'', html_content, re.DOTALL) + js_content = js_match.group(1) + + # Check for broken string patterns that caused the original bug + broken_patterns = [ + r"'\s*\n\s*'", # Broken string literal across lines + r'"\s*\n\s*"', # Broken string literal across lines + r'reconstructed \+= .*\'\n', # Unescaped newline in string + ] + + for pattern in broken_patterns: + matches = re.findall(pattern, js_content) + assert not matches, f"Found broken string pattern: {pattern} - matches: {matches}" + + def test_edit_mode_proper_brace_escaping(self): + """Test that braces are properly escaped in f-string templates.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test", + edit_mode=True + ) + + # Extract JavaScript + js_match = re.search(r'', html_content, re.DOTALL) + js_content = js_match.group(1) + + # Check for inconsistent brace patterns + inconsistent_patterns = [ + r'} else if.*{{', # Mixed single and double braces + r'}} else if.*{[^{]', # Mixed double and single braces + ] + + for pattern in inconsistent_patterns: + matches = re.findall(pattern, js_content) + assert not matches, f"Found inconsistent brace pattern: {pattern}" + + def test_edit_mode_template_literal_syntax(self): + """Test that template literals are properly escaped.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test", + edit_mode=True + ) + + # Extract JavaScript + js_match = re.search(r'', html_content, re.DOTALL) + js_content = js_match.group(1) + + # Check for problematic template literal patterns + # Should NOT find double-escaped template literals like ${{ + problematic_patterns = [ + r'\$\{\{.*?\}\}', # Double-escaped template literals + ] + + for pattern in problematic_patterns: + matches = re.findall(pattern, js_content) + assert not matches, f"Found problematic template literal: {pattern}" + + def test_edit_mode_contains_content_div(self): + """Test that edit mode HTML contains the markdown-content div.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test Content", + edit_mode=True + ) + + # Should contain the content container + assert 'id="markdown-content"' in html_content + assert 'MARKITECT_EDIT_MODE = true' in html_content + assert 'markitect-edit-mode' in html_content + + def test_edit_mode_error_handling_elements(self): + """Test that edit mode includes proper error handling UI elements.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test", + edit_mode=True + ) + + # Should contain error handling elements + assert 'id="markitect-status"' in html_content + assert 'id="status-message"' in html_content + assert 'id="error-details"' in html_content + assert 'reportEditModeError' in html_content + + def test_edit_mode_vs_normal_mode_differences(self): + """Test that edit mode and normal mode generate different output appropriately.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + test_content = "# Test Header\n\nTest content." + + # Generate both modes + normal_html = doc_manager._generate_html_template( + title="Test", + markdown_content=test_content, + edit_mode=False + ) + + edit_html = doc_manager._generate_html_template( + title="Test", + markdown_content=test_content, + edit_mode=True + ) + + # Edit mode should have additional elements + assert len(edit_html) > len(normal_html) + assert 'MarkitectEditor' in edit_html + assert 'MarkitectEditor' not in normal_html + assert 'markitect-edit-mode' in edit_html + assert 'markitect-edit-mode' not in normal_html + + def test_edit_mode_javascript_execution_flow(self): + """Test the logical flow of JavaScript execution in edit mode.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test", + edit_mode=True + ) + + # Extract JavaScript + js_match = re.search(r'', html_content, re.DOTALL) + js_content = js_match.group(1) + + # Check for proper execution flow elements + flow_elements = [ + 'DOMContentLoaded', # Event listener setup + 'MARKITECT_EDIT_MODE', # Mode check + 'new MarkitectEditor', # Editor instantiation + 'makeContentEditable', # Content enhancement + 'handleSectionClick' # Interaction handler + ] + + for element in flow_elements: + assert element in js_content, f"Missing execution flow element: {element}" + + def test_newline_escaping_in_javascript_strings(self): + """Test that newlines in JavaScript strings are properly escaped.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test\n\nMultiple\nLines", + edit_mode=True + ) + + # Extract JavaScript + js_match = re.search(r'', html_content, re.DOTALL) + js_content = js_match.group(1) + + # Look for the specific section that was broken + # Should find properly escaped newlines like '\\n\\n' + assert '\\\\n\\\\n' in js_content, "Newlines not properly escaped in JavaScript strings" + + # Should NOT find unescaped newlines in string contexts + # This regex looks for string concatenation with actual newlines + broken_newline_pattern = r"'\s*\+\s*text\s*\+\s*'\s*\n" + matches = re.findall(broken_newline_pattern, js_content) + assert not matches, f"Found unescaped newlines in string concatenation: {matches}" + + +class TestEditModeIntegration: + """Integration tests for the complete edit mode functionality.""" + + def test_md_render_edit_command_execution(self): + """Test that the md-render --edit command executes without errors.""" + import tempfile + from markitect.plugins.builtin.markdown_commands import md_render_command + from click.testing import CliRunner + + runner = CliRunner() + + with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file: + md_file.write("# Test Document\n\nThis is a test paragraph.\n\n## Section 2\n\nAnother paragraph.") + md_file_path = md_file.name + + with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as html_file: + html_file_path = html_file.name + + try: + # Test the command + result = runner.invoke(md_render_command, [ + md_file_path, + '--edit', + '--output', html_file_path + ]) + + assert result.exit_code == 0, f"Command failed: {result.output}" + + # Verify the output file exists and contains edit mode elements + html_content = Path(html_file_path).read_text() + assert 'MarkitectEditor' in html_content + assert 'markitect-edit-mode' in html_content + + # Verify JavaScript syntax + js_match = re.search(r'', html_content, re.DOTALL) + assert js_match, "No JavaScript found in output" + + finally: + Path(md_file_path).unlink(missing_ok=True) + Path(html_file_path).unlink(missing_ok=True) + + def test_save_functionality_javascript_presence(self): + """Test that the save functionality JavaScript is properly included.""" + from markitect.document_manager import DocumentManager + + doc_manager = DocumentManager() + + html_content = doc_manager._generate_html_template( + title="Test", + markdown_content="# Test Content", + edit_mode=True + ) + + # Check for save-related functionality + save_elements = [ + 'Save & Download', # Button text + 'markitectEditor.save()', # Save function call + 'getMarkdownContent', # Content extraction + 'Blob', # File creation + 'download' # Download attribute + ] + + for element in save_elements: + assert element in html_content, f"Save functionality element missing: {element}" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tools/validate_js_syntax.py b/tools/validate_js_syntax.py new file mode 100755 index 00000000..dbb996e8 --- /dev/null +++ b/tools/validate_js_syntax.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +JavaScript syntax validation tool for MarkiTect build process. + +This tool validates that all generated JavaScript in edit mode is syntactically correct, +preventing the regression that caused edit mode to fail completely. +""" + +import subprocess +import tempfile +import sys +import re +from pathlib import Path +from typing import List, Tuple, Optional + + +class JavaScriptValidator: + """Validates JavaScript syntax in MarkiTect templates.""" + + def __init__(self): + self.errors = [] + self.warnings = [] + + def validate_document_manager_templates(self) -> bool: + """Validate JavaScript templates in DocumentManager.""" + try: + # Import the template generation method directly to avoid database dependency + from markitect.document_manager import DocumentManager + + # Create a mock DocumentManager to access the template method + class MockDatabaseManager: + pass + + doc_manager = DocumentManager.__new__(DocumentManager) + doc_manager.database_manager = MockDatabaseManager() + + # Test various configurations + test_cases = [ + { + 'title': 'Basic Edit Mode', + 'markdown': '# Test\n\nBasic content.', + 'edit_mode': True, + 'theme': 'github' + }, + { + 'title': 'Complex Content', + 'markdown': '''# Header 1 + +## Header 2 + +Paragraph with **bold** and *italic* text. + +- List item 1 +- List item 2 + +1. Numbered item +2. Another item + +> Blockquote text + +```python +code block +``` + +Final paragraph.''', + 'edit_mode': True, + 'theme': 'dark' + }, + { + 'title': 'Special Characters', + 'markdown': "# Test 'quotes' and \"double quotes\"\n\nContent with $pecial ch@racters & symbols!", + 'edit_mode': True, + 'theme': 'github' + } + ] + + all_valid = True + + for test_case in test_cases: + print(f"Validating: {test_case['title']}") + + html_content = doc_manager._generate_html_template( + title=test_case['title'], + markdown_content=test_case['markdown'], + edit_mode=test_case['edit_mode'], + editor_theme=test_case.get('theme', 'github') + ) + + is_valid, errors = self._validate_html_javascript(html_content, test_case['title']) + + if not is_valid: + all_valid = False + self.errors.extend(errors) + else: + print(f" āœ… {test_case['title']} - JavaScript syntax valid") + + return all_valid + + except Exception as e: + self.errors.append(f"Failed to validate document manager templates: {e}") + return False + + def _validate_html_javascript(self, html_content: str, context: str) -> Tuple[bool, List[str]]: + """Extract and validate JavaScript from HTML content.""" + errors = [] + + # Extract all JavaScript blocks + js_blocks = re.findall(r']*>(.*?)', html_content, re.DOTALL) + + if not js_blocks: + errors.append(f"{context}: No JavaScript blocks found") + return False, errors + + for i, js_content in enumerate(js_blocks): + # Skip empty blocks or blocks with only external src + if not js_content.strip() or 'src=' in js_content: + continue + + is_valid, js_errors = self._validate_javascript_syntax(js_content, f"{context} block {i+1}") + if not is_valid: + errors.extend(js_errors) + + return len(errors) == 0, errors + + def _validate_javascript_syntax(self, js_content: str, context: str) -> Tuple[bool, List[str]]: + """Validate JavaScript syntax using Node.js.""" + errors = [] + + try: + with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f: + f.write(js_content) + temp_js_path = f.name + + # Use Node.js to check syntax + result = subprocess.run( + ['node', '-c', temp_js_path], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode != 0: + errors.append(f"{context}: JavaScript syntax error - {result.stderr.strip()}") + + # Add helpful debugging info + lines = js_content.split('\n') + for line_num, line in enumerate(lines[:20], 1): # Show first 20 lines + if line.strip(): + print(f" Line {line_num}: {line[:100]}") # First 100 chars + + Path(temp_js_path).unlink(missing_ok=True) + + except subprocess.TimeoutExpired: + errors.append(f"{context}: JavaScript validation timeout") + except FileNotFoundError: + errors.append(f"{context}: Node.js not available for validation") + except Exception as e: + errors.append(f"{context}: Validation error - {e}") + + return len(errors) == 0, errors + + def check_common_issues(self) -> bool: + """Check for common JavaScript issues that have caused problems.""" + try: + from markitect.document_manager import DocumentManager + + # Create a mock DocumentManager to access the template method + class MockDatabaseManager: + pass + + doc_manager = DocumentManager.__new__(DocumentManager) + doc_manager.database_manager = MockDatabaseManager() + + html_content = doc_manager._generate_html_template( + title="Issue Check", + markdown_content="# Test\n\nTest content.", + edit_mode=True + ) + + js_match = re.search(r'', html_content, re.DOTALL) + if not js_match: + self.errors.append("No JavaScript found for common issues check") + return False + + js_content = js_match.group(1) + issues_found = False + + # Check for specific issues that have caused problems + issue_patterns = [ + (r"'\s*\n\s*'", "Broken string literal across lines"), + (r'"\s*\n\s*"', "Broken string literal across lines"), + (r'} else if.*{{.*}} else if.*{[^{]', "Inconsistent brace escaping"), + (r'\$\{\{.*?\}\}', "Double-escaped template literals"), + (r'reconstructed \+= .*\'\n', "Unescaped newline in string concatenation"), + ] + + for pattern, description in issue_patterns: + matches = re.findall(pattern, js_content) + if matches: + self.errors.append(f"Found {description}: {matches[:3]}...") # Show first 3 matches + issues_found = True + + # Check for required elements + required_elements = [ + 'MarkitectEditor', + 'updateStatus', + 'makeContentEditable', + 'DOMContentLoaded' + ] + + for element in required_elements: + if element not in js_content: + self.errors.append(f"Missing required element: {element}") + issues_found = True + + return not issues_found + + except Exception as e: + self.errors.append(f"Failed to check common issues: {e}") + return False + + def validate_all(self) -> bool: + """Run all validation checks.""" + print("šŸ” Validating JavaScript syntax in MarkiTect templates...") + + all_checks_passed = True + + # Run all validation checks + checks = [ + ("Document Manager Templates", self.validate_document_manager_templates), + ("Common Issues", self.check_common_issues), + ] + + for check_name, check_func in checks: + print(f"\nšŸ“‹ Running {check_name} check...") + try: + if not check_func(): + all_checks_passed = False + print(f" āŒ {check_name} check failed") + else: + print(f" āœ… {check_name} check passed") + except Exception as e: + all_checks_passed = False + self.errors.append(f"{check_name} check failed with exception: {e}") + print(f" āŒ {check_name} check failed with exception") + + return all_checks_passed + + def report_results(self) -> None: + """Print validation results.""" + print("\n" + "="*60) + print("JavaScript Validation Results") + print("="*60) + + if self.errors: + print(f"\nāŒ {len(self.errors)} Error(s) Found:") + for i, error in enumerate(self.errors, 1): + print(f" {i}. {error}") + + if self.warnings: + print(f"\nāš ļø {len(self.warnings)} Warning(s):") + for i, warning in enumerate(self.warnings, 1): + print(f" {i}. {warning}") + + if not self.errors and not self.warnings: + print("\nāœ… All JavaScript validation checks passed!") + + print("\n" + "="*60) + + +def main(): + """Main validation function.""" + validator = JavaScriptValidator() + + success = validator.validate_all() + validator.report_results() + + return 0 if success else 1 + + +if __name__ == "__main__": + exit_code = main() + sys.exit(exit_code) \ No newline at end of file