- Enhanced dark theme with brighter link colors (#79c0ff, #a5d6ff) - Added corresponding light theme link colors (#0969da, #0550ae) - Fixed template parsing bug where 'light' theme was falling back to 'basic' - All theme system tests continue to pass 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1609 lines
58 KiB
Python
1609 lines
58 KiB
Python
"""
|
|
Clean Document Manager - Simplified version with only clean editor support
|
|
"""
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
|
|
|
|
class CleanDocumentManager:
|
|
"""
|
|
Simplified document manager that only supports the clean editor implementation.
|
|
All legacy code has been removed for clarity and maintainability.
|
|
"""
|
|
|
|
def __init__(self, db_manager=None):
|
|
self.db_manager = db_manager
|
|
|
|
def render_file(self, input_file: str, output_file: str, template: str = None, css: str = None,
|
|
edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True) -> Dict[str, Any]:
|
|
"""
|
|
Render a markdown file to HTML with optional clean editing capabilities.
|
|
"""
|
|
input_path = Path(input_file)
|
|
output_path = Path(output_file)
|
|
|
|
if not input_path.exists():
|
|
raise FileNotFoundError(f"Input file not found: {input_file}")
|
|
|
|
# Read markdown content
|
|
markdown_content = input_path.read_text(encoding='utf-8')
|
|
|
|
# Extract title from markdown (first h1 heading)
|
|
title = self._extract_title_from_markdown(markdown_content)
|
|
|
|
# Get original filename without extension
|
|
original_filename = input_path.stem
|
|
|
|
# Get version information
|
|
version_info = self._get_version_info()
|
|
|
|
# Generate HTML content
|
|
html_content = self._generate_html_template(
|
|
markdown_content=markdown_content,
|
|
title=title,
|
|
css=css,
|
|
template=template,
|
|
edit_mode=edit_mode,
|
|
editor_theme=editor_theme,
|
|
keyboard_shortcuts=keyboard_shortcuts,
|
|
original_filename=original_filename,
|
|
version_info=version_info
|
|
)
|
|
|
|
# Write HTML file
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(html_content, encoding='utf-8')
|
|
|
|
return {
|
|
'success': True,
|
|
'input_file': str(input_path),
|
|
'output_file': str(output_path),
|
|
'edit_mode': edit_mode,
|
|
'editor_theme': editor_theme
|
|
}
|
|
|
|
def _extract_title_from_markdown(self, markdown_content: str) -> str:
|
|
"""Extract title from first h1 heading in markdown."""
|
|
match = re.search(r'^#\s+(.+)', markdown_content, re.MULTILINE)
|
|
if match:
|
|
return match.group(1).strip()
|
|
return "Markdown Document"
|
|
|
|
def _get_version_info(self) -> dict:
|
|
"""Get repository name and version information."""
|
|
version_info = {
|
|
'repo_name': 'Markitect',
|
|
'version': '0.3.0',
|
|
'git_info': ''
|
|
}
|
|
|
|
try:
|
|
# Try to get version from package metadata
|
|
from importlib.metadata import version as get_version
|
|
version_info['version'] = get_version('markitect')
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
# Try to get git information
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
# Get git commit hash and status
|
|
try:
|
|
git_hash = subprocess.check_output(
|
|
['git', 'rev-parse', '--short', 'HEAD'],
|
|
cwd=Path(__file__).parent,
|
|
stderr=subprocess.DEVNULL
|
|
).decode().strip()
|
|
|
|
# Check if there are uncommitted changes
|
|
try:
|
|
subprocess.check_output(
|
|
['git', 'diff-index', '--quiet', 'HEAD', '--'],
|
|
cwd=Path(__file__).parent,
|
|
stderr=subprocess.DEVNULL
|
|
)
|
|
git_status = ''
|
|
except subprocess.CalledProcessError:
|
|
git_status = '-modified'
|
|
|
|
version_info['git_info'] = f" (git:{git_hash}{git_status})"
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
return version_info
|
|
|
|
def _get_template_css(self, template: str = None) -> str:
|
|
"""Generate layered theme CSS styles."""
|
|
# Import layered theme functions
|
|
from markitect.plugins.builtin.markdown_commands import (
|
|
parse_theme_string, combine_theme_properties, TEMPLATE_STYLES
|
|
)
|
|
|
|
# Handle layered themes or fall back to legacy
|
|
if template and ',' in template:
|
|
# New layered theme system
|
|
theme_list = parse_theme_string(template)
|
|
combined_props = combine_theme_properties(theme_list)
|
|
return self._generate_layered_css(combined_props)
|
|
else:
|
|
# Legacy single theme or fallback
|
|
if not template or template not in TEMPLATE_STYLES:
|
|
# Use default layered themes or the specified theme
|
|
theme_list = parse_theme_string(template or 'basic')
|
|
combined_props = combine_theme_properties(theme_list)
|
|
return self._generate_layered_css(combined_props)
|
|
else:
|
|
# Legacy theme - convert to layered
|
|
theme_list = parse_theme_string(template)
|
|
combined_props = combine_theme_properties(theme_list)
|
|
return self._generate_layered_css(combined_props)
|
|
|
|
def _generate_layered_css(self, properties: dict) -> str:
|
|
"""Generate CSS from combined theme properties."""
|
|
|
|
# Set defaults for missing properties (properties override defaults)
|
|
defaults = {
|
|
'body_background': '#ffffff',
|
|
'body_color': '#333333',
|
|
'font_family': '-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif',
|
|
'max_width': '800px',
|
|
'heading_color': '#333333', # Use same as body color by default
|
|
'heading_style': 'simple',
|
|
'text_align': 'left',
|
|
'code_background': '#f6f8fa',
|
|
'code_color': '#333333',
|
|
'border_color': '#d0d7de',
|
|
'blockquote_border': '#dfe2e5',
|
|
'blockquote_color': '#6a737d',
|
|
'table_border': '#d0d7de',
|
|
'table_header_bg': '#f6f8fa',
|
|
'accent_color': None,
|
|
'secondary_color': None
|
|
}
|
|
|
|
# Merge defaults first, then override with theme properties
|
|
props = {**defaults, **properties}
|
|
|
|
# Base CSS
|
|
base_css = f"""
|
|
body {{
|
|
font-family: {props['font_family']};
|
|
max-width: {props['max_width']};
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
line-height: 1.6;
|
|
color: {props['body_color']};
|
|
background-color: {props['body_background']};
|
|
}}
|
|
#markdown-content {{
|
|
min-height: 200px;
|
|
}}"""
|
|
|
|
# Heading styles
|
|
heading_css = ""
|
|
if props['heading_style'] == 'underlined':
|
|
heading_css = f"""
|
|
h1, h2, h3, h4, h5, h6 {{
|
|
color: {props['heading_color']};
|
|
border-bottom: 1px solid {props['border_color']};
|
|
padding-bottom: 0.3em;
|
|
}}"""
|
|
elif props['heading_style'] == 'centered':
|
|
heading_css = f"""
|
|
h1, h2, h3, h4, h5, h6 {{
|
|
color: {props['heading_color']};
|
|
margin-top: 2rem;
|
|
margin-bottom: 1rem;
|
|
}}
|
|
h1 {{
|
|
text-align: center;
|
|
font-size: 2.2em;
|
|
border-bottom: 2px solid {props['heading_color']};
|
|
padding-bottom: 0.5rem;
|
|
}}"""
|
|
else: # simple
|
|
heading_css = f"""
|
|
h1, h2, h3, h4, h5, h6 {{
|
|
color: {props['heading_color']};
|
|
}}"""
|
|
|
|
# Text alignment
|
|
text_css = ""
|
|
if props['text_align'] == 'justify':
|
|
text_css = """
|
|
p {
|
|
text-align: justify;
|
|
margin-bottom: 1.2rem;
|
|
}"""
|
|
|
|
# Element styling
|
|
element_css = f"""
|
|
pre {{
|
|
background-color: {props['code_background']};
|
|
color: {props['code_color']};
|
|
padding: 1rem;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
border: 1px solid {props['border_color']};
|
|
}}
|
|
code {{
|
|
background-color: {props['code_background']};
|
|
color: {props['code_color']};
|
|
padding: 0.2em 0.4em;
|
|
border-radius: 3px;
|
|
font-size: 0.9em;
|
|
}}
|
|
pre code {{
|
|
background: none;
|
|
padding: 0;
|
|
}}
|
|
blockquote {{
|
|
border-left: 4px solid {props['blockquote_border']};
|
|
margin: 0;
|
|
padding-left: 1rem;
|
|
color: {props['blockquote_color']};
|
|
}}
|
|
table {{
|
|
font-size: 0.85em;
|
|
border-collapse: collapse;
|
|
margin: 1rem 0;
|
|
width: 100%;
|
|
border: 1px solid {props['table_border']};
|
|
}}
|
|
th, td {{
|
|
font-size: inherit;
|
|
border: 1px solid {props['table_border']};
|
|
padding: 0.5rem;
|
|
text-align: left;
|
|
}}
|
|
th {{
|
|
background-color: {props['table_header_bg']};
|
|
font-weight: 600;
|
|
}}"""
|
|
|
|
# Link styling
|
|
link_css = ""
|
|
if props.get('link_color'):
|
|
link_css = f"""
|
|
a {{
|
|
color: {props['link_color']};
|
|
text-decoration: underline;
|
|
}}"""
|
|
if props.get('link_hover_color'):
|
|
link_css += f"""
|
|
a:hover {{
|
|
color: {props['link_hover_color']};
|
|
}}"""
|
|
else:
|
|
link_css += """
|
|
a:hover {
|
|
opacity: 0.8;
|
|
}"""
|
|
|
|
# Branding accents (if specified and no link_color already set)
|
|
accent_css = ""
|
|
if props.get('accent_color') and not props.get('link_color'):
|
|
accent_css = f"""
|
|
a {{
|
|
color: {props['accent_color']};
|
|
}}
|
|
a:hover {{
|
|
opacity: 0.8;
|
|
}}"""
|
|
|
|
return f"<style>{base_css}{heading_css}{text_css}{element_css}{link_css}{accent_css}</style>"
|
|
|
|
def _get_legacy_template_css(self, template: str) -> str:
|
|
"""Legacy CSS generation - kept for backward compatibility."""
|
|
# Import template styles
|
|
from markitect.plugins.builtin.markdown_commands import TEMPLATE_STYLES
|
|
|
|
# Use basic as default if no template specified
|
|
if not template or template not in TEMPLATE_STYLES:
|
|
template = 'basic'
|
|
|
|
style_config = TEMPLATE_STYLES[template]
|
|
|
|
# Base CSS that's common to all templates
|
|
base_css = f"""
|
|
body {{
|
|
font-family: {style_config['font_family']};
|
|
max-width: {style_config['max_width']};
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
line-height: 1.6;
|
|
color: {style_config['body_color']};
|
|
}}
|
|
#markdown-content {{
|
|
min-height: 200px;
|
|
}}"""
|
|
|
|
# Convert legacy template config to layered format
|
|
legacy_config = TEMPLATE_STYLES[template]
|
|
layered_props = {
|
|
'font_family': legacy_config['font_family'],
|
|
'max_width': legacy_config['max_width'],
|
|
'body_color': legacy_config['body_color'],
|
|
}
|
|
return self._generate_layered_css(layered_props)
|
|
|
|
def _generate_html_template(self, markdown_content: str, title: str, css: str = None, template: str = None,
|
|
edit_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None) -> str:
|
|
"""Generate clean HTML template."""
|
|
|
|
# Escape the markdown content for JavaScript
|
|
js_markdown_content = json.dumps(markdown_content)
|
|
|
|
# Handle CSS styles
|
|
css_content = ""
|
|
if css:
|
|
try:
|
|
css_path = Path(css)
|
|
if css_path.exists():
|
|
css_file_content = css_path.read_text(encoding='utf-8')
|
|
css_content = f"<style>\n{css_file_content}\n</style>"
|
|
else:
|
|
css_content = f'<link rel="stylesheet" href="{css}">'
|
|
except Exception:
|
|
css_content = f'<link rel="stylesheet" href="{css}">'
|
|
|
|
# Generate template-specific CSS
|
|
default_css = self._get_template_css(template)
|
|
|
|
# Load clean editor JavaScript files
|
|
editor_scripts = ""
|
|
editor_config = ""
|
|
body_classes = ""
|
|
|
|
if edit_mode:
|
|
body_classes = ' class="markitect-edit-mode"'
|
|
|
|
# Configuration for clean editor
|
|
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.3.0"
|
|
editor_config = f"""
|
|
const MARKITECT_EDIT_MODE = true;
|
|
const MARKITECT_EDITOR_CONFIG = {{
|
|
theme: '{editor_theme}',
|
|
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
|
|
autosave: false,
|
|
sections: true,
|
|
originalFilename: '{original_filename}',
|
|
version: '{version_str}',
|
|
repoName: '{version_info['repo_name'] if version_info else 'Markitect'}'
|
|
}};
|
|
|
|
// Make config available globally
|
|
window.editorConfig = MARKITECT_EDITOR_CONFIG;"""
|
|
|
|
# Load clean editor architecture
|
|
editor_scripts = self._get_clean_editor_scripts()
|
|
|
|
# Generate the complete HTML template
|
|
html_template = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{title}</title>
|
|
{css_content}
|
|
{default_css}
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
|
onload="window.markitectMarkedLoaded = true"
|
|
onerror="window.markitectMarkedError = true"></script>
|
|
</head>
|
|
<body{body_classes}>
|
|
|
|
<div id="markdown-content"></div>
|
|
|
|
<script>
|
|
const markdownContent = {js_markdown_content};
|
|
{editor_config}
|
|
|
|
{editor_scripts}
|
|
|
|
// Always render content first (graceful degradation)
|
|
document.addEventListener('DOMContentLoaded', function() {{
|
|
console.log("Rendering content...");
|
|
|
|
const contentDiv = document.getElementById('markdown-content');
|
|
|
|
// Step 1: Ensure content is always displayed
|
|
if (contentDiv) {{
|
|
if (typeof marked !== 'undefined') {{
|
|
try {{
|
|
contentDiv.innerHTML = marked.parse(markdownContent);
|
|
console.log("✓ Content rendered successfully");
|
|
console.log('✓ Markdown rendered successfully');
|
|
}} catch (error) {{
|
|
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
|
|
console.error("Content rendered with errors");
|
|
console.error("Markdown parsing failed:", error.message);
|
|
}}
|
|
}} else {{
|
|
// Fallback: display raw markdown with basic formatting
|
|
const fallbackHtml = markdownContent
|
|
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
|
.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>')
|
|
.replace(/\\*(.*?)\\*/g, '<em>$1</em>')
|
|
.replace(/^- (.*$)/gim, '<li>$1</li>')
|
|
.replace(/\\n\\n/g, '<br><br>')
|
|
.replace(/\\n/g, '<br>');
|
|
contentDiv.innerHTML = '<div style="white-space: pre-wrap;">' + fallbackHtml + '</div>';
|
|
console.warn("Content rendered with fallback parser");
|
|
console.warn("CDN library failed to load - using basic fallback rendering");
|
|
}}
|
|
}}
|
|
|
|
// Step 2: Initialize edit capabilities if enabled
|
|
if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) {{
|
|
console.log("Initializing clean edit capabilities...");
|
|
try {{
|
|
console.log("Creating clean editor instance...");
|
|
initializeCleanEditor();
|
|
console.log("✓ Clean edit mode active - click any section to edit");
|
|
}} catch (error) {{
|
|
console.error("Clean edit mode failed to initialize:", error);
|
|
}}
|
|
}}
|
|
}});
|
|
|
|
// Handle CDN loading errors
|
|
window.addEventListener('load', function() {{
|
|
if (window.markitectMarkedError) {{
|
|
console.error("CDN library failed to load - network or firewall blocking marked.js");
|
|
}}
|
|
}});
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html_template
|
|
|
|
def _get_clean_editor_scripts(self) -> str:
|
|
"""Get the complete clean editor JavaScript code."""
|
|
return """
|
|
// Clean Editor Architecture
|
|
/**
|
|
* Test-Driven Section Editor Implementation
|
|
*
|
|
* A clean, object-oriented approach to handling section editing
|
|
* that can be tested independently of the DOM.
|
|
*/
|
|
|
|
// Enums for clear state management
|
|
const EditState = Object.freeze({
|
|
ORIGINAL: 'original',
|
|
EDITING: 'editing',
|
|
MODIFIED: 'modified',
|
|
SAVED: 'saved'
|
|
});
|
|
|
|
const SectionType = Object.freeze({
|
|
HEADING: 'heading',
|
|
PARAGRAPH: 'paragraph',
|
|
LIST: 'list',
|
|
CODE: 'code',
|
|
BLOCKQUOTE: 'blockquote'
|
|
});
|
|
|
|
/**
|
|
* Section class - Core business logic for a single editable section
|
|
*/
|
|
class Section {
|
|
constructor(id, originalMarkdown, sectionType = SectionType.PARAGRAPH) {
|
|
this.id = id;
|
|
this.originalMarkdown = originalMarkdown;
|
|
this.currentMarkdown = originalMarkdown;
|
|
this.editingMarkdown = null;
|
|
this.pendingMarkdown = null;
|
|
this.sectionType = sectionType;
|
|
this.state = EditState.ORIGINAL;
|
|
this.domElement = null;
|
|
this.lastSaved = null;
|
|
this.created = new Date();
|
|
}
|
|
|
|
startEdit() {
|
|
if (this.state === EditState.EDITING) {
|
|
throw new Error(`Section ${this.id} is already being edited`);
|
|
}
|
|
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
|
|
this.state = EditState.EDITING;
|
|
return this.editingMarkdown;
|
|
}
|
|
|
|
updateContent(markdown) {
|
|
if (this.state !== EditState.EDITING) {
|
|
throw new Error(`Section ${this.id} is not in editing state`);
|
|
}
|
|
this.editingMarkdown = markdown;
|
|
}
|
|
|
|
acceptChanges() {
|
|
if (this.state !== EditState.EDITING) {
|
|
throw new Error(`Section ${this.id} is not in editing state`);
|
|
}
|
|
this.currentMarkdown = this.editingMarkdown;
|
|
this.editingMarkdown = null;
|
|
this.pendingMarkdown = null;
|
|
this.state = EditState.SAVED;
|
|
this.lastSaved = new Date();
|
|
return this.currentMarkdown;
|
|
}
|
|
|
|
cancelChanges() {
|
|
if (this.state !== EditState.EDITING) {
|
|
throw new Error(`Section ${this.id} is not in editing state`);
|
|
}
|
|
this.editingMarkdown = null;
|
|
if (this.pendingMarkdown !== null) {
|
|
this.state = EditState.MODIFIED;
|
|
return this.pendingMarkdown;
|
|
} else if (this.lastSaved !== null) {
|
|
this.state = EditState.SAVED;
|
|
return this.currentMarkdown;
|
|
} else {
|
|
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
|
return this.currentMarkdown;
|
|
}
|
|
}
|
|
|
|
resetToOriginal() {
|
|
this.currentMarkdown = this.originalMarkdown;
|
|
this.editingMarkdown = null;
|
|
this.pendingMarkdown = null;
|
|
this.lastSaved = null;
|
|
this.state = EditState.ORIGINAL;
|
|
return this.originalMarkdown;
|
|
}
|
|
|
|
stopEditing() {
|
|
if (this.state !== EditState.EDITING) {
|
|
return this.state;
|
|
}
|
|
|
|
// If we have editing changes that differ from current content, preserve them as pending
|
|
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
|
|
this.pendingMarkdown = this.editingMarkdown;
|
|
this.state = EditState.MODIFIED; // Has pending changes
|
|
} else {
|
|
// No changes made during this edit session
|
|
this.pendingMarkdown = null;
|
|
if (this.lastSaved !== null) {
|
|
this.state = EditState.SAVED;
|
|
} else {
|
|
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
|
}
|
|
}
|
|
|
|
this.editingMarkdown = null;
|
|
return this.state;
|
|
}
|
|
|
|
hasChanges() {
|
|
return this.currentMarkdown !== this.originalMarkdown;
|
|
}
|
|
|
|
isEditing() {
|
|
return this.state === EditState.EDITING;
|
|
}
|
|
|
|
getStatus() {
|
|
return {
|
|
id: this.id,
|
|
state: this.state,
|
|
hasChanges: this.hasChanges(),
|
|
isEditing: this.isEditing(),
|
|
contentLength: this.currentMarkdown.length,
|
|
lastSaved: this.lastSaved,
|
|
sectionType: this.sectionType
|
|
};
|
|
}
|
|
|
|
static generateId(content, position) {
|
|
const str = content.substring(0, 100) + position.toString();
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
const char = str.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + char;
|
|
hash = hash & hash;
|
|
}
|
|
return `section_${Math.abs(hash)}_${position}`;
|
|
}
|
|
|
|
static detectType(markdown) {
|
|
const trimmed = markdown.trim();
|
|
if (trimmed.startsWith('#')) return SectionType.HEADING;
|
|
if (trimmed.startsWith('```')) return SectionType.CODE;
|
|
if (trimmed.startsWith('>')) return SectionType.BLOCKQUOTE;
|
|
if (trimmed.startsWith('-') || trimmed.startsWith('*') || /^\\d+\\./.test(trimmed)) {
|
|
return SectionType.LIST;
|
|
}
|
|
return SectionType.PARAGRAPH;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SectionManager class - Manages the collection of sections
|
|
*/
|
|
class SectionManager {
|
|
constructor() {
|
|
this.sections = new Map();
|
|
// Note: Removed single editingSection tracking to allow multiple concurrent edits
|
|
this.listeners = new Map();
|
|
}
|
|
|
|
on(event, callback) {
|
|
if (!this.listeners.has(event)) {
|
|
this.listeners.set(event, []);
|
|
}
|
|
this.listeners.get(event).push(callback);
|
|
}
|
|
|
|
emit(event, data) {
|
|
if (this.listeners.has(event)) {
|
|
this.listeners.get(event).forEach(callback => callback(data));
|
|
}
|
|
}
|
|
|
|
createSectionsFromMarkdown(markdownContent) {
|
|
const lines = markdownContent.split('\\n');
|
|
const sections = [];
|
|
let currentSection = '';
|
|
let position = 0;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const isHeading = /^#{1,6}\\s/.test(line);
|
|
const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
|
|
const isNewSection = isHeading || isNewParagraph;
|
|
|
|
if (isNewSection && currentSection.trim()) {
|
|
const sectionId = Section.generateId(currentSection, position);
|
|
const sectionType = Section.detectType(currentSection);
|
|
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
|
sections.push(section);
|
|
this.sections.set(sectionId, section);
|
|
position++;
|
|
currentSection = line;
|
|
} else {
|
|
if (currentSection) currentSection += '\\n';
|
|
currentSection += line;
|
|
}
|
|
}
|
|
|
|
if (currentSection.trim()) {
|
|
const sectionId = Section.generateId(currentSection, position);
|
|
const sectionType = Section.detectType(currentSection);
|
|
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
|
sections.push(section);
|
|
this.sections.set(sectionId, section);
|
|
}
|
|
|
|
this.emit('sections-created', { sections, count: sections.length });
|
|
return sections;
|
|
}
|
|
|
|
startEditing(sectionId) {
|
|
const section = this.sections.get(sectionId);
|
|
if (!section) {
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
}
|
|
|
|
// Check if section is already being edited
|
|
if (section.isEditing()) {
|
|
console.log('Section already in editing state:', sectionId);
|
|
return section.editingMarkdown;
|
|
}
|
|
|
|
const content = section.startEdit();
|
|
// Note: No longer tracking single editingSection - allowing multiple
|
|
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
|
return content;
|
|
}
|
|
|
|
updateContent(sectionId, markdown) {
|
|
const section = this.sections.get(sectionId);
|
|
if (!section) {
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
}
|
|
section.updateContent(markdown);
|
|
this.emit('content-updated', { sectionId, markdown, section: section.getStatus() });
|
|
}
|
|
|
|
acceptChanges(sectionId) {
|
|
const section = this.sections.get(sectionId);
|
|
if (!section) {
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
}
|
|
|
|
// Check if the edited content contains new headings that would create splits
|
|
const newContent = section.editingMarkdown;
|
|
const originalContent = section.originalMarkdown;
|
|
const shouldSplit = this.checkForSectionSplits(newContent, originalContent);
|
|
|
|
if (shouldSplit) {
|
|
// Handle section splitting
|
|
this.handleSectionSplit(sectionId, newContent);
|
|
} else {
|
|
// Normal accept without splitting
|
|
const content = section.acceptChanges();
|
|
// Note: No longer tracking single editingSection
|
|
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
|
|
}
|
|
|
|
return section.currentMarkdown;
|
|
}
|
|
|
|
checkForSectionSplits(content, originalContent) {
|
|
if (!content) return false;
|
|
|
|
// Split by lines and check for headings
|
|
const lines = content.split('\\n');
|
|
const originalLines = originalContent ? originalContent.split('\\n') : [];
|
|
|
|
let newHeadingCount = 0;
|
|
let originalHeadingCount = 0;
|
|
|
|
// Count headings in new content
|
|
for (const line of lines) {
|
|
if (/^#{1,6}\\s/.test(line.trim())) {
|
|
newHeadingCount++;
|
|
}
|
|
}
|
|
|
|
// Count headings in original content
|
|
for (const line of originalLines) {
|
|
if (/^#{1,6}\\s/.test(line.trim())) {
|
|
originalHeadingCount++;
|
|
}
|
|
}
|
|
|
|
// Split if:
|
|
// 1. We have multiple headings now, OR
|
|
// 2. We added headings where there were none before, OR
|
|
// 3. We have more headings than we started with
|
|
return newHeadingCount > 1 ||
|
|
(originalHeadingCount === 0 && newHeadingCount > 0) ||
|
|
newHeadingCount > originalHeadingCount;
|
|
}
|
|
|
|
handleSectionSplit(originalSectionId, content) {
|
|
console.log('Splitting section:', originalSectionId);
|
|
|
|
const originalSection = this.sections.get(originalSectionId);
|
|
if (!originalSection) return;
|
|
|
|
// Accept the current changes first
|
|
originalSection.acceptChanges();
|
|
|
|
// Split the content into new sections
|
|
const newSections = this.createSectionsFromContent(content, originalSectionId);
|
|
|
|
// Get all sections as an ordered array to maintain document order
|
|
const allSectionsArray = Array.from(this.sections.values());
|
|
const originalIndex = allSectionsArray.findIndex(s => s.id === originalSectionId);
|
|
|
|
// Clear the sections map and rebuild it with proper order
|
|
this.sections.clear();
|
|
|
|
// Add sections before the original
|
|
for (let i = 0; i < originalIndex; i++) {
|
|
const section = allSectionsArray[i];
|
|
this.sections.set(section.id, section);
|
|
}
|
|
|
|
// Add the new split sections
|
|
newSections.forEach(section => {
|
|
this.sections.set(section.id, section);
|
|
});
|
|
|
|
// Add sections after the original
|
|
for (let i = originalIndex + 1; i < allSectionsArray.length; i++) {
|
|
const section = allSectionsArray[i];
|
|
this.sections.set(section.id, section);
|
|
}
|
|
|
|
// Note: No longer tracking single editingSection
|
|
|
|
// Emit event to trigger UI re-render
|
|
this.emit('section-split', {
|
|
originalSectionId,
|
|
newSections: newSections.map(s => s.getStatus()),
|
|
allSections: Array.from(this.sections.values())
|
|
});
|
|
}
|
|
|
|
createSectionsFromContent(content, baseSectionId) {
|
|
const lines = content.split('\\n');
|
|
const sections = [];
|
|
let currentSection = '';
|
|
let position = 0;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const isHeading = /^#{1,6}\\s/.test(line.trim());
|
|
|
|
if (isHeading) {
|
|
// When we encounter a heading, complete any previous section
|
|
if (currentSection.trim()) {
|
|
const sectionId = `${baseSectionId}_split_${position}`;
|
|
const sectionType = Section.detectType(currentSection);
|
|
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
|
sections.push(section);
|
|
position++;
|
|
}
|
|
// Start new section with this heading
|
|
currentSection = line;
|
|
} else {
|
|
// Add content to current section
|
|
if (currentSection) currentSection += '\\n';
|
|
currentSection += line;
|
|
}
|
|
}
|
|
|
|
// Add the final section if it has content
|
|
if (currentSection.trim()) {
|
|
const sectionId = `${baseSectionId}_split_${position}`;
|
|
const sectionType = Section.detectType(currentSection);
|
|
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
|
sections.push(section);
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
cancelChanges(sectionId) {
|
|
const section = this.sections.get(sectionId);
|
|
if (!section) {
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
}
|
|
const content = section.cancelChanges();
|
|
// Note: No longer tracking single editingSection
|
|
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
|
|
return content;
|
|
}
|
|
|
|
resetToOriginal(sectionId) {
|
|
const section = this.sections.get(sectionId);
|
|
if (!section) {
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
}
|
|
const content = section.resetToOriginal();
|
|
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
|
|
return content;
|
|
}
|
|
|
|
stopEditing(sectionId) {
|
|
const section = this.sections.get(sectionId);
|
|
if (!section) {
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
}
|
|
|
|
const newState = section.stopEditing();
|
|
// Note: No longer tracking single editingSection
|
|
|
|
this.emit('edit-stopped', { sectionId, newState, section: section.getStatus() });
|
|
return newState;
|
|
}
|
|
|
|
getAllSections() {
|
|
return Array.from(this.sections.values());
|
|
}
|
|
|
|
getDocumentMarkdown() {
|
|
return this.getAllSections()
|
|
.map(section => section.currentMarkdown)
|
|
.join('\\n\\n');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DOM Renderer - Handles DOM interactions
|
|
*/
|
|
class DOMRenderer {
|
|
constructor(sectionManager, containerElement) {
|
|
this.sectionManager = sectionManager;
|
|
this.container = containerElement;
|
|
// Note: Removed single currentSection tracking to allow multiple concurrent edits
|
|
this.editingSections = new Set(); // Track multiple editing sections
|
|
|
|
this.handleSectionClick = this.handleSectionClick.bind(this);
|
|
this.handleAccept = this.handleAccept.bind(this);
|
|
this.handleCancel = this.handleCancel.bind(this);
|
|
this.handleReset = this.handleReset.bind(this);
|
|
this.handleKeydown = this.handleKeydown.bind(this);
|
|
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
this.sectionManager.on('sections-created', (data) => {
|
|
this.renderAllSections(data.sections);
|
|
});
|
|
this.sectionManager.on('edit-started', (data) => {
|
|
this.showEditor(data.sectionId, data.content);
|
|
});
|
|
this.sectionManager.on('edit-stopped', (data) => {
|
|
this.hideEditor(data.sectionId);
|
|
// Don't update content - let pending changes remain
|
|
});
|
|
this.sectionManager.on('changes-accepted', (data) => {
|
|
this.hideEditor(data.sectionId);
|
|
this.updateSectionContent(data.sectionId, data.content);
|
|
});
|
|
this.sectionManager.on('changes-cancelled', (data) => {
|
|
this.hideEditor(data.sectionId);
|
|
this.updateSectionContent(data.sectionId, data.content);
|
|
});
|
|
this.sectionManager.on('section-reset', (data) => {
|
|
this.updateTextareaContent(data.content, data.sectionId);
|
|
});
|
|
this.sectionManager.on('section-split', (data) => {
|
|
console.log('Handling section split in UI');
|
|
this.handleSectionSplit(data);
|
|
});
|
|
}
|
|
|
|
renderAllSections(sections) {
|
|
this.container.innerHTML = '';
|
|
sections.forEach(section => {
|
|
const element = this.createSectionElement(section);
|
|
section.domElement = element;
|
|
this.container.appendChild(element);
|
|
});
|
|
this.container.addEventListener('click', this.handleSectionClick);
|
|
}
|
|
|
|
createSectionElement(section) {
|
|
const element = document.createElement('div');
|
|
element.setAttribute('data-section-id', section.id);
|
|
|
|
if (typeof marked !== 'undefined') {
|
|
element.innerHTML = marked.parse(section.currentMarkdown);
|
|
} else {
|
|
element.innerHTML = `<p>${section.currentMarkdown}</p>`;
|
|
}
|
|
|
|
// Setup styling and event handlers
|
|
this.setupSectionElement(element);
|
|
|
|
return element;
|
|
}
|
|
|
|
handleSectionClick(event) {
|
|
// Don't handle clicks on form elements or buttons
|
|
if (event.target.closest('textarea, button, input')) {
|
|
return;
|
|
}
|
|
|
|
const sectionElement = event.target.closest('.markitect-section-editable');
|
|
if (!sectionElement) return;
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|
if (!sectionId) return;
|
|
|
|
// Check if this section is already being edited
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
if (section && section.isEditing()) {
|
|
console.log('Section already being edited:', sectionId);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('Starting edit for section:', sectionId);
|
|
this.sectionManager.startEditing(sectionId);
|
|
} catch (error) {
|
|
console.error('Failed to start editing:', error);
|
|
}
|
|
}
|
|
|
|
showEditor(sectionId, content) {
|
|
const element = this.findSectionElement(sectionId);
|
|
if (!element) return;
|
|
|
|
this.hideCurrentEditor();
|
|
|
|
const editorContainer = document.createElement('div');
|
|
editorContainer.style.cssText = `
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
`;
|
|
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = content;
|
|
textarea.style.cssText = `
|
|
flex: 1;
|
|
min-height: 100px;
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
border: 2px solid #007acc;
|
|
border-radius: 6px;
|
|
padding: 12px;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
resize: vertical;
|
|
`;
|
|
|
|
textarea.addEventListener('input', () => {
|
|
this.sectionManager.updateContent(sectionId, textarea.value);
|
|
});
|
|
textarea.addEventListener('keydown', this.handleKeydown);
|
|
|
|
const controls = document.createElement('div');
|
|
controls.style.cssText = `
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
`;
|
|
|
|
const createButton = (text, color, handler) => {
|
|
const btn = document.createElement('button');
|
|
btn.textContent = text;
|
|
btn.style.cssText = `
|
|
padding: 8px 12px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
background: ${color};
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
min-width: 70px;
|
|
`;
|
|
btn.addEventListener('click', handler);
|
|
return btn;
|
|
};
|
|
|
|
controls.appendChild(createButton('✓ Accept', '#4caf50', () => this.handleAccept(sectionId)));
|
|
controls.appendChild(createButton('✗ Cancel', '#f44336', () => this.handleCancel(sectionId)));
|
|
controls.appendChild(createButton('🔄 Reset', '#ff9800', () => this.handleReset(sectionId)));
|
|
|
|
editorContainer.appendChild(textarea);
|
|
editorContainer.appendChild(controls);
|
|
|
|
element.innerHTML = '';
|
|
element.appendChild(editorContainer);
|
|
|
|
textarea.focus();
|
|
// Track this section as being edited
|
|
this.editingSections.add(sectionId);
|
|
}
|
|
|
|
hideCurrentEditor() {
|
|
// This method is no longer needed since we support multiple editors
|
|
// Individual editors are hidden via hideEditor(sectionId)
|
|
}
|
|
|
|
hideEditor(sectionId) {
|
|
// Remove from editing sections set
|
|
this.editingSections.delete(sectionId);
|
|
|
|
// Force re-render the section to ensure it displays correctly
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
if (section) {
|
|
this.updateSectionContent(sectionId, section.currentMarkdown);
|
|
}
|
|
}
|
|
|
|
updateSectionContent(sectionId, content) {
|
|
const element = this.findSectionElement(sectionId);
|
|
if (!element) return;
|
|
|
|
if (typeof marked !== 'undefined') {
|
|
element.innerHTML = marked.parse(content);
|
|
} else {
|
|
element.innerHTML = `<p>${content}</p>`;
|
|
}
|
|
|
|
// Restore the section styling and click behavior
|
|
this.setupSectionElement(element);
|
|
}
|
|
|
|
setupSectionElement(element) {
|
|
element.className = 'markitect-section-editable';
|
|
element.style.cssText = `
|
|
margin: 16px 0;
|
|
padding: 12px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
border: 2px solid transparent;
|
|
`;
|
|
|
|
// Remove any existing event listeners to avoid duplicates
|
|
element.removeEventListener('mouseenter', element._mouseenterHandler);
|
|
element.removeEventListener('mouseleave', element._mouseleaveHandler);
|
|
|
|
// Create new handlers and store references
|
|
element._mouseenterHandler = () => {
|
|
element.style.backgroundColor = 'rgba(33, 150, 243, 0.05)';
|
|
element.style.borderColor = 'rgba(33, 150, 243, 0.2)';
|
|
};
|
|
|
|
element._mouseleaveHandler = () => {
|
|
element.style.backgroundColor = '';
|
|
element.style.borderColor = 'transparent';
|
|
};
|
|
|
|
element.addEventListener('mouseenter', element._mouseenterHandler);
|
|
element.addEventListener('mouseleave', element._mouseleaveHandler);
|
|
}
|
|
|
|
updateTextareaContent(content, sectionId) {
|
|
// Find the specific textarea for this section
|
|
const element = this.findSectionElement(sectionId);
|
|
if (element) {
|
|
const textarea = element.querySelector('textarea');
|
|
if (textarea) {
|
|
textarea.value = content;
|
|
}
|
|
}
|
|
}
|
|
|
|
handleSectionSplit(data) {
|
|
// Clear the editor state for the original section
|
|
this.editingSections.delete(data.originalSectionId);
|
|
|
|
// Find the original section element and its position
|
|
const originalElement = this.findSectionElement(data.originalSectionId);
|
|
if (!originalElement) {
|
|
console.error('Original section element not found');
|
|
return;
|
|
}
|
|
|
|
// Get the position of the original element
|
|
const originalPosition = Array.from(this.container.children).indexOf(originalElement);
|
|
|
|
// Create new section elements for the split sections
|
|
const newElements = [];
|
|
data.newSections.forEach(sectionData => {
|
|
const section = this.sectionManager.sections.get(sectionData.id);
|
|
if (section) {
|
|
const element = this.createSectionElement(section);
|
|
section.domElement = element;
|
|
newElements.push(element);
|
|
}
|
|
});
|
|
|
|
// Remove the original element
|
|
originalElement.remove();
|
|
|
|
// Insert new elements at the original position
|
|
if (originalPosition < this.container.children.length) {
|
|
const nextElement = this.container.children[originalPosition];
|
|
newElements.forEach(element => {
|
|
this.container.insertBefore(element, nextElement);
|
|
});
|
|
} else {
|
|
// If original was at the end, just append
|
|
newElements.forEach(element => {
|
|
this.container.appendChild(element);
|
|
});
|
|
}
|
|
|
|
// Show success message
|
|
console.log(`Section split into ${data.newSections.length} sections`);
|
|
|
|
// Notify the main editor about the split
|
|
if (window.markitectCleanEditor) {
|
|
window.markitectCleanEditor.showMessage(
|
|
`✂️ Section split into ${data.newSections.length} sections!`,
|
|
'success'
|
|
);
|
|
}
|
|
}
|
|
|
|
findSectionElement(sectionId) {
|
|
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
|
|
}
|
|
|
|
handleAccept(sectionId) {
|
|
try {
|
|
console.log('Accepting changes for section:', sectionId);
|
|
this.sectionManager.acceptChanges(sectionId);
|
|
console.log('Changes accepted successfully');
|
|
} catch (error) {
|
|
console.error('Failed to accept changes:', error);
|
|
}
|
|
}
|
|
|
|
handleCancel(sectionId) {
|
|
try {
|
|
console.log('Canceling changes for section:', sectionId);
|
|
this.sectionManager.cancelChanges(sectionId);
|
|
console.log('Changes canceled successfully');
|
|
} catch (error) {
|
|
console.error('Failed to cancel changes:', error);
|
|
}
|
|
}
|
|
|
|
handleReset(sectionId) {
|
|
try {
|
|
this.sectionManager.resetToOriginal(sectionId);
|
|
} catch (error) {
|
|
console.error('Failed to reset section:', error);
|
|
}
|
|
}
|
|
|
|
handleKeydown(event) {
|
|
if (!this.currentSection) return;
|
|
if (event.ctrlKey || event.metaKey) {
|
|
switch (event.key) {
|
|
case 'Enter':
|
|
event.preventDefault();
|
|
this.handleAccept(this.currentSection);
|
|
break;
|
|
case 'Escape':
|
|
event.preventDefault();
|
|
this.handleCancel(this.currentSection);
|
|
break;
|
|
}
|
|
}
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
this.handleCancel(this.currentSection);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main Editor Integration
|
|
*/
|
|
class MarkitectCleanEditor {
|
|
constructor(markdownContent, containerElement, options = {}) {
|
|
this.options = {
|
|
theme: 'github',
|
|
keyboardShortcuts: true,
|
|
autosave: false,
|
|
...options
|
|
};
|
|
|
|
this.sectionManager = new SectionManager();
|
|
this.domRenderer = new DOMRenderer(this.sectionManager, containerElement);
|
|
this.originalMarkdown = markdownContent;
|
|
this.initialize();
|
|
}
|
|
|
|
initialize() {
|
|
try {
|
|
const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
|
|
console.log(`✓ Initialized clean editor with ${sections.length} sections`);
|
|
|
|
// Add global control panel
|
|
this.addGlobalControls();
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to initialize clean editor:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
addGlobalControls() {
|
|
// Create floating control panel
|
|
const panel = document.createElement('div');
|
|
panel.id = 'markitect-global-controls';
|
|
panel.innerHTML = `
|
|
<div class="control-header">
|
|
<h3>📝 Editor</h3>
|
|
<div class="control-status" id="editor-status">Ready</div>
|
|
</div>
|
|
<div class="control-actions">
|
|
<button id="save-document" class="control-btn primary">💾 Save Document</button>
|
|
<button id="reset-all" class="control-btn warning">🔄 Reset All</button>
|
|
<button id="show-status" class="control-btn secondary">📊 Show Status</button>
|
|
</div>
|
|
`;
|
|
|
|
// Style the panel
|
|
panel.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: white;
|
|
border: 2px solid #007acc;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
z-index: 1000;
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
min-width: 200px;
|
|
max-width: 250px;
|
|
`;
|
|
|
|
// Add internal styling
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#markitect-global-controls .control-header h3 {
|
|
margin: 0 0 8px 0;
|
|
font-size: 16px;
|
|
color: #007acc;
|
|
}
|
|
#markitect-global-controls .control-status {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-bottom: 12px;
|
|
}
|
|
#markitect-global-controls .control-btn {
|
|
display: block;
|
|
width: 100%;
|
|
margin: 6px 0;
|
|
padding: 10px 12px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
#markitect-global-controls .control-btn.primary {
|
|
background: #007acc;
|
|
color: white;
|
|
}
|
|
#markitect-global-controls .control-btn.primary:hover {
|
|
background: #005a9f;
|
|
}
|
|
#markitect-global-controls .control-btn.warning {
|
|
background: #ff9800;
|
|
color: white;
|
|
}
|
|
#markitect-global-controls .control-btn.warning:hover {
|
|
background: #f57c00;
|
|
}
|
|
#markitect-global-controls .control-btn.secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
#markitect-global-controls .control-btn.secondary:hover {
|
|
background: #545b62;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
document.body.appendChild(panel);
|
|
|
|
// Add event listeners
|
|
document.getElementById('save-document').addEventListener('click', () => this.saveDocument());
|
|
document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections());
|
|
document.getElementById('show-status').addEventListener('click', () => this.showStatus());
|
|
|
|
// Update status periodically
|
|
this.statusInterval = setInterval(() => this.updateGlobalStatus(), 2000);
|
|
}
|
|
|
|
updateGlobalStatus() {
|
|
const statusEl = document.getElementById('editor-status');
|
|
if (!statusEl) return;
|
|
|
|
const sections = this.sectionManager.getAllSections();
|
|
const modified = sections.filter(s => s.hasChanges()).length;
|
|
const editing = sections.filter(s => s.isEditing()).length;
|
|
|
|
if (editing > 0) {
|
|
statusEl.textContent = `Editing ${editing} section${editing !== 1 ? 's' : ''}`;
|
|
statusEl.style.color = '#007acc';
|
|
} else if (modified > 0) {
|
|
statusEl.textContent = `${modified} section${modified !== 1 ? 's' : ''} modified`;
|
|
statusEl.style.color = '#ff9800';
|
|
} else {
|
|
statusEl.textContent = 'All sections saved';
|
|
statusEl.style.color = '#28a745';
|
|
}
|
|
}
|
|
|
|
saveDocument() {
|
|
const markdown = this.getDocumentMarkdown();
|
|
const blob = new Blob([markdown], { type: 'text/markdown' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Generate intelligent filename
|
|
const filename = this.generateSaveFilename();
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.style.display = 'none';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
console.log('📄 Document saved as:', filename);
|
|
this.showMessage(`Document saved as: ${filename}`, 'success');
|
|
}
|
|
|
|
generateSaveFilename() {
|
|
// Try to get original filename from config
|
|
let baseName = 'document';
|
|
|
|
// Method 1: Use original filename from config if available
|
|
if (typeof MARKITECT_EDITOR_CONFIG !== 'undefined' && MARKITECT_EDITOR_CONFIG.originalFilename) {
|
|
baseName = MARKITECT_EDITOR_CONFIG.originalFilename;
|
|
}
|
|
|
|
// Method 2: Try to extract from page title
|
|
if (baseName === 'document') {
|
|
const title = document.title;
|
|
if (title && title !== 'Markdown Document' && !title.includes('Debug') && !title.includes('Test')) {
|
|
baseName = title.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
|
|
.replace(/\s+/g, '-') // Replace spaces with dashes
|
|
.replace(/-+/g, '-') // Collapse multiple dashes
|
|
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
|
}
|
|
}
|
|
|
|
// Method 3: Try to extract from URL pathname
|
|
if (baseName === 'document') {
|
|
const urlPath = window.location.pathname;
|
|
const match = urlPath.match(/\/([^\/]+)\.html?$/);
|
|
if (match) {
|
|
const urlBaseName = match[1];
|
|
if (!urlBaseName.includes('debug') && !urlBaseName.includes('test')) {
|
|
baseName = urlBaseName.replace(/_/g, '-');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 4: Try to extract from first heading
|
|
if (baseName === 'document') {
|
|
const firstHeading = this.sectionManager.getAllSections()
|
|
.find(section => section.sectionType === 'heading');
|
|
if (firstHeading) {
|
|
baseName = firstHeading.originalMarkdown
|
|
.replace(/^#+\s*/, '') // Remove markdown heading syntax
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '')
|
|
.substring(0, 30); // Limit length
|
|
}
|
|
}
|
|
|
|
// Generate timestamp
|
|
const now = new Date();
|
|
const timestamp = now.toISOString()
|
|
.replace(/T/, '-')
|
|
.replace(/:/g, '-')
|
|
.replace(/\.\d{3}Z$/, '');
|
|
|
|
// Check if there are modifications
|
|
const hasModifications = this.sectionManager.getAllSections()
|
|
.some(section => section.hasChanges());
|
|
|
|
if (hasModifications) {
|
|
return `${baseName}-edited-${timestamp}.md`;
|
|
} else {
|
|
return `${baseName}-${timestamp}.md`;
|
|
}
|
|
}
|
|
|
|
resetAllSections() {
|
|
if (confirm('Reset all content to original markdown? This will lose all edits and remove split sections.')) {
|
|
// Clear the section manager completely
|
|
this.sectionManager.sections.clear();
|
|
// Note: No longer tracking single editingSection
|
|
|
|
// Recreate sections from original markdown
|
|
const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
|
|
|
|
console.log('🔄 All sections reset to original structure');
|
|
this.showMessage('Document reset to original structure', 'info');
|
|
}
|
|
}
|
|
|
|
showStatus() {
|
|
const sections = this.sectionManager.getAllSections();
|
|
const total = sections.length;
|
|
const modified = sections.filter(s => s.hasChanges()).length;
|
|
const editing = sections.filter(s => s.isEditing()).length;
|
|
|
|
// Get the actual save filename that will be used
|
|
const saveFilename = this.generateSaveFilename();
|
|
|
|
const message = `${window.editorConfig.repoName} ${window.editorConfig.version}
|
|
Save file: ${saveFilename}
|
|
Source: ${window.editorConfig.originalFilename}
|
|
${window.location.protocol}//${window.location.host}${window.location.pathname}
|
|
|
|
Document Status:
|
|
• Total sections: ${total}
|
|
• Modified sections: ${modified}
|
|
• Currently editing: ${editing}
|
|
• Unsaved changes: ${modified > 0 || editing > 0 ? 'Yes' : 'No'}
|
|
|
|
SECTION BEHAVIOR:
|
|
• Each section is a logical unit (heading + content until next heading)
|
|
• Content with line breaks stays in one section
|
|
• To split content: Create new headings (# ## ###)
|
|
• Sections don't auto-split on line breaks
|
|
|
|
EDITING CONTROLS:
|
|
• Click any section to edit its content
|
|
• Accept (✓) - Save changes to that section
|
|
• Cancel (✗) - Discard changes, return to previous state
|
|
• Reset (🔄) - Restore original content for that section
|
|
• Save Document - Download all current content
|
|
• Reset All - Restore entire document to original state`;
|
|
|
|
alert(message);
|
|
}
|
|
|
|
showMessage(message, type = 'info') {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.textContent = message;
|
|
|
|
const colors = {
|
|
'success': '#28a745',
|
|
'error': '#dc3545',
|
|
'info': '#007acc'
|
|
};
|
|
|
|
messageDiv.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: ${colors[type] || colors.info};
|
|
color: white;
|
|
padding: 12px 20px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
z-index: 10001;
|
|
font-size: 14px;
|
|
max-width: 400px;
|
|
text-align: center;
|
|
`;
|
|
|
|
document.body.appendChild(messageDiv);
|
|
|
|
setTimeout(() => {
|
|
if (messageDiv.parentNode) {
|
|
messageDiv.parentNode.removeChild(messageDiv);
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
getDocumentMarkdown() {
|
|
return this.sectionManager.getDocumentMarkdown();
|
|
}
|
|
}
|
|
|
|
// Initialize the clean editor system
|
|
let markitectCleanEditor;
|
|
|
|
function initializeCleanEditor() {
|
|
const container = document.getElementById('markdown-content');
|
|
if (!container) {
|
|
console.error('Markdown content container not found');
|
|
return;
|
|
}
|
|
|
|
if (typeof window.MarkitectEditor === 'undefined') {
|
|
console.error('MarkitectEditor not found');
|
|
return;
|
|
}
|
|
|
|
markitectCleanEditor = new window.MarkitectEditor.MarkitectCleanEditor(markdownContent, container);
|
|
window.markitectCleanEditor = markitectCleanEditor; // Make globally available
|
|
console.log('✅ Clean section editor initialized successfully');
|
|
}
|
|
|
|
// Export for testing and usage
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
|
|
} else {
|
|
window.MarkitectEditor = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
|
|
}
|
|
""" |