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

rule-systems.mddocs/

0

# Rule Systems

1

2

@tiptap/core provides powerful rule systems for transforming input and pasted content. InputRules enable markdown-like shortcuts during typing, while PasteRules transform content when pasted into the editor.

3

4

## Capabilities

5

6

### InputRule

7

8

InputRules automatically transform text as you type, enabling markdown-like shortcuts and other input transformations.

9

10

```typescript { .api }

11

/**

12

* Rule for transforming text input based on patterns

13

*/

14

class InputRule {

15

/**

16

* Create a new input rule

17

* @param config - Rule configuration

18

*/

19

constructor(config: {

20

/** Pattern to match against input text */

21

find: RegExp | ((value: string) => RegExpMatchArray | null);

22

23

/** Handler function to process matches */

24

handler: (props: InputRuleHandlerProps) => void | null;

25

});

26

27

/** Pattern used to match input */

28

find: RegExp | ((value: string) => RegExpMatchArray | null);

29

30

/** Handler function for processing matches */

31

handler: (props: InputRuleHandlerProps) => void | null;

32

}

33

34

interface InputRuleHandlerProps {

35

/** Current editor state */

36

state: EditorState;

37

38

/** Range of the matched text */

39

range: { from: number; to: number };

40

41

/** RegExp match result */

42

match: RegExpMatchArray;

43

44

/** Access to single commands */

45

commands: SingleCommands;

46

47

/** Create command chain */

48

chain: () => ChainedCommands;

49

50

/** Check command executability */

51

can: () => CanCommands;

52

}

53

54

/**

55

* Create input rules plugin for ProseMirror

56

* @param config - Plugin configuration

57

* @returns ProseMirror plugin

58

*/

59

function inputRulesPlugin(config: {

60

editor: Editor;

61

rules: InputRule[];

62

}): Plugin;

63

```

64

65

**Usage Examples:**

66

67

```typescript

68

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

69

70

// Markdown-style heading rule

71

const headingRule = new InputRule({

72

find: /^(#{1,6})\s(.*)$/,

73

handler: ({ range, match, commands }) => {

74

const level = match[1].length;

75

const text = match[2];

76

77

commands.deleteRange(range);

78

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

79

commands.insertContent(text);

80

}

81

});

82

83

// Bold text rule

84

const boldRule = new InputRule({

85

find: /\*\*([^*]+)\*\*$/,

86

handler: ({ range, match, commands }) => {

87

const text = match[1];

88

89

commands.deleteRange(range);

90

commands.insertContent({

91

type: 'text',

92

text,

93

marks: [{ type: 'bold' }]

94

});

95

}

96

});

97

98

// Horizontal rule

99

const hrRule = new InputRule({

100

find: /^---$/,

101

handler: ({ range, commands }) => {

102

commands.deleteRange(range);

103

commands.insertContent({ type: 'horizontalRule' });

104

}

105

});

106

107

// Code block rule

108

const codeBlockRule = new InputRule({

109

find: /^```([a-zA-Z]*)?\s$/,

110

handler: ({ range, match, commands }) => {

111

const language = match[1] || null;

112

113

commands.deleteRange(range);

114

commands.setNode('codeBlock', { language });

115

}

116

});

117

118

// Blockquote rule

119

const blockquoteRule = new InputRule({

120

find: /^>\s(.*)$/,

121

handler: ({ range, match, commands }) => {

122

const text = match[1];

123

124

commands.deleteRange(range);

125

commands.wrapIn('blockquote');

126

commands.insertContent(text);

127

}

128

});

129

130

// List item rule

131

const listItemRule = new InputRule({

132

find: /^[*-]\s(.*)$/,

133

handler: ({ range, match, commands }) => {

134

const text = match[1];

135

136

commands.deleteRange(range);

137

commands.wrapIn('bulletList');

138

commands.insertContent(text);

139

}

140

});

141

142

// Using function-based find pattern

143

const smartQuoteRule = new InputRule({

144

find: (value: string) => {

145

const match = value.match(/"([^"]+)"$/);

146

return match;

147

},

148

handler: ({ range, match, commands }) => {

149

const text = match[1];

150

151

commands.deleteRange(range);

152

commands.insertContent(`"${text}"`); // Use smart quotes

153

}

154

});

155

```

156

157

### PasteRule

158

159

PasteRules transform content when it's pasted into the editor, allowing custom handling of different content types.

160

161

```typescript { .api }

162

/**

163

* Rule for transforming pasted content based on patterns

164

*/

165

class PasteRule {

166

/**

167

* Create a new paste rule

168

* @param config - Rule configuration

169

*/

170

constructor(config: {

171

/** Pattern to match against pasted content */

172

find: RegExp | ((value: string) => RegExpMatchArray | null);

173

174

/** Handler function to process matches */

175

handler: (props: PasteRuleHandlerProps) => void | null;

176

});

177

178

/** Pattern used to match pasted content */

179

find: RegExp | ((value: string) => RegExpMatchArray | null);

180

181

/** Handler function for processing matches */

182

handler: (props: PasteRuleHandlerProps) => void | null;

183

}

184

185

interface PasteRuleHandlerProps {

186

/** Current editor state */

187

state: EditorState;

188

189

/** Range where content will be pasted */

190

range: { from: number; to: number };

191

192

/** RegExp match result */

193

match: RegExpMatchArray;

194

195

/** Access to single commands */

196

commands: SingleCommands;

197

198

/** Create command chain */

199

chain: () => ChainedCommands;

200

201

/** Check command executability */

202

can: () => CanCommands;

203

204

/** The pasted text content */

205

pastedText: string;

206

207

/** Drop event (if paste was triggered by drag and drop) */

208

dropEvent?: DragEvent;

209

}

210

211

/**

212

* Create paste rules plugin for ProseMirror

213

* @param config - Plugin configuration

214

* @returns Array of ProseMirror plugins

215

*/

216

function pasteRulesPlugin(config: {

217

editor: Editor;

218

rules: PasteRule[];

219

}): Plugin[];

220

```

221

222

**Usage Examples:**

223

224

```typescript

225

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

226

227

// URL to link conversion

228

const urlTolinkRule = new PasteRule({

229

find: /https?:\/\/[^\s]+/g,

230

handler: ({ range, match, commands }) => {

231

const url = match[0];

232

233

commands.deleteRange(range);

234

commands.insertContent({

235

type: 'text',

236

text: url,

237

marks: [{ type: 'link', attrs: { href: url } }]

238

});

239

}

240

});

241

242

// YouTube URL to embed

243

const youtubeRule = new PasteRule({

244

find: /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/,

245

handler: ({ range, match, commands }) => {

246

const videoId = match[1];

247

248

commands.deleteRange(range);

249

commands.insertContent({

250

type: 'youtube',

251

attrs: { videoId }

252

});

253

}

254

});

255

256

// Email to mailto link

257

const emailRule = new PasteRule({

258

find: /[\w.-]+@[\w.-]+\.\w+/g,

259

handler: ({ range, match, commands }) => {

260

const email = match[0];

261

262

commands.deleteRange(range);

263

commands.insertContent({

264

type: 'text',

265

text: email,

266

marks: [{ type: 'link', attrs: { href: `mailto:${email}` } }]

267

});

268

}

269

});

270

271

// GitHub issue/PR references

272

const githubRefRule = new PasteRule({

273

find: /#(\d+)/g,

274

handler: ({ range, match, commands }) => {

275

const issueNumber = match[1];

276

277

commands.deleteRange(range);

278

commands.insertContent({

279

type: 'githubRef',

280

attrs: {

281

number: parseInt(issueNumber),

282

type: 'issue'

283

}

284

});

285

}

286

});

287

288

// Code detection and formatting

289

const codeRule = new PasteRule({

290

find: /`([^`]+)`/g,

291

handler: ({ range, match, commands }) => {

292

const code = match[1];

293

294

commands.deleteRange(range);

295

commands.insertContent({

296

type: 'text',

297

text: code,

298

marks: [{ type: 'code' }]

299

});

300

}

301

});

302

303

// Image URL to image node

304

const imageRule = new PasteRule({

305

find: /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp))/gi,

306

handler: ({ range, match, commands }) => {

307

const src = match[0];

308

309

commands.deleteRange(range);

310

commands.insertContent({

311

type: 'image',

312

attrs: { src }

313

});

314

}

315

});

316

317

// Markdown table detection

318

const tableRule = new PasteRule({

319

find: /^\|(.+)\|\s*\n\|[-\s|]+\|\s*\n((?:\|.+\|\s*\n?)*)/m,

320

handler: ({ range, match, commands, chain }) => {

321

const headerRow = match[1];

322

const rows = match[2];

323

324

// Parse markdown table and convert to table node

325

const headers = headerRow.split('|').map(h => h.trim()).filter(Boolean);

326

const tableRows = rows.split('\n').filter(Boolean).map(row =>

327

row.split('|').map(cell => cell.trim()).filter(Boolean)

328

);

329

330

commands.deleteRange(range);

331

chain()

332

.insertTable({ rows: tableRows.length + 1, cols: headers.length })

333

.run();

334

}

335

});

336

```

337

338

### Rule Extension Integration

339

340

How to integrate input and paste rules into extensions.

341

342

```typescript { .api }

343

/**

344

* Extension methods for adding rules

345

*/

346

interface ExtensionConfig {

347

/**

348

* Add input rules to the extension

349

* @returns Array of input rules

350

*/

351

addInputRules?(): InputRule[];

352

353

/**

354

* Add paste rules to the extension

355

* @returns Array of paste rules

356

*/

357

addPasteRules?(): PasteRule[];

358

}

359

```

360

361

**Usage Examples:**

362

363

```typescript

364

import { Extension, InputRule, PasteRule } from '@tiptap/core';

365

366

// Extension with input and paste rules

367

const MarkdownExtension = Extension.create({

368

name: 'markdown',

369

370

addInputRules() {

371

return [

372

// Heading rules

373

new InputRule({

374

find: /^(#{1,6})\s(.*)$/,

375

handler: ({ range, match, commands }) => {

376

const level = match[1].length;

377

const text = match[2];

378

379

commands.deleteRange(range);

380

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

381

commands.insertContent(text);

382

}

383

}),

384

385

// Bold text rule

386

new InputRule({

387

find: /\*\*([^*]+)\*\*$/,

388

handler: ({ range, match, commands }) => {

389

const text = match[1];

390

391

commands.deleteRange(range);

392

commands.insertContent({

393

type: 'text',

394

text,

395

marks: [{ type: 'bold' }]

396

});

397

}

398

}),

399

400

// Italic text rule

401

new InputRule({

402

find: /\*([^*]+)\*$/,

403

handler: ({ range, match, commands }) => {

404

const text = match[1];

405

406

commands.deleteRange(range);

407

commands.insertContent({

408

type: 'text',

409

text,

410

marks: [{ type: 'italic' }]

411

});

412

}

413

})

414

];

415

},

416

417

addPasteRules() {

418

return [

419

// Convert URLs to links

420

new PasteRule({

421

find: /https?:\/\/[^\s]+/g,

422

handler: ({ range, match, commands }) => {

423

const url = match[0];

424

425

commands.deleteRange(range);

426

commands.insertContent({

427

type: 'text',

428

text: url,

429

marks: [{ type: 'link', attrs: { href: url } }]

430

});

431

}

432

}),

433

434

// Convert email addresses to mailto links

435

new PasteRule({

436

find: /[\w.-]+@[\w.-]+\.\w+/g,

437

handler: ({ range, match, commands }) => {

438

const email = match[0];

439

440

commands.deleteRange(range);

441

commands.insertContent({

442

type: 'text',

443

text: email,

444

marks: [{ type: 'link', attrs: { href: `mailto:${email}` } }]

445

});

446

}

447

})

448

];

449

}

450

});

451

452

// Node with specific input rules

453

const HeadingNode = Node.create({

454

name: 'heading',

455

group: 'block',

456

content: 'inline*',

457

458

addAttributes() {

459

return {

460

level: {

461

default: 1,

462

rendered: false,

463

},

464

};

465

},

466

467

addInputRules() {

468

return [

469

new InputRule({

470

find: /^(#{1,6})\s(.*)$/,

471

handler: ({ range, match, commands }) => {

472

const level = match[1].length;

473

const text = match[2];

474

475

commands.deleteRange(range);

476

commands.setNode(this.name, { level });

477

commands.insertContent(text);

478

}

479

})

480

];

481

}

482

});

483

484

// Mark with input and paste rules

485

const LinkMark = Mark.create({

486

name: 'link',

487

488

addAttributes() {

489

return {

490

href: {

491

default: null,

492

},

493

};

494

},

495

496

addInputRules() {

497

return [

498

// Markdown link syntax: [text](url)

499

new InputRule({

500

find: /\[([^\]]+)\]\(([^)]+)\)$/,

501

handler: ({ range, match, commands }) => {

502

const text = match[1];

503

const href = match[2];

504

505

commands.deleteRange(range);

506

commands.insertContent({

507

type: 'text',

508

text,

509

marks: [{ type: this.name, attrs: { href } }]

510

});

511

}

512

})

513

];

514

},

515

516

addPasteRules() {

517

return [

518

// Auto-link URLs

519

new PasteRule({

520

find: /https?:\/\/[^\s]+/g,

521

handler: ({ range, match, commands }) => {

522

const url = match[0];

523

524

commands.deleteRange(range);

525

commands.insertContent({

526

type: 'text',

527

text: url,

528

marks: [{ type: this.name, attrs: { href: url } }]

529

});

530

}

531

})

532

];

533

}

534

});

535

```

536

537

### Advanced Rule Patterns

538

539

Complex rule patterns and techniques for advanced transformations.

540

541

```typescript { .api }

542

// Advanced input rule patterns

543

544

// Multi-line rule detection

545

const codeBlockRule = new InputRule({

546

find: /^```(\w+)?\s*\n([\s\S]*?)```$/,

547

handler: ({ range, match, commands }) => {

548

const language = match[1];

549

const code = match[2];

550

551

commands.deleteRange(range);

552

commands.insertContent({

553

type: 'codeBlock',

554

attrs: { language },

555

content: [{ type: 'text', text: code }]

556

});

557

}

558

});

559

560

// Context-aware rules

561

const smartListRule = new InputRule({

562

find: /^(\d+)\.\s(.*)$/,

563

handler: ({ range, match, commands, state }) => {

564

const number = parseInt(match[1]);

565

const text = match[2];

566

567

// Check if we're already in a list

568

const isInList = state.selection.$from.node(-2)?.type.name === 'orderedList';

569

570

commands.deleteRange(range);

571

572

if (isInList) {

573

commands.splitListItem('listItem');

574

} else {

575

commands.wrapIn('orderedList', { start: number });

576

}

577

578

commands.insertContent(text);

579

}

580

});

581

582

// Conditional rule application

583

const conditionalRule = new InputRule({

584

find: /^@(\w+)\s(.*)$/,

585

handler: ({ range, match, commands, state, can }) => {

586

const mentionType = match[1];

587

const text = match[2];

588

589

// Only apply if we can insert mentions

590

if (!can().insertMention) {

591

return null; // Don't handle this rule

592

}

593

594

commands.deleteRange(range);

595

commands.insertMention({ type: mentionType, label: text });

596

}

597

});

598

599

// Rule with side effects

600

const trackingRule = new InputRule({

601

find: /^\$track\s(.*)$/,

602

handler: ({ range, match, commands }) => {

603

const eventName = match[1];

604

605

// Track the event

606

analytics?.track(eventName);

607

608

commands.deleteRange(range);

609

commands.insertContent(`Tracked: ${eventName}`);

610

}

611

});

612

```

613

614

### Rule Debugging and Testing

615

616

Utilities for debugging and testing rule behavior.

617

618

```typescript { .api }

619

// Debug rule matching

620

function debugInputRule(rule: InputRule, text: string): boolean {

621

if (typeof rule.find === 'function') {

622

const result = rule.find(text);

623

console.log('Function match result:', result);

624

return !!result;

625

} else {

626

const match = text.match(rule.find);

627

console.log('RegExp match result:', match);

628

return !!match;

629

}

630

}

631

632

// Test rule handler

633

function testRuleHandler(

634

rule: InputRule,

635

text: string,

636

mockCommands: Partial<SingleCommands>

637

): void {

638

const match = typeof rule.find === 'function'

639

? rule.find(text)

640

: text.match(rule.find);

641

642

if (match) {

643

rule.handler({

644

state: mockState,

645

range: { from: 0, to: text.length },

646

match,

647

commands: mockCommands as SingleCommands,

648

chain: () => ({} as ChainedCommands),

649

can: () => ({} as CanCommands)

650

});

651

}

652

}

653

654

// Rule performance testing

655

function benchmarkRule(rule: InputRule, testCases: string[]): number {

656

const start = performance.now();

657

658

testCases.forEach(text => {

659

if (typeof rule.find === 'function') {

660

rule.find(text);

661

} else {

662

text.match(rule.find);

663

}

664

});

665

666

return performance.now() - start;

667

}

668

```

669

670

**Usage Examples:**

671

672

```typescript

673

// Debug heading rule

674

const headingRule = new InputRule({

675

find: /^(#{1,6})\s(.*)$/,

676

handler: ({ range, match, commands }) => {

677

console.log('Heading match:', match);

678

// ... handler logic

679

}

680

});

681

682

debugInputRule(headingRule, '## My Heading'); // true

683

debugInputRule(headingRule, 'Regular text'); // false

684

685

// Test rule performance

686

const testCases = [

687

'# Heading 1',

688

'## Heading 2',

689

'Regular paragraph',

690

'**Bold text**',

691

'More normal text'

692

];

693

694

const time = benchmarkRule(headingRule, testCases);

695

console.log(`Rule processed ${testCases.length} cases in ${time}ms`);

696

697

// Test rule in isolation

698

testRuleHandler(headingRule, '## Test Heading', {

699

deleteRange: (range) => console.log('Delete range:', range),

700

setNode: (type, attrs) => console.log('Set node:', type, attrs),

701

insertContent: (content) => console.log('Insert content:', content)

702

});

703

```