or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

collaboration.mdcommands-and-editing.mdcursors-and-enhancements.mdhistory.mdindex.mdinput-and-keymaps.mdmarkdown.mdmenus-and-ui.mdmodel-and-schema.mdschema-definitions.mdstate-management.mdtables.mdtransformations.mdview-and-rendering.md

markdown.mddocs/

0

# Markdown

1

2

The markdown system provides bidirectional conversion between ProseMirror documents and Markdown text. It supports custom parsing and serialization rules while maintaining document fidelity.

3

4

## Capabilities

5

6

### Markdown Parser

7

8

Convert Markdown text to ProseMirror documents.

9

10

```typescript { .api }

11

/**

12

* Markdown parser for converting Markdown to ProseMirror documents

13

*/

14

class MarkdownParser {

15

/**

16

* Create a markdown parser

17

*/

18

constructor(schema: Schema, tokenizer: any, tokens: { [name: string]: ParseSpec });

19

20

/**

21

* Parse markdown text into a ProseMirror document

22

*/

23

parse(text: string): Node;

24

25

/**

26

* Parse markdown text into a document fragment

27

*/

28

parseSlice(text: string): Slice;

29

}

30

```

31

32

### Markdown Serializer

33

34

Convert ProseMirror documents to Markdown text.

35

36

```typescript { .api }

37

/**

38

* Markdown serializer for converting ProseMirror documents to Markdown

39

*/

40

class MarkdownSerializer {

41

/**

42

* Create a markdown serializer

43

*/

44

constructor(

45

nodes: { [name: string]: (state: MarkdownSerializerState, node: Node) => void },

46

marks: { [name: string]: MarkSerializerSpec }

47

);

48

49

/**

50

* Serialize a ProseMirror node to markdown text

51

*/

52

serialize(content: Node, options?: { tightLists?: boolean }): string;

53

}

54

55

/**

56

* Serialization state management

57

*/

58

class MarkdownSerializerState {

59

/**

60

* Current output text

61

*/

62

out: string;

63

64

/**

65

* Write text to output

66

*/

67

write(content?: string): void;

68

69

/**

70

* Close the current block

71

*/

72

closeBlock(node: Node): void;

73

74

/**

75

* Write text with proper escaping

76

*/

77

text(text: string, escape?: boolean): void;

78

79

/**

80

* Render node content

81

*/

82

render(node: Node, parent: Node, index: number): void;

83

84

/**

85

* Render inline content with marks

86

*/

87

renderInline(parent: Node): void;

88

89

/**

90

* Render a list

91

*/

92

renderList(node: Node, delim: string, firstDelim?: (index: number) => string): void;

93

94

/**

95

* Wrap text with delimiters

96

*/

97

wrapBlock(delim: string, firstDelim: string | null, node: Node, f: () => void): void;

98

99

/**

100

* Ensure block separation

101

*/

102

ensureNewLine(): void;

103

104

/**

105

* Prepare block for content

106

*/

107

prepare(str: string): string;

108

109

/**

110

* Quote text for safe output

111

*/

112

quote(str: string): string;

113

114

/**

115

* Repeat string n times

116

*/

117

repeat(str: string, n: number): string;

118

119

/**

120

* Get mark delimiter

121

*/

122

markString(mark: Mark, open: boolean, parent: Node, index: number): string;

123

124

/**

125

* Get current mark attributes

126

*/

127

getMarkAttrs(mark: Mark): Attrs;

128

}

129

```

130

131

### Parse Specifications

132

133

Define how Markdown tokens map to ProseMirror nodes.

134

135

```typescript { .api }

136

/**

137

* Specification for parsing a markdown token

138

*/

139

interface ParseSpec {

140

/**

141

* Node type to create

142

*/

143

node?: string;

144

145

/**

146

* Mark type to create

147

*/

148

mark?: string;

149

150

/**

151

* Attributes for the node/mark

152

*/

153

attrs?: Attrs | ((token: any) => Attrs);

154

155

/**

156

* Content handling

157

*/

158

content?: string;

159

160

/**

161

* Custom parsing function

162

*/

163

parse?: (state: any, token: any) => void;

164

165

/**

166

* Ignore this token

167

*/

168

ignore?: boolean;

169

}

170

171

/**

172

* Mark serialization specification

173

*/

174

type MarkSerializerSpec = {

175

/**

176

* Opening delimiter

177

*/

178

open: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string);

179

180

/**

181

* Closing delimiter

182

*/

183

close: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string);

184

185

/**

186

* Mixed content handling

187

*/

188

mixable?: boolean;

189

190

/**

191

* Expel marks when serializing

192

*/

193

expelEnclosingWhitespace?: boolean;

194

195

/**

196

* Escape function

197

*/

198

escape?: boolean;

199

};

200

```

201

202

### Default Implementations

203

204

Pre-configured parser and serializer for standard Markdown.

205

206

```typescript { .api }

207

/**

208

* Default markdown parser instance

209

*/

210

const defaultMarkdownParser: MarkdownParser;

211

212

/**

213

* Default markdown serializer instance

214

*/

215

const defaultMarkdownSerializer: MarkdownSerializer;

216

217

/**

218

* Markdown-compatible schema

219

*/

220

const schema: Schema;

221

```

222

223

**Usage Examples:**

224

225

```typescript

226

import {

227

MarkdownParser,

228

MarkdownSerializer,

229

defaultMarkdownParser,

230

defaultMarkdownSerializer,

231

schema as markdownSchema

232

} from "@tiptap/pm/markdown";

233

234

// Basic usage with defaults

235

const markdownText = `

236

# Hello World

237

238

This is **bold** and *italic* text.

239

240

- List item 1

241

- List item 2

242

243

\`\`\`javascript

244

console.log("Hello, world!");

245

\`\`\`

246

`;

247

248

// Parse markdown to ProseMirror

249

const doc = defaultMarkdownParser.parse(markdownText);

250

251

// Serialize ProseMirror to markdown

252

const serialized = defaultMarkdownSerializer.serialize(doc);

253

254

// Create editor with markdown schema

255

const state = EditorState.create({

256

schema: markdownSchema,

257

doc

258

});

259

260

// Custom markdown parser

261

const customParser = new MarkdownParser(mySchema, markdownit(), {

262

// Built-in tokens

263

blockquote: { block: "blockquote" },

264

paragraph: { block: "paragraph" },

265

list_item: { block: "list_item" },

266

bullet_list: { block: "bullet_list", attrs: { tight: true } },

267

ordered_list: {

268

block: "ordered_list",

269

attrs: (tok) => ({ order: +tok.attrGet("start") || 1, tight: true })

270

},

271

heading: {

272

block: "heading",

273

attrs: (tok) => ({ level: +tok.tag.slice(1) })

274

},

275

code_block: {

276

block: "code_block",

277

attrs: (tok) => ({ params: tok.info || "" })

278

},

279

fence: {

280

block: "code_block",

281

attrs: (tok) => ({ params: tok.info || "" })

282

},

283

hr: { node: "horizontal_rule" },

284

image: {

285

node: "image",

286

attrs: (tok) => ({

287

src: tok.attrGet("src"),

288

title: tok.attrGet("title") || null,

289

alt: tok.children?.[0]?.content || null

290

})

291

},

292

hardbreak: { node: "hard_break" },

293

294

// Inline tokens

295

em: { mark: "em" },

296

strong: { mark: "strong" },

297

link: {

298

mark: "link",

299

attrs: (tok) => ({

300

href: tok.attrGet("href"),

301

title: tok.attrGet("title") || null

302

})

303

},

304

code_inline: { mark: "code", noCloseToken: true },

305

306

// Custom tokens

307

custom_callout: {

308

block: "callout",

309

attrs: (tok) => ({ type: tok.info })

310

}

311

});

312

313

// Custom markdown serializer

314

const customSerializer = new MarkdownSerializer({

315

// Node serializers

316

blockquote(state, node) {

317

state.wrapBlock("> ", null, node, () => state.renderContent(node));

318

},

319

320

code_block(state, node) {

321

state.write("```" + (node.attrs.params || "") + "\n");

322

state.text(node.textContent, false);

323

state.ensureNewLine();

324

state.write("```");

325

state.closeBlock(node);

326

},

327

328

heading(state, node) {

329

state.write(state.repeat("#", node.attrs.level) + " ");

330

state.renderInline(node);

331

state.closeBlock(node);

332

},

333

334

horizontal_rule(state, node) {

335

state.write(node.attrs.markup || "---");

336

state.closeBlock(node);

337

},

338

339

bullet_list(state, node) {

340

state.renderList(node, " ", () => "* ");

341

},

342

343

ordered_list(state, node) {

344

const start = node.attrs.order || 1;

345

const maxW = String(start + node.childCount - 1).length;

346

const space = state.repeat(" ", maxW + 2);

347

state.renderList(node, space, (i) => {

348

const nStr = String(start + i);

349

return state.repeat(" ", maxW - nStr.length) + nStr + ". ";

350

});

351

},

352

353

list_item(state, node) {

354

state.renderContent(node);

355

},

356

357

paragraph(state, node) {

358

state.renderInline(node);

359

state.closeBlock(node);

360

},

361

362

image(state, node) {

363

state.write("![" + state.esc(node.attrs.alt || "") + "](" +

364

state.esc(node.attrs.src) +

365

(node.attrs.title ? " " + state.quote(node.attrs.title) : "") + ")");

366

},

367

368

hard_break(state, node, parent, index) {

369

for (let i = index + 1; i < parent.childCount; i++) {

370

if (parent.child(i).type != node.type) {

371

state.write("\\\n");

372

return;

373

}

374

}

375

},

376

377

text(state, node) {

378

state.text(node.text);

379

}

380

}, {

381

// Mark serializers

382

em: { open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true },

383

strong: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },

384

link: {

385

open: (_state, mark, parent, index) => {

386

return isPlainURL(mark, parent, index) ? "<" : "[";

387

},

388

close: (state, mark, parent, index) => {

389

return isPlainURL(mark, parent, index) ? ">" :

390

"](" + state.esc(mark.attrs.href) +

391

(mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")";

392

}

393

},

394

code: { open: "`", close: "`", escape: false }

395

});

396

397

function isPlainURL(link: Mark, parent: Node, index: number) {

398

if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;

399

const content = parent.child(index);

400

if (!content.isText || content.text != link.attrs.href ||

401

content.marks[content.marks.length - 1] != link) return false;

402

if (index == parent.childCount - 1) return true;

403

const next = parent.child(index + 1);

404

return !link.isInSet(next.marks);

405

}

406

```

407

408

## Advanced Markdown Features

409

410

### Custom Extensions

411

412

Add support for extended Markdown syntax.

413

414

```typescript

415

// GitHub Flavored Markdown extensions

416

class GFMExtension {

417

static addToParser(parser: MarkdownParser): MarkdownParser {

418

const tokens = {

419

...parser.tokens,

420

421

// Strikethrough

422

s: { mark: "strikethrough" },

423

424

// Task lists

425

task_list_item: {

426

block: "task_list_item",

427

attrs: (tok) => ({ checked: tok.attrGet("checked") === "true" })

428

},

429

430

// Tables

431

table: { block: "table" },

432

thead: { ignore: true },

433

tbody: { ignore: true },

434

tr: { block: "table_row" },

435

th: { block: "table_header" },

436

td: { block: "table_cell" },

437

438

// Footnotes

439

footnote_ref: {

440

node: "footnote_ref",

441

attrs: (tok) => ({ id: tok.meta.id, label: tok.meta.label })

442

}

443

};

444

445

return new MarkdownParser(parser.schema, parser.tokenizer, tokens);

446

}

447

448

static addToSerializer(serializer: MarkdownSerializer): MarkdownSerializer {

449

const nodes = {

450

...serializer.nodes,

451

452

task_list_item(state, node) {

453

const checked = node.attrs.checked ? "[x]" : "[ ]";

454

state.write(checked + " ");

455

state.renderContent(node);

456

},

457

458

table(state, node) {

459

state.renderTable(node);

460

},

461

462

footnote_ref(state, node) {

463

state.write(`[^${node.attrs.label}]`);

464

}

465

};

466

467

const marks = {

468

...serializer.marks,

469

470

strikethrough: {

471

open: "~~",

472

close: "~~",

473

mixable: true,

474

expelEnclosingWhitespace: true

475

}

476

};

477

478

return new MarkdownSerializer(nodes, marks);

479

}

480

}

481

482

// Math extension

483

class MathExtension {

484

static addToParser(parser: MarkdownParser): MarkdownParser {

485

const tokens = {

486

...parser.tokens,

487

488

math_inline: {

489

mark: "math",

490

attrs: (tok) => ({ content: tok.content })

491

},

492

493

math_block: {

494

block: "math_block",

495

attrs: (tok) => ({ content: tok.content })

496

}

497

};

498

499

return new MarkdownParser(parser.schema, parser.tokenizer, tokens);

500

}

501

502

static addToSerializer(serializer: MarkdownSerializer): MarkdownSerializer {

503

const nodes = {

504

...serializer.nodes,

505

506

math_block(state, node) {

507

state.write("$$\n");

508

state.text(node.attrs.content, false);

509

state.ensureNewLine();

510

state.write("$$");

511

state.closeBlock(node);

512

}

513

};

514

515

const marks = {

516

...serializer.marks,

517

518

math: {

519

open: "$",

520

close: "$",

521

escape: false

522

}

523

};

524

525

return new MarkdownSerializer(nodes, marks);

526

}

527

}

528

```

529

530

### Markdown Import/Export

531

532

Handle markdown file operations with metadata preservation.

533

534

```typescript

535

class MarkdownConverter {

536

constructor(

537

private parser: MarkdownParser,

538

private serializer: MarkdownSerializer

539

) {}

540

541

// Import markdown with frontmatter

542

importMarkdown(text: string): { doc: Node; metadata?: any } {

543

const { content, data } = this.extractFrontmatter(text);

544

const doc = this.parser.parse(content);

545

546

return { doc, metadata: data };

547

}

548

549

// Export with frontmatter preservation

550

exportMarkdown(doc: Node, metadata?: any): string {

551

const content = this.serializer.serialize(doc);

552

553

if (metadata) {

554

const frontmatter = this.serializeFrontmatter(metadata);

555

return `${frontmatter}\n${content}`;

556

}

557

558

return content;

559

}

560

561

private extractFrontmatter(text: string): { content: string; data?: any } {

562

const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;

563

const match = text.match(frontmatterRegex);

564

565

if (match) {

566

try {

567

const data = yaml.parse(match[1]);

568

return { content: match[2], data };

569

} catch (error) {

570

console.warn("Failed to parse frontmatter:", error);

571

}

572

}

573

574

return { content: text };

575

}

576

577

private serializeFrontmatter(data: any): string {

578

try {

579

const yamlContent = yaml.stringify(data).trim();

580

return `---\n${yamlContent}\n---`;

581

} catch (error) {

582

console.warn("Failed to serialize frontmatter:", error);

583

return "";

584

}

585

}

586

587

// Convert between different markdown flavors

588

convertFlavor(

589

text: string,

590

fromFlavor: "commonmark" | "gfm" | "custom",

591

toFlavor: "commonmark" | "gfm" | "custom"

592

): string {

593

// Parse with source flavor

594

const sourceParser = this.getParserForFlavor(fromFlavor);

595

const doc = sourceParser.parse(text);

596

597

// Serialize with target flavor

598

const targetSerializer = this.getSerializerForFlavor(toFlavor);

599

return targetSerializer.serialize(doc);

600

}

601

602

private getParserForFlavor(flavor: string): MarkdownParser {

603

switch (flavor) {

604

case "gfm":

605

return GFMExtension.addToParser(this.parser);

606

case "custom":

607

return MathExtension.addToParser(this.parser);

608

default:

609

return this.parser;

610

}

611

}

612

613

private getSerializerForFlavor(flavor: string): MarkdownSerializer {

614

switch (flavor) {

615

case "gfm":

616

return GFMExtension.addToSerializer(this.serializer);

617

case "custom":

618

return MathExtension.addToSerializer(this.serializer);

619

default:

620

return this.serializer;

621

}

622

}

623

}

624

```

625

626

### Live Markdown Editing

627

628

Implement live markdown preview and editing modes.

629

630

```typescript

631

class MarkdownEditor {

632

private view: EditorView;

633

private isMarkdownMode = false;

634

635

constructor(

636

container: HTMLElement,

637

private parser: MarkdownParser,

638

private serializer: MarkdownSerializer

639

) {

640

this.createEditor(container);

641

this.setupModeToggle(container);

642

}

643

644

private createEditor(container: HTMLElement) {

645

this.view = new EditorView(container, {

646

state: EditorState.create({

647

schema: this.parser.schema,

648

plugins: [

649

// Add markdown-specific plugins

650

this.createMarkdownPlugin(),

651

keymap({

652

"Mod-m": () => this.toggleMode(),

653

"Mod-Shift-p": () => this.showPreview()

654

})

655

]

656

})

657

});

658

}

659

660

private createMarkdownPlugin(): Plugin {

661

return new Plugin({

662

state: {

663

init: () => ({ isMarkdownMode: false }),

664

apply: (tr, value) => {

665

const meta = tr.getMeta("markdown-mode");

666

if (meta !== undefined) {

667

return { isMarkdownMode: meta };

668

}

669

return value;

670

}

671

},

672

673

props: {

674

decorations: (state) => {

675

const pluginState = this.getMarkdownPluginState(state);

676

if (pluginState.isMarkdownMode) {

677

return this.createMarkdownDecorations(state);

678

}

679

return null;

680

}

681

}

682

});

683

}

684

685

private createMarkdownDecorations(state: EditorState): DecorationSet {

686

const decorations: Decoration[] = [];

687

688

// Add syntax highlighting decorations

689

state.doc.descendants((node, pos) => {

690

if (node.isText && node.text) {

691

const text = node.text;

692

693

// Highlight markdown syntax

694

const patterns = [

695

{ regex: /(\*\*|__)(.*?)\1/g, class: "markdown-bold" },

696

{ regex: /(\*|_)(.*?)\1/g, class: "markdown-italic" },

697

{ regex: /(`)(.*?)\1/g, class: "markdown-code" },

698

{ regex: /^(#{1,6})\s/gm, class: "markdown-heading" },

699

{ regex: /^\s*[-*+]\s/gm, class: "markdown-list" }

700

];

701

702

for (const pattern of patterns) {

703

let match;

704

while ((match = pattern.regex.exec(text)) !== null) {

705

decorations.push(

706

Decoration.inline(

707

pos + match.index,

708

pos + match.index + match[0].length,

709

{ class: pattern.class }

710

)

711

);

712

}

713

}

714

}

715

});

716

717

return DecorationSet.create(state.doc, decorations);

718

}

719

720

toggleMode(): boolean {

721

this.isMarkdownMode = !this.isMarkdownMode;

722

723

if (this.isMarkdownMode) {

724

// Switch to markdown text mode

725

const markdown = this.serializer.serialize(this.view.state.doc);

726

const textDoc = this.view.state.schema.node("doc", null, [

727

this.view.state.schema.node("code_block", { params: "markdown" },

728

this.view.state.schema.text(markdown)

729

)

730

]);

731

732

this.view.dispatch(

733

this.view.state.tr

734

.replaceWith(0, this.view.state.doc.content.size, textDoc)

735

.setMeta("markdown-mode", true)

736

);

737

} else {

738

// Switch back to rich text mode

739

const codeBlock = this.view.state.doc.firstChild;

740

if (codeBlock && codeBlock.type.name === "code_block") {

741

const markdown = codeBlock.textContent;

742

try {

743

const richDoc = this.parser.parse(markdown);

744

this.view.dispatch(

745

this.view.state.tr

746

.replaceWith(0, this.view.state.doc.content.size, richDoc)

747

.setMeta("markdown-mode", false)

748

);

749

} catch (error) {

750

console.error("Failed to parse markdown:", error);

751

return false;

752

}

753

}

754

}

755

756

return true;

757

}

758

759

private getMarkdownPluginState(state: EditorState) {

760

// Get markdown plugin state helper

761

return state.plugins.find(p => p.spec.key === "markdown")?.getState(state) || {};

762

}

763

764

showPreview() {

765

const markdown = this.serializer.serialize(this.view.state.doc);

766

const previewWindow = window.open("", "_blank");

767

768

if (previewWindow) {

769

previewWindow.document.write(`

770

<html>

771

<head>

772

<title>Markdown Preview</title>

773

<style>

774

body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }

775

pre { background: #f5f5f5; padding: 10px; border-radius: 4px; }

776

code { background: #f5f5f5; padding: 2px 4px; border-radius: 2px; }

777

</style>

778

</head>

779

<body>

780

<pre><code>${markdown.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code></pre>

781

</body>

782

</html>

783

`);

784

}

785

}

786

}

787

```

788

789

## Types

790

791

```typescript { .api }

792

/**

793

* Token parsing specification

794

*/

795

interface ParseSpec {

796

node?: string;

797

mark?: string;

798

attrs?: Attrs | ((token: any) => Attrs);

799

content?: string;

800

parse?: (state: any, token: any) => void;

801

ignore?: boolean;

802

noCloseToken?: boolean;

803

}

804

805

/**

806

* Mark serialization specification

807

*/

808

type MarkSerializerSpec = {

809

open: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string);

810

close: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string);

811

mixable?: boolean;

812

expelEnclosingWhitespace?: boolean;

813

escape?: boolean;

814

};

815

816

/**

817

* Serialization options

818

*/

819

interface SerializationOptions {

820

tightLists?: boolean;

821

}

822

```