Transformer Middleware

Overview

The BlockSuite Transformer system is a powerful framework for handling document transformations, including import/export operations, content modifications, and data conversions. It provides a flexible and extensible architecture through its middleware and slots system.

Transformer Basics

Core Concepts

The transformer system revolves around these key concepts:

  • Transformer: The main class that handles document transformations

  • Middleware: Extensions that can modify the transformation process

  • Slots: Hooks into different stages of the transformation

  • Snapshots: Data structures representing document states

Basic Usage

// Creating a transformer
const transformer = new Transformer({
  schema,
  blobCRUD,
  docCRUD,
  middlewares: [] // Optional middleware array
});

// Using transformer with middleware
const transformer = doc.getTransformer([
  titleMiddleware(doc.workspace.meta.docMetas),
  embedSyncedDocMiddleware('content'),
  defaultImageProxyMiddleware
]);

Middleware System

What is Middleware?

Middleware in BlockSuite's transformer system are functions that can intercept and modify the transformation process. They provide a way to extend the core functionality without modifying the base code.

Middleware Structure

type TransformerMiddleware = (options: TransformerMiddlewareOptions) => void;

type TransformerMiddlewareOptions = {
  assetsManager: AssetsManager;
  slots: TransformerSlots;
  docCRUD: DocCRUD;
  adapterConfigs: Map<string, unknown>;
  transformerConfigs: Map<string, unknown>;
};

Creating Middleware

const myMiddleware = (): TransformerMiddleware => {
  return ({ slots, adapterConfigs }) => {
    // Middleware implementation
  };
};

Slots System

Overview

The slots system provides hooks into different stages of the transformation process through a reactive, type-safe interface.

Slot Types

type TransformerSlots = {
  beforeImport: Subject<BeforeImportPayload>;
  afterImport: Subject<AfterImportPayload>;
  beforeExport: Subject<BeforeExportPayload>;
  afterExport: Subject<AfterExportPayload>;
};

Payload Types

BeforeImport Payloads

type BeforeImportPayload =
  | BeforeImportBlockPayload
  | {
      snapshot: SliceSnapshot;
      type: 'slice';
    }
  | {
      snapshot: DocSnapshot;
      type: 'page';
    }
  | {
      snapshot: CollectionInfoSnapshot;
      type: 'info';
    };

AfterImport Payloads

type AfterImportPayload =
  | AfterImportBlockPayload
  | {
      snapshot: DocSnapshot;
      type: 'page';
      page: Store;
    }
  | {
      snapshot: SliceSnapshot;
      type: 'slice';
      slice: Slice;
    }
  | {
      snapshot: CollectionInfoSnapshot;
      type: 'info';
    };

Using Slots

Basic Slot Usage

const basicMiddleware = (): TransformerMiddleware => {
  return ({ slots }) => {
    slots.beforeImport.subscribe(payload => {
      if (payload.type === 'page') {
        // Handle page import
      }
    });
  };
};

Multiple Slots Example

const multiSlotMiddleware = (): TransformerMiddleware => {
  return ({ slots }) => {
    // Before import handling
    slots.beforeImport.subscribe(payload => {
      // Handle pre-import
    });

    // After import handling
    slots.afterImport.subscribe(payload => {
      // Handle post-import
    });
  };
};

Best Practices

Middleware Development

  1. Single Responsibility
  • Each middleware should do one thing well

  • Keep middleware focused and simple

  1. Error Handling
  • Implement proper error handling

  • Use try-catch blocks

  • Provide meaningful error messages

  1. Type Safety
  • Always check payload types

  • Use TypeScript's type system effectively

  • Handle all possible payload types

  1. Performance
  • Keep middleware lightweight

  • Avoid blocking operations

  • Use async/await when necessary

Slot Usage Guidelines

When to Use Each Slot

  1. beforeImport
  • Data validation

  • Content modification

  • State setup

  • Format transformation

  1. afterImport
  • Post-processing

  • Component updates

  • State cleanup

  • Side effects

  1. beforeExport
  • Data preparation

  • Metadata addition

  • Content formatting

  • Export configuration

  1. afterExport
  • Cleanup

  • System notifications

  • Analytics

  • UI updates

Examples

1. Title Middleware

const titleMiddleware = (metas: DocMeta[]): TransformerMiddleware =>
  ({ slots, adapterConfigs }) => {
    slots.beforeExport.subscribe(() => {
      for (const meta of metas) {
        adapterConfigs.set('title:' + meta.id, meta.title);
      }
    });
  };

2. File Name Middleware

const fileNameMiddleware = (fileName?: string): TransformerMiddleware =>
  ({ slots }) => {
    slots.beforeImport.subscribe(payload => {
      if (payload.type !== 'page') return;
      if (!fileName) return;
      
      payload.snapshot.meta.title = fileName;
      payload.snapshot.blocks.props.title = {
        '$blocksuite:internal:text$': true,
        delta: [{ insert: fileName }]
      };
    });
  };

3. Paste Middleware

const pasteMiddleware = (std: EditorHost['std']): TransformerMiddleware => {
  return ({ slots }) => {
    let tr: PasteTr | undefined;
    
    slots.beforeImport.subscribe(payload => {
      if (payload.type === 'slice') {
        const text = std.selection.find(TextSelection);
        if (!text) return;
        tr = new PasteTr(std, text, payload.snapshot);
        if (tr.canMerge()) {
          tr.merge();
        }
      }
    });

    slots.afterImport.subscribe(payload => {
      if (tr && payload.type === 'slice') {
        tr.pasted();
        tr.focusPasted();
        tr.convertToLinkedDoc();
      }
    });
  };
};

4. Complete Example

const completeMiddleware = (): TransformerMiddleware => {
  return ({ slots, adapterConfigs }) => {
    // Before import handling
    slots.beforeImport.subscribe(payload => {
      if (payload.type === 'page') {
        console.log('Importing page:', payload.snapshot.meta.title);
      }
    });

    // After import handling
    slots.afterImport.subscribe(payload => {
      if (payload.type === 'page') {
        console.log('Page imported:', payload.page.id);
      }
    });

    // Before export handling
    slots.beforeExport.subscribe(payload => {
      if (payload.type === 'page') {
        console.log('Exporting page:', payload.page.id);
      }
    });

    // After export handling
    slots.afterExport.subscribe(payload => {
      if (payload.type === 'page') {
        console.log('Page exported:', payload.snapshot.meta.title);
      }
    });
  };
};

Conclusion

The BlockSuite Transformer system provides a robust and flexible framework for handling document transformations. By understanding and properly utilizing the middleware and slots system, you can create powerful and maintainable extensions to the core functionality.

Key takeaways:

  • Use middleware for extending functionality

  • Leverage slots for fine-grained control

  • Follow best practices for development

  • Implement proper error handling

  • Consider performance implications

  • Maintain type safety

This system allows for great flexibility while maintaining a clean and maintainable codebase.