feat: implement insert mode with heading protection and fix content display bugs
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled

This commit implements a comprehensive insert mode that preserves document structure
by protecting heading levels 1-3 from modification while allowing full content editing.

## Insert Mode Features
- CLI integration with --insert flag for md-render command
- Protected heading display (read-only) for levels 1-3
- Content-only editing for sections with protected headings
- Full editing capability for heading levels 4-6
- Theme-aware CSS styling for all UI themes
- Modal confirmation dialogs with proper positioning
- Section splitting with automatic protection inheritance
- Validation to prevent protected heading modifications

## Implementation Details
- Added MARKITECT_INSERT_MODE JavaScript flag and configuration
- Enhanced Section class with heading level detection and protection methods
- Added getHeadingText() and getHeadingContent() methods for content separation
- Implemented insert mode UI with protected heading display above content editor
- Added comprehensive CSS styling for insert mode components and modals
- Updated CLI with --insert option and mutual exclusion with --edit

## Bug Fixes
- Fixed JavaScript syntax errors caused by unescaped newline characters in string literals
- Corrected split('\n') and join('\n') calls to use proper escaping for Python string context
- Fixed heading level 3 display showing "null" by improving regex pattern matching
- Resolved content not displaying in edit/insert modes due to JavaScript parsing failures

## Documentation
- Updated UserInterfaceFramework.md with complete Insert Mode Editor section
- Added behavioral comparison table between edit and insert modes
- Updated Component Integration Matrix to reflect new capabilities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-28 23:55:21 +01:00
parent dd3a00040a
commit c7a83070f8
3 changed files with 486 additions and 22 deletions

View File

@@ -56,7 +56,7 @@ class CleanDocumentManager:
}
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, nodogtag: bool = False) -> Dict[str, Any]:
edit_mode: bool = False, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, nodogtag: bool = False) -> Dict[str, Any]:
"""
Render a markdown file to HTML with optional clean editing capabilities.
"""
@@ -85,6 +85,7 @@ class CleanDocumentManager:
css=css,
template=template,
edit_mode=edit_mode,
insert_mode=insert_mode,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
original_filename=original_filename,
@@ -619,6 +620,236 @@ class CleanDocumentManager:
.ui-scroll-indicator.disabled.ui-scroll-indicator-down::before {{
border-top-color: {props.get('editor_secondary_button', '#6c757d')};
}}
/* Insert Mode Specific Styles */
.markitect-insert-mode .ui-edit-floater-panel {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 4px 12px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')};
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-floater-header {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-floater-header h3 {{
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-inline-panel {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 2px 8px {props.get('editor_shadow', 'rgba(0,0,0,0.1)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
padding: 12px;
margin: 8px 0;
}}
.markitect-insert-mode .ui-insert-protected-panel {{
border-left: 4px solid #ff9800;
}}
.markitect-insert-mode .ui-insert-heading-display {{
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 14px;
font-weight: bold;
padding: 8px 12px;
background: {props.get('editor_warning_bg', '#fff3cd')};
border: 1px solid {props.get('editor_warning_border', '#ffeaa7')};
border-radius: 4px;
border-left: 4px solid #007bff;
color: {props.get('editor_warning_text', '#856404')};
margin-bottom: 8px;
}}
.markitect-insert-mode .ui-insert-content-editor {{
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
color: {props.get('editor_text_color', '#212529')};
background: {props.get('editor_button_bg', '#ffffff')};
}}
.markitect-insert-mode .ui-insert-content-editor:focus {{
border-color: {props.get('editor_focus_color', '#007bff')};
box-shadow: 0 0 0 2px {props.get('editor_focus_color', '#007bff')}33;
}}
.markitect-insert-mode .ui-edit-button {{
background: {props.get('editor_button_bg', '#ffffff')};
color: {props.get('editor_text_color', '#212529')};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
min-width: 70px;
font-weight: 500;
transition: all 0.2s;
}}
.markitect-insert-mode .ui-edit-button:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.markitect-insert-mode .ui-edit-button:active,
.markitect-insert-mode .ui-edit-button.active {{
background: {props.get('editor_button_active', '#dee2e6')};
}}
.markitect-insert-mode .ui-edit-button-accept {{
background: #4caf50;
color: white;
}}
.markitect-insert-mode .ui-edit-button-accept:hover {{
background: #388e3c;
}}
.markitect-insert-mode .ui-edit-button-cancel {{
background: #f44336;
color: white;
}}
.markitect-insert-mode .ui-edit-button-cancel:hover {{
background: #d32f2f;
}}
.markitect-insert-mode .ui-edit-button-reset {{
background: #ff9800;
color: white;
}}
.markitect-insert-mode .ui-edit-button-reset:hover {{
background: #f57c00;
}}
.markitect-insert-mode .ui-edit-section-frame {{
border: 2px solid {props.get('editor_focus_color', '#007bff')};
box-shadow: 0 0 0 3px {props.get('editor_focus_color', '#007bff')}33;
}}
/* Modal Overlay and Dialog Styles for Insert Mode */
.markitect-insert-mode .ui-edit-modal-overlay {{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}}
.markitect-insert-mode .ui-edit-modal-overlay.active {{
opacity: 1;
visibility: visible;
}}
.markitect-insert-mode .ui-edit-modal {{
background: {props['editor_panel_bg']};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
box-shadow: 0 8px 32px {props.get('editor_shadow', 'rgba(0,0,0,0.2)')};
color: {props.get('editor_text_color', '#212529')};
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
width: 90%;
overflow: hidden;
transform: scale(0.9) translateY(-20px);
transition: transform 0.3s;
}}
.markitect-insert-mode .ui-edit-modal-overlay.active .ui-edit-modal {{
transform: scale(1) translateY(0);
}}
.markitect-insert-mode .ui-edit-modal-header {{
padding: 20px 24px 16px;
border-bottom: 1px solid {props.get('editor_panel_border', '#dee2e6')};
display: flex;
justify-content: space-between;
align-items: center;
}}
.markitect-insert-mode .ui-edit-modal-title {{
margin: 0;
font-size: 18px;
font-weight: 600;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-modal-close {{
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: {props.get('editor_text_color', '#212529')};
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}}
.markitect-insert-mode .ui-edit-modal-close:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
.markitect-insert-mode .ui-edit-modal-body {{
padding: 20px 24px;
max-height: 400px;
overflow-y: auto;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-modal-section {{
margin-bottom: 8px;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-modal-footer {{
padding: 16px 24px 20px;
border-top: 1px solid {props.get('editor_panel_border', '#dee2e6')};
text-align: right;
}}
/* Confirmation Dialog Styles for Insert Mode */
.markitect-insert-mode .ui-edit-confirmation-modal {{
max-width: 500px;
}}
.markitect-insert-mode .ui-edit-confirmation-content {{
font-size: 16px;
line-height: 1.5;
margin-bottom: 24px;
color: {props.get('editor_text_color', '#212529')};
}}
.markitect-insert-mode .ui-edit-confirmation-warning {{
background: {props.get('editor_warning_bg', '#fff3cd')};
color: {props.get('editor_warning_text', '#856404')};
border: 1px solid {props.get('editor_warning_border', '#ffeaa7')};
border-radius: 6px;
padding: 12px 16px;
margin: 16px 0;
font-size: 14px;
line-height: 1.4;
}}
.markitect-insert-mode .ui-edit-confirmation-buttons {{
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
}}
.markitect-insert-mode .ui-edit-button-confirm {{
background: #dc3545;
color: white;
border: 1px solid #dc3545;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}}
.markitect-insert-mode .ui-edit-button-confirm:hover {{
background: #c82333;
border-color: #bd2130;
}}
.markitect-insert-mode .ui-edit-button-cancel {{
background: {props.get('editor_button_bg', '#ffffff')};
color: {props.get('editor_text_color', '#212529')};
border: 1px solid {props.get('editor_panel_border', '#dee2e6')};
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}}
.markitect-insert-mode .ui-edit-button-cancel:hover {{
background: {props.get('editor_button_hover', '#e9ecef')};
}}
"""
return f"<style>{base_css}{heading_css}{text_css}{element_css}{link_css}{accent_css}{ui_css}</style>"
@@ -658,7 +889,7 @@ class CleanDocumentManager:
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, nodogtag: bool = False) -> str:
edit_mode: bool = False, insert_mode: bool = False, editor_theme: str = 'github', keyboard_shortcuts: bool = True, original_filename: str = 'document', version_info: dict = None, nodogtag: bool = False) -> str:
"""Generate clean HTML template."""
# Add dogtag to markdown content if not disabled
@@ -716,6 +947,28 @@ class CleanDocumentManager:
editor_config = f"""
const MARKITECT_EDIT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
mode: 'edit',
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;"""
elif insert_mode:
body_classes = ' class="markitect-insert-mode"'
# Configuration for insert mode editor
version_str = f"{version_info['repo_name']} v{version_info['version']}{version_info['git_info']}" if version_info else "Markitect v0.5.0.dev"
editor_config = f"""
const MARKITECT_INSERT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {{
mode: 'insert',
restrictedHeadingLevels: [1, 2, 3],
theme: '{editor_theme}',
keyboardShortcuts: {str(keyboard_shortcuts).lower()},
autosave: false,
@@ -728,7 +981,8 @@ class CleanDocumentManager:
// Make config available globally
window.editorConfig = MARKITECT_EDITOR_CONFIG;"""
# Load clean editor architecture
# Load clean editor architecture for both edit and insert modes
if edit_mode or insert_mode:
editor_scripts = self._get_clean_editor_scripts()
# Generate the complete HTML template
@@ -793,15 +1047,21 @@ class CleanDocumentManager:
}}
}}
// Step 2: Initialize edit capabilities if enabled
if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) {{
console.log("Initializing clean edit capabilities...");
// Step 2: Initialize edit/insert capabilities if enabled
if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) ||
(typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE)) {{
const mode = (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE) ? 'insert' : 'edit';
console.log(`Initializing clean ${{mode}} capabilities...`);
try {{
console.log("Creating clean editor instance...");
initializeCleanEditor();
console.log("✓ Clean edit mode active - click any section to edit");
if (mode === 'insert') {{
console.log("✓ Clean insert mode active - click any section to edit (headings 1-3 protected)");
}} else {{
console.log("✓ Clean edit mode active - click any section to edit");
}}
}} catch (error) {{
console.error("Clean edit mode failed to initialize:", error);
console.error(`Clean ${{mode}} mode failed to initialize:`, error);
}}
}}
@@ -863,6 +1123,7 @@ class Section {
this.editingMarkdown = null;
this.pendingMarkdown = null;
this.sectionType = sectionType;
this.headingLevel = Section.detectHeadingLevel(originalMarkdown);
this.state = EditState.ORIGINAL;
this.domElement = null;
this.lastSaved = null;
@@ -987,6 +1248,39 @@ class Section {
}
return SectionType.PARAGRAPH;
}
static detectHeadingLevel(markdown) {
const trimmed = markdown.trim();
const match = trimmed.match(/^(#{1,6})\s/);
return match ? match[1].length : null;
}
isHeading() {
return this.sectionType === SectionType.HEADING;
}
isProtectedHeading() {
if (!this.isHeading()) return false;
// Check if we're in insert mode and if this heading level is protected
const config = window.editorConfig || {};
const restrictedLevels = config.restrictedHeadingLevels || [];
return config.mode === 'insert' && restrictedLevels.includes(this.headingLevel);
}
getHeadingText() {
if (!this.isHeading()) return null;
// Extract first line for heading text
const firstLine = this.originalMarkdown.trim().split('\\n')[0];
const match = firstLine.match(/^(#{1,6})\s+(.+)$/);
return match ? match[2] : null;
}
getHeadingContent() {
if (!this.isHeading()) return this.currentMarkdown;
const lines = this.currentMarkdown.split('\\n');
// Return content after the heading line
return lines.slice(1).join('\\n');
}
}
/**
@@ -1020,7 +1314,7 @@ class SectionManager {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isHeading = /^#{1,6}\\s/.test(line);
const isHeading = /^#{1,6}\s/.test(line);
const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
const isNewSection = isHeading || isNewParagraph;
@@ -1083,6 +1377,15 @@ class SectionManager {
throw new Error(`Section ${sectionId} not found`);
}
// For protected headings in insert mode, validate that heading hasn't changed
if (section.isProtectedHeading()) {
const originalHeadingLine = section.originalMarkdown.split('\\n')[0];
const newHeadingLine = section.editingMarkdown.split('\\n')[0];
if (originalHeadingLine !== newHeadingLine) {
throw new Error(`Cannot modify protected heading in insert mode. Heading level ${section.headingLevel} is read-only.`);
}
}
// Check if the edited content contains new headings that would create splits
const newContent = section.editingMarkdown;
const originalContent = section.originalMarkdown;
@@ -1113,14 +1416,14 @@ class SectionManager {
// Count headings in new content
for (const line of lines) {
if (/^#{1,6}\\s/.test(line.trim())) {
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())) {
if (/^#{1,6}\s/.test(line.trim())) {
originalHeadingCount++;
}
}
@@ -1188,7 +1491,7 @@ class SectionManager {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isHeading = /^#{1,6}\\s/.test(line.trim());
const isHeading = /^#{1,6}\s/.test(line.trim());
if (isHeading) {
// When we encounter a heading, complete any previous section
@@ -1373,18 +1676,54 @@ class DOMRenderer {
this.hideCurrentEditor();
const section = this.sectionManager.sections.get(sectionId);
const isProtectedHeading = section && section.isProtectedHeading();
const editorContainer = document.createElement('div');
editorContainer.className = 'ui-edit-inline-panel';
editorContainer.className = isProtectedHeading ? 'ui-edit-inline-panel ui-insert-protected-panel' : 'ui-edit-inline-panel';
editorContainer.style.cssText = `
display: flex;
gap: 12px;
align-items: flex-start;
flex-direction: column;
gap: 8px;
width: 100%;
`;
// If this is a protected heading, show the heading display
if (isProtectedHeading) {
const headingDisplay = document.createElement('div');
headingDisplay.className = 'ui-insert-heading-display';
const headingText = section.getHeadingText();
const headingLevel = section.headingLevel;
const headingMarkdown = '#'.repeat(headingLevel) + ' ' + headingText;
headingDisplay.textContent = headingMarkdown;
headingDisplay.style.cssText = `
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 14px;
font-weight: bold;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
border-left: 4px solid #007bff;
color: #333;
`;
editorContainer.appendChild(headingDisplay);
}
// Create content editing area
const editingArea = document.createElement('div');
editingArea.style.cssText = `
display: flex;
gap: 12px;
align-items: flex-start;
`;
const textarea = document.createElement('textarea');
textarea.className = 'ui-edit-textarea ui-edit-textarea-main';
textarea.value = content;
textarea.className = isProtectedHeading ? 'ui-edit-textarea ui-insert-content-editor' : 'ui-edit-textarea ui-edit-textarea-main';
// For protected headings, only show content after the heading
const textareaContent = isProtectedHeading ? section.getHeadingContent() : content;
textarea.value = textareaContent;
textarea.style.cssText = `
flex: 1;
min-height: 100px;
@@ -1397,7 +1736,14 @@ class DOMRenderer {
`;
textarea.addEventListener('input', () => {
this.sectionManager.updateContent(sectionId, textarea.value);
if (isProtectedHeading) {
// Reconstruct full content with protected heading
const headingLine = section.originalMarkdown.split('\\n')[0];
const fullContent = headingLine + '\\n' + textarea.value;
this.sectionManager.updateContent(sectionId, fullContent);
} else {
this.sectionManager.updateContent(sectionId, textarea.value);
}
});
textarea.addEventListener('keydown', this.handleKeydown);
@@ -1420,8 +1766,9 @@ class DOMRenderer {
controls.appendChild(createButton('✗ Cancel', 'ui-edit-button ui-edit-button-cancel', () => this.handleCancel(sectionId)));
controls.appendChild(createButton('🔄 Reset', 'ui-edit-button ui-edit-button-reset', () => this.handleReset(sectionId)));
editorContainer.appendChild(textarea);
editorContainer.appendChild(controls);
editingArea.appendChild(textarea);
editingArea.appendChild(controls);
editorContainer.appendChild(editingArea);
element.innerHTML = '';
element.appendChild(editorContainer);