Compare commits
4 Commits
v0.3.0
...
3a53e0aa58
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a53e0aa58 | |||
| 64d1606740 | |||
| 1022e2597f | |||
| 50170f75df |
11
Makefile
11
Makefile
@@ -83,6 +83,7 @@ help:
|
|||||||
@echo " status - Show git status for repo and submodules"
|
@echo " status - Show git status for repo and submodules"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
@echo " check-deps - Check dependency status"
|
@echo " check-deps - Check dependency status"
|
||||||
|
@echo " validate-js - Validate JavaScript syntax in templates"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Documentation:"
|
@echo "Documentation:"
|
||||||
@echo " update-digest - Update ProjectStatusDigest.md (requires Claude Code)"
|
@echo " update-digest - Update ProjectStatusDigest.md (requires Claude Code)"
|
||||||
@@ -1340,3 +1341,13 @@ cost-help:
|
|||||||
@echo "💰 Currency: Costs calculated in USD and EUR"
|
@echo "💰 Currency: Costs calculated in USD and EUR"
|
||||||
@echo "🤖 Model: Default claude-sonnet-4 pricing"
|
@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
|
||||||
|
|
||||||
|
|||||||
@@ -574,35 +574,35 @@ class DocumentManager:
|
|||||||
const sections = content.querySelectorAll('.markitect-section-editable');
|
const sections = content.querySelectorAll('.markitect-section-editable');
|
||||||
let reconstructed = '';
|
let reconstructed = '';
|
||||||
|
|
||||||
sections.forEach(section => {
|
sections.forEach(section => {{
|
||||||
const tagName = section.tagName.toLowerCase();
|
const tagName = section.tagName.toLowerCase();
|
||||||
const text = section.textContent.trim();
|
const text = section.textContent.trim();
|
||||||
|
|
||||||
if (tagName.startsWith('h')) {
|
if (tagName.startsWith('h')) {{
|
||||||
const level = parseInt(tagName.charAt(1));
|
const level = parseInt(tagName.charAt(1));
|
||||||
reconstructed += '#'.repeat(level) + ' ' + text + '\n\n';
|
reconstructed += '#'.repeat(level) + ' ' + text + '\\n\\n';
|
||||||
} else if (tagName === 'p') {
|
}} else if (tagName === 'p') {{
|
||||||
reconstructed += text + '\n\n';
|
reconstructed += text + '\\n\\n';
|
||||||
} else if (tagName === 'blockquote') {
|
}} else if (tagName === 'blockquote') {{
|
||||||
reconstructed += '> ' + text + '\n\n';
|
reconstructed += '> ' + text + '\\n\\n';
|
||||||
} else if (tagName === 'pre') {
|
}} else if (tagName === 'pre') {{
|
||||||
reconstructed += '```\n' + text + '\n```\n\n';
|
reconstructed += '```\\n' + text + '\\n```\\n\\n';
|
||||||
} else if (tagName === 'ul') {
|
}} else if (tagName === 'ul') {{
|
||||||
const items = section.querySelectorAll('li');
|
const items = section.querySelectorAll('li');
|
||||||
items.forEach(item => {
|
items.forEach(item => {{
|
||||||
reconstructed += '- ' + item.textContent.trim() + '\n';
|
reconstructed += '- ' + item.textContent.trim() + '\\n';
|
||||||
});
|
}});
|
||||||
reconstructed += '\n';
|
reconstructed += '\\n';
|
||||||
} else if (tagName === 'ol') {
|
}} else if (tagName === 'ol') {{
|
||||||
const items = section.querySelectorAll('li');
|
const items = section.querySelectorAll('li');
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {{
|
||||||
reconstructed += `${index + 1}. ` + item.textContent.trim() + '\n';
|
reconstructed += (index + 1) + '. ' + item.textContent.trim() + '\\n';
|
||||||
});
|
}});
|
||||||
reconstructed += '\n';
|
reconstructed += '\\n';
|
||||||
} else {
|
}} else {{
|
||||||
reconstructed += text + '\n\n';
|
reconstructed += text + '\\n\\n';
|
||||||
}
|
}}
|
||||||
});
|
}});
|
||||||
|
|
||||||
return reconstructed.trim();
|
return reconstructed.trim();
|
||||||
}
|
}
|
||||||
@@ -617,9 +617,82 @@ class DocumentManager:
|
|||||||
# Edit mode status and error reporting section
|
# Edit mode status and error reporting section
|
||||||
edit_mode_html = ""
|
edit_mode_html = ""
|
||||||
if edit_mode:
|
if edit_mode:
|
||||||
|
# Get version info for header
|
||||||
|
try:
|
||||||
|
import markitect
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Get base version
|
||||||
|
version = "0.3.0" # fallback
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version as get_version
|
||||||
|
version = get_version('markitect')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get git commit with timestamp and local changes info
|
||||||
|
git_info = ""
|
||||||
|
try:
|
||||||
|
repo_path = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
# Get commit hash and timestamp
|
||||||
|
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
|
||||||
|
capture_output=True, text=True, cwd=repo_path)
|
||||||
|
if result.returncode == 0:
|
||||||
|
commit_hash = result.stdout.strip()
|
||||||
|
|
||||||
|
# Get commit timestamp
|
||||||
|
timestamp_result = subprocess.run(['git', 'show', '-s', '--format=%ci', 'HEAD'],
|
||||||
|
capture_output=True, text=True, cwd=repo_path)
|
||||||
|
commit_time = ""
|
||||||
|
if timestamp_result.returncode == 0:
|
||||||
|
from datetime import datetime
|
||||||
|
# Parse git timestamp and format it nicely
|
||||||
|
git_time = timestamp_result.stdout.strip()
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(git_time.replace(' +', '+'))
|
||||||
|
commit_time = f" ({dt.strftime('%Y-%m-%d %H:%M')})"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
git_info = f"+{commit_hash}{commit_time}"
|
||||||
|
|
||||||
|
# Check for uncommitted changes
|
||||||
|
status_result = subprocess.run(['git', 'status', '--porcelain'],
|
||||||
|
capture_output=True, text=True, cwd=repo_path)
|
||||||
|
if status_result.returncode == 0 and status_result.stdout.strip():
|
||||||
|
# Get timestamp of most recent uncommitted change
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
latest_change = 0
|
||||||
|
for line in status_result.stdout.strip().split('\n'):
|
||||||
|
if line.strip():
|
||||||
|
# Extract filename (skip first 3 chars which are status indicators)
|
||||||
|
filename = line[3:].strip()
|
||||||
|
try:
|
||||||
|
file_path = repo_path / filename
|
||||||
|
if file_path.exists():
|
||||||
|
mtime = os.path.getmtime(file_path)
|
||||||
|
latest_change = max(latest_change, mtime)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if latest_change > 0:
|
||||||
|
change_dt = datetime.fromtimestamp(latest_change)
|
||||||
|
git_info += f" including local changes until {change_dt.strftime('%Y-%m-%d %H:%M')}"
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
version_info = f"{version}{git_info}"
|
||||||
|
except:
|
||||||
|
version_info = "0.3.0"
|
||||||
|
|
||||||
edit_mode_html = f"""
|
edit_mode_html = f"""
|
||||||
<div id="markitect-status" style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 12px; margin-bottom: 20px; font-family: monospace; font-size: 14px;">
|
<div id="markitect-status" style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 12px; margin-bottom: 20px; font-family: monospace; font-size: 14px;">
|
||||||
<div style="font-weight: bold; color: #1976d2;">📝 Markitect Edit Mode</div>
|
<div style="font-weight: bold; color: #1976d2;">📝 Markitect Edit Mode <span style="font-weight: normal; color: #666;">v{version_info}</span></div>
|
||||||
<div id="status-message" style="margin-top: 8px;">Loading edit capabilities...</div>
|
<div id="status-message" style="margin-top: 8px;">Loading edit capabilities...</div>
|
||||||
<div id="error-details" style="display: none; background: #ffebee; border: 1px solid #f44336; padding: 8px; margin-top: 8px; border-radius: 4px;">
|
<div id="error-details" style="display: none; background: #ffebee; border: 1px solid #f44336; padding: 8px; margin-top: 8px; border-radius: 4px;">
|
||||||
<div style="font-weight: bold; color: #c62828;">❌ Edit Mode Failed</div>
|
<div style="font-weight: bold; color: #c62828;">❌ Edit Mode Failed</div>
|
||||||
@@ -659,20 +732,56 @@ class DocumentManager:
|
|||||||
// Define editor class first (if in edit mode)
|
// Define editor class first (if in edit mode)
|
||||||
{editor_scripts if edit_mode else ''}
|
{editor_scripts if edit_mode else ''}
|
||||||
|
|
||||||
// Error reporting utility
|
// Enhanced error reporting utility
|
||||||
function reportEditModeError(errorMsg, technicalDetails) {{
|
function reportEditModeError(errorMsg, technicalDetails, errorType = 'error') {{
|
||||||
const statusDiv = document.getElementById('markitect-status');
|
const statusDiv = document.getElementById('markitect-status');
|
||||||
const errorDiv = document.getElementById('error-details');
|
const errorDiv = document.getElementById('error-details');
|
||||||
const errorText = document.getElementById('error-text');
|
const errorText = document.getElementById('error-text');
|
||||||
const statusMsg = document.getElementById('status-message');
|
const statusMsg = document.getElementById('status-message');
|
||||||
const browserInfo = document.getElementById('browser-info');
|
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 (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(' ');
|
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);
|
||||||
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
||||||
// Status update utility
|
// Status update utility
|
||||||
function updateStatus(message, isError = false) {{
|
function updateStatus(message, isError = false) {{
|
||||||
const statusMsg = document.getElementById('status-message');
|
const statusMsg = document.getElementById('status-message');
|
||||||
|
|||||||
@@ -1533,7 +1533,7 @@ def md_ingest_command(ctx, file_path):
|
|||||||
doc_manager = DocumentManager(config.get('db_manager'))
|
doc_manager = DocumentManager(config.get('db_manager'))
|
||||||
|
|
||||||
# Process the file
|
# Process the file
|
||||||
result = doc_manager.ingest_file(file_path)
|
result = doc_manager.ingest_file(Path(file_path))
|
||||||
|
|
||||||
if config.get('verbose', False):
|
if config.get('verbose', False):
|
||||||
click.echo(f"Processing results:")
|
click.echo(f"Processing results:")
|
||||||
@@ -2981,3 +2981,5 @@ class FilenameDecoder:
|
|||||||
def decode_batch(self, filenames):
|
def decode_batch(self, filenames):
|
||||||
"""Process multiple filenames in batch."""
|
"""Process multiple filenames in batch."""
|
||||||
return [self.decode(filename) for filename in filenames]
|
return [self.decode(filename) for filename in filenames]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
365
tests/test_issue_144_edit_mode_regression.py
Normal file
365
tests/test_issue_144_edit_mode_regression.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
# 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'<script>(.*?)</script>', 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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
html_content = doc_manager._generate_html_template(
|
||||||
|
title="Test",
|
||||||
|
markdown_content="# Test",
|
||||||
|
edit_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract JavaScript
|
||||||
|
js_match = re.search(r'<script>(.*?)</script>', 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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
html_content = doc_manager._generate_html_template(
|
||||||
|
title="Test",
|
||||||
|
markdown_content="# Test",
|
||||||
|
edit_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract JavaScript
|
||||||
|
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||||
|
js_content = js_match.group(1)
|
||||||
|
|
||||||
|
# Check for inconsistent brace patterns
|
||||||
|
inconsistent_patterns = [
|
||||||
|
r'(?<!})} else if.*{{', # Single brace followed by double (incorrect)
|
||||||
|
r'}} else if.*}(?!})', # Double brace followed by single closing (incorrect)
|
||||||
|
]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
html_content = doc_manager._generate_html_template(
|
||||||
|
title="Test",
|
||||||
|
markdown_content="# Test",
|
||||||
|
edit_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract JavaScript
|
||||||
|
js_match = re.search(r'<script>(.*?)</script>', 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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
html_content = doc_manager._generate_html_template(
|
||||||
|
title="Test",
|
||||||
|
markdown_content="# Test",
|
||||||
|
edit_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract JavaScript
|
||||||
|
js_match = re.search(r'<script>(.*?)</script>', 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
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
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'<script>(.*?)</script>', 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' in the JavaScript
|
||||||
|
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_save_functionality_javascript_presence(self):
|
||||||
|
"""Test that the save functionality JavaScript is properly included."""
|
||||||
|
from markitect.document_manager import DocumentManager
|
||||||
|
|
||||||
|
# Create a mock DocumentManager to avoid database dependency
|
||||||
|
class MockDatabaseManager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||||
|
doc_manager.database_manager = MockDatabaseManager()
|
||||||
|
|
||||||
|
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"])
|
||||||
283
tools/validate_js_syntax.py
Executable file
283
tools/validate_js_syntax.py
Executable file
@@ -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'<script[^>]*>(.*?)</script>', 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'<script>(.*?)</script>', 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)
|
||||||
Reference in New Issue
Block a user