refactor: Still trying to reorganize edit mode to be more robust
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / security-scan (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 / test-summary (push) Has been cancelled

This commit is contained in:
2025-11-04 21:59:22 +01:00
parent 85faf502c4
commit c5a5b26797
487 changed files with 94669 additions and 144 deletions

View File

@@ -1234,7 +1234,37 @@ document.addEventListener('DOMContentLoaded', function() {
documentControls.setEventHandlers({
'save-document': () => {
console.log('Save document clicked');
// TODO: Implement save functionality
try {
// Get current markdown content from section manager
const currentMarkdown = sectionManager.getDocumentMarkdown();
// Create filename with timestamp suffix following the established convention
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
// Extract original filename from config or use default
const originalFilename = window.editorConfig?.originalFilename || 'document';
const editedFilename = `${originalFilename}-edited-${timestamp}.md`;
// Create and download the file
const blob = new Blob([currentMarkdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = editedFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Log success to debug panel
debugPanel.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS');
console.log(`Document successfully saved as: ${editedFilename}`);
} catch (error) {
debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR');
console.error('Save error:', error);
}
},
'reset-all': () => {
console.log('Reset all clicked');

View File

@@ -1978,10 +1978,18 @@ def md_list_command(ctx, output_format, names_only):
help='Copy referenced assets to output directory')
@click.option('--no-ship-assets', is_flag=True,
help='Don\'t copy referenced assets to output directory')
@click.option('--verbose', '-v', is_flag=True,
help='Show detailed output including asset operations')
@click.option('--silent', '-s', is_flag=True,
help='Suppress non-essential output')
@click.option('--image-max-width', type=str, default=None,
help='Maximum width for images (default: 12cm, supports px, em, %, cm, in, etc.)')
@click.option('--image-max-height', type=str, default=None,
help='Maximum height for images (default: 20cm, supports px, em, %, cm, in, etc.)')
@click.pass_context
def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_theme,
keyboard_shortcuts, use_publication_dir, dont_use_publication_dir, nodogtag,
ship_assets, no_ship_assets):
ship_assets, no_ship_assets, verbose, silent, image_max_width, image_max_height):
"""
Render a markdown file to HTML with basic templates and live preview capabilities.
@@ -2013,10 +2021,35 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
if edit and insert:
raise click.BadParameter("Cannot use both --edit and --insert flags simultaneously. Choose one mode.")
# Check environment variables for edit/insert modes (if not set via CLI flags)
import os
if not edit and not insert:
if os.environ.get('MARKITECT_EDIT_MODE', '').lower() in ('true', '1', 'yes'):
edit = True
elif os.environ.get('MARKITECT_INSERT_MODE', '').lower() in ('true', '1', 'yes'):
insert = True
# Validate asset shipping flags
if ship_assets and no_ship_assets:
raise click.BadParameter("Cannot use both --ship-assets and --no-ship-assets flags simultaneously.")
# Validate verbosity flags
if verbose and silent:
raise click.BadParameter("Cannot use both --verbose and --silent flags simultaneously.")
# Handle image size configuration with environment variable support
import os
# Get image max width (CLI > ENV > default)
final_image_max_width = image_max_width
if final_image_max_width is None:
final_image_max_width = os.environ.get('MARKITECT_IMAGE_MAX_WIDTH', '12cm')
# Get image max height (CLI > ENV > default)
final_image_max_height = image_max_height
if final_image_max_height is None:
final_image_max_height = os.environ.get('MARKITECT_IMAGE_MAX_HEIGHT', '20cm')
# Determine output path with environment variable support
if output:
output_path = Path(output)
@@ -2066,7 +2099,7 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
if should_ship_assets:
if output_is_directory:
# For directory output, ship to the same directory as the HTML file
_ship_assets(input_path, output_path.parent, config.get('verbose', False))
_ship_assets(input_path, output_path.parent, verbose, silent)
# For file output, we don't ship assets (shouldn't reach here anyway)
# Initialize clean document manager
@@ -2081,11 +2114,14 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
edit_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag)
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
click.echo(f"✓ Rendered with interactive editing capabilities to: {output_path}")
if not silent:
click.echo(f"✓ Rendered with interactive editing capabilities to: {output_path}")
if config.get('verbose', False):
if verbose:
click.echo(f"Editor theme: {editor_theme}")
click.echo(f"Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
click.echo(f"Theme: {theme or 'default'}")
@@ -2097,11 +2133,14 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
insert_mode=True,
editor_theme=editor_theme,
keyboard_shortcuts=keyboard_shortcuts,
nodogtag=nodogtag)
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
click.echo(f"✓ Rendered with interactive insert capabilities to: {output_path}")
if not silent:
click.echo(f"✓ Rendered with interactive insert capabilities to: {output_path}")
if config.get('verbose', False):
if verbose:
click.echo(f"Editor theme: {editor_theme}")
click.echo(f"Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
click.echo(f"Heading protection: levels 1-3 read-only")
@@ -2113,10 +2152,13 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
template=theme, css=css,
edit_mode=False,
insert_mode=False,
nodogtag=nodogtag)
click.echo(f"✓ Rendered to: {output_path}")
nodogtag=nodogtag,
image_max_width=final_image_max_width,
image_max_height=final_image_max_height)
if not silent:
click.echo(f"✓ Rendered to: {output_path}")
if config.get('verbose', False):
if verbose:
click.echo(f"Theme: {theme or 'default'}")
click.echo(f"CSS: {css or 'default'}")
@@ -3482,18 +3524,28 @@ class FilenameDecoder:
return [self.decode(filename) for filename in filenames]
def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False, silent: bool = False):
"""
Ship (copy) assets referenced in markdown file to output directory.
Args:
input_path: Path to the markdown file
output_dir: Directory where assets should be copied
verbose: Whether to print verbose output
verbose: Whether to print detailed output
silent: Whether to suppress non-essential output
"""
import shutil
import hashlib
from markitect.assets.discovery import discover_assets_from_markdown
def get_file_hash(file_path):
"""Get SHA-256 hash of file content for content comparison."""
hash_sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha256.update(chunk)
return hash_sha256.hexdigest()
try:
# Read the markdown content
markdown_content = input_path.read_text(encoding='utf-8')
@@ -3524,14 +3576,49 @@ def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
# Create destination directory
dest_path.parent.mkdir(parents=True, exist_ok=True)
# Check if we need to copy (timestamp-based)
# Check if we need to copy (smart comparison for cross-filesystem compatibility)
should_copy = True
if dest_path.exists():
source_mtime = asset_ref.resolved_path.stat().st_mtime
dest_mtime = dest_path.stat().st_mtime
if source_mtime <= dest_mtime:
should_copy = False
skipped_count += 1
source_stat = asset_ref.resolved_path.stat()
dest_stat = dest_path.stat()
# Detect if we're in a cross-filesystem scenario where timestamps might be unreliable
# Heuristics: different filesystems, or timestamps that don't make sense
is_cross_fs = (
# Different device IDs suggests different filesystems
source_stat.st_dev != dest_stat.st_dev or
# Destination path starts with /mnt/ (common WSL Windows mount)
str(dest_path).startswith('/mnt/') or
# Very large timestamp differences (>1 hour) for same content suggest sync issues
abs(source_stat.st_mtime - dest_stat.st_mtime) > 3600
)
if is_cross_fs:
# Use content-based comparison for cross-filesystem scenarios
if source_stat.st_size == dest_stat.st_size:
try:
source_hash = get_file_hash(asset_ref.resolved_path)
dest_hash = get_file_hash(dest_path)
if source_hash == dest_hash:
should_copy = False
skipped_count += 1
if verbose:
click.echo(f" → Content verified (cross-fs): {asset_ref.asset_path}")
# If hashes differ, should_copy remains True
except (OSError, IOError):
if verbose:
click.echo(f" ⚠ Could not verify content, will copy: {asset_ref.asset_path}")
pass
# If sizes differ, should_copy remains True
else:
# Use fast timestamp comparison for same-filesystem scenarios
if source_stat.st_mtime <= dest_stat.st_mtime and source_stat.st_size == dest_stat.st_size:
should_copy = False
skipped_count += 1
if verbose:
click.echo(f" → Timestamp verified: {asset_ref.asset_path}")
# If timestamp suggests newer source or different size, should_copy remains True
if should_copy:
shutil.copy2(asset_ref.resolved_path, dest_path)
@@ -3541,12 +3628,21 @@ def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
elif verbose:
click.echo(f" → Skipped (up-to-date): {asset_ref.asset_path}")
# Summary
if verbose or shipped_count > 0:
# Summary - provide feedback based on verbosity settings
total_assets = shipped_count + skipped_count + missing_count
if total_assets > 0 and not silent:
if shipped_count > 0:
click.echo(f"✓ Shipped {shipped_count} assets")
if skipped_count > 0:
click.echo(f" → Skipped {skipped_count} up-to-date assets")
elif skipped_count > 0:
click.echo(f"✓ All {skipped_count} assets up-to-date")
# Additional details for verbose or when there are mixed results
if verbose or (shipped_count > 0 and skipped_count > 0):
if skipped_count > 0 and shipped_count > 0:
click.echo(f"{skipped_count} already up-to-date")
# Always show missing assets as it's important information
if missing_count > 0:
click.echo(f"{missing_count} assets not found", err=True)

View File

@@ -32,6 +32,31 @@ class FloatingMenu {
const targetElement = this.renderer.findSectionElement(this.sectionId);
if (!targetElement) return null;
// Get content dimensions and position
const rect = targetElement.getBoundingClientRect();
const viewport = {
width: window.innerWidth,
height: window.innerHeight
};
// Calculate content width and responsive extension
const contentWidth = rect.width;
const buttonAreaWidth = 120; // Space needed for buttons
const minMenuWidth = Math.max(300, contentWidth); // At least content width or 300px
const preferredMenuWidth = contentWidth + buttonAreaWidth;
// Check if we have space to extend to the right
const spaceOnRight = viewport.width - rect.right;
const canExtendRight = spaceOnRight >= buttonAreaWidth + 20; // 20px margin
// Determine final menu width
let menuWidth;
if (canExtendRight && viewport.width >= 800) { // Only on wide screens
menuWidth = Math.min(preferredMenuWidth, viewport.width - rect.left - 20);
} else {
menuWidth = Math.min(minMenuWidth, viewport.width - 40); // 20px margins
}
// Create floating menu element
this.element = document.createElement('div');
this.element.className = 'ui-edit-floating-menu';
@@ -42,65 +67,101 @@ class FloatingMenu {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 16px;
min-width: 300px;
padding: 0;
width: ${menuWidth}px;
box-sizing: border-box;
`;
// Smart positioning with viewport boundary detection
const rect = targetElement.getBoundingClientRect();
const viewport = {
width: window.innerWidth,
height: window.innerHeight
};
// Add headline
const headline = document.createElement('div');
headline.className = 'ui-edit-headline';
headline.textContent = `Editing ${this.type === 'image' ? 'Image' : 'Section'}`;
headline.style.cssText = `
background: #f8f9fa;
border-bottom: 1px solid #ddd;
padding: 8px 16px;
font-weight: 600;
font-size: 12px;
color: #495057;
border-radius: 8px 8px 0 0;
text-transform: uppercase;
letter-spacing: 0.5px;
`;
// Calculate initial position (below the section)
// Create content wrapper with padding
const contentWrapper = document.createElement('div');
contentWrapper.style.cssText = `
padding: 16px;
`;
this.element.appendChild(headline);
// Position directly over content (overlay positioning)
let left = rect.left;
let top = rect.bottom + 10;
let top = rect.top;
// Adjust horizontal position if menu would go off-screen
const menuWidth = 350; // Estimated menu width
// Ensure menu doesn't go off-screen horizontally
if (left + menuWidth > viewport.width) {
left = viewport.width - menuWidth - 20; // 20px margin from edge
left = viewport.width - menuWidth - 20;
}
if (left < 10) {
left = 10; // Minimum margin from left edge
left = 10;
}
// Adjust vertical position if menu would go off-screen
const menuHeight = 300; // Estimated menu height
if (top + menuHeight > viewport.height) {
// Position above the section instead
top = rect.top - menuHeight - 10;
if (top < 10) {
// If still off-screen, position at viewport top
top = 10;
}
// For vertical positioning, prefer staying on top of content
// Only move if absolutely necessary
const menuHeight = this.type === 'image' ? 350 : 200; // Better height estimates
const wouldGoOffBottom = top + menuHeight > viewport.height;
const wouldGoOffTop = top < 10;
if (wouldGoOffBottom && !wouldGoOffTop) {
// Try to fit by moving up, but keep some overlay if possible
const maxTop = viewport.height - menuHeight - 10;
top = Math.max(rect.top - 50, maxTop); // Prefer staying near original position
} else if (wouldGoOffTop) {
top = 10; // Minimum distance from top
}
// Otherwise, keep the original overlay position
this.element.style.left = `${left}px`;
this.element.style.top = `${top}px`;
// Add content
// Add content to wrapper
if (contentElement) {
this.element.appendChild(contentElement);
contentWrapper.appendChild(contentElement);
}
if (controlsElement) {
this.element.appendChild(controlsElement);
contentWrapper.appendChild(controlsElement);
}
// Add close button
this.element.appendChild(contentWrapper);
// Add close button to headline
const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.style.cssText = `
position: absolute;
top: 8px;
top: 4px;
right: 8px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #666;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease;
`;
closeButton.addEventListener('mouseover', () => {
closeButton.style.backgroundColor = '#e9ecef';
});
closeButton.addEventListener('mouseout', () => {
closeButton.style.backgroundColor = 'transparent';
});
closeButton.addEventListener('click', (event) => {
event.stopPropagation();
this.hide();
@@ -360,13 +421,36 @@ class DOMRenderer {
// Create content area for text editing
const editorContent = document.createElement('div');
editorContent.className = 'ui-edit-editor-content';
editorContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
min-width: 0;
`;
// Check if we have space for side-by-side layout
const targetElement = this.findSectionElement(sectionId);
const rect = targetElement ? targetElement.getBoundingClientRect() : null;
const viewport = { width: window.innerWidth, height: window.innerHeight };
const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
if (hasWideLayout) {
// Side-by-side layout: textarea on left, controls on right
editorContent.style.cssText = `
display: flex;
gap: 16px;
flex: 1;
min-width: 0;
align-items: flex-start;
`;
} else {
// Stacked layout: textarea above, controls below
editorContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
min-width: 0;
`;
}
// Create textarea container
const textareaContainer = document.createElement('div');
textareaContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
// Create textarea
const textarea = document.createElement('textarea');
@@ -381,55 +465,81 @@ class DOMRenderer {
font-size: 14px;
line-height: 1.5;
resize: vertical;
box-sizing: border-box;
`;
// Create controls
const controls = document.createElement('div');
controls.style.cssText = `
display: flex;
gap: 8px;
justify-content: flex-end;
`;
if (hasWideLayout) {
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
min-width: 100px;
flex-shrink: 0;
`;
} else {
controls.style.cssText = `
display: flex;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
`;
}
const acceptButton = document.createElement('button');
acceptButton.textContent = 'Accept';
acceptButton.textContent = hasWideLayout ? '✓' : 'Accept';
acceptButton.style.cssText = `
background: #28a745;
color: white;
border: none;
padding: 8px 16px;
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
border-radius: 4px;
cursor: pointer;
${hasWideLayout ? 'width: 100%;' : ''}
font-size: ${hasWideLayout ? '14px' : '13px'};
`;
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.textContent = hasWideLayout ? '✗' : 'Cancel';
cancelButton.style.cssText = `
background: #dc3545;
color: white;
border: none;
padding: 8px 16px;
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
border-radius: 4px;
cursor: pointer;
${hasWideLayout ? 'width: 100%;' : ''}
font-size: ${hasWideLayout ? '14px' : '13px'};
`;
const resetButton = document.createElement('button');
resetButton.textContent = '↺ Reset';
resetButton.textContent = hasWideLayout ? '↺' : '↺ Reset';
resetButton.style.cssText = `
background: #fd7e14;
color: white;
border: none;
padding: 8px 16px;
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
border-radius: 4px;
cursor: pointer;
${hasWideLayout ? 'width: 100%;' : ''}
font-size: ${hasWideLayout ? '14px' : '13px'};
`;
controls.appendChild(acceptButton);
controls.appendChild(cancelButton);
controls.appendChild(resetButton);
editorContent.appendChild(textarea);
editorContent.appendChild(controls);
// Assemble the layout
textareaContainer.appendChild(textarea);
if (hasWideLayout) {
editorContent.appendChild(textareaContainer);
editorContent.appendChild(controls);
} else {
editorContent.appendChild(textareaContainer);
editorContent.appendChild(controls);
}
// Create floating menu
const floatingMenu = new FloatingMenu(sectionId, 'text', this);
@@ -494,16 +604,52 @@ class DOMRenderer {
stagingState.currentImageSrc = imageSrc;
}
// Check if we have space for side-by-side layout
const targetElement = this.findSectionElement(sectionId);
const rect = targetElement ? targetElement.getBoundingClientRect() : null;
const viewport = { width: window.innerWidth, height: window.innerHeight };
const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
// Create image editor content area
const editorContent = document.createElement('div');
editorContent.className = 'ui-edit-image-content';
editorContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 15px;
flex: 1;
min-width: 0;
`;
if (hasWideLayout) {
// Side-by-side layout: content on left, controls on right
editorContent.style.cssText = `
display: flex;
gap: 16px;
flex: 1;
min-width: 0;
align-items: flex-start;
`;
} else {
// Stacked layout: content above, controls below
editorContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 15px;
flex: 1;
min-width: 0;
`;
}
// Create content container for image and alt text
const contentContainer = document.createElement('div');
contentContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
if (!hasWideLayout) {
contentContainer.style.cssText += `
display: flex;
flex-direction: column;
gap: 15px;
`;
} else {
contentContainer.style.cssText += `
display: flex;
flex-direction: column;
gap: 12px;
`;
}
// Image preview with drop zone
const imagePreview = document.createElement('div');
@@ -718,27 +864,37 @@ class DOMRenderer {
}
};
// Assemble content
editorContent.appendChild(imagePreview);
editorContent.appendChild(altTextContainer);
editorContent.appendChild(changeIndicator);
editorContent.appendChild(fileInput);
// Assemble content container
contentContainer.appendChild(imagePreview);
contentContainer.appendChild(altTextContainer);
contentContainer.appendChild(changeIndicator);
contentContainer.appendChild(fileInput);
// Create controls
const controls = document.createElement('div');
controls.className = 'ui-edit-controls';
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
`;
if (hasWideLayout) {
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
min-width: 100px;
flex-shrink: 0;
`;
} else {
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
`;
}
const acceptBtn = document.createElement('button');
acceptBtn.textContent = '✓ Accept';
acceptBtn.textContent = hasWideLayout ? '✓' : '✓ Accept';
acceptBtn.style.cssText = `
padding: 8px 12px;
font-size: 12px;
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
font-size: ${hasWideLayout ? '14px' : '12px'};
border-radius: 6px;
border: none;
color: white;
@@ -751,10 +907,10 @@ class DOMRenderer {
`;
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '✗ Cancel';
cancelBtn.textContent = hasWideLayout ? '✗' : '✗ Cancel';
cancelBtn.style.cssText = `
padding: 8px 12px;
font-size: 12px;
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
font-size: ${hasWideLayout ? '14px' : '12px'};
border-radius: 6px;
border: none;
color: white;
@@ -767,10 +923,10 @@ class DOMRenderer {
`;
const resetBtn = document.createElement('button');
resetBtn.textContent = '↺ Reset';
resetBtn.textContent = hasWideLayout ? '↺' : '↺ Reset';
resetBtn.style.cssText = `
padding: 8px 12px;
font-size: 12px;
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
font-size: ${hasWideLayout ? '14px' : '12px'};
border-radius: 6px;
border: none;
color: white;
@@ -871,12 +1027,21 @@ class DOMRenderer {
}
});
// Assemble the final layout
if (hasWideLayout) {
editorContent.appendChild(contentContainer);
editorContent.appendChild(controls);
} else {
editorContent.appendChild(contentContainer);
editorContent.appendChild(controls);
}
// Create floating menu
const floatingMenu = new FloatingMenu(sectionId, 'image', this);
this.currentFloatingMenu = floatingMenu;
this.editingSections.add(sectionId);
floatingMenu.show(editorContent, controls);
floatingMenu.show(editorContent);
}
/**

View File

@@ -340,39 +340,51 @@ class SectionManager {
}
createSectionsFromMarkdown(markdownContent) {
const lines = markdownContent.split('\n');
// Split content into blocks separated by double newlines
const blocks = markdownContent.split(/\n\s*\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;
for (const block of blocks) {
const trimmedBlock = block.trim();
if (!trimmedBlock) continue;
if (isNewSection && currentSection.trim()) {
// Check if this block should be split further
const lines = trimmedBlock.split('\n');
let currentSection = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isHeading = /^#{1,6}\s/.test(line.trim());
const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
// Each heading or image starts a new section
if ((isHeading || isImage) && currentSection.trim()) {
// Save the previous section
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;
}
}
// Save the final section from this block
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);
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;
}