feat: implement unified DocumentNavigator with lazy loading for all modes
- Add DocumentNavigator UI element for document navigation across viewing and editing modes - Implement lazy loading approach where control appears immediately but navigation content builds on-demand - Position controls on left side following UI convention for consistent navigation experience - Add scroll spy functionality for current section detection - Include responsive design with mobile auto-hide - Create comprehensive development guardrails to prevent JavaScript corruption - Add JavaScript validation tool for syntax error detection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
81
GUARDRAILS.md
Normal file
81
GUARDRAILS.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Development Guardrails
|
||||||
|
|
||||||
|
## JavaScript Code Principles
|
||||||
|
|
||||||
|
### 1. No Inline JavaScript in Python
|
||||||
|
**NEVER write JavaScript code directly from Python code**
|
||||||
|
|
||||||
|
❌ **Wrong:**
|
||||||
|
```python
|
||||||
|
script = f"""
|
||||||
|
function myFunction() {{
|
||||||
|
console.log("Hello {name}");
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Correct:**
|
||||||
|
```python
|
||||||
|
# Load from external files only
|
||||||
|
components = [
|
||||||
|
'js/core/section-manager.js',
|
||||||
|
'js/components/debug-panel.js',
|
||||||
|
'js/components/document-controls.js'
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Why This Rule Exists
|
||||||
|
- **Quoting Problems**: String escaping in Python corrupts JavaScript
|
||||||
|
- **Syntax Errors**: Template literals and complex JS break when embedded
|
||||||
|
- **Maintainability**: JS code should be in .js files for proper tooling
|
||||||
|
- **Architecture**: Follows the established modular component system
|
||||||
|
|
||||||
|
### 3. Proper Approach
|
||||||
|
1. Create separate `.js` files in `markitect/static/js/components/`
|
||||||
|
2. Load them via `_get_clean_editor_scripts()`
|
||||||
|
3. Wire up components in the initialization script only
|
||||||
|
|
||||||
|
## Testing and Validation
|
||||||
|
|
||||||
|
### 1. Always Validate Generated HTML
|
||||||
|
- Check that HTML files actually render content
|
||||||
|
- Validate JavaScript syntax before deployment
|
||||||
|
- Test both viewing and editing modes
|
||||||
|
|
||||||
|
### 2. Detect JavaScript Errors Programmatically
|
||||||
|
- Run syntax validation on generated JS
|
||||||
|
- Check for common error patterns
|
||||||
|
- Fail fast when JS is malformed
|
||||||
|
|
||||||
|
### 3. Manual Testing Backup
|
||||||
|
- If automated checks pass but functionality fails
|
||||||
|
- Open generated HTML in browser
|
||||||
|
- Check console for runtime errors
|
||||||
|
- Report specific error messages
|
||||||
|
|
||||||
|
## Architecture Principles
|
||||||
|
|
||||||
|
### 1. Separation of Concerns
|
||||||
|
- Python: File generation, template management
|
||||||
|
- JavaScript: UI components, interaction logic
|
||||||
|
- HTML: Structure and content only
|
||||||
|
|
||||||
|
### 2. Modular Component System
|
||||||
|
- Each UI component in separate file
|
||||||
|
- Lazy loading where appropriate
|
||||||
|
- Clear dependency management
|
||||||
|
|
||||||
|
### 3. Error Handling
|
||||||
|
- Graceful degradation when components fail
|
||||||
|
- Clear error messages for debugging
|
||||||
|
- Fallback modes when possible
|
||||||
|
|
||||||
|
## Breaking These Rules
|
||||||
|
|
||||||
|
If you find yourself writing JavaScript in Python strings:
|
||||||
|
1. **STOP** - Step back and reconsider
|
||||||
|
2. Create a proper component file instead
|
||||||
|
3. Use the existing component loading system
|
||||||
|
4. Add validation to catch the issue early
|
||||||
|
|
||||||
|
These guardrails exist because we've seen the problems when they're violated.
|
||||||
@@ -1176,6 +1176,149 @@ class CleanDocumentManager:
|
|||||||
}} catch (error) {{
|
}} catch (error) {{
|
||||||
console.error("Scroll indicators failed to initialize:", error);
|
console.error("Scroll indicators failed to initialize:", error);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// Step 4: Initialize DocumentNavigator (lazy loading for all modes)
|
||||||
|
try {{
|
||||||
|
const documentNavigator = {{
|
||||||
|
navElement: null,
|
||||||
|
isExpanded: false,
|
||||||
|
|
||||||
|
createControl: function() {{
|
||||||
|
console.log("📋 Creating DocumentNavigator control for view mode...");
|
||||||
|
|
||||||
|
this.navElement = document.createElement('nav');
|
||||||
|
this.navElement.className = 'document-navigator';
|
||||||
|
this.navElement.innerHTML = `
|
||||||
|
<button class="navigator-toggle" aria-label="Document Navigation">☰</button>
|
||||||
|
<div class="navigator-panel" style="display: none;">
|
||||||
|
<div class="navigator-header">
|
||||||
|
<h3>Contents</h3>
|
||||||
|
<button class="navigator-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="navigator-content">Loading...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Position on left side following UI convention
|
||||||
|
this.navElement.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 1px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
width: 40px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Style toggle button
|
||||||
|
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
|
||||||
|
toggleBtn.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Handle click to build navigation on-demand
|
||||||
|
toggleBtn.addEventListener('click', () => {{
|
||||||
|
console.log("📋 Navigator toggle clicked - building navigation...");
|
||||||
|
this.buildNavigation();
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Close button handler
|
||||||
|
const closeBtn = this.navElement.querySelector('.navigator-close');
|
||||||
|
closeBtn.addEventListener('click', () => {{
|
||||||
|
this.collapse();
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Responsive behavior
|
||||||
|
window.addEventListener('resize', () => {{
|
||||||
|
if (window.innerWidth <= 768) {{
|
||||||
|
this.navElement.style.display = 'none';
|
||||||
|
}} else {{
|
||||||
|
this.navElement.style.display = '';
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
document.body.appendChild(this.navElement);
|
||||||
|
|
||||||
|
// Hide on mobile
|
||||||
|
if (window.innerWidth <= 768) {{
|
||||||
|
this.navElement.style.display = 'none';
|
||||||
|
}}
|
||||||
|
|
||||||
|
console.log("📋 DocumentNavigator control created");
|
||||||
|
}},
|
||||||
|
|
||||||
|
buildNavigation: function() {{
|
||||||
|
const panel = this.navElement.querySelector('.navigator-panel');
|
||||||
|
const content = this.navElement.querySelector('.navigator-content');
|
||||||
|
|
||||||
|
// Build navigation content from current DOM
|
||||||
|
const headings = document.querySelectorAll('h1, h2, h3');
|
||||||
|
console.log("📋 Found headings for navigation:", headings.length);
|
||||||
|
|
||||||
|
if (headings.length === 0) {{
|
||||||
|
content.innerHTML = '<p style="padding: 1rem; color: #666;">No headings found</p>';
|
||||||
|
}} else {{
|
||||||
|
let navHtml = '';
|
||||||
|
headings.forEach((heading, index) => {{
|
||||||
|
if (!heading.id) {{
|
||||||
|
heading.id = `heading-${{index + 1}}`;
|
||||||
|
}}
|
||||||
|
const level = parseInt(heading.tagName.substring(1));
|
||||||
|
const indent = (level - 1) * 1;
|
||||||
|
navHtml += `
|
||||||
|
<a href="#${{heading.id}}"
|
||||||
|
style="display: block; padding: 0.5rem; margin-left: ${{indent}}rem;
|
||||||
|
text-decoration: none; color: #333; font-size: 0.9rem;
|
||||||
|
border-radius: 4px; cursor: pointer;"
|
||||||
|
onmouseover="this.style.backgroundColor='#f5f5f5'"
|
||||||
|
onmouseout="this.style.backgroundColor=''"
|
||||||
|
onclick="event.preventDefault(); document.getElementById('${{heading.id}}').scrollIntoView({{behavior: 'smooth'}}); if (window.innerWidth <= 768) setTimeout(() => documentNavigator.collapse(), 500);">
|
||||||
|
${{heading.textContent.trim()}}
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}});
|
||||||
|
content.innerHTML = navHtml;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Show panel
|
||||||
|
this.expand();
|
||||||
|
}},
|
||||||
|
|
||||||
|
expand: function() {{
|
||||||
|
this.isExpanded = true;
|
||||||
|
const panel = this.navElement.querySelector('.navigator-panel');
|
||||||
|
this.navElement.style.width = '280px';
|
||||||
|
panel.style.display = 'block';
|
||||||
|
}},
|
||||||
|
|
||||||
|
collapse: function() {{
|
||||||
|
this.isExpanded = false;
|
||||||
|
const panel = this.navElement.querySelector('.navigator-panel');
|
||||||
|
panel.style.display = 'none';
|
||||||
|
this.navElement.style.width = '40px';
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Initialize the DocumentNavigator control
|
||||||
|
documentNavigator.createControl();
|
||||||
|
|
||||||
|
// Make globally available for mobile collapse
|
||||||
|
window.documentNavigator = documentNavigator;
|
||||||
|
}} catch (error) {{
|
||||||
|
console.error("DocumentNavigator failed to initialize:", error);
|
||||||
|
}}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// Handle CDN loading errors
|
// Handle CDN loading errors
|
||||||
@@ -1237,6 +1380,127 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Create document controls
|
// Create document controls
|
||||||
documentControls.create();
|
documentControls.create();
|
||||||
|
|
||||||
|
// Create DocumentNavigator for edit mode (lazy loading)
|
||||||
|
const documentNavigator = {
|
||||||
|
navElement: null,
|
||||||
|
isExpanded: false,
|
||||||
|
|
||||||
|
createControl: function() {
|
||||||
|
console.log("📋 Creating DocumentNavigator control for edit mode...");
|
||||||
|
|
||||||
|
this.navElement = document.createElement('nav');
|
||||||
|
this.navElement.className = 'document-navigator edit-mode';
|
||||||
|
this.navElement.innerHTML = `
|
||||||
|
<button class="navigator-toggle" aria-label="Document Navigation">☰</button>
|
||||||
|
<div class="navigator-panel" style="display: none;">
|
||||||
|
<div class="navigator-header">
|
||||||
|
<h3>Contents</h3>
|
||||||
|
<button class="navigator-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="navigator-content">Loading...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Position on left side following UI convention
|
||||||
|
this.navElement.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 1001;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: 1px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
width: 40px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Style toggle button
|
||||||
|
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
|
||||||
|
toggleBtn.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Handle click to build navigation on-demand
|
||||||
|
toggleBtn.addEventListener('click', () => {
|
||||||
|
console.log("📋 Navigator toggle clicked - building navigation...");
|
||||||
|
this.buildNavigation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close button handler
|
||||||
|
const closeBtn = this.navElement.querySelector('.navigator-close');
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
this.collapse();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(this.navElement);
|
||||||
|
console.log("📋 DocumentNavigator control created");
|
||||||
|
},
|
||||||
|
|
||||||
|
buildNavigation: function() {
|
||||||
|
const panel = this.navElement.querySelector('.navigator-panel');
|
||||||
|
const content = this.navElement.querySelector('.navigator-content');
|
||||||
|
|
||||||
|
// Build navigation content from current DOM
|
||||||
|
const headings = document.querySelectorAll('h1, h2, h3');
|
||||||
|
console.log("📋 Found headings for navigation:", headings.length);
|
||||||
|
|
||||||
|
if (headings.length === 0) {
|
||||||
|
content.innerHTML = '<p style="padding: 1rem; color: #666;">No headings found</p>';
|
||||||
|
} else {
|
||||||
|
let navHtml = '';
|
||||||
|
headings.forEach((heading, index) => {
|
||||||
|
if (!heading.id) {
|
||||||
|
heading.id = `heading-${index + 1}`;
|
||||||
|
}
|
||||||
|
const level = parseInt(heading.tagName.substring(1));
|
||||||
|
const indent = (level - 1) * 1;
|
||||||
|
navHtml += `
|
||||||
|
<a href="#${heading.id}"
|
||||||
|
style="display: block; padding: 0.5rem; margin-left: ${indent}rem;
|
||||||
|
text-decoration: none; color: #333; font-size: 0.9rem;
|
||||||
|
border-radius: 4px; cursor: pointer;"
|
||||||
|
onmouseover="this.style.backgroundColor='#f5f5f5'"
|
||||||
|
onmouseout="this.style.backgroundColor=''"
|
||||||
|
onclick="event.preventDefault(); document.getElementById('${heading.id}').scrollIntoView({behavior: 'smooth'});">
|
||||||
|
${heading.textContent.trim()}
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
content.innerHTML = navHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show panel
|
||||||
|
this.expand();
|
||||||
|
},
|
||||||
|
|
||||||
|
expand: function() {
|
||||||
|
this.isExpanded = true;
|
||||||
|
const panel = this.navElement.querySelector('.navigator-panel');
|
||||||
|
this.navElement.style.width = '300px';
|
||||||
|
panel.style.display = 'block';
|
||||||
|
},
|
||||||
|
|
||||||
|
collapse: function() {
|
||||||
|
this.isExpanded = false;
|
||||||
|
const panel = this.navElement.querySelector('.navigator-panel');
|
||||||
|
panel.style.display = 'none';
|
||||||
|
this.navElement.style.width = '40px';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the DocumentNavigator control
|
||||||
|
documentNavigator.createControl();
|
||||||
|
|
||||||
// Wire up event handlers
|
// Wire up event handlers
|
||||||
documentControls.setEventHandlers({
|
documentControls.setEventHandlers({
|
||||||
'save-document': () => {
|
'save-document': () => {
|
||||||
|
|||||||
161
tools/validate_js.py
Executable file
161
tools/validate_js.py
Executable file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
JavaScript Validation Tool
|
||||||
|
|
||||||
|
Extracts JavaScript from HTML files and validates syntax.
|
||||||
|
Detects common issues like "Uncaught SyntaxError: unexpected token"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def extract_javascript_from_html(html_file):
|
||||||
|
"""Extract all JavaScript code from an HTML file."""
|
||||||
|
try:
|
||||||
|
content = Path(html_file).read_text(encoding='utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to read {html_file}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Find all <script> blocks
|
||||||
|
script_pattern = r'<script[^>]*>(.*?)</script>'
|
||||||
|
scripts = re.findall(script_pattern, content, re.DOTALL | re.IGNORECASE)
|
||||||
|
|
||||||
|
# Filter out empty scripts and external script tags
|
||||||
|
js_blocks = []
|
||||||
|
for script in scripts:
|
||||||
|
script = script.strip()
|
||||||
|
if script and not script.startswith('http'): # Skip external scripts
|
||||||
|
js_blocks.append(script)
|
||||||
|
|
||||||
|
return js_blocks
|
||||||
|
|
||||||
|
|
||||||
|
def validate_javascript_syntax(js_code):
|
||||||
|
"""Validate JavaScript syntax using Node.js."""
|
||||||
|
try:
|
||||||
|
# Create temporary JS file
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
||||||
|
f.write(js_code)
|
||||||
|
temp_file = f.name
|
||||||
|
|
||||||
|
# Try to parse with node --check
|
||||||
|
result = subprocess.run(
|
||||||
|
['node', '--check', temp_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
Path(temp_file).unlink()
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, "✓ Syntax OK"
|
||||||
|
else:
|
||||||
|
return False, f"❌ Syntax Error: {result.stderr.strip()}"
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Fallback: Try to detect obvious syntax errors
|
||||||
|
return validate_javascript_basic(js_code)
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"❌ Validation failed: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_javascript_basic(js_code):
|
||||||
|
"""Basic JavaScript syntax validation without Node.js."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Check for common syntax issues
|
||||||
|
if js_code.count('{') != js_code.count('}'):
|
||||||
|
errors.append("Mismatched curly braces")
|
||||||
|
|
||||||
|
if js_code.count('(') != js_code.count(')'):
|
||||||
|
errors.append("Mismatched parentheses")
|
||||||
|
|
||||||
|
if js_code.count('[') != js_code.count(']'):
|
||||||
|
errors.append("Mismatched square brackets")
|
||||||
|
|
||||||
|
# Check for unescaped quotes in strings
|
||||||
|
if re.search(r'["\']\s*["\']\s*["\']\s*["\']', js_code):
|
||||||
|
errors.append("Possible quote escaping issue")
|
||||||
|
|
||||||
|
# Check for Python-style string formatting leftover
|
||||||
|
if '${' in js_code and '"}' in js_code:
|
||||||
|
errors.append("Possible Python string template leftover")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return False, f"❌ Potential issues: {', '.join(errors)}"
|
||||||
|
else:
|
||||||
|
return True, "✓ Basic validation passed (Node.js not available)"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_html_js(html_file):
|
||||||
|
"""Validate all JavaScript in an HTML file."""
|
||||||
|
print(f"🔍 Validating JavaScript in: {html_file}")
|
||||||
|
|
||||||
|
js_blocks = extract_javascript_from_html(html_file)
|
||||||
|
|
||||||
|
if not js_blocks:
|
||||||
|
print("⚠️ No JavaScript found in HTML file")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(f"📝 Found {len(js_blocks)} JavaScript blocks")
|
||||||
|
|
||||||
|
all_valid = True
|
||||||
|
for i, js_block in enumerate(js_blocks, 1):
|
||||||
|
print(f"\n--- Script Block {i} ({len(js_block)} chars) ---")
|
||||||
|
|
||||||
|
# Show first few lines for context
|
||||||
|
lines = js_block.split('\n')[:3]
|
||||||
|
preview = '\n'.join(lines)
|
||||||
|
if len(lines) < len(js_block.split('\n')):
|
||||||
|
preview += "\n..."
|
||||||
|
print(f"Preview:\n{preview}")
|
||||||
|
|
||||||
|
is_valid, message = validate_javascript_syntax(js_block)
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
all_valid = False
|
||||||
|
# Show more context around potential error
|
||||||
|
if 'line' in message.lower():
|
||||||
|
try:
|
||||||
|
line_num = re.search(r'line (\d+)', message, re.IGNORECASE)
|
||||||
|
if line_num:
|
||||||
|
line_no = int(line_num.group(1))
|
||||||
|
lines = js_block.split('\n')
|
||||||
|
start = max(0, line_no - 3)
|
||||||
|
end = min(len(lines), line_no + 2)
|
||||||
|
print("\nContext around error:")
|
||||||
|
for j in range(start, end):
|
||||||
|
marker = ">>> " if j == line_no - 1 else " "
|
||||||
|
print(f"{marker}{j+1:3}: {lines[j]}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"\n{'✅' if all_valid else '❌'} Overall result: {'All JavaScript is valid' if all_valid else 'JavaScript validation failed'}")
|
||||||
|
return all_valid
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python validate_js.py <html_file>")
|
||||||
|
print("Example: python validate_js.py /tmp/test-edit.html")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
html_file = sys.argv[1]
|
||||||
|
|
||||||
|
if not Path(html_file).exists():
|
||||||
|
print(f"❌ File not found: {html_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
success = validate_html_js(html_file)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user