npm package: @snapblog/editor
Description: The core editor for Snapblog.
Latest version: 0.0.34

# @snapblog/editor

> **Version**: 0.0.33  
> **Mission**: Transform Snapblog Editor into the premier TypeScript-native rich text editor that surpasses TipTap, Lexical, and Quill.

## Table of Contents

1. [Overview](#overview)
2. [Architecture](#architecture)
3. [Installation & Setup](#installation--setup)
4. [Core Components](#core-components)
5. [Extension System](#extension-system)
6. [Available Extensions](#available-extensions)
7. [API Reference](#api-reference)
8. [Hooks](#hooks)
9. [Type System](#type-system)
10. [Development Guide](#development-guide)
11. [Testing](#testing)
12. [Performance](#performance)

## Overview

@snapblog/editor is a powerful, TypeScript-native rich text editor built on top of ProseMirror. It provides a modular extension system, React integration, and a comprehensive API for building sophisticated text editing experiences.

### Key Features

- **TypeScript-First**: Full type safety with comprehensive type definitions
- **Modular Architecture**: Extension-based system for maximum flexibility
- **React Integration**: Native React components and hooks
- **ProseMirror Foundation**: Built on the robust ProseMirror framework
- **Command System**: Type-safe command registry with chainable API
- **State Management**: Centralized state management with EditorController
- **Event System**: Comprehensive event handling and subscription
- **Testing Support**: Built-in testing utilities and Jest configuration

## Architecture

```
@snapblog/editor/
├── core/                    # Core editor functionality
│   ├── Editor.ts           # Main editor class
│   ├── extension-system/   # Extension management
│   ├── models/            # Core models (CommandManager, EventManager)
│   ├── store/             # State management (EditorController)
│   ├── types/             # TypeScript definitions
│   ├── utils/             # Utility functions
│   └── testing/           # Testing utilities
├── components/            # React components
├── contexts/              # React context providers
├── extensions/            # Built-in extensions
└── hooks/                 # React hooks
```

### Core Principles

1. **Modularity**: Everything is an extension
2. **Type Safety**: Comprehensive TypeScript support
3. **Performance**: Optimized for large documents
4. **Extensibility**: Easy to extend and customize
5. **Developer Experience**: Intuitive API and excellent tooling

## Installation & Setup

### Installation

```bash
npm install @snapblog/editor
```

### Peer Dependencies

```bash
npm install react@^19.1.0 react-dom@^19.1.0
```

### Basic Setup

```tsx
import React from 'react';
import { EditorProvider, EditorComponent } from '@snapblog/editor';
import { BoldExtension, ItalicExtension, ParagraphExtension } from '@snapblog/editor';

function App() {
  const config = {
    extensions: [
      new ParagraphExtension(),
      new BoldExtension(),
      new ItalicExtension(),
    ],
    content: '<p>Hello, world!</p>',
  };

  return (
    <EditorProvider config={config}>
      <EditorComponent />
    </EditorProvider>
  );
}
```

## Core Components

### Editor Class

The main `Editor` class is the heart of the editor system.

```typescript
import { Editor, EditorConfig } from '@snapblog/editor';

const config: EditorConfig = {
  element: document.getElementById('editor'),
  extensions: [/* extensions */],
  content: 'Initial content',
};

const editor = new Editor(config);
```

#### Key Properties

- `view: EditorView` - ProseMirror editor view
- `state: EditorState` - Current editor state
- `schema: Schema` - Document schema
- `commands` - Type-safe command registry
- `chain` - Chainable command API
- `$state` - State inspection utilities
- `history` - Undo/redo functionality
- `plugins` - Plugin management

#### Key Methods

- `executeCommand(command)` - Execute a command
- `subscribe(event, callback)` - Subscribe to events
- `getHTML()` - Get HTML content
- `getJSON()` - Get JSON representation
- `setContent(content)` - Set editor content
- `destroy()` - Clean up resources

### EditorProvider

React context provider that manages the editor instance.

```tsx
interface EditorProviderProps {
  children: React.ReactNode;
  config: Omit<EditorConfig, 'element'>;
  onReady?: (editor: EditorInstance) => void;
  onError?: (error: Error) => void;
}
```

### EditorComponent

Simple React component that renders the editor container.

```tsx
const EditorComponent = forwardRef<HTMLDivElement>((props, ref) => {
  return <div ref={ref} id="editor-container" {...props} />;
});
```

## Extension System

### BaseExtension

All extensions inherit from the `BaseExtension` class:

```typescript
export abstract class BaseExtension<Options = any, Storage = any> {
  abstract get name(): string;
  
  // Schema methods
  addNodes(): Record<string, NodeSpec> { return {}; }
  addMarks(): Record<string, MarkSpec> { return {}; }
  
  // Interaction methods
  addKeymap(schema: Schema): Keymap { return {}; }
  addInputRules(schema: Schema): InputRule[] { return []; }
  addProseMirrorPlugins(schema: Schema): Plugin[] { return []; }
  
  // Command and state methods
  addCommands(schema: Schema): Record<string, any> { return {}; }
  addState(schema: Schema): Record<string, any> { return {}; }
  
  // Lifecycle methods
  async onInit(schema: Schema): Promise<void> {}
  async onDestroy(): Promise<void> {}
  
  // Configuration methods
  withShortcuts(shortcuts: string[]): this
  withInputRules(rules: string[]): this
  withHTMLAttributes(attrs: Record<string, any>): this
  withDependencies(deps: string[]): this
}
```

### Extension Types

#### Mark Extensions (Inline Formatting)

Marks are inline formatting that can be applied to text ranges:

```typescript
import { toggleMark } from 'prosemirror-commands';
import { BaseExtension } from '@snapblog/editor';

// Extend the Commands interface
declare module "@snapblog/editor" {
  interface Commands<ReturnType> {
    highlight: {
      toggle: (options?: { color?: string }) => ReturnType;
      setColor: (options: { color: string }) => ReturnType;
      unset: () => ReturnType;
    }
  }
}

export class HighlightExtension extends BaseExtension {
  get name() {
    return 'highlight';
  }

  addMarks() {
    return {
      highlight: {
        attrs: {
          color: { default: '#ffff00' },
        },
        parseDOM: [
          {
            tag: 'mark[data-color]',
            getAttrs: (node) => ({
              color: (node as HTMLElement).getAttribute('data-color'),
            }),
          },
        ],
        toDOM: (mark) => [
          'mark',
          {
            'data-color': mark.attrs.color,
            style: `background-color: ${mark.attrs.color}`,
          },
          0,
        ],
      },
    };
  }

  addCommands(schema) {
    return {
      highlight: {
        toggle: (options = {}) => toggleMark(schema.marks.highlight, {
          color: options.color || '#ffff00',
        }),
        setColor: (options) => toggleMark(schema.marks.highlight, {
          color: options.color,
        }),
        unset: () => toggleMark(schema.marks.highlight),
      },
    };
  }

  addKeymap(schema) {
    return {
      'Mod-Shift-h': this.addCommands(schema).highlight.toggle(),
    };
  }
}
```

#### Node Extensions (Block Elements)

Nodes are block-level or inline elements that form the document structure:

```typescript
import { setBlockType } from 'prosemirror-commands';
import { textblockTypeInputRule } from 'prosemirror-inputrules';
import { BaseExtension } from '@snapblog/editor';

declare module "@snapblog/editor" {
  interface Commands<ReturnType> {
    callout: {
      set: (options?: { type?: 'info' | 'warning' | 'error' }) => ReturnType;
      toggle: (options?: { type?: 'info' | 'warning' | 'error' }) => ReturnType;
    }
  }
}

export class CalloutExtension extends BaseExtension {
  get name() {
    return 'callout';
  }

  addNodes() {
    return {
      callout: {
        attrs: {
          type: { default: 'info' },
        },
        content: 'block+',
        group: 'block',
        defining: true,
        parseDOM: [
          {
            tag: 'div[data-callout]',
            getAttrs: (node) => ({
              type: (node as HTMLElement).getAttribute('data-type') || 'info',
            }),
          },
        ],
        toDOM: (node) => [
          'div',
          {
            'data-callout': '',
            'data-type': node.attrs.type,
            class: `callout callout-${node.attrs.type}`,
          },
          0,
        ],
      },
    };
  }

  addCommands(schema) {
    return {
      callout: {
        set: (options = {}) => setBlockType(schema.nodes.callout, {
          type: options.type || 'info',
        }),
        toggle: (options = {}) => (state, dispatch) => {
          const { $from } = state.selection;
          const currentNode = $from.node();
          
          if (currentNode.type === schema.nodes.callout) {
            return setBlockType(schema.nodes.paragraph)(state, dispatch);
          } else {
            return setBlockType(schema.nodes.callout, {
              type: options.type || 'info',
            })(state, dispatch);
          }
        },
      },
    };
  }

  addInputRules(schema) {
    return [
      textblockTypeInputRule(
        /^:::(info|warning|error)\s$/,
        schema.nodes.callout,
        (match) => ({ type: match[1] })
      ),
    ];
  }

  addKeymap(schema) {
    return {
      'Mod-Shift-c': this.addCommands(schema).callout.toggle(),
    };
  }
}
```

### Extension with Options and Storage

```typescript
interface MyExtensionOptions {
  enabled: boolean;
  color: string;
  shortcuts: string[];
}

interface MyExtensionStorage {
  count: number;
  lastUsed: Date;
}

export class MyExtension extends BaseExtension<MyExtensionOptions, MyExtensionStorage> {
  get name() {
    return 'myExtension';
  }

  constructor(options: Partial<MyExtensionOptions> = {}) {
    super({
      enabled: true,
      color: '#000000',
      shortcuts: ['Mod-m'],
      ...options,
    });
  }

  addStorage(): MyExtensionStorage {
    return {
      count: 0,
      lastUsed: new Date(),
    };
  }
}
```

## Available Extensions

### Text Formatting

- **BoldExtension** - Bold text formatting (`Mod-b`)
- **ItalicExtension** - Italic text formatting (`Mod-i`)
- **UnderlineExtension** - Underline text formatting (`Mod-u`)
- **StrikethroughExtension** - Strikethrough text formatting
- **HighlightExtension** - Text highlighting
- **SubscriptExtension** - Subscript formatting
- **SuperscriptExtension** - Superscript formatting

### Structure

- **ParagraphExtension** - Basic paragraph support
- **HeadingExtension** - Heading levels (H1-H6)
- **HardBreakExtension** - Line breaks (`Shift-Enter`)
- **ListsExtension** - Ordered and unordered lists

### Advanced

- **LinkExtension** - Link support with URL validation
- **CodeBlockExtension** - Code block formatting
- **HistoryExtension** - Undo/redo functionality
- **DropCursorExtension** - Visual drop cursor
- **BaseKeymapExtension** - Basic keyboard shortcuts
- **CustomStateExtension** - Custom state management

### Usage Example

```typescript
import {
  ParagraphExtension,
  BoldExtension,
  ItalicExtension,
  HeadingExtension,
  ListsExtension,
  LinkExtension,
  HistoryExtension,
} from '@snapblog/editor';

const extensions = [
  new ParagraphExtension(),
  new BoldExtension(),
  new ItalicExtension(),
  new HeadingExtension({ levels: [1, 2, 3] }),
  new ListsExtension(),
  new LinkExtension({ openOnClick: true }),
  new HistoryExtension({ depth: 100 }),
];
```

## API Reference

### Commands API

The editor provides a type-safe command system:

```typescript
// Direct command execution
editor.commands.bold.toggle();
editor.commands.heading.setLevel({ level: 2 });

// Chainable commands
editor.chain()
  .bold.toggle()
  .italic.toggle()
  .run();

// Command availability checking
if (editor.can.bold.toggle()) {
  editor.commands.bold.toggle();
}

// Active state checking
const isBold = editor.isActive.bold();
const isHeading = editor.isActive.heading({ level: 2 });
```

### State API

```typescript
// State inspection
const currentState = editor.$state.current();
const selection = editor.$state.selection();
const doc = editor.$state.doc();

// State utilities
const isEmpty = editor.$state.isEmpty();
const canUndo = editor.$state.canUndo();
const canRedo = editor.$state.canRedo();
```

### Content API

```typescript
// Get content
const html = editor.getHTML();
const json = editor.getJSON();

// Set content
editor.setContent('<p>New content</p>');
editor.setContent({ type: 'doc', content: [/* nodes */] });

// Clear content
editor.commands.clearContent();
```

### Event API

```typescript
// Subscribe to events
const unsubscribe = editor.subscribe('update', ({ editor, transaction }) => {
  console.log('Editor updated', { editor, transaction });
});

// Unsubscribe
unsubscribe();

// Available events
// - 'update' - Content changed
// - 'selectionUpdate' - Selection changed
// - 'focus' - Editor focused
// - 'blur' - Editor blurred
// - 'destroy' - Editor destroyed
```

## Hooks

### useEditorController

Access the EditorController instance:

```tsx
import { useEditorController } from '@snapblog/editor';

function MyComponent() {
  const controller = useEditorController();
  const editor = controller.getEditor();
  
  return (
    <button onClick={() => editor?.commands.bold.toggle()}>
      Toggle Bold
    </button>
  );
}
```

### useEditorState

Subscribe to editor state changes:

```tsx
import { useEditorState } from '@snapblog/editor';

function MyComponent() {
  const { editor, isReady, errors } = useEditorState();
  
  if (!isReady) return <div>Loading...</div>;
  if (errors.length > 0) return <div>Error: {errors[0].message}</div>;
  
  return <div>Editor is ready!</div>;
}
```

## Type System

### Command Types

```typescript
interface Commands<ReturnType> {
  bold: {
    toggle: () => ReturnType;
  };
  italic: {
    toggle: () => ReturnType;
  };
  heading: {
    setLevel: (options: { level: number }) => ReturnType;
  };
  // ... other commands
}

interface TypeSafeCommandRegistry {
  commands: Commands<boolean>;
  can: Commands<boolean>;
  isActive: Commands<boolean>;
  chain: () => ChainedCommands;
  $state: StateAPI;
  history: HistoryAPI;
  plugins: PluginAPI;
}
```

### Extension State Types

```typescript
interface ExtensionStates {
  bold: {
    isActive: boolean;
    canEnable: boolean;
    canDisable: boolean;
    attributes: Record<string, any>;
  };
  // ... other extension states
}
```

## Extension Development

### Development Workflow

1. **Create Extension File**
   ```bash
   # Create your extension file
   touch src/extensions/MyExtension.ts
   ```

2. **Implement Extension**
   ```typescript
   import { BaseExtension } from '@snapblog/editor';
   
   export class MyExtension extends BaseExtension {
     get name() {
       return 'myExtension';
     }
     
     // Add your implementation
   }
   ```

3. **Export Extension**
   ```typescript
   // In src/extensions/index.ts
   export { MyExtension } from './MyExtension';
   ```

4. **Test Extension**
   ```typescript
   import { createTestEditor } from '@snapblog/editor/testing';
   import { MyExtension } from './MyExtension';
   
   const editor = createTestEditor({
     extensions: [new MyExtension()],
   });
   ```

### Extension Best Practices

#### 1. Type Safety
Always extend the Commands and ExtensionStates interfaces:

```typescript
declare module "@snapblog/editor" {
  interface Commands<ReturnType> {
    myExtension: {
      toggle: () => ReturnType;
    }
  }
  
  interface ExtensionStates {
    myExtension: {
      isActive: boolean;
      canToggle: boolean;
    }
  }
}
```

#### 2. Error Handling
```typescript
export class MyExtension extends BaseExtension {
  addCommands(schema) {
    return {
      myExtension: {
        toggle: () => (state, dispatch) => {
          try {
            // Your command logic
            return true;
          } catch (error) {
            console.error('MyExtension command failed:', error);
            return false;
          }
        },
      },
    };
  }
}
```

#### 3. Performance Optimization
```typescript
export class MyExtension extends BaseExtension {
  // Cache expensive computations
  private cachedSchema: Schema | null = null;
  private cachedCommands: any = null;
  
  addCommands(schema) {
    if (this.cachedSchema === schema && this.cachedCommands) {
      return this.cachedCommands;
    }
    
    this.cachedSchema = schema;
    this.cachedCommands = {
      // Your commands
    };
    
    return this.cachedCommands;
  }
}
```

#### 4. Lifecycle Management
```typescript
export class MyExtension extends BaseExtension {
  private eventListeners: (() => void)[] = [];
  
  async onInit(schema: Schema): Promise<void> {
    // Setup resources
    const cleanup = this.setupEventListener();
    this.eventListeners.push(cleanup);
  }
  
  async onDestroy(): Promise<void> {
    // Cleanup resources
    this.eventListeners.forEach(cleanup => cleanup());
    this.eventListeners = [];
  }
}
```

### Testing Extensions

#### Unit Testing
```typescript
import { createTestEditor } from '@snapblog/editor/testing';
import { MyExtension } from '../MyExtension';

describe('MyExtension', () => {
  let editor: any;
  
  beforeEach(() => {
    editor = createTestEditor({
      extensions: [new MyExtension()],
      content: '<p>Test content</p>',
    });
  });
  
  afterEach(() => {
    editor?.destroy();
  });
  
  it('should toggle correctly', () => {
    expect(editor.isActive.myExtension()).toBe(false);
    
    editor.commands.myExtension.toggle();
    expect(editor.isActive.myExtension()).toBe(true);
    
    editor.commands.myExtension.toggle();
    expect(editor.isActive.myExtension()).toBe(false);
  });
});
```

#### Integration Testing
```typescript
import { render, screen } from '@testing-library/react';
import { EditorProvider, EditorComponent } from '@snapblog/editor';
import { MyExtension } from '../MyExtension';

test('MyExtension works in React component', () => {
  const config = {
    extensions: [new MyExtension()],
    content: '<p>Test</p>',
  };
  
  render(
    <EditorProvider config={config}>
      <EditorComponent />
    </EditorProvider>
  );
  
  // Test your extension behavior
});
```

### Troubleshooting

#### Common Issues

1. **Extension Not Loading**
   - Check if extension is properly exported
   - Verify extension name is unique
   - Ensure no circular dependencies

2. **Commands Not Working**
   - Verify Commands interface is extended
   - Check command return types
   - Ensure schema is passed correctly

3. **Type Errors**
   - Make sure to declare module augmentations
   - Check TypeScript configuration
   - Verify import paths

4. **Performance Issues**
   - Profile extension methods
   - Cache expensive operations
   - Avoid heavy computations in render cycle

## Development Guide

### Project Structure

```
src/
├── core/                    # Core functionality
│   ├── Editor.ts           # Main editor class
│   ├── extension-system/   # Extension base classes
│   ├── models/            # Core models
│   ├── store/             # State management
│   ├── types/             # Type definitions
│   └── utils/             # Utilities
├── components/            # React components
├── contexts/              # React contexts
├── extensions/            # Built-in extensions
├── hooks/                 # React hooks
└── index.ts              # Main export
```

### Building

```bash
# Build the package
npm run build

# The build outputs to dist/
# - dist/index.es.js (ES modules)
# - dist/index.umd.js (UMD)
# - dist/index.d.ts (TypeScript definitions)
```

### Development Workflow

1. Make changes to source files
2. Run tests: `npm test`
3. Build: `npm run build`
4. Test in snapblog package

## Testing

### Test Structure

```
__tests__/
├── Editor.test.ts          # Core editor tests
└── custom-nodes.test.tsx   # Custom node tests
```

### Running Tests

```bash
# Run all tests
npm test

# Run tests in watch mode
npm test -- --watch

# Run tests with coverage
npm test -- --coverage
```

### Test Utilities

The editor provides testing utilities in `core/testing/`:

```typescript
import { createTestEditor } from '@snapblog/editor/testing';

const editor = createTestEditor({
  extensions: [new BoldExtension()],
  content: '<p>Test content</p>',
});

// Test commands
editor.commands.bold.toggle();
expect(editor.isActive.bold()).toBe(true);
```

## Performance

### Optimization Strategies

1. **Lazy Loading**: Extensions are loaded on-demand
2. **Memoization**: React components use proper memoization
3. **Efficient Updates**: Only necessary DOM updates are performed
4. **Plugin Optimization**: Plugins are optimized for performance

### Performance Monitoring

```typescript
// Monitor editor performance
editor.subscribe('update', ({ transaction }) => {
  console.log('Transaction time:', transaction.time);
});

// Check document size
const docSize = editor.$state.doc().nodeSize;
console.log('Document size:', docSize);
```

### Best Practices

1. **Minimize Extensions**: Only include necessary extensions
2. **Optimize Content**: Use efficient content structures
3. **Batch Updates**: Group multiple changes into single transactions
4. **Memory Management**: Properly destroy editor instances

---

## Contributing

See the main project's `IMPROVEMENT_CHECKLIST.md` for current development priorities and contribution guidelines.

## License

See the main project's LICENSE file.