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

extension-system.mddocs/

0

# Extension System

1

2

The extension system is the core of @tiptap/core's modularity, allowing you to add functionality through Extensions, Nodes, and Marks. Extensions handle non-content features, Nodes represent document structure, and Marks represent text formatting.

3

4

## Capabilities

5

6

### Extension Base Class

7

8

Extensions add functionality that doesn't directly represent document content, such as commands, keyboard shortcuts, or plugins.

9

10

```typescript { .api }

11

/**

12

* Base class for creating editor extensions

13

*/

14

class Extension<Options = any, Storage = any> {

15

/**

16

* Create a new extension

17

* @param config - Extension configuration

18

* @returns Extension instance

19

*/

20

static create<O = any, S = any>(

21

config?: Partial<ExtensionConfig<O, S>>

22

): Extension<O, S>;

23

24

/**

25

* Configure the extension with new options

26

* @param options - Options to merge with defaults

27

* @returns New extension instance with updated options

28

*/

29

configure(options?: Partial<Options>): Extension<Options, Storage>;

30

31

/**

32

* Extend the extension with additional configuration

33

* @param extendedConfig - Additional configuration to apply

34

* @returns New extended extension instance

35

*/

36

extend<ExtendedOptions = Options, ExtendedStorage = Storage>(

37

extendedConfig?: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>>

38

): Extension<ExtendedOptions, ExtendedStorage>;

39

}

40

41

interface ExtensionConfig<Options = any, Storage = any> {

42

/** Unique name for the extension */

43

name: string;

44

45

/** Default options for the extension */

46

defaultOptions?: Options;

47

48

/** Priority for loading order (higher loads later) */

49

priority?: number;

50

51

/** Initialize storage for sharing data between extensions */

52

addStorage?(): Storage;

53

54

/** Add commands to the editor */

55

addCommands?(): Commands;

56

57

/** Add keyboard shortcuts */

58

addKeymap?(): Record<string, any>;

59

60

/** Add input rules for text transformation */

61

addInputRules?(): InputRule[];

62

63

/** Add paste rules for paste transformation */

64

addPasteRules?(): PasteRule[];

65

66

/** Add global attributes to all nodes */

67

addGlobalAttributes?(): GlobalAttributes[];

68

69

/** Add custom node view renderer */

70

addNodeView?(): NodeViewRenderer;

71

72

/** Add ProseMirror plugins */

73

addProseMirrorPlugins?(): Plugin[];

74

75

/** Called when extension is created */

76

onCreate?(this: { options: Options; storage: Storage }): void;

77

78

/** Called when editor content is updated */

79

onUpdate?(this: { options: Options; storage: Storage }): void;

80

81

/** Called before editor is destroyed */

82

onDestroy?(this: { options: Options; storage: Storage }): void;

83

84

/** Called when selection changes */

85

onSelectionUpdate?(this: { options: Options; storage: Storage }): void;

86

87

/** Called on every transaction */

88

onTransaction?(this: { options: Options; storage: Storage }): void;

89

90

/** Called when editor gains focus */

91

onFocus?(this: { options: Options; storage: Storage }): void;

92

93

/** Called when editor loses focus */

94

onBlur?(this: { options: Options; storage: Storage }): void;

95

}

96

```

97

98

**Usage Examples:**

99

100

```typescript

101

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

102

103

// Simple extension with commands

104

const CustomExtension = Extension.create({

105

name: 'customExtension',

106

107

addCommands() {

108

return {

109

customCommand: (text: string) => ({ commands }) => {

110

return commands.insertContent(text);

111

}

112

};

113

},

114

115

addKeymap() {

116

return {

117

'Mod-k': () => this.editor.commands.customCommand('Shortcut pressed!'),

118

};

119

}

120

});

121

122

// Extension with options and storage

123

const CounterExtension = Extension.create<{ step: number }, { count: number }>({

124

name: 'counter',

125

126

defaultOptions: {

127

step: 1,

128

},

129

130

addStorage() {

131

return {

132

count: 0,

133

};

134

},

135

136

addCommands() {

137

return {

138

increment: () => ({ editor }) => {

139

this.storage.count += this.options.step;

140

return true;

141

},

142

getCount: () => () => {

143

return this.storage.count;

144

}

145

};

146

}

147

});

148

149

// Configure extension

150

const customCounter = CounterExtension.configure({ step: 5 });

151

152

// Extend extension

153

const AdvancedCounter = CounterExtension.extend({

154

name: 'advancedCounter',

155

156

addCommands() {

157

return {

158

...this.parent?.(),

159

reset: () => ({ editor }) => {

160

this.storage.count = 0;

161

return true;

162

}

163

};

164

}

165

});

166

```

167

168

### Node Class

169

170

Nodes represent structural elements in the document like paragraphs, headings, lists, and custom block or inline elements.

171

172

```typescript { .api }

173

/**

174

* Base class for creating document nodes

175

*/

176

class Node<Options = any, Storage = any> {

177

/**

178

* Create a new node extension

179

* @param config - Node configuration

180

* @returns Node extension instance

181

*/

182

static create<O = any, S = any>(

183

config?: Partial<NodeConfig<O, S>>

184

): Node<O, S>;

185

186

/**

187

* Configure the node with new options

188

* @param options - Options to merge with defaults

189

* @returns New node instance with updated options

190

*/

191

configure(options?: Partial<Options>): Node<Options, Storage>;

192

193

/**

194

* Extend the node with additional configuration

195

* @param extendedConfig - Additional configuration to apply

196

* @returns New extended node instance

197

*/

198

extend<ExtendedOptions = Options, ExtendedStorage = Storage>(

199

extendedConfig?: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>>

200

): Node<ExtendedOptions, ExtendedStorage>;

201

}

202

203

interface NodeConfig<Options = any, Storage = any> extends ExtensionConfig<Options, Storage> {

204

/** Content expression defining allowed child content */

205

content?: string | ((this: { options: Options }) => string);

206

207

/** Marks that can be applied to this node */

208

marks?: string | ((this: { options: Options }) => string);

209

210

/** Node group (e.g., 'block', 'inline') */

211

group?: string | ((this: { options: Options }) => string);

212

213

/** Whether this is an inline node */

214

inline?: boolean | ((this: { options: Options }) => boolean);

215

216

/** Whether this node is atomic (cannot be directly edited) */

217

atom?: boolean | ((this: { options: Options }) => boolean);

218

219

/** Whether this node can be selected */

220

selectable?: boolean | ((this: { options: Options }) => boolean);

221

222

/** Whether this node can be dragged */

223

draggable?: boolean | ((this: { options: Options }) => boolean);

224

225

/** Defines how to parse HTML into this node */

226

parseHTML?(): HTMLParseRule[];

227

228

/** Defines how to render this node as HTML */

229

renderHTML?(props: { node: ProseMirrorNode; HTMLAttributes: Record<string, any> }): DOMOutputSpec;

230

231

/** Defines how to render this node as text */

232

renderText?(props: { node: ProseMirrorNode }): string;

233

234

/** Add custom node view */

235

addNodeView?(): NodeViewRenderer;

236

237

/** Define node attributes */

238

addAttributes?(): Record<string, Attribute>;

239

}

240

241

interface HTMLParseRule {

242

tag?: string;

243

node?: string;

244

mark?: string;

245

style?: string;

246

priority?: number;

247

consuming?: boolean;

248

context?: string;

249

getAttrs?: (node: HTMLElement) => Record<string, any> | null | false;

250

}

251

252

interface Attribute {

253

default?: any;

254

rendered?: boolean;

255

renderHTML?: (attributes: Record<string, any>) => Record<string, any> | null;

256

parseHTML?: (element: HTMLElement) => any;

257

keepOnSplit?: boolean;

258

isRequired?: boolean;

259

}

260

```

261

262

**Usage Examples:**

263

264

```typescript

265

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

266

267

// Simple custom node

268

const CalloutNode = Node.create({

269

name: 'callout',

270

group: 'block',

271

content: 'block+',

272

273

addAttributes() {

274

return {

275

type: {

276

default: 'info',

277

parseHTML: element => element.getAttribute('data-type'),

278

renderHTML: attributes => ({

279

'data-type': attributes.type,

280

}),

281

},

282

};

283

},

284

285

parseHTML() {

286

return [

287

{

288

tag: 'div[data-callout]',

289

getAttrs: node => ({ type: node.getAttribute('data-type') }),

290

},

291

];

292

},

293

294

renderHTML({ node, HTMLAttributes }) {

295

return [

296

'div',

297

{

298

'data-callout': '',

299

'data-type': node.attrs.type,

300

...HTMLAttributes,

301

},

302

0, // Content goes here

303

];

304

},

305

306

addCommands() {

307

return {

308

setCallout: (type: string) => ({ commands }) => {

309

return commands.wrapIn(this.name, { type });

310

},

311

};

312

},

313

});

314

315

// Inline node example

316

const MentionNode = Node.create({

317

name: 'mention',

318

group: 'inline',

319

inline: true,

320

selectable: false,

321

atom: true,

322

323

addAttributes() {

324

return {

325

id: {

326

default: null,

327

parseHTML: element => element.getAttribute('data-id'),

328

renderHTML: attributes => ({

329

'data-id': attributes.id,

330

}),

331

},

332

label: {

333

default: null,

334

parseHTML: element => element.getAttribute('data-label'),

335

renderHTML: attributes => ({

336

'data-label': attributes.label,

337

}),

338

},

339

};

340

},

341

342

parseHTML() {

343

return [

344

{

345

tag: 'span[data-mention]',

346

},

347

];

348

},

349

350

renderHTML({ node, HTMLAttributes }) {

351

return [

352

'span',

353

{

354

'data-mention': '',

355

'data-id': node.attrs.id,

356

'data-label': node.attrs.label,

357

...HTMLAttributes,

358

},

359

`@${node.attrs.label}`,

360

];

361

},

362

363

renderText({ node }) {

364

return `@${node.attrs.label}`;

365

},

366

367

addCommands() {

368

return {

369

insertMention: (options: { id: string; label: string }) => ({ commands }) => {

370

return commands.insertContent({

371

type: this.name,

372

attrs: options,

373

});

374

},

375

};

376

},

377

});

378

```

379

380

### Mark Class

381

382

Marks represent text formatting that can be applied to ranges of text, such as bold, italic, links, or custom formatting.

383

384

```typescript { .api }

385

/**

386

* Base class for creating text marks

387

*/

388

class Mark<Options = any, Storage = any> {

389

/**

390

* Create a new mark extension

391

* @param config - Mark configuration

392

* @returns Mark extension instance

393

*/

394

static create<O = any, S = any>(

395

config?: Partial<MarkConfig<O, S>>

396

): Mark<O, S>;

397

398

/**

399

* Configure the mark with new options

400

* @param options - Options to merge with defaults

401

* @returns New mark instance with updated options

402

*/

403

configure(options?: Partial<Options>): Mark<Options, Storage>;

404

405

/**

406

* Extend the mark with additional configuration

407

* @param extendedConfig - Additional configuration to apply

408

* @returns New extended mark instance

409

*/

410

extend<ExtendedOptions = Options, ExtendedStorage = Storage>(

411

extendedConfig?: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>>

412

): Mark<ExtendedOptions, ExtendedStorage>;

413

414

/**

415

* Handle mark exit behavior (for marks like links)

416

* @param options - Exit options

417

* @returns Whether the exit was handled

418

*/

419

static handleExit(options: {

420

editor: Editor;

421

mark: ProseMirrorMark

422

}): boolean;

423

}

424

425

interface MarkConfig<Options = any, Storage = any> extends ExtensionConfig<Options, Storage> {

426

/** Whether the mark is inclusive (extends to typed text) */

427

inclusive?: boolean | ((this: { options: Options }) => boolean);

428

429

/** Marks that this mark excludes */

430

excludes?: string | ((this: { options: Options }) => string);

431

432

/** Mark group */

433

group?: string | ((this: { options: Options }) => string);

434

435

/** Whether mark can span across different nodes */

436

spanning?: boolean | ((this: { options: Options }) => boolean);

437

438

/** Whether this is a code mark (excludes other formatting) */

439

code?: boolean | ((this: { options: Options }) => boolean);

440

441

/** Defines how to parse HTML into this mark */

442

parseHTML?(): HTMLParseRule[];

443

444

/** Defines how to render this mark as HTML */

445

renderHTML?(props: {

446

mark: ProseMirrorMark;

447

HTMLAttributes: Record<string, any>

448

}): DOMOutputSpec;

449

450

/** Add custom mark view */

451

addMarkView?(): MarkViewRenderer;

452

453

/** Define mark attributes */

454

addAttributes?(): Record<string, Attribute>;

455

456

/** Handle exit behavior when typing at mark boundary */

457

onExit?(): boolean;

458

}

459

```

460

461

**Usage Examples:**

462

463

```typescript

464

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

465

466

// Simple formatting mark

467

const HighlightMark = Mark.create({

468

name: 'highlight',

469

470

addAttributes() {

471

return {

472

color: {

473

default: 'yellow',

474

parseHTML: element => element.getAttribute('data-color'),

475

renderHTML: attributes => ({

476

'data-color': attributes.color,

477

}),

478

},

479

};

480

},

481

482

parseHTML() {

483

return [

484

{

485

tag: 'mark',

486

},

487

{

488

style: 'background-color',

489

getAttrs: value => ({ color: value }),

490

},

491

];

492

},

493

494

renderHTML({ mark, HTMLAttributes }) {

495

return [

496

'mark',

497

{

498

style: `background-color: ${mark.attrs.color}`,

499

...HTMLAttributes,

500

},

501

0,

502

];

503

},

504

505

addCommands() {

506

return {

507

setHighlight: (color: string = 'yellow') => ({ commands }) => {

508

return commands.setMark(this.name, { color });

509

},

510

toggleHighlight: (color: string = 'yellow') => ({ commands }) => {

511

return commands.toggleMark(this.name, { color });

512

},

513

unsetHighlight: () => ({ commands }) => {

514

return commands.unsetMark(this.name);

515

},

516

};

517

},

518

});

519

520

// Link mark with exit handling

521

const LinkMark = Mark.create({

522

name: 'link',

523

inclusive: false,

524

525

addAttributes() {

526

return {

527

href: {

528

default: null,

529

},

530

target: {

531

default: null,

532

},

533

rel: {

534

default: null,

535

},

536

};

537

},

538

539

parseHTML() {

540

return [

541

{

542

tag: 'a[href]',

543

getAttrs: node => ({

544

href: node.getAttribute('href'),

545

target: node.getAttribute('target'),

546

rel: node.getAttribute('rel'),

547

}),

548

},

549

];

550

},

551

552

renderHTML({ mark, HTMLAttributes }) {

553

return [

554

'a',

555

{

556

href: mark.attrs.href,

557

target: mark.attrs.target,

558

rel: mark.attrs.rel,

559

...HTMLAttributes,

560

},

561

0,

562

];

563

},

564

565

addCommands() {

566

return {

567

setLink: (attributes: { href: string; target?: string; rel?: string }) => ({ commands }) => {

568

return commands.setMark(this.name, attributes);

569

},

570

571

toggleLink: (attributes: { href: string; target?: string; rel?: string }) => ({ commands }) => {

572

return commands.toggleMark(this.name, attributes);

573

},

574

575

unsetLink: () => ({ commands }) => {

576

return commands.unsetMark(this.name);

577

},

578

};

579

},

580

581

// Handle exit when typing at end of link

582

onExit() {

583

return this.editor.commands.unsetMark(this.name);

584

},

585

});

586

587

// Use static handleExit for complex exit behavior

588

Mark.handleExit({ editor, mark: linkMark });

589

```

590

591

### Node and Mark Views

592

593

Custom rendering for nodes and marks using framework components or custom DOM manipulation.

594

595

```typescript { .api }

596

/**

597

* Node view renderer function type

598

*/

599

type NodeViewRenderer = (props: {

600

editor: Editor;

601

node: ProseMirrorNode;

602

getPos: () => number;

603

HTMLAttributes: Record<string, any>;

604

decorations: readonly Decoration[];

605

extension: Node;

606

}) => NodeView;

607

608

/**

609

* Mark view renderer function type

610

*/

611

type MarkViewRenderer = (props: {

612

editor: Editor;

613

mark: ProseMirrorMark;

614

HTMLAttributes: Record<string, any>;

615

extension: Mark;

616

}) => MarkView;

617

618

interface NodeView {

619

dom: HTMLElement;

620

contentDOM?: HTMLElement | null;

621

update?(node: ProseMirrorNode, decorations: readonly Decoration[]): boolean;

622

selectNode?(): void;

623

deselectNode?(): void;

624

setSelection?(anchor: number, head: number, root: Document | ShadowRoot): void;

625

stopEvent?(event: Event): boolean;

626

ignoreMutation?(record: MutationRecord): boolean | void;

627

destroy?(): void;

628

}

629

630

interface MarkView {

631

dom: HTMLElement;

632

contentDOM?: HTMLElement | null;

633

update?(mark: ProseMirrorMark): boolean;

634

destroy?(): void;

635

}

636

```

637

638

**Usage Examples:**

639

640

```typescript

641

// Custom node view

642

const CustomParagraphNode = Node.create({

643

name: 'customParagraph',

644

group: 'block',

645

content: 'inline*',

646

647

addNodeView() {

648

return ({ node, getPos, editor }) => {

649

const dom = document.createElement('div');

650

const contentDOM = document.createElement('p');

651

652

dom.className = 'custom-paragraph-wrapper';

653

contentDOM.className = 'custom-paragraph-content';

654

655

// Add custom controls

656

const controls = document.createElement('div');

657

controls.className = 'paragraph-controls';

658

controls.innerHTML = '<button>Edit</button>';

659

660

dom.appendChild(controls);

661

dom.appendChild(contentDOM);

662

663

return {

664

dom,

665

contentDOM,

666

update: (newNode) => {

667

return newNode.type === node.type;

668

},

669

selectNode: () => {

670

dom.classList.add('ProseMirror-selectednode');

671

},

672

deselectNode: () => {

673

dom.classList.remove('ProseMirror-selectednode');

674

}

675

};

676

};

677

}

678

});

679

680

// Custom mark view

681

const CustomEmphasisMark = Mark.create({

682

name: 'customEmphasis',

683

684

addMarkView() {

685

return ({ mark, HTMLAttributes }) => {

686

const dom = document.createElement('em');

687

const contentDOM = document.createElement('span');

688

689

dom.className = 'custom-emphasis';

690

contentDOM.className = 'emphasis-content';

691

692

dom.appendChild(contentDOM);

693

694

return {

695

dom,

696

contentDOM,

697

update: (newMark) => {

698

return newMark.type === mark.type;

699

}

700

};

701

};

702

}

703

});

704

```