CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tiptap--core

Headless rich text editor built on ProseMirror with extensible architecture for building custom editors

94

1.00x
Overview
Eval results
Files

utilities.mddocs/

Utilities

@tiptap/core provides a comprehensive set of utility functions for type checking, object manipulation, string processing, platform detection, and DOM operations. These utilities help with common tasks in editor development.

Capabilities

Type Guards

Functions for runtime type checking and validation.

/**
 * Check if value is a function
 * @param value - Value to check
 * @returns Whether value is a function
 */
function isFunction(value: unknown): value is Function;

/**
 * Check if value is a string
 * @param value - Value to check
 * @returns Whether value is a string
 */
function isString(value: unknown): value is string;

/**
 * Check if value is a number
 * @param value - Value to check
 * @returns Whether value is a number
 */
function isNumber(value: unknown): value is number;

/**
 * Check if value is a regular expression
 * @param value - Value to check
 * @returns Whether value is a RegExp
 */
function isRegExp(value: unknown): value is RegExp;

/**
 * Check if value is a plain object (not array, function, etc.)
 * @param value - Value to check
 * @returns Whether value is a plain object
 */
function isPlainObject(value: unknown): value is Record<string, any>;

/**
 * Check if object has no own properties
 * @param value - Object to check
 * @returns Whether object is empty
 */
function isEmptyObject(value: Record<string, any>): boolean;

Usage Examples:

import { 
  isFunction, 
  isString, 
  isNumber, 
  isPlainObject 
} from '@tiptap/core';

// Type checking in extension configuration
function processExtensionConfig(config: any) {
  if (isString(config.name)) {
    console.log('Extension name:', config.name);
  }
  
  if (isFunction(config.addCommands)) {
    const commands = config.addCommands();
    // Process commands
  }
  
  if (isPlainObject(config.defaultOptions)) {
    // Merge with existing options
  }
}

// Validate node attributes
function validateAttributes(attrs: unknown): Record<string, any> {
  if (!isPlainObject(attrs)) {
    return {};
  }
  
  const validated: Record<string, any> = {};
  
  for (const [key, value] of Object.entries(attrs)) {
    if (isString(value) || isNumber(value)) {
      validated[key] = value;
    }
  }
  
  return validated;
}

// Safe function calling
function safeCall(fn: unknown, ...args: any[]): any {
  if (isFunction(fn)) {
    return fn(...args);
  }
  return null;
}

// Dynamic attribute handling
function processAttribute(value: unknown): string {
  if (isString(value)) {
    return value;
  }
  if (isNumber(value)) {
    return value.toString();
  }
  if (isPlainObject(value)) {
    return JSON.stringify(value);
  }
  return '';
}

Object Utilities

Functions for manipulating and working with objects and arrays.

/**
 * Merge multiple attribute objects
 * @param attributes - Objects to merge
 * @returns Merged attributes object
 */
function mergeAttributes(...attributes: Record<string, any>[]): Record<string, any>;

/**
 * Deep merge two objects
 * @param target - Target object to merge into
 * @param source - Source object to merge from
 * @returns Merged object
 */
function mergeDeep(
  target: Record<string, any>, 
  source: Record<string, any>
): Record<string, any>;

/**
 * Delete properties from an object
 * @param obj - Object to modify
 * @param propOrProps - Property name or array of property names to delete
 * @returns Modified object
 */
function deleteProps(
  obj: Record<string, any>,
  propOrProps: string | string[]
): Record<string, any>;

/**
 * Call function or return value
 * @param value - Function to call or value to return
 * @param context - Context for function call
 * @param props - Arguments for function call
 * @returns Function result or original value
 */
function callOrReturn<T>(
  value: T | ((...args: any[]) => T),
  context?: any,
  ...props: any[]
): T;

/**
 * Check if object includes specified values
 * @param object - Object to check
 * @param values - Values to look for
 * @param options - Comparison options
 * @returns Whether object includes the values
 */
function objectIncludes(
  object: Record<string, any>,
  values: Record<string, any>,
  options?: { strict?: boolean }
): boolean;

/**
 * Remove duplicate items from array
 * @param array - Array to deduplicate
 * @param by - Optional key function for comparison
 * @returns Array without duplicates
 */
function removeDuplicates<T>(
  array: T[],
  by?: (item: T) => any
): T[];

/**
 * Find duplicate items in array
 * @param items - Array to check for duplicates
 * @returns Array of duplicate items
 */
function findDuplicates<T>(items: T[]): T[];

Usage Examples:

import { 
  mergeAttributes, 
  mergeDeep, 
  deleteProps,
  callOrReturn,
  removeDuplicates 
} from '@tiptap/core';

// Merge HTML attributes
const baseAttrs = { class: 'editor', id: 'main' };
const userAttrs = { class: 'custom', 'data-test': 'true' };
const finalAttrs = mergeAttributes(baseAttrs, userAttrs);
// { class: 'editor custom', id: 'main', 'data-test': 'true' }

// Deep merge configuration objects
const defaultConfig = {
  ui: { theme: 'light', colors: { primary: 'blue' } },
  features: { spellcheck: true }
};
const userConfig = {
  ui: { colors: { secondary: 'green' } },
  features: { autosave: true }
};
const finalConfig = mergeDeep(defaultConfig, userConfig);
// {
//   ui: { theme: 'light', colors: { primary: 'blue', secondary: 'green' } },
//   features: { spellcheck: true, autosave: true }
// }

// Clean up object properties
const rawData = { 
  name: 'test', 
  password: 'secret', 
  temp: 'remove-me',
  internal: 'also-remove' 
};
const cleanData = deleteProps(rawData, ['password', 'temp', 'internal']);
// { name: 'test' }

// Dynamic value resolution
const dynamicValue = callOrReturn(
  () => new Date().toISOString(),
  null
); // Returns current timestamp

const staticValue = callOrReturn('static-string'); // Returns 'static-string'

// Extension configuration
function configureExtension(config: any) {
  return {
    name: callOrReturn(config.name),
    priority: callOrReturn(config.priority, config),
    options: callOrReturn(config.defaultOptions, config)
  };
}

// Remove duplicate extensions
const extensions = [ext1, ext2, ext1, ext3, ext2];
const uniqueExtensions = removeDuplicates(extensions, ext => ext.name);

// Deduplicate by complex key
const items = [
  { id: 1, name: 'Item 1', category: 'A' },
  { id: 2, name: 'Item 2', category: 'B' },
  { id: 1, name: 'Item 1 Updated', category: 'A' }
];
const uniqueItems = removeDuplicates(items, item => `${item.id}-${item.category}`);

Platform Detection

Functions for detecting the current platform and environment.

/**
 * Check if running on Android
 * @returns Whether current platform is Android
 */
function isAndroid(): boolean;

/**
 * Check if running on iOS
 * @returns Whether current platform is iOS
 */
function isiOS(): boolean;

/**
 * Check if running on macOS
 * @returns Whether current platform is macOS
 */
function isMacOS(): boolean;

Usage Examples:

import { isAndroid, isiOS, isMacOS } from '@tiptap/core';

// Platform-specific keyboard shortcuts
function getKeyboardShortcuts() {
  const isMac = isMacOS();
  
  return {
    bold: isMac ? 'Cmd+B' : 'Ctrl+B',
    italic: isMac ? 'Cmd+I' : 'Ctrl+I',
    undo: isMac ? 'Cmd+Z' : 'Ctrl+Z',
    redo: isMac ? 'Cmd+Shift+Z' : 'Ctrl+Y'
  };
}

// Platform-specific behavior
function setupEditor() {
  const isMobile = isAndroid() || isiOS();
  
  return new Editor({
    // Different options for mobile vs desktop
    autofocus: !isMobile,
    editable: true,
    extensions: [
      // Platform-specific extensions
      ...(isMobile ? [TouchExtension] : [DesktopExtension])
    ]
  });
}

// Touch-friendly UI on mobile
function EditorToolbar() {
  const isMobile = isAndroid() || isiOS();
  
  return (
    <div className={`toolbar ${isMobile ? 'toolbar-mobile' : 'toolbar-desktop'}`}>
      {/* Larger buttons on mobile */}
    </div>
  );
}

// Handle paste behavior
function handlePaste(event: ClipboardEvent) {
  const isIOS = isiOS();
  
  if (isIOS) {
    // iOS-specific paste handling
    // iOS has different clipboard API behavior
  } else {
    // Standard paste handling
  }
}

DOM Utilities

Functions for working with DOM elements and HTML.

/**
 * Create DOM element from HTML string
 * @param html - HTML string to parse
 * @returns DOM element
 */
function elementFromString(html: string): Element;

/**
 * Create style tag with CSS content
 * @param css - CSS content for the style tag
 * @param nonce - Optional nonce for Content Security Policy
 * @returns HTMLStyleElement
 */
function createStyleTag(css: string, nonce?: string): HTMLStyleElement;

Usage Examples:

import { elementFromString, createStyleTag } from '@tiptap/core';

// Create DOM elements from HTML
const element = elementFromString('<div class="custom">Content</div>');
document.body.appendChild(element);

// Create complex elements
const complexElement = elementFromString(`
  <div class="editor-widget">
    <h3>Widget Title</h3>
    <p>Widget content with <strong>formatting</strong></p>
    <button onclick="handleClick()">Action</button>
  </div>
`);

// Add custom styles
const customCSS = `
  .tiptap-editor {
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 1rem;
  }
  
  .tiptap-editor h1 {
    margin-top: 0;
  }
`;

const styleTag = createStyleTag(customCSS);
document.head.appendChild(styleTag);

// Add styles with CSP nonce
const secureStyleTag = createStyleTag(customCSS, 'random-nonce-value');
document.head.appendChild(secureStyleTag);

// Dynamic element creation in node views
function createNodeViewElement(node: ProseMirrorNode): Element {
  const html = `
    <div class="custom-node" data-type="${node.type.name}">
      <div class="node-controls">
        <button class="edit-btn">Edit</button>
        <button class="delete-btn">Delete</button>
      </div>
      <div class="node-content"></div>
    </div>
  `;
  
  return elementFromString(html);
}

String Utilities

Functions for string processing and manipulation.

/**
 * Escape string for use in regular expressions
 * @param string - String to escape
 * @returns Escaped string safe for RegExp
 */
function escapeForRegEx(string: string): string;

/**
 * Parse value from string with type coercion
 * @param value - String value to parse
 * @returns Parsed value with appropriate type
 */
function fromString(value: string): any;

Usage Examples:

import { escapeForRegEx, fromString } from '@tiptap/core';

// Escape user input for regex
function createSearchPattern(userInput: string): RegExp {
  const escaped = escapeForRegEx(userInput);
  return new RegExp(escaped, 'gi');
}

// Safe regex creation
const userSearch = 'search (with) special [chars]';
const pattern = createSearchPattern(userSearch);
// Creates regex that matches literal string, not regex pattern

// Parse string values with type coercion
const stringValues = ['true', 'false', '42', '3.14', 'null', 'undefined', 'text'];

stringValues.forEach(str => {
  const parsed = fromString(str);
  console.log(`"${str}" → ${parsed} (${typeof parsed})`);
});
// "true" → true (boolean)
// "false" → false (boolean)  
// "42" → 42 (number)
// "3.14" → 3.14 (number)
// "null" → null (object)
// "undefined" → undefined (undefined)
// "text" → "text" (string)

// Attribute parsing
function parseNodeAttributes(rawAttrs: Record<string, string>): Record<string, any> {
  const parsed: Record<string, any> = {};
  
  for (const [key, value] of Object.entries(rawAttrs)) {
    parsed[key] = fromString(value);
  }
  
  return parsed;
}

// HTML attribute handling
const htmlAttrs = {
  'data-level': '2',
  'data-active': 'true',
  'data-count': '42',
  'data-name': 'heading'
};

const typedAttrs = parseNodeAttributes(htmlAttrs);
// {
//   'data-level': 2,
//   'data-active': true,
//   'data-count': 42,
//   'data-name': 'heading'
// }

Math Utilities

Mathematical helper functions.

/**
 * Clamp value between minimum and maximum
 * @param value - Value to clamp
 * @param min - Minimum allowed value
 * @param max - Maximum allowed value
 * @returns Clamped value
 */
function minMax(value: number, min: number, max: number): number;

Usage Examples:

import { minMax } from '@tiptap/core';

// Clamp user input values
function setFontSize(size: number): void {
  const clampedSize = minMax(size, 8, 72);
  editor.commands.updateAttributes('textStyle', { fontSize: `${clampedSize}px` });
}

// Clamp table dimensions
function createTable(rows: number, cols: number): void {
  const safeRows = minMax(rows, 1, 20);
  const safeCols = minMax(cols, 1, 10);
  
  editor.commands.insertTable({ rows: safeRows, cols: safeCols });
}

// Clamp scroll position
function scrollToPosition(pos: number): void {
  const docSize = editor.state.doc.content.size;
  const safePos = minMax(pos, 0, docSize);
  
  editor.commands.focus(safePos);
}

// UI range controls
function HeadingLevelControl() {
  const [level, setLevel] = useState(1);
  
  const handleLevelChange = (newLevel: number) => {
    const clampedLevel = minMax(newLevel, 1, 6);
    setLevel(clampedLevel);
    editor.commands.setNode('heading', { level: clampedLevel });
  };
  
  return (
    <input
      type="range"
      min={1}
      max={6}
      value={level}
      onChange={e => handleLevelChange(parseInt(e.target.value))}
    />
  );
}

Utility Composition

Examples of combining utilities for complex operations.

// Complex utility compositions for real-world use cases

// Safe configuration merger
function createExtensionConfig<T>(
  defaults: T,
  userConfig?: Partial<T>,
  validator?: (config: T) => boolean
): T {
  let config = defaults;
  
  if (isPlainObject(userConfig)) {
    config = mergeDeep(config, userConfig as Record<string, any>) as T;
  }
  
  if (isFunction(validator) && !validator(config)) {
    console.warn('Invalid configuration, using defaults');
    return defaults;
  }
  
  return config;
}

// Platform-aware DOM manipulation
function createPlatformOptimizedElement(html: string): Element {
  const element = elementFromString(html);
  const isMobile = isAndroid() || isiOS();
  
  if (isMobile) {
    element.classList.add('mobile-optimized');
    // Add touch-friendly attributes
    element.setAttribute('touch-action', 'manipulation');
  }
  
  return element;
}

// Attribute sanitization pipeline
function sanitizeAttributes(
  attrs: unknown,
  allowedKeys: string[],
  typeValidators: Record<string, (value: any) => boolean> = {}
): Record<string, any> {
  if (!isPlainObject(attrs)) {
    return {};
  }
  
  const sanitized: Record<string, any> = {};
  
  for (const key of allowedKeys) {
    const value = attrs[key];
    const validator = typeValidators[key];
    
    if (value !== undefined) {
      if (!validator || validator(value)) {
        sanitized[key] = value;
      }
    }
  }
  
  return sanitized;
}

// Usage examples
const extension = createExtensionConfig(
  { enabled: true, count: 0 },
  { count: 5 },
  config => isNumber(config.count) && config.count >= 0
);

const mobileElement = createPlatformOptimizedElement(`
  <button class="editor-button">Click me</button>
`);

const cleanAttrs = sanitizeAttributes(
  { level: 2, color: 'red', invalid: {} },
  ['level', 'color', 'size'],
  {
    level: isNumber,
    color: isString,
    size: isNumber
  }
);
// Result: { level: 2, color: 'red' }

Transaction Position Tracking

The Tracker class provides position tracking during document transactions, allowing you to track how positions change as the document is modified.

/**
 * Tracks position changes during document transactions
 */
class Tracker {
  /** The transaction being tracked */
  readonly transaction: Transaction;
  
  /** Current step index in the transaction */
  readonly currentStep: number;
  
  /**
   * Create a new position tracker
   * @param transaction - Transaction to track positions in
   */
  constructor(transaction: Transaction);
  
  /**
   * Map a position through transaction steps to get current position
   * @param position - Original position to map
   * @returns TrackerResult with new position and deletion status
   */
  map(position: number): TrackerResult;
}

interface TrackerResult {
  /** New position after mapping through transaction steps */
  position: number;
  /** Whether the content at this position was deleted */
  deleted: boolean;
}

Usage Examples:

import { Tracker } from '@tiptap/core';

// Track position changes during complex operations
function trackPositionDuringEdit(editor: Editor, initialPos: number) {
  const { tr } = editor.state;
  const tracker = new Tracker(tr);
  
  // Perform some operations
  tr.insertText('New text', 10);
  tr.delete(20, 30);
  tr.setSelection(TextSelection.create(tr.doc, 15));
  
  // Check where our original position ended up
  const result = tracker.map(initialPos);
  
  if (result.deleted) {
    console.log(`Content at position ${initialPos} was deleted`);
  } else {
    console.log(`Position ${initialPos} is now at ${result.position}`);
  }
  
  return result;
}

// Track multiple positions simultaneously
function trackMultiplePositions(
  transaction: Transaction,
  positions: number[]
): Map<number, TrackerResult> {
  const tracker = new Tracker(transaction);
  const results = new Map<number, TrackerResult>();
  
  positions.forEach(pos => {
    results.set(pos, tracker.map(pos));
  });
  
  return results;
}

// Use in custom command to maintain cursor position
function insertContentAndMaintainPosition(content: string) {
  return ({ tr, state }: CommandProps) => {
    const { selection } = state;
    const tracker = new Tracker(tr);
    
    // Insert content at current position
    tr.insertText(content, selection.from);
    
    // Track where the cursor should be after insertion
    const newCursorResult = tracker.map(selection.from + content.length);
    
    if (!newCursorResult.deleted) {
      tr.setSelection(TextSelection.create(tr.doc, newCursorResult.position));
    }
    
    return true;
  };
}

// Advanced usage: Track complex document changes
function performComplexEdit(editor: Editor) {
  const { tr } = editor.state;
  const tracker = new Tracker(tr);
  
  // Store important positions before making changes
  const bookmarks = [50, 100, 150, 200];
  
  // Perform multiple operations
  tr.delete(10, 20);           // Delete range
  tr.insertText('Replacement', 10);  // Insert replacement
  tr.setMark(40, 60, schema.marks.bold.create()); // Add formatting
  
  // Check what happened to our bookmarked positions
  const updatedBookmarks = bookmarks.map(pos => {
    const result = tracker.map(pos);
    return {
      original: pos,
      current: result.position,
      deleted: result.deleted
    };
  });
  
  console.log('Position changes:', updatedBookmarks);
  
  // Apply the transaction
  editor.view.dispatch(tr);
  
  return updatedBookmarks;
}

// Use with undo/redo to maintain selection
function smartUndo(editor: Editor) {
  const { selection } = editor.state;
  const tracker = new Tracker(editor.state.tr);
  
  // Perform undo
  editor.commands.undo();
  
  // Try to maintain a similar selection position
  const mappedResult = tracker.map(selection.from);
  
  if (!mappedResult.deleted) {
    editor.commands.setTextSelection(mappedResult.position);
  }
}

Install with Tessl CLI

npx tessl i tessl/npm-tiptap--core

docs

command-system.md

document-helpers.md

editor-core.md

extension-system.md

index.md

rule-systems.md

utilities.md

tile.json