or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

command-system.mddocument-helpers.mdeditor-core.mdextension-system.mdindex.mdrule-systems.mdutilities.md

utilities.mddocs/

0

# Utilities

1

2

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

3

4

## Capabilities

5

6

### Type Guards

7

8

Functions for runtime type checking and validation.

9

10

```typescript { .api }

11

/**

12

* Check if value is a function

13

* @param value - Value to check

14

* @returns Whether value is a function

15

*/

16

function isFunction(value: unknown): value is Function;

17

18

/**

19

* Check if value is a string

20

* @param value - Value to check

21

* @returns Whether value is a string

22

*/

23

function isString(value: unknown): value is string;

24

25

/**

26

* Check if value is a number

27

* @param value - Value to check

28

* @returns Whether value is a number

29

*/

30

function isNumber(value: unknown): value is number;

31

32

/**

33

* Check if value is a regular expression

34

* @param value - Value to check

35

* @returns Whether value is a RegExp

36

*/

37

function isRegExp(value: unknown): value is RegExp;

38

39

/**

40

* Check if value is a plain object (not array, function, etc.)

41

* @param value - Value to check

42

* @returns Whether value is a plain object

43

*/

44

function isPlainObject(value: unknown): value is Record<string, any>;

45

46

/**

47

* Check if object has no own properties

48

* @param value - Object to check

49

* @returns Whether object is empty

50

*/

51

function isEmptyObject(value: Record<string, any>): boolean;

52

```

53

54

**Usage Examples:**

55

56

```typescript

57

import {

58

isFunction,

59

isString,

60

isNumber,

61

isPlainObject

62

} from '@tiptap/core';

63

64

// Type checking in extension configuration

65

function processExtensionConfig(config: any) {

66

if (isString(config.name)) {

67

console.log('Extension name:', config.name);

68

}

69

70

if (isFunction(config.addCommands)) {

71

const commands = config.addCommands();

72

// Process commands

73

}

74

75

if (isPlainObject(config.defaultOptions)) {

76

// Merge with existing options

77

}

78

}

79

80

// Validate node attributes

81

function validateAttributes(attrs: unknown): Record<string, any> {

82

if (!isPlainObject(attrs)) {

83

return {};

84

}

85

86

const validated: Record<string, any> = {};

87

88

for (const [key, value] of Object.entries(attrs)) {

89

if (isString(value) || isNumber(value)) {

90

validated[key] = value;

91

}

92

}

93

94

return validated;

95

}

96

97

// Safe function calling

98

function safeCall(fn: unknown, ...args: any[]): any {

99

if (isFunction(fn)) {

100

return fn(...args);

101

}

102

return null;

103

}

104

105

// Dynamic attribute handling

106

function processAttribute(value: unknown): string {

107

if (isString(value)) {

108

return value;

109

}

110

if (isNumber(value)) {

111

return value.toString();

112

}

113

if (isPlainObject(value)) {

114

return JSON.stringify(value);

115

}

116

return '';

117

}

118

```

119

120

### Object Utilities

121

122

Functions for manipulating and working with objects and arrays.

123

124

```typescript { .api }

125

/**

126

* Merge multiple attribute objects

127

* @param attributes - Objects to merge

128

* @returns Merged attributes object

129

*/

130

function mergeAttributes(...attributes: Record<string, any>[]): Record<string, any>;

131

132

/**

133

* Deep merge two objects

134

* @param target - Target object to merge into

135

* @param source - Source object to merge from

136

* @returns Merged object

137

*/

138

function mergeDeep(

139

target: Record<string, any>,

140

source: Record<string, any>

141

): Record<string, any>;

142

143

/**

144

* Delete properties from an object

145

* @param obj - Object to modify

146

* @param propOrProps - Property name or array of property names to delete

147

* @returns Modified object

148

*/

149

function deleteProps(

150

obj: Record<string, any>,

151

propOrProps: string | string[]

152

): Record<string, any>;

153

154

/**

155

* Call function or return value

156

* @param value - Function to call or value to return

157

* @param context - Context for function call

158

* @param props - Arguments for function call

159

* @returns Function result or original value

160

*/

161

function callOrReturn<T>(

162

value: T | ((...args: any[]) => T),

163

context?: any,

164

...props: any[]

165

): T;

166

167

/**

168

* Check if object includes specified values

169

* @param object - Object to check

170

* @param values - Values to look for

171

* @param options - Comparison options

172

* @returns Whether object includes the values

173

*/

174

function objectIncludes(

175

object: Record<string, any>,

176

values: Record<string, any>,

177

options?: { strict?: boolean }

178

): boolean;

179

180

/**

181

* Remove duplicate items from array

182

* @param array - Array to deduplicate

183

* @param by - Optional key function for comparison

184

* @returns Array without duplicates

185

*/

186

function removeDuplicates<T>(

187

array: T[],

188

by?: (item: T) => any

189

): T[];

190

191

/**

192

* Find duplicate items in array

193

* @param items - Array to check for duplicates

194

* @returns Array of duplicate items

195

*/

196

function findDuplicates<T>(items: T[]): T[];

197

```

198

199

**Usage Examples:**

200

201

```typescript

202

import {

203

mergeAttributes,

204

mergeDeep,

205

deleteProps,

206

callOrReturn,

207

removeDuplicates

208

} from '@tiptap/core';

209

210

// Merge HTML attributes

211

const baseAttrs = { class: 'editor', id: 'main' };

212

const userAttrs = { class: 'custom', 'data-test': 'true' };

213

const finalAttrs = mergeAttributes(baseAttrs, userAttrs);

214

// { class: 'editor custom', id: 'main', 'data-test': 'true' }

215

216

// Deep merge configuration objects

217

const defaultConfig = {

218

ui: { theme: 'light', colors: { primary: 'blue' } },

219

features: { spellcheck: true }

220

};

221

const userConfig = {

222

ui: { colors: { secondary: 'green' } },

223

features: { autosave: true }

224

};

225

const finalConfig = mergeDeep(defaultConfig, userConfig);

226

// {

227

// ui: { theme: 'light', colors: { primary: 'blue', secondary: 'green' } },

228

// features: { spellcheck: true, autosave: true }

229

// }

230

231

// Clean up object properties

232

const rawData = {

233

name: 'test',

234

password: 'secret',

235

temp: 'remove-me',

236

internal: 'also-remove'

237

};

238

const cleanData = deleteProps(rawData, ['password', 'temp', 'internal']);

239

// { name: 'test' }

240

241

// Dynamic value resolution

242

const dynamicValue = callOrReturn(

243

() => new Date().toISOString(),

244

null

245

); // Returns current timestamp

246

247

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

248

249

// Extension configuration

250

function configureExtension(config: any) {

251

return {

252

name: callOrReturn(config.name),

253

priority: callOrReturn(config.priority, config),

254

options: callOrReturn(config.defaultOptions, config)

255

};

256

}

257

258

// Remove duplicate extensions

259

const extensions = [ext1, ext2, ext1, ext3, ext2];

260

const uniqueExtensions = removeDuplicates(extensions, ext => ext.name);

261

262

// Deduplicate by complex key

263

const items = [

264

{ id: 1, name: 'Item 1', category: 'A' },

265

{ id: 2, name: 'Item 2', category: 'B' },

266

{ id: 1, name: 'Item 1 Updated', category: 'A' }

267

];

268

const uniqueItems = removeDuplicates(items, item => `${item.id}-${item.category}`);

269

```

270

271

### Platform Detection

272

273

Functions for detecting the current platform and environment.

274

275

```typescript { .api }

276

/**

277

* Check if running on Android

278

* @returns Whether current platform is Android

279

*/

280

function isAndroid(): boolean;

281

282

/**

283

* Check if running on iOS

284

* @returns Whether current platform is iOS

285

*/

286

function isiOS(): boolean;

287

288

/**

289

* Check if running on macOS

290

* @returns Whether current platform is macOS

291

*/

292

function isMacOS(): boolean;

293

```

294

295

**Usage Examples:**

296

297

```typescript

298

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

299

300

// Platform-specific keyboard shortcuts

301

function getKeyboardShortcuts() {

302

const isMac = isMacOS();

303

304

return {

305

bold: isMac ? 'Cmd+B' : 'Ctrl+B',

306

italic: isMac ? 'Cmd+I' : 'Ctrl+I',

307

undo: isMac ? 'Cmd+Z' : 'Ctrl+Z',

308

redo: isMac ? 'Cmd+Shift+Z' : 'Ctrl+Y'

309

};

310

}

311

312

// Platform-specific behavior

313

function setupEditor() {

314

const isMobile = isAndroid() || isiOS();

315

316

return new Editor({

317

// Different options for mobile vs desktop

318

autofocus: !isMobile,

319

editable: true,

320

extensions: [

321

// Platform-specific extensions

322

...(isMobile ? [TouchExtension] : [DesktopExtension])

323

]

324

});

325

}

326

327

// Touch-friendly UI on mobile

328

function EditorToolbar() {

329

const isMobile = isAndroid() || isiOS();

330

331

return (

332

<div className={`toolbar ${isMobile ? 'toolbar-mobile' : 'toolbar-desktop'}`}>

333

{/* Larger buttons on mobile */}

334

</div>

335

);

336

}

337

338

// Handle paste behavior

339

function handlePaste(event: ClipboardEvent) {

340

const isIOS = isiOS();

341

342

if (isIOS) {

343

// iOS-specific paste handling

344

// iOS has different clipboard API behavior

345

} else {

346

// Standard paste handling

347

}

348

}

349

```

350

351

### DOM Utilities

352

353

Functions for working with DOM elements and HTML.

354

355

```typescript { .api }

356

/**

357

* Create DOM element from HTML string

358

* @param html - HTML string to parse

359

* @returns DOM element

360

*/

361

function elementFromString(html: string): Element;

362

363

/**

364

* Create style tag with CSS content

365

* @param css - CSS content for the style tag

366

* @param nonce - Optional nonce for Content Security Policy

367

* @returns HTMLStyleElement

368

*/

369

function createStyleTag(css: string, nonce?: string): HTMLStyleElement;

370

```

371

372

**Usage Examples:**

373

374

```typescript

375

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

376

377

// Create DOM elements from HTML

378

const element = elementFromString('<div class="custom">Content</div>');

379

document.body.appendChild(element);

380

381

// Create complex elements

382

const complexElement = elementFromString(`

383

<div class="editor-widget">

384

<h3>Widget Title</h3>

385

<p>Widget content with <strong>formatting</strong></p>

386

<button onclick="handleClick()">Action</button>

387

</div>

388

`);

389

390

// Add custom styles

391

const customCSS = `

392

.tiptap-editor {

393

border: 1px solid #ccc;

394

border-radius: 4px;

395

padding: 1rem;

396

}

397

398

.tiptap-editor h1 {

399

margin-top: 0;

400

}

401

`;

402

403

const styleTag = createStyleTag(customCSS);

404

document.head.appendChild(styleTag);

405

406

// Add styles with CSP nonce

407

const secureStyleTag = createStyleTag(customCSS, 'random-nonce-value');

408

document.head.appendChild(secureStyleTag);

409

410

// Dynamic element creation in node views

411

function createNodeViewElement(node: ProseMirrorNode): Element {

412

const html = `

413

<div class="custom-node" data-type="${node.type.name}">

414

<div class="node-controls">

415

<button class="edit-btn">Edit</button>

416

<button class="delete-btn">Delete</button>

417

</div>

418

<div class="node-content"></div>

419

</div>

420

`;

421

422

return elementFromString(html);

423

}

424

```

425

426

### String Utilities

427

428

Functions for string processing and manipulation.

429

430

```typescript { .api }

431

/**

432

* Escape string for use in regular expressions

433

* @param string - String to escape

434

* @returns Escaped string safe for RegExp

435

*/

436

function escapeForRegEx(string: string): string;

437

438

/**

439

* Parse value from string with type coercion

440

* @param value - String value to parse

441

* @returns Parsed value with appropriate type

442

*/

443

function fromString(value: string): any;

444

```

445

446

**Usage Examples:**

447

448

```typescript

449

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

450

451

// Escape user input for regex

452

function createSearchPattern(userInput: string): RegExp {

453

const escaped = escapeForRegEx(userInput);

454

return new RegExp(escaped, 'gi');

455

}

456

457

// Safe regex creation

458

const userSearch = 'search (with) special [chars]';

459

const pattern = createSearchPattern(userSearch);

460

// Creates regex that matches literal string, not regex pattern

461

462

// Parse string values with type coercion

463

const stringValues = ['true', 'false', '42', '3.14', 'null', 'undefined', 'text'];

464

465

stringValues.forEach(str => {

466

const parsed = fromString(str);

467

console.log(`"${str}" → ${parsed} (${typeof parsed})`);

468

});

469

// "true" → true (boolean)

470

// "false" → false (boolean)

471

// "42" → 42 (number)

472

// "3.14" → 3.14 (number)

473

// "null" → null (object)

474

// "undefined" → undefined (undefined)

475

// "text" → "text" (string)

476

477

// Attribute parsing

478

function parseNodeAttributes(rawAttrs: Record<string, string>): Record<string, any> {

479

const parsed: Record<string, any> = {};

480

481

for (const [key, value] of Object.entries(rawAttrs)) {

482

parsed[key] = fromString(value);

483

}

484

485

return parsed;

486

}

487

488

// HTML attribute handling

489

const htmlAttrs = {

490

'data-level': '2',

491

'data-active': 'true',

492

'data-count': '42',

493

'data-name': 'heading'

494

};

495

496

const typedAttrs = parseNodeAttributes(htmlAttrs);

497

// {

498

// 'data-level': 2,

499

// 'data-active': true,

500

// 'data-count': 42,

501

// 'data-name': 'heading'

502

// }

503

```

504

505

### Math Utilities

506

507

Mathematical helper functions.

508

509

```typescript { .api }

510

/**

511

* Clamp value between minimum and maximum

512

* @param value - Value to clamp

513

* @param min - Minimum allowed value

514

* @param max - Maximum allowed value

515

* @returns Clamped value

516

*/

517

function minMax(value: number, min: number, max: number): number;

518

```

519

520

**Usage Examples:**

521

522

```typescript

523

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

524

525

// Clamp user input values

526

function setFontSize(size: number): void {

527

const clampedSize = minMax(size, 8, 72);

528

editor.commands.updateAttributes('textStyle', { fontSize: `${clampedSize}px` });

529

}

530

531

// Clamp table dimensions

532

function createTable(rows: number, cols: number): void {

533

const safeRows = minMax(rows, 1, 20);

534

const safeCols = minMax(cols, 1, 10);

535

536

editor.commands.insertTable({ rows: safeRows, cols: safeCols });

537

}

538

539

// Clamp scroll position

540

function scrollToPosition(pos: number): void {

541

const docSize = editor.state.doc.content.size;

542

const safePos = minMax(pos, 0, docSize);

543

544

editor.commands.focus(safePos);

545

}

546

547

// UI range controls

548

function HeadingLevelControl() {

549

const [level, setLevel] = useState(1);

550

551

const handleLevelChange = (newLevel: number) => {

552

const clampedLevel = minMax(newLevel, 1, 6);

553

setLevel(clampedLevel);

554

editor.commands.setNode('heading', { level: clampedLevel });

555

};

556

557

return (

558

<input

559

type="range"

560

min={1}

561

max={6}

562

value={level}

563

onChange={e => handleLevelChange(parseInt(e.target.value))}

564

/>

565

);

566

}

567

```

568

569

### Utility Composition

570

571

Examples of combining utilities for complex operations.

572

573

```typescript { .api }

574

// Complex utility compositions for real-world use cases

575

576

// Safe configuration merger

577

function createExtensionConfig<T>(

578

defaults: T,

579

userConfig?: Partial<T>,

580

validator?: (config: T) => boolean

581

): T {

582

let config = defaults;

583

584

if (isPlainObject(userConfig)) {

585

config = mergeDeep(config, userConfig as Record<string, any>) as T;

586

}

587

588

if (isFunction(validator) && !validator(config)) {

589

console.warn('Invalid configuration, using defaults');

590

return defaults;

591

}

592

593

return config;

594

}

595

596

// Platform-aware DOM manipulation

597

function createPlatformOptimizedElement(html: string): Element {

598

const element = elementFromString(html);

599

const isMobile = isAndroid() || isiOS();

600

601

if (isMobile) {

602

element.classList.add('mobile-optimized');

603

// Add touch-friendly attributes

604

element.setAttribute('touch-action', 'manipulation');

605

}

606

607

return element;

608

}

609

610

// Attribute sanitization pipeline

611

function sanitizeAttributes(

612

attrs: unknown,

613

allowedKeys: string[],

614

typeValidators: Record<string, (value: any) => boolean> = {}

615

): Record<string, any> {

616

if (!isPlainObject(attrs)) {

617

return {};

618

}

619

620

const sanitized: Record<string, any> = {};

621

622

for (const key of allowedKeys) {

623

const value = attrs[key];

624

const validator = typeValidators[key];

625

626

if (value !== undefined) {

627

if (!validator || validator(value)) {

628

sanitized[key] = value;

629

}

630

}

631

}

632

633

return sanitized;

634

}

635

636

// Usage examples

637

const extension = createExtensionConfig(

638

{ enabled: true, count: 0 },

639

{ count: 5 },

640

config => isNumber(config.count) && config.count >= 0

641

);

642

643

const mobileElement = createPlatformOptimizedElement(`

644

<button class="editor-button">Click me</button>

645

`);

646

647

const cleanAttrs = sanitizeAttributes(

648

{ level: 2, color: 'red', invalid: {} },

649

['level', 'color', 'size'],

650

{

651

level: isNumber,

652

color: isString,

653

size: isNumber

654

}

655

);

656

// Result: { level: 2, color: 'red' }

657

```

658

659

### Transaction Position Tracking

660

661

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

662

663

```typescript { .api }

664

/**

665

* Tracks position changes during document transactions

666

*/

667

class Tracker {

668

/** The transaction being tracked */

669

readonly transaction: Transaction;

670

671

/** Current step index in the transaction */

672

readonly currentStep: number;

673

674

/**

675

* Create a new position tracker

676

* @param transaction - Transaction to track positions in

677

*/

678

constructor(transaction: Transaction);

679

680

/**

681

* Map a position through transaction steps to get current position

682

* @param position - Original position to map

683

* @returns TrackerResult with new position and deletion status

684

*/

685

map(position: number): TrackerResult;

686

}

687

688

interface TrackerResult {

689

/** New position after mapping through transaction steps */

690

position: number;

691

/** Whether the content at this position was deleted */

692

deleted: boolean;

693

}

694

```

695

696

**Usage Examples:**

697

698

```typescript

699

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

700

701

// Track position changes during complex operations

702

function trackPositionDuringEdit(editor: Editor, initialPos: number) {

703

const { tr } = editor.state;

704

const tracker = new Tracker(tr);

705

706

// Perform some operations

707

tr.insertText('New text', 10);

708

tr.delete(20, 30);

709

tr.setSelection(TextSelection.create(tr.doc, 15));

710

711

// Check where our original position ended up

712

const result = tracker.map(initialPos);

713

714

if (result.deleted) {

715

console.log(`Content at position ${initialPos} was deleted`);

716

} else {

717

console.log(`Position ${initialPos} is now at ${result.position}`);

718

}

719

720

return result;

721

}

722

723

// Track multiple positions simultaneously

724

function trackMultiplePositions(

725

transaction: Transaction,

726

positions: number[]

727

): Map<number, TrackerResult> {

728

const tracker = new Tracker(transaction);

729

const results = new Map<number, TrackerResult>();

730

731

positions.forEach(pos => {

732

results.set(pos, tracker.map(pos));

733

});

734

735

return results;

736

}

737

738

// Use in custom command to maintain cursor position

739

function insertContentAndMaintainPosition(content: string) {

740

return ({ tr, state }: CommandProps) => {

741

const { selection } = state;

742

const tracker = new Tracker(tr);

743

744

// Insert content at current position

745

tr.insertText(content, selection.from);

746

747

// Track where the cursor should be after insertion

748

const newCursorResult = tracker.map(selection.from + content.length);

749

750

if (!newCursorResult.deleted) {

751

tr.setSelection(TextSelection.create(tr.doc, newCursorResult.position));

752

}

753

754

return true;

755

};

756

}

757

758

// Advanced usage: Track complex document changes

759

function performComplexEdit(editor: Editor) {

760

const { tr } = editor.state;

761

const tracker = new Tracker(tr);

762

763

// Store important positions before making changes

764

const bookmarks = [50, 100, 150, 200];

765

766

// Perform multiple operations

767

tr.delete(10, 20); // Delete range

768

tr.insertText('Replacement', 10); // Insert replacement

769

tr.setMark(40, 60, schema.marks.bold.create()); // Add formatting

770

771

// Check what happened to our bookmarked positions

772

const updatedBookmarks = bookmarks.map(pos => {

773

const result = tracker.map(pos);

774

return {

775

original: pos,

776

current: result.position,

777

deleted: result.deleted

778

};

779

});

780

781

console.log('Position changes:', updatedBookmarks);

782

783

// Apply the transaction

784

editor.view.dispatch(tr);

785

786

return updatedBookmarks;

787

}

788

789

// Use with undo/redo to maintain selection

790

function smartUndo(editor: Editor) {

791

const { selection } = editor.state;

792

const tracker = new Tracker(editor.state.tr);

793

794

// Perform undo

795

editor.commands.undo();

796

797

// Try to maintain a similar selection position

798

const mappedResult = tracker.map(selection.from);

799

800

if (!mappedResult.deleted) {

801

editor.commands.setTextSelection(mappedResult.position);

802

}

803

}

804

```