npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@snapblog/editor

v0.0.34

Published

The core editor for Snapblog.

Readme

@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
  2. Architecture
  3. Installation & Setup
  4. Core Components
  5. Extension System
  6. Available Extensions
  7. API Reference
  8. Hooks
  9. Type System
  10. Development Guide
  11. Testing
  12. 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

npm install @snapblog/editor

Peer Dependencies

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

Basic Setup

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.

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.

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.

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

Extension System

BaseExtension

All extensions inherit from the BaseExtension class:

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:

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:

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

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

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:

// 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

// 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

// 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

// 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:

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:

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

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

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

Extension Development

Development Workflow

  1. Create Extension File

    # Create your extension file
    touch src/extensions/MyExtension.ts
  2. Implement Extension

    import { BaseExtension } from '@snapblog/editor';
       
    export class MyExtension extends BaseExtension {
      get name() {
        return 'myExtension';
      }
         
      // Add your implementation
    }
  3. Export Extension

    // In src/extensions/index.ts
    export { MyExtension } from './MyExtension';
  4. Test Extension

    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:

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

2. Error Handling

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

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

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

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

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

# 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

# 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/:

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

// 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.