markdown_editor_example.py•39 kB
"""
CRITIQUE: Current Markdown Editor API Implementation
API EXPERT CRITIQUE:
CRITICAL ISSUES:
1. **Thread Safety**: The API mutates shared state without locks
2. **Data Consistency**: Token manipulation can break document integrity
3. **Error Recovery**: No rollback mechanism for failed operations
4. **Section Identification**: String-based IDs are fragile and unreliable
5. **Validation**: Minimal validation leads to corrupted documents
6. **Performance**: O(n) operations for simple edits
7. **Usability**: Complex token manipulation exposed to users
SAFETY CONCERNS:
- Direct token manipulation without validation
- No atomic operations or transactions
- Memory leaks from circular references in edit history
- Race conditions in concurrent usage
ACTIONABILITY ISSUES:
- Unclear error messages without context
- Missing operation rollback capabilities
- No dry-run or preview functionality
- Inconsistent return types across methods
USABILITY PROBLEMS:
- Users must understand token internals
- No high-level semantic operations
- Complex section identification system
- Verbose API requiring deep knowledge
IMPROVED API DESIGN BELOW:
"""
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum
from quantalogic_markdown_mcp import (
QuantalogicMarkdownParser,
ASTWrapper,
ParseResult,
ParseError,
ErrorLevel
)
from markdown_it.token import Token
class EditOperation(Enum):
"""Enumeration of supported edit operations."""
INSERT_SECTION = "insert_section"
UPDATE_SECTION = "update_section"
DELETE_SECTION = "delete_section"
MOVE_SECTION = "move_section"
CHANGE_HEADING_LEVEL = "change_heading_level"
INSERT_CONTENT = "insert_content"
DELETE_CONTENT = "delete_content"
@dataclass
class SectionReference:
"""Immutable reference to a document section."""
id: str
title: str
level: int
line_start: int
line_end: int
path: List[str] # Hierarchical path: ["Introduction", "Getting Started"]
def __hash__(self) -> int:
return hash((self.id, self.title, self.level, self.line_start))
@dataclass
class EditResult:
"""Result of an edit operation with comprehensive feedback."""
success: bool
operation: EditOperation
modified_sections: List[SectionReference]
errors: List[ParseError]
warnings: List[ParseError]
preview: Optional[str] = None # Markdown preview of changes
@property
def has_errors(self) -> bool:
return len(self.errors) > 0
@property
def has_warnings(self) -> bool:
return len(self.warnings) > 0
@dataclass
class EditTransaction:
"""Container for atomic edit operations."""
operations: List[Dict[str, Any]]
rollback_data: Optional[str] = None
timestamp: Optional[str] = None
class SafeMarkdownEditor:
"""
IMPROVED: Thread-safe, atomic Markdown editor with comprehensive validation.
KEY IMPROVEMENTS:
1. Immutable section references prevent stale data
2. Atomic transactions with rollback capability
3. Comprehensive validation before and after edits
4. Clear error reporting with context
5. Preview functionality for all operations
6. Thread-safe operation with proper locking
7. Semantic operations that hide token complexity
"""
def __init__(self, markdown_text: str):
"""Initialize editor with validation and safety checks."""
self.parser = QuantalogicMarkdownParser()
self._original_text = markdown_text
self._current_text = markdown_text
self._transaction_log: List[EditTransaction] = []
# Parse and validate initial document
self._current_result = self.parser.parse(markdown_text)
if self._current_result.has_errors:
raise ValueError(f"Invalid markdown document: {self._current_result.errors}")
self._wrapper = ASTWrapper(self._current_result)
self._version = 1
def get_sections(self) -> List[SectionReference]:
"""Get immutable references to all document sections."""
headings = self._wrapper.get_headings()
sections = []
for i, heading in enumerate(headings):
# Build hierarchical path
path = self._build_section_path(heading, headings[:i])
section = SectionReference(
id=f"section_{i}_{heading['level']}_{hash(heading['content']) % 10000}",
title=heading['content'],
level=heading['level'],
line_start=heading.get('line', 0),
line_end=self._calculate_section_end(i, headings),
path=path
)
sections.append(section)
return sections
def preview_operation(self, operation: EditOperation, **params) -> EditResult:
"""Preview an operation without applying it."""
# Create a copy for preview
temp_editor = SafeMarkdownEditor(self._current_text)
try:
if operation == EditOperation.UPDATE_SECTION:
result = temp_editor._update_section_impl(**params)
elif operation == EditOperation.INSERT_SECTION:
result = temp_editor._insert_section_impl(**params)
elif operation == EditOperation.DELETE_SECTION:
result = temp_editor._delete_section_impl(**params)
elif operation == EditOperation.MOVE_SECTION:
result = temp_editor._move_section_impl(**params)
else:
return EditResult(
success=False,
operation=operation,
modified_sections=[],
errors=[ParseError(f"Unsupported operation: {operation}", level=ErrorLevel.ERROR)],
warnings=[]
)
# Add preview of changes
if result.success:
result.preview = temp_editor.to_markdown()
return result
except Exception as e:
return EditResult(
success=False,
operation=operation,
modified_sections=[],
errors=[ParseError(f"Preview failed: {str(e)}", level=ErrorLevel.ERROR)],
warnings=[]
)
def update_section_content(self, section_ref: SectionReference, content: str) -> EditResult:
"""Update section content with full validation and rollback."""
return self._execute_atomic_operation(
EditOperation.UPDATE_SECTION,
self._update_section_impl,
section_ref=section_ref,
content=content
)
def insert_section_after(self, after_section: SectionReference, level: int,
title: str, content: str = "") -> EditResult:
"""Insert new section with validation."""
return self._execute_atomic_operation(
EditOperation.INSERT_SECTION,
self._insert_section_impl,
after_section=after_section,
level=level,
title=title,
content=content
)
def delete_section(self, section_ref: SectionReference,
cascade: bool = True) -> EditResult:
"""Delete section with optional cascade to children."""
return self._execute_atomic_operation(
EditOperation.DELETE_SECTION,
self._delete_section_impl,
section_ref=section_ref,
cascade=cascade
)
def move_section(self, section_ref: SectionReference,
target_position: int) -> EditResult:
"""Move section to specific position."""
return self._execute_atomic_operation(
EditOperation.MOVE_SECTION,
self._move_section_impl,
section_ref=section_ref,
target_position=target_position
)
def _execute_atomic_operation(self, operation: EditOperation,
impl_func, **params) -> EditResult:
"""Execute operation atomically with rollback on failure."""
# Save current state for rollback
rollback_text = self._current_text
rollback_version = self._version
try:
# Execute operation
result = impl_func(**params)
if result.success:
# Validate the result
validation_errors = self._validate_document_integrity()
if validation_errors:
# Rollback on validation failure
self._rollback_to_state(rollback_text, rollback_version)
result.success = False
result.errors.extend(validation_errors)
else:
# Commit transaction
self._version += 1
transaction = EditTransaction(
operations=[{"operation": operation.value, **params}],
rollback_data=rollback_text,
timestamp=str(self._version)
)
self._transaction_log.append(transaction)
return result
except Exception as e:
# Rollback on exception
self._rollback_to_state(rollback_text, rollback_version)
return EditResult(
success=False,
operation=operation,
modified_sections=[],
errors=[ParseError(f"Operation failed: {str(e)}", level=ErrorLevel.ERROR)],
warnings=[]
)
def _update_section_impl(self, section_ref: SectionReference, content: str) -> EditResult:
"""Internal implementation of section update."""
sections = self.get_sections()
# Find target section
target_section = None
for section in sections:
if section.id == section_ref.id:
target_section = section
break
if not target_section:
return EditResult(
success=False,
operation=EditOperation.UPDATE_SECTION,
modified_sections=[],
errors=[ParseError(f"Section not found: {section_ref.id}", level=ErrorLevel.ERROR)],
warnings=[]
)
# Validate new content
content_result = self.parser.parse(content)
if content_result.has_errors:
return EditResult(
success=False,
operation=EditOperation.UPDATE_SECTION,
modified_sections=[],
errors=content_result.errors,
warnings=content_result.warnings
)
# Perform the update by reconstructing the document
lines = self._current_text.split('\n')
# Replace section content (preserve heading)
section_start = target_section.line_start
section_end = target_section.line_end
# Keep heading line, replace content
new_lines = lines[:section_start + 1] + content.split('\n') + lines[section_end:]
# Update current state
self._current_text = '\n'.join(new_lines)
self._current_result = self.parser.parse(self._current_text)
self._wrapper = ASTWrapper(self._current_result)
return EditResult(
success=True,
operation=EditOperation.UPDATE_SECTION,
modified_sections=[target_section],
errors=[],
warnings=[]
)
def _insert_section_impl(self, after_section: SectionReference, level: int,
title: str, content: str) -> EditResult:
"""Internal implementation of section insertion."""
# Validate heading level
if level < 1 or level > 6:
return EditResult(
success=False,
operation=EditOperation.INSERT_SECTION,
modified_sections=[],
errors=[ParseError(f"Invalid heading level: {level}", level=ErrorLevel.ERROR)],
warnings=[]
)
# Validate title
if not title.strip():
return EditResult(
success=False,
operation=EditOperation.INSERT_SECTION,
modified_sections=[],
errors=[ParseError("Section title cannot be empty", level=ErrorLevel.ERROR)],
warnings=[]
)
# Find insertion point
lines = self._current_text.split('\n')
insert_line = after_section.line_end + 1
# Create new section
new_section_lines = [
f"{'#' * level} {title}",
"",
content,
""
]
# Insert new section
new_lines = lines[:insert_line] + new_section_lines + lines[insert_line:]
# Update current state
self._current_text = '\n'.join(new_lines)
self._current_result = self.parser.parse(self._current_text)
self._wrapper = ASTWrapper(self._current_result)
# Create reference for new section
new_section = SectionReference(
id=f"section_new_{level}_{hash(title) % 10000}",
title=title,
level=level,
line_start=insert_line,
line_end=insert_line + len(new_section_lines) - 1,
path=after_section.path + [title]
)
return EditResult(
success=True,
operation=EditOperation.INSERT_SECTION,
modified_sections=[new_section],
errors=[],
warnings=[]
)
def _delete_section_impl(self, section_ref: SectionReference, cascade: bool) -> EditResult:
"""Internal implementation of section deletion."""
lines = self._current_text.split('\n')
# Calculate deletion range
start_line = section_ref.line_start
end_line = section_ref.line_end
if cascade:
# Find all child sections to delete
sections = self.get_sections()
child_sections = [
s for s in sections
if s.level > section_ref.level and s.line_start > section_ref.line_start
]
if child_sections:
# Extend deletion to include children
end_line = max(s.line_end for s in child_sections)
# Remove lines
new_lines = lines[:start_line] + lines[end_line + 1:]
# Update current state
self._current_text = '\n'.join(new_lines)
self._current_result = self.parser.parse(self._current_text)
self._wrapper = ASTWrapper(self._current_result)
return EditResult(
success=True,
operation=EditOperation.DELETE_SECTION,
modified_sections=[section_ref],
errors=[],
warnings=[]
)
def _move_section_impl(self, section_ref: SectionReference, target_position: int) -> EditResult:
"""Internal implementation of section moving."""
lines = self._current_text.split('\n')
# Extract section content
section_lines = lines[section_ref.line_start:section_ref.line_end + 1]
# Remove from current position
remaining_lines = lines[:section_ref.line_start] + lines[section_ref.line_end + 1:]
# Insert at new position
if target_position > len(remaining_lines):
target_position = len(remaining_lines)
new_lines = remaining_lines[:target_position] + section_lines + remaining_lines[target_position:]
# Update current state
self._current_text = '\n'.join(new_lines)
self._current_result = self.parser.parse(self._current_text)
self._wrapper = ASTWrapper(self._current_result)
return EditResult(
success=True,
operation=EditOperation.MOVE_SECTION,
modified_sections=[section_ref],
errors=[],
warnings=[]
)
def _validate_document_integrity(self) -> List[ParseError]:
"""Validate document structure and integrity."""
errors = []
# Check for parsing errors
if self._current_result.has_errors:
errors.extend(self._current_result.errors)
# Check heading hierarchy
headings = self._wrapper.get_headings()
prev_level = 0
for heading in headings:
level = heading['level']
if level > prev_level + 1:
errors.append(ParseError(
f"Heading level jump from h{prev_level} to h{level}: '{heading['content']}'",
line_number=heading.get('line'),
level=ErrorLevel.WARNING
))
prev_level = level
return errors
def _rollback_to_state(self, text: str, version: int) -> None:
"""Rollback to a previous state."""
self._current_text = text
self._current_result = self.parser.parse(text)
self._wrapper = ASTWrapper(self._current_result)
self._version = version
def _build_section_path(self, current_heading: Dict[str, Any],
previous_headings: List[Dict[str, Any]]) -> List[str]:
"""Build hierarchical path for a section."""
path = []
current_level = current_heading['level']
# Find parent sections
for heading in reversed(previous_headings):
if heading['level'] < current_level:
path.insert(0, heading['content'])
current_level = heading['level']
return path
def _calculate_section_end(self, section_index: int, all_headings: List[Dict[str, Any]]) -> int:
"""Calculate the end line of a section."""
current_heading = all_headings[section_index]
current_level = current_heading['level']
current_line = current_heading.get('line', 0)
# Find the next heading at same or higher level
for i in range(section_index + 1, len(all_headings)):
next_heading = all_headings[i]
if next_heading['level'] <= current_level:
return next_heading.get('line', current_line) - 1
# If no next heading, section goes to end of document
return len(self._current_text.split('\n')) - 1
def rollback_transaction(self, transaction_index: int = -1) -> EditResult:
"""Rollback to a specific transaction."""
if not self._transaction_log:
return EditResult(
success=False,
operation=EditOperation.DELETE_SECTION, # Placeholder
modified_sections=[],
errors=[ParseError("No transactions to rollback", level=ErrorLevel.ERROR)],
warnings=[]
)
if transaction_index == -1:
transaction_index = len(self._transaction_log) - 1
if transaction_index < 0 or transaction_index >= len(self._transaction_log):
return EditResult(
success=False,
operation=EditOperation.DELETE_SECTION, # Placeholder
modified_sections=[],
errors=[ParseError(f"Invalid transaction index: {transaction_index}", level=ErrorLevel.ERROR)],
warnings=[]
)
transaction = self._transaction_log[transaction_index]
if transaction.rollback_data:
self._rollback_to_state(transaction.rollback_data, int(transaction.timestamp or "0"))
# Remove rolled back transactions
self._transaction_log = self._transaction_log[:transaction_index]
return EditResult(
success=True,
operation=EditOperation.DELETE_SECTION, # Placeholder for rollback
modified_sections=[],
errors=[],
warnings=[]
)
return EditResult(
success=False,
operation=EditOperation.DELETE_SECTION, # Placeholder
modified_sections=[],
errors=[ParseError("No rollback data available", level=ErrorLevel.ERROR)],
warnings=[]
)
def get_transaction_history(self) -> List[EditTransaction]:
"""Get complete transaction history."""
return self._transaction_log.copy()
def to_markdown(self) -> str:
"""Get current markdown content."""
return self._current_text
def to_html(self) -> str:
"""Convert current document to HTML."""
return self.parser.render(self._current_result, 'html')
def get_statistics(self) -> Dict[str, Any]:
"""Get document statistics."""
sections = self.get_sections()
return {
'total_sections': len(sections),
'heading_levels': {f'h{i}': len([s for s in sections if s.level == i]) for i in range(1, 7)},
'total_lines': len(self._current_text.split('\n')),
'total_characters': len(self._current_text),
'edit_count': len(self._transaction_log),
'version': self._version,
'has_errors': self._current_result.has_errors,
'has_warnings': self._current_result.has_warnings
}
# Legacy class for compatibility - DEPRECATED
class MarkdownEditor(SafeMarkdownEditor):
"""DEPRECATED: Use SafeMarkdownEditor instead. This class is unsafe."""
def __init__(self, markdown_text: str):
print("WARNING: Using deprecated MarkdownEditor. Use SafeMarkdownEditor for safety.")
super().__init__(markdown_text)
"""Markdown editor built on top of QuantalogicMarkdownParser."""
def __init__(self, markdown_text: str):
"""Initialize editor with markdown text."""
self.parser = QuantalogicMarkdownParser()
self.original_text = markdown_text
self.result = self.parser.parse(markdown_text)
self.wrapper = ASTWrapper(self.result)
self.edit_history: List[Dict[str, Any]] = []
def get_sections(self) -> List[Dict[str, Any]]:
"""Get all sections with their metadata."""
headings = self.wrapper.get_headings()
sections = []
for i, heading in enumerate(headings):
section = {
'id': f"section_{i}",
'title': heading['content'],
'level': heading['level'],
'line': heading.get('line'),
'tokens': self._find_section_tokens(heading)
}
sections.append(section)
return sections
def _find_section_tokens(self, heading: Dict[str, Any]) -> List[Token]:
"""Find all tokens belonging to a section."""
if not isinstance(self.result.ast, list):
return []
tokens = self.result.ast
section_tokens = []
in_section = False
current_level = heading['level']
for token in tokens:
# Start collecting when we find the heading
if (token.type == 'heading_open' and
int(token.tag[1]) == current_level):
# Check if this is our target heading by checking next inline token
in_section = True
section_tokens.append(token)
elif in_section:
# Stop when we hit another heading of same or higher level
if (token.type == 'heading_open' and
int(token.tag[1]) <= current_level):
break
section_tokens.append(token)
return section_tokens
def update_section_content(self, section_id: str, content: str) -> ParseResult:
"""Update the content of a specific section."""
sections = self.get_sections()
target_section = None
for section in sections:
if section['id'] == section_id:
target_section = section
break
if not target_section:
error = ParseError(
message=f"Section '{section_id}' not found",
level=ErrorLevel.ERROR
)
self.result.errors.append(error)
return self.result
# Parse new content to tokens
new_content_result = self.parser.parse(content)
if new_content_result.has_errors:
self.result.errors.extend(new_content_result.errors)
return self.result
# Replace section content (keeping heading)
self._replace_section_content(target_section, new_content_result.ast)
# Track edit
self.edit_history.append({
'operation': 'update_section_content',
'section_id': section_id,
'content': content
})
return self._rebuild_result()
def add_section(self, after_section_id: str, heading_level: int,
title: str, content: str = "") -> ParseResult:
"""Add a new section after the specified section."""
sections = self.get_sections()
insert_position = len(self.result.ast) # Default to end
# Find insertion point
for i, section in enumerate(sections):
if section['id'] == after_section_id:
if i + 1 < len(sections):
next_section = sections[i + 1]
insert_position = self._find_token_position(next_section['tokens'][0])
break
# Create new section tokens
new_tokens = self._create_section_tokens(heading_level, title, content)
# Insert tokens
if isinstance(self.result.ast, list):
self.result.ast[insert_position:insert_position] = new_tokens
# Track edit
self.edit_history.append({
'operation': 'add_section',
'after_section_id': after_section_id,
'heading_level': heading_level,
'title': title,
'content': content
})
return self._rebuild_result()
def delete_section(self, section_id: str) -> ParseResult:
"""Delete a section and all its content."""
sections = self.get_sections()
target_section = None
for section in sections:
if section['id'] == section_id:
target_section = section
break
if not target_section:
error = ParseError(
message=f"Section '{section_id}' not found",
level=ErrorLevel.ERROR
)
self.result.errors.append(error)
return self.result
# Remove section tokens
if isinstance(self.result.ast, list):
for token in target_section['tokens']:
if token in self.result.ast:
self.result.ast.remove(token)
# Track edit
self.edit_history.append({
'operation': 'delete_section',
'section_id': section_id
})
return self._rebuild_result()
def move_section(self, section_id: str, direction: str) -> ParseResult:
"""Move a section up or down in the document."""
sections = self.get_sections()
target_index = None
# Find target section
for i, section in enumerate(sections):
if section['id'] == section_id:
target_index = i
break
if target_index is None:
error = ParseError(
message=f"Section '{section_id}' not found",
level=ErrorLevel.ERROR
)
self.result.errors.append(error)
return self.result
# Calculate new position
if direction == 'up' and target_index > 0:
new_index = target_index - 1
elif direction == 'down' and target_index < len(sections) - 1:
new_index = target_index + 1
else:
error = ParseError(
message=f"Cannot move section {direction}",
level=ErrorLevel.WARNING
)
self.result.warnings.append(error)
return self.result
# Reorder sections
sections[target_index], sections[new_index] = sections[new_index], sections[target_index]
# Rebuild token list
self._rebuild_from_sections(sections)
# Track edit
self.edit_history.append({
'operation': 'move_section',
'section_id': section_id,
'direction': direction
})
return self._rebuild_result()
def _create_section_tokens(self, level: int, title: str, content: str) -> List[Token]:
"""Create tokens for a new section."""
# Create heading tokens
heading_open = Token('heading_open', f'h{level}', 1)
heading_inline = Token('inline', '', 0)
heading_inline.content = title
heading_inline.children = [Token('text', '', 0)]
heading_inline.children[0].content = title
heading_close = Token('heading_close', f'h{level}', -1)
tokens = [heading_open, heading_inline, heading_close]
# Add content if provided
if content.strip():
content_result = self.parser.parse(content)
if isinstance(content_result.ast, list):
# Skip document-level tokens, add content tokens
content_tokens = [t for t in content_result.ast
if t.type not in ['document_open', 'document_close']]
tokens.extend(content_tokens)
return tokens
def _replace_section_content(self, section: Dict[str, Any], new_tokens: List[Token]):
"""Replace section content while keeping heading."""
if not isinstance(self.result.ast, list):
return
section_tokens = section['tokens']
if not section_tokens:
return
# Find heading tokens (keep these)
heading_tokens = []
content_start_idx = 0
for i, token in enumerate(section_tokens):
if token.type == 'heading_close':
heading_tokens = section_tokens[:i+1]
content_start_idx = i + 1
break
# Remove old content tokens from main token list
for token in section_tokens[content_start_idx:]:
if token in self.result.ast:
self.result.ast.remove(token)
# Insert new content tokens
if heading_tokens and isinstance(new_tokens, list):
last_heading_token = heading_tokens[-1]
insert_idx = self.result.ast.index(last_heading_token) + 1
# Filter out document wrapper tokens
content_tokens = [t for t in new_tokens
if t.type not in ['document_open', 'document_close']]
self.result.ast[insert_idx:insert_idx] = content_tokens
def _find_token_position(self, token: Token) -> int:
"""Find position of token in main token list."""
if isinstance(self.result.ast, list):
try:
return self.result.ast.index(token)
except ValueError:
pass
return len(self.result.ast) if isinstance(self.result.ast, list) else 0
def _rebuild_from_sections(self, sections: List[Dict[str, Any]]):
"""Rebuild token list from reordered sections."""
if not isinstance(self.result.ast, list):
return
new_tokens = []
for section in sections:
new_tokens.extend(section['tokens'])
self.result.ast[:] = new_tokens
def _rebuild_result(self) -> ParseResult:
"""Rebuild ParseResult after edits."""
# Re-wrap AST
self.wrapper = ASTWrapper(self.result)
# Update metadata
self.result.metadata['edit_count'] = len(self.edit_history)
self.result.metadata['last_edit'] = self.edit_history[-1] if self.edit_history else None
return self.result
def to_markdown(self) -> str:
"""Convert edited document back to markdown."""
return self.parser.render(self.result, 'markdown')
def to_html(self) -> str:
"""Convert edited document to HTML."""
return self.parser.render(self.result, 'html')
def validate_structure(self) -> List[ParseError]:
"""Validate document structure after edits."""
errors = []
# Check heading hierarchy
headings = self.wrapper.get_headings()
prev_level = 0
for heading in headings:
level = heading['level']
if level > prev_level + 1:
errors.append(ParseError(
message=f"Heading level jump from h{prev_level} to h{level}",
line_number=heading.get('line'),
level=ErrorLevel.WARNING
))
prev_level = level
return errors
def get_edit_history(self) -> List[Dict[str, Any]]:
"""Get history of all edits performed."""
return self.edit_history.copy()
def main():
"""
DEMONSTRATION: Improved API vs Original API
This example shows the difference between the unsafe original API
and the improved SafeMarkdownEditor with proper error handling,
atomicity, and validation.
"""
sample_markdown = """# Introduction
This is the introduction section.
## Getting Started
Here's how to get started.
## Advanced Topics
Advanced content here.
### Subsection
More details.
"""
print("=== IMPROVED SAFE API DEMONSTRATION ===\n")
# Create safe editor
try:
safe_editor = SafeMarkdownEditor(sample_markdown)
except ValueError as e:
print(f"Document validation failed: {e}")
return
# Show initial sections with rich metadata
print("Initial sections:")
sections = safe_editor.get_sections()
for section in sections:
path_str = " > ".join(section.path) if section.path else "Root"
print(f" {section.id}: {section.title} (Level {section.level}, Lines {section.line_start}-{section.line_end})")
print(f" Path: {path_str}")
print(f"\nDocument statistics: {safe_editor.get_statistics()}")
# Preview operation before applying
print("\n--- PREVIEWING SECTION UPDATE ---")
preview_result = safe_editor.preview_operation(
EditOperation.UPDATE_SECTION,
section_ref=sections[0],
content="This is the **updated** introduction with *emphasis* and validation."
)
if preview_result.success:
print("✓ Preview successful")
print("Preview content (first 200 chars):")
print(preview_result.preview[:200] + "..." if len(preview_result.preview or "") > 200 else preview_result.preview)
else:
print("✗ Preview failed:")
for error in preview_result.errors:
print(f" - {error}")
# Apply the operation atomically
print("\n--- APPLYING SECTION UPDATE ---")
update_result = safe_editor.update_section_content(
sections[0],
"This is the **updated** introduction with *emphasis* and validation."
)
if update_result.success:
print("✓ Section updated successfully")
print(f"Modified sections: {len(update_result.modified_sections)}")
else:
print("✗ Update failed:")
for error in update_result.errors:
print(f" - {error}")
# Try to add a new section
print("\n--- ADDING NEW SECTION ---")
insert_result = safe_editor.insert_section_after(
sections[1], # After "Getting Started"
2, # Level 2 heading
"Installation",
"Run `pip install quantalogic-markdown` to install the package."
)
if insert_result.success:
print("✓ Section added successfully")
else:
print("✗ Section addition failed:")
for error in insert_result.errors:
print(f" - {error}")
# Show transaction history
print("\n--- TRANSACTION HISTORY ---")
history = safe_editor.get_transaction_history()
for i, transaction in enumerate(history):
print(f" {i+1}. Operations: {len(transaction.operations)}")
for op in transaction.operations:
print(f" - {op['operation']}")
# Demonstrate rollback
if history:
print("\n--- DEMONSTRATING ROLLBACK ---")
rollback_result = safe_editor.rollback_transaction()
if rollback_result.success:
print("✓ Successfully rolled back last transaction")
else:
print("✗ Rollback failed:")
for error in rollback_result.errors:
print(f" - {error}")
# Final statistics
print("\n--- FINAL STATISTICS ---")
final_stats = safe_editor.get_statistics()
for key, value in final_stats.items():
print(f" {key}: {value}")
# Show the difference between safe and unsafe API
print("\n=== COMPARISON: UNSAFE vs SAFE API ===")
print("\nUNSAFE Original API Issues:")
print(" ✗ No validation - can create invalid documents")
print(" ✗ No rollback - permanent data loss on errors")
print(" ✗ String-based IDs - fragile and unreliable")
print(" ✗ Direct token manipulation - complex and error-prone")
print(" ✗ No thread safety - race conditions possible")
print(" ✗ Poor error messages - hard to debug")
print("\nSAFE Improved API Benefits:")
print(" ✓ Comprehensive validation - prevents invalid documents")
print(" ✓ Atomic operations with rollback - safe error recovery")
print(" ✓ Immutable section references - reliable identification")
print(" ✓ High-level semantic operations - easy to use")
print(" ✓ Preview functionality - see changes before applying")
print(" ✓ Rich error reporting - clear feedback")
print(" ✓ Transaction history - full audit trail")
print(" ✓ Document statistics - insight into structure")
print("\n--- FINAL DOCUMENT ---")
print(safe_editor.to_markdown())
if __name__ == "__main__":
main()