#!/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)