Advanced

    Advanced Topics

    Deep dives into performance, the plugin system, real-time collaboration patterns, security hardening, and accessibility standards compliance.

    Performance Optimization

    Handling large documents and memory efficiency

    For documents over 10,000 words, consider virtual scrolling or chunked editing.
    // Optimizing for large documents (10k+ words)
    
    // 1. Virtual scrolling for very long documents
    class VirtualDocumentEditor {
      constructor(container, content) {
        this.container = container;
        this.chunks = this.splitIntoChunks(content);
        this.visibleRange = { start: 0, end: 10 };
        this.editors = new Map();
        
        this.setupIntersectionObserver();
      }
      
      splitIntoChunks(html) {
        // Split by paragraphs, keeping 50-100 paragraphs per chunk
        const doc = new DOMParser().parseFromString(html, 'text/html');
        const paragraphs = Array.from(doc.body.children);
        const chunks = [];
        
        for (let i = 0; i < paragraphs.length; i += 50) {
          chunks.push(paragraphs.slice(i, i + 50).map(p => p.outerHTML).join(''));
        }
        
        return chunks;
      }
      
      setupIntersectionObserver() {
        this.observer = new IntersectionObserver((entries) => {
          entries.forEach(entry => {
            const chunkIndex = parseInt(entry.target.dataset.chunk);
            
            if (entry.isIntersecting) {
              this.activateChunk(chunkIndex);
            } else {
              this.deactivateChunk(chunkIndex);
            }
          });
        }, { rootMargin: '200px' });
      }
      
      activateChunk(index) {
        if (this.editors.has(index)) return;
        
        const chunkEl = this.container.querySelector(`[data-chunk="${index}"]`);
        const editor = Scribe.init(chunkEl, {
          // Minimal features for inactive chunks
          onChange: (html) => {
            this.chunks[index] = html;
          }
        });
        
        this.editors.set(index, editor);
      }
      
      deactivateChunk(index) {
        const editor = this.editors.get(index);
        if (editor) {
          // Save content before destroying
          this.chunks[index] = editor.getHTML();
          editor.destroy();
          this.editors.delete(index);
        }
      }
    }
    
    // 2. Debounced change handlers
    const editor = Scribe.init('#editor', {
      // Don't use onChange for expensive operations
    });
    
    // Debounce expensive operations
    let saveTimer;
    editor.on('change', (html) => {
      clearTimeout(saveTimer);
      saveTimer = setTimeout(() => {
        saveToServer(html);
        updateWordCount(html);
      }, 500);
    });
    
    // 3. Use requestAnimationFrame for DOM measurements
    editor.on('selectionChange', (selection) => {
      requestAnimationFrame(() => {
        positionToolbar(selection?.rect);
      });
    });

    Plugin Architecture

    Extend Scribe with custom functionality

    Plugin Lifecycle

    ┌─────────────────────────────────────────────────┐
    │  Plugin Registration                            │
    │                                                 │
    │  createEditor({ plugins: [myPlugin] })          │
    │                    │                            │
    │                    ▼                            │
    │  ┌──────────────────────────────────────────┐  │
    │  │  For each plugin:                         │  │
    │  │  1. Register commands → commands.set()    │  │
    │  │  2. Register shortcuts → shortcuts.push() │  │
    │  │  3. Call init(editor)                     │  │
    │  └──────────────────────────────────────────┘  │
    │                    │                            │
    │                    ▼                            │
    │  ┌──────────────────────────────────────────┐  │
    │  │  Editor ready - plugins active            │  │
    │  │  Commands available via editor.exec()     │  │
    │  └──────────────────────────────────────────┘  │
    │                    │                            │
    │                    ▼                            │
    │  ┌──────────────────────────────────────────┐  │
    │  │  editor.destroy()                         │  │
    │  │  → Calls plugin.destroy() for cleanup     │  │
    │  └──────────────────────────────────────────┘  │
    └─────────────────────────────────────────────────┘
    // Scribe Plugin Interface
    interface Plugin {
      name: string;
      
      // Lifecycle hooks
      init?(editor: EditorInstance): void;
      destroy?(): void;
      
      // Extend functionality
      commands?: Record<string, CommandHandler>;
      shortcuts?: ShortcutConfig[];
      toolbarItems?: ToolbarItem[];
    }
    
    // Example: Custom highlight plugin
    const highlightPlugin: Plugin = {
      name: 'highlight',
      
      commands: {
        highlight: (editor, color: string = '#ffff00') => {
          const selection = window.getSelection();
          if (!selection || selection.isCollapsed) return;
          
          const range = selection.getRangeAt(0);
          const mark = document.createElement('mark');
          mark.style.backgroundColor = color;
          
          range.surroundContents(mark);
        },
        
        removeHighlight: (editor) => {
          const selection = window.getSelection();
          if (!selection) return;
          
          const mark = selection.anchorNode?.parentElement;
          if (mark?.tagName === 'MARK') {
            const parent = mark.parentNode;
            while (mark.firstChild) {
              parent?.insertBefore(mark.firstChild, mark);
            }
            parent?.removeChild(mark);
          }
        }
      },
      
      shortcuts: [
        { key: 'h', ctrl: true, shift: true, command: 'highlight' }
      ],
      
      toolbarItems: [
        {
          name: 'highlight',
          icon: 'highlighter',
          title: 'Highlight (Ctrl+Shift+H)',
          command: 'highlight',
          isActive: (state) => false // Would need custom state detection
        }
      ],
      
      init(editor) {
        console.log('Highlight plugin initialized');
        
        // Add custom styles
        const style = document.createElement('style');
        style.textContent = `
          mark { padding: 0.1em 0.2em; border-radius: 0.2em; }
        `;
        editor.getDocument().head.appendChild(style);
      },
      
      destroy() {
        console.log('Highlight plugin destroyed');
      }
    };
    
    // Register plugin
    const editor = createEditor({
      target: '#editor',
      plugins: [highlightPlugin]
    });
    
    // Use plugin command
    editor.exec('highlight', '#ff0');

    Real-time Collaboration

    Multi-user editing with conflict resolution

    Collaboration requires a WebSocket server. For production, use Yjs with a provider like Liveblocks, Hocuspocus, or y-websocket.
    // Collaboration architecture overview
    // 
    // Two main approaches:
    // 1. Operational Transformation (OT) - Google Docs style
    // 2. CRDT - Yjs, Automerge style
    //
    // Scribe is designed to be "collaboration-ready" - you can integrate
    // either approach without modifying the core editor.
    
    // Basic WebSocket sync (last-write-wins)
    class BasicCollaboration {
      constructor(editor, roomId) {
        this.editor = editor;
        this.ws = new WebSocket(`wss://collab.example.com/${roomId}`);
        this.isRemoteChange = false;
        
        this.ws.onmessage = this.handleRemoteChange.bind(this);
        this.editor.on('change', this.handleLocalChange.bind(this));
      }
      
      handleLocalChange(html) {
        if (this.isRemoteChange) return;
        
        this.ws.send(JSON.stringify({
          type: 'update',
          html,
          timestamp: Date.now()
        }));
      }
      
      handleRemoteChange(event) {
        const { type, html } = JSON.parse(event.data);
        if (type !== 'update') return;
        
        // Prevent echo
        this.isRemoteChange = true;
        this.editor.setHTML(html);
        this.isRemoteChange = false;
      }
    }

    Security

    XSS prevention and content sandboxing

    Never Allow

    <script>, <iframe>,onclick, javascript:

    Careful With

    <style>, style attr,data: URLs

    Always Sanitize

    Pasted content, user uploads, external HTML
    // XSS Prevention in Rich Text Editors
    //
    // The #1 security concern: user-generated HTML can contain malicious scripts
    
    // Scribe's built-in sanitizer
    const editor = Scribe.init('#editor', {
      sanitize: {
        // Whitelist approach - only allow specific tags
        allowedTags: [
          'p', 'br', 'span', 'div',
          'strong', 'b', 'em', 'i', 'u', 's', 'del', 'ins',
          'a', 'img',
          'ul', 'ol', 'li',
          'blockquote', 'pre', 'code',
          'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
          'table', 'tr', 'td', 'th', 'thead', 'tbody',
          'hr'
        ],
        
        // Whitelist attributes per tag
        allowedAttributes: {
          'a': ['href', 'target', 'rel'],
          'img': ['src', 'alt', 'width', 'height'],
          'td': ['colspan', 'rowspan'],
          '*': ['class', 'id'] // All tags can have class/id
        },
        
        // URL schemes - blocks javascript:, data:, etc.
        allowedSchemes: ['http', 'https', 'mailto', 'tel'],
        
        // Remove empty elements
        stripEmpty: true
      }
    });
    
    // What the sanitizer removes:
    // - <script> tags
    // - <iframe> tags
    // - onclick, onerror, etc. event handlers
    // - javascript: URLs
    // - data: URLs (except for allowed images)
    // - <style> tags and style attributes (optional)
    // - Unknown tags

    Accessibility

    ARIA, keyboard navigation, and screen reader support

    Required ARIA

    role="textbox"
    aria-multiline="true"
    aria-label="..."
    aria-describedby="..."

    Toolbar ARIA

    role="toolbar"
    aria-label="Formatting"
    aria-pressed on toggle buttons
    Arrow key navigation
    // Accessibility best practices for RTEs
    
    // 1. ARIA attributes
    const editorEl = document.getElementById('editor');
    editorEl.setAttribute('role', 'textbox');
    editorEl.setAttribute('aria-multiline', 'true');
    editorEl.setAttribute('aria-label', 'Rich text editor');
    editorEl.setAttribute('aria-describedby', 'editor-help');
    
    // Help text for screen readers
    const helpText = document.createElement('div');
    helpText.id = 'editor-help';
    helpText.className = 'sr-only'; // Visually hidden
    helpText.textContent = 'Use Ctrl+B for bold, Ctrl+I for italic, Ctrl+K for link';
    
    // 2. Toolbar accessibility
    function createAccessibleToolbar(editor) {
      const toolbar = document.createElement('div');
      toolbar.setAttribute('role', 'toolbar');
      toolbar.setAttribute('aria-label', 'Formatting options');
      toolbar.setAttribute('aria-controls', 'editor');
      
      // Button with proper ARIA
      const boldBtn = document.createElement('button');
      boldBtn.setAttribute('aria-label', 'Bold');
      boldBtn.setAttribute('aria-pressed', 'false');
      boldBtn.innerHTML = '<strong>B</strong>';
      
      boldBtn.onclick = () => {
        editor.bold();
        boldBtn.setAttribute('aria-pressed', 
          boldBtn.getAttribute('aria-pressed') === 'true' ? 'false' : 'true'
        );
      };
      
      toolbar.appendChild(boldBtn);
      
      // Keyboard navigation within toolbar
      toolbar.addEventListener('keydown', (e) => {
        const buttons = toolbar.querySelectorAll('button');
        const current = document.activeElement;
        const index = Array.from(buttons).indexOf(current);
        
        if (e.key === 'ArrowRight') {
          e.preventDefault();
          buttons[(index + 1) % buttons.length].focus();
        } else if (e.key === 'ArrowLeft') {
          e.preventDefault();
          buttons[(index - 1 + buttons.length) % buttons.length].focus();
        }
      });
      
      return toolbar;
    }
    
    // 3. Announce formatting changes
    function announceFormat(format, applied) {
      const announcement = document.createElement('div');
      announcement.setAttribute('role', 'status');
      announcement.setAttribute('aria-live', 'polite');
      announcement.className = 'sr-only';
      announcement.textContent = `${format} ${applied ? 'applied' : 'removed'}`;
      
      document.body.appendChild(announcement);
      setTimeout(() => announcement.remove(), 1000);
    }
    
    // 4. Focus management
    editor.on('focus', () => {
      // Ensure toolbar buttons show focus state
      toolbar.classList.add('editor-focused');
    });
    
    editor.on('blur', () => {
      toolbar.classList.remove('editor-focused');
    });