Skip to content

Commit 35645d9

Browse files
bdbchCopilot
andauthored
Markdown Support (#6821)
* feat(markdown): add core markdown support with parser, serializer, and tokenizer * remove autosort imports ignore * remove unnecessary comments * WIP: markdown * feat: add Markdown serialization support and demo implementation - Implemented `getMarkdown()` method in the Editor class for Markdown serialization. - Enhanced MarkdownManager to support JSONContent and improved rendering logic. - Added Markdown rendering capabilities to Emoji, Heading, BulletList, ListItem, and Paragraph nodes. - Created a Markdown preview demo in React with styling. * WIP: nested node rendering * feat: enhance markdown support with new extensions and rendering capabilities * feat: add markdown rendering support for various extensions including image, task list, and mathematics * chore: clean up unused styles in markdown demo * fix(youtube): update markdown rendering format for YouTube videos * feat(table): add table extension with markdown rendering support * feat(table): implement markdown rendering for tables * fix(table): improve handling of cell content in markdown rendering * feat(markdown): enhance text node rendering with mark handling * fix(list-item): improve markdown rendering for nested list items * feat(markdown): enhance mark handling in node rendering for improved markdown output * feat(markdown): refactor mark handling logic and introduce utility functions for better readability and maintainability * feat(markdown): implement markdown parsing and rendering for various elements including lists, headings, and text * feat(markdown): add custom React node support and extend markdown features with YouTube, images, mentions, and mathematics * feat(markdown): implement contentType option for parsing JSON, HTML, and Markdown; add example usage * Revert "feat(markdown): implement contentType option for parsing JSON, HTML, and Markdown; add example usage" This reverts commit 114e8f5. * feat: add markdown extension for Tiptap - Introduced a new package `@tiptap/markdown` for markdown parsing and serialization. - Implemented `MarkdownManager` to handle markdown content and extensions. - Added `Markdown` extension to integrate markdown functionality into the Tiptap editor. - Enhanced editor capabilities with methods to get and set content as markdown. - Created utility functions for handling markdown block wrapping and mark management. - Updated type definitions to support markdown-related types and helpers. - Added README and package.json for the new markdown package. * feat: export commands and TypeScript typings from @tiptap/core * refactor: reorganize Markdown type imports in Extendable.ts * refactor: remove markdown registration logic from setupExtensions * refactor: simplify tsup configuration by merging entry points * fix markdown preview * feat: enhance markdown parsing with custom tokenizers and highlight support * feat: add TableKit extension and enhance markdown parsing for tables * fix: improve table header and cell parsing for markdown * fix: remove deprecated '@types/marked' dependency from pnpm-lock.yaml * update demo and commands export * allow nested task lists * feat(task-list): add support for nested task lists in markdown parsing and rendering * feat(list-item, task-item): refactor rendering to use renderNestedMarkdownContent utility for improved nested markdown support * feat(task-list): integrate parseIndentedBlocks utility for improved nested task list parsing * feat(markdown): implement utilities for parsing and rendering nested markdown content * feat(markdown): add allowedAttributes option to filter attributes in markdown specifications * fix(markdown): use 'as const' for tokenizer level in atom and inline markdown specs * feat(markdown): enhance type definitions for createAtomBlockMarkdownSpec, createBlockMarkdownSpec, and createInlineMarkdownSpec functions * test(markdown): add comprehensive tests for MarkdownManager functionality and extensions * fix(markdown): improve attribute parsing by handling quoted strings correctly * feat(markdown): add custom nested parser support for task lists and add tests * Feature/markdown support tests (#7050) * feat(markdown): update AtomBlockMarkdownSpec to use nodeName and enhance task list conversion tests * add tests for ordered list * fix(tests): correct subitem numbering in ordered list test cases * feat(markdown): enhance ExtendableMarkdownSpec and update createBlockMarkdownSpec for improved markdown handling * fix(markdown): update expected input format in custom atom, block, and inline tests for consistency * fix(tests): normalize JSON structure in markdown conversion tests for consistency * fix(ordered-list): update start attribute handling in ordered list parsing * feat(ordered-list): implement tokenizer for ordered list parsing and nesting structure * feat(ordered-list): refactor tokenizer and add utility functions for ordered list parsing * fix(markdown): update content handling in createBlockMarkdownSpec and createInlineMarkdownSpec for improved rendering * feat(markdown): add HTML parsing support to MarkdownManager and update demo content * refactor(mention): remove old comment * refactor(markdown): remove deprecated name property from ExtendableMarkdownSpec * update changesets so the markdown package can be versioned seperated * add initial version * feat(markdown): changeset * temporary add markdown scripts * chore: group markdown scripts * feat(markdown): add rendering helpers for markdown processing * Register provided extensions in MarkdownManager Remove explicit parameter types and eslint-disable on tokenize in createInlineMarkdownSpec to simplify the signature and avoid unused param linting. * Remove match field from ExtendableMarkdownSpec * Refactor markdown API to new spec names Replace legacy `markdown` config with explicit fields: markdownTokenName, parseMarkdown, renderMarkdown, markdownTokenizer, and markdownOptions. Update create*MarkdownSpec helpers, Extendable types, MarkdownManager, extensions, and tests to use the new API * Update changeset example to import commands * Remove MarkdownPreview demo and tweak parser demo Import KaTeX stylesheet for math rendering. Replace customReactNode insertion to insert a :::react fenced Markdown block via editor.chain().insertContent(..., { asMarkdown: true }). * Use markdownOptions extension field * Replace contentAsMarkdown/asMarkdown with contentType Add a ContentType type and an assumeContentType helper to determine when string content should be treated as markdown. Update the Markdown extension to accept editor.options.contentType and command-level contentType, parsing string input to JSON only when the content type is 'markdown'. Update demos and the changeset/docs to use the new option. * Remove redundant markdown spec validation tests * Allow tokenizer start to be string or function * fix tests * Feature/markdown support nested blocks (#7068) * allow parsing of nested content * refactor: add support for nested block tokens & reinitialization of the lexer * use correct block separator inside block markdown specs * update ordered-list tokenizer.start function * remove debug console.log from renderTableToMarkdown function * update ESLint rule to ignore unused vars with leading underscores remove commented eslint-disable directive in createInlineMarkdownSpec * refactor: remove unused variable from tokenize function in createAtomBlockMarkdownSpec * fix: use substring instead of substr for ID generation in insertMention function * fix: improve loop condition for block pattern matching in createBlockMarkdownSpec function * fix: ensure start method in markdownTokenizer returns -1 for unmatched task list items * fix: import ExtendableConfig type in MarkdownManager for consistency * fix: throw error on HTML parsing failure instead of returning null * Update tests/cypress/integration/markdown/manager.spec.ts Co-authored-by: Copilot <[email protected]> * fix: replace console.error with throw for error handling in Markdown extension * fix: remove incomplete markdown parsing implementation for underline extension * fix: update regex match handling to return -1 for no match in markdown specifications * fix: update MarkdownManager to reset lexer and improve extension registration * feat: add TODO comments for markdown support in Details and DetailsSummary components * feat: add support for nested React components in markdown content * feat: implement markdown parsing and rendering for underline extension * Change markup for atom nodes to not conflict with block level nodes * feat: add details and summary components with markdown support * fix: correct markdown block syntax by adding closing delimiter * feat: add markdown demo with custom React components and extended features * feat: add markdown demo with custom React components and extended features * feat: add serialization demo for Tiptap content with Markdown support * feat: add custom Markdown syntax support with styled components * feat: add styles for inline and mark custom syntax types * fix: correct expected input format for custom atom in Markdown spec * fix: add closing delimiter for YouTube markdown syntax * fix: add closing delimiter for YouTube markdown syntax * fix: add type annotations for marked.js extension and tokenizer * fix: correct markdown capitalization in documentation * fix: remove redundant export statement for commands and TypeScript typings * feat: add baseExtensions property to ExtensionManager and update related components * fix: update parseMarkdown to allow indented code blocks * feat: add extensions property to Markdown extension options * refactor: change registerExtension method to public visibility * fix: update media query breakpoints from 900px to 400px for responsive layout docs: clarify markdownTokenName description and add reference to Marked Lexer * refactor: simplify layout and remove unused styles in Markdown demo * fix: adjust gap and overflow properties in Markdown demo split panel * docs: add JSDoc comments for wrapInMarkdownBlock function --------- Co-authored-by: Copilot <[email protected]>
1 parent 616d6f9 commit 35645d9

File tree

90 files changed

+6341
-128
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+6341
-128
lines changed

.changeset/commands-exported.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@tiptap/core": minor
3+
---
4+
5+
All commands and their corresponding TypeScript types are now exported from `@tiptap/core` so they can be imported and referenced directly by consumers. This makes it easier to build typed helpers, extensions, and tests that depend on the command signatures.
6+
7+
Why:
8+
- Previously some command option types were only available as internal types or scattered across files, which made it awkward for downstream users to import and reuse them.
9+
10+
```ts
11+
import { commands } from '@tiptap/core'
12+
```
13+
14+
Notes:
15+
- This is a non-breaking, additive change. It improves ergonomics for TypeScript consumers.
16+
- If you rely on previously private/internal types, prefer the exported types from `@tiptap/core` going forward.

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
33
"changelog": "@changesets/cli/changelog",
44
"commit": false,
5-
"fixed": [["@tiptap/*"]],
5+
"fixed": [["@tiptap/*", "!@tiptap/markdown"], ["@tiptap/markdown"]],
66
"linked": [],
77
"access": "public",
88
"baseBranch": "main",

.changeset/hip-rats-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tiptap/core': patch
3+
---
4+
5+
The extension manager now provides a new property `baseExtensions` that contains an unflattened array of extensions

.changeset/silly-singers-sleep.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@tiptap/core': minor
3+
'@tiptap/markdown': minor
4+
---
5+
6+
Add comprehensive bidirectional markdown support to Tiptap through a new `@tiptap/markdown` package and Markdown utilities in `@tiptap/core`.
7+
8+
**New Package: `@tiptap/markdown`** - A new official extension that provides full Markdown parsing and serialization capabilities using [MarkedJS](https://marked.js.org) as the underlying Markdown parser.
9+
10+
**Core Features:**
11+
12+
**Extension API**
13+
- **`Markdown` Extension**: Main extension that adds Markdown support to your editor
14+
- **`MarkdownManager`**: Core engine for parsing and serializing Markdown
15+
- Parse Markdown strings to Tiptap JSON: `editor.markdown.parse(markdown)`
16+
- Serialize Tiptap JSON to Markdown: `editor.markdown.serialize(json)`
17+
- Access to underlying marked.js instance: `editor.markdown.instance`
18+
19+
#### Editor Methods
20+
- **`editor.getMarkdown()`**: Serialize current editor content to Markdown string
21+
- **`editor.markdown`**: Access to MarkdownManager instance for advanced operations
22+
23+
**Editor Options:**
24+
- **`contentType`**: Control the type of content that is inserted into the editor. Can be `json`, `html` or `markdown` - defaults to `json` and will automatically detect invalid content types (like JSON when it is actually Markdown).
25+
```typescript
26+
new Editor({
27+
content: '# Hello World',
28+
contentType: 'markdown'
29+
})
30+
```
31+
32+
**Command Options:** All content commands now support an `contentType` option:
33+
- **`setContent(markdown, { contentType: 'markdown' })`**: Replace editor content with markdown
34+
- **`insertContent(markdown, { contentType: 'markdown' })`**: Insert markdown at cursor position
35+
- **`insertContentAt(position, markdown, { contentType: 'markdown' })`**: Insert Markdown at specific position
36+
37+
For more, check [the documentation](https://tiptap.dev/docs/editor/markdown).

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ module.exports = {
7474
'no-redeclare': 'off',
7575
'@typescript-eslint/no-redeclare': ['error'],
7676
'no-unused-vars': 'off',
77-
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
77+
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true, argsIgnorePattern: '^_' }],
7878
'no-use-before-define': 'off',
7979
'@typescript-eslint/no-use-before-define': ['error'],
8080
'no-dupe-class-members': 'off',

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ dist
88
.env.*
99
.eslintcache
1010

11+
.instructions
12+
1113
# Log files
1214
npm-debug.log*
1315
yarn-debug.log*

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,6 @@ For help, discussion about best practices, or any other conversation that would
107107
</tr>
108108
</table>
109109

110-
<table>
111-
112-
</table>
113-
114110
[iFixit](https://www.ifixit.com/), [ApostropheCMS](https://apostrophecms.com/), [Novadiscovery](http://www.novadiscovery.com/), [Omics Data Automation](https://www.omicsautomation.com), [Flow Mobile](https://www.flowmobile.app/), [DocIQ](https://www.dociq.io/) and [hundreds of awesome individuals](https://github.com/sponsors/ueberdosis).
115111

116112
### Contributing

demos/setup/style.scss

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,95 @@ textarea {
160160
}
161161
}
162162

163+
/* Details */
164+
[data-type='details'] {
165+
display: flex;
166+
gap: 0.25rem;
167+
margin: 1.5rem 0;
168+
border: 1px solid var(--gray-3);
169+
border-radius: 0.5rem;
170+
padding: 0.5rem;
171+
172+
summary {
173+
font-weight: 700;
174+
}
175+
176+
> button {
177+
align-items: center;
178+
background: transparent;
179+
border-radius: 4px;
180+
display: flex;
181+
font-size: 0.625rem;
182+
height: 1.25rem;
183+
justify-content: center;
184+
line-height: 1;
185+
margin-top: 0.1rem;
186+
padding: 0;
187+
width: 1.25rem;
188+
189+
&:hover {
190+
background-color: var(--gray-3);
191+
}
192+
193+
&::before {
194+
content: '\25B6';
195+
}
196+
}
197+
198+
&.is-open > button::before {
199+
transform: rotate(90deg);
200+
}
201+
202+
> div {
203+
display: flex;
204+
flex-direction: column;
205+
gap: 1rem;
206+
width: 100%;
207+
208+
> [data-type='detailsContent'] > :last-child {
209+
margin-bottom: 0.5rem;
210+
}
211+
}
212+
213+
.details {
214+
margin: 0.5rem 0;
215+
}
216+
}
217+
218+
[data-type='taskList'] {
219+
list-style: none;
220+
padding: 0;
221+
222+
[data-type='taskList'] {
223+
padding-left: 1.5rem;
224+
}
225+
226+
li {
227+
display: flex;
228+
align-items: flex-start;
229+
gap: 0.25rem;
230+
}
231+
232+
> li > div > p:first-child {
233+
margin: 0;
234+
}
235+
236+
> li {
237+
&:not(:first-child) {
238+
margin-top: 0.125rem;
239+
}
240+
241+
&:not(:last-child) {
242+
margin-bottom: 0.125rem;
243+
}
244+
}
245+
246+
[data-checked='true'] > div > p:first-child {
247+
text-decoration: line-through;
248+
color: var(--gray-4);
249+
}
250+
}
251+
163252
button:not([disabled]),
164253
select:not([disabled]) {
165254
cursor: pointer;

demos/src/Markdown/CustomSyntax/React/index.html

Whitespace-only changes.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import './styles.scss'
2+
3+
import Image from '@tiptap/extension-image'
4+
import { TableKit } from '@tiptap/extension-table'
5+
import { Markdown } from '@tiptap/markdown'
6+
import {
7+
createAtomBlockMarkdownSpec,
8+
createBlockMarkdownSpec,
9+
createInlineMarkdownSpec,
10+
EditorContent,
11+
Mark,
12+
Node,
13+
useEditor,
14+
} from '@tiptap/react'
15+
import StarterKit from '@tiptap/starter-kit'
16+
import { useState } from 'react'
17+
18+
const CustomNode = Node.create({
19+
name: 'custom',
20+
group: 'block',
21+
content: 'block*',
22+
23+
renderHTML() {
24+
return ['div', { 'data-type': 'custom' }, 0]
25+
},
26+
27+
parseHTML() {
28+
return [
29+
{
30+
tag: 'div[data-type="custom"]',
31+
},
32+
]
33+
},
34+
35+
...createBlockMarkdownSpec({
36+
nodeName: 'custom',
37+
content: 'block',
38+
}),
39+
})
40+
41+
const CustomAtom = Node.create({
42+
name: 'customAtom',
43+
group: 'block',
44+
atom: true,
45+
46+
renderHTML() {
47+
return ['div', { 'data-type': 'atom' }]
48+
},
49+
50+
parseHTML() {
51+
return [
52+
{
53+
tag: 'div[data-type="atom"]',
54+
},
55+
]
56+
},
57+
58+
addNodeView() {
59+
return () => {
60+
const el = document.createElement('div')
61+
el.setAttribute('data-type', 'atom')
62+
el.textContent = 'This is an atom node.'
63+
64+
return {
65+
dom: el,
66+
}
67+
}
68+
},
69+
70+
...createAtomBlockMarkdownSpec({
71+
nodeName: 'atom',
72+
}),
73+
})
74+
75+
const CustomInline = Node.create({
76+
name: 'customInline',
77+
group: 'inline',
78+
inline: true,
79+
content: 'inline*',
80+
81+
renderHTML() {
82+
return ['span', { 'data-type': 'inline' }, 0]
83+
},
84+
85+
parseHTML() {
86+
return [
87+
{
88+
tag: 'span[data-type="inline"]',
89+
},
90+
]
91+
},
92+
93+
...createInlineMarkdownSpec({
94+
nodeName: 'customInline',
95+
content: 'inline',
96+
}),
97+
})
98+
99+
const CustomMark = Mark.create({
100+
name: 'customMark',
101+
102+
renderHTML() {
103+
return ['span', { 'data-type': 'mark' }, 0]
104+
},
105+
106+
parseHTML() {
107+
return [
108+
{
109+
tag: 'span[data-type="mark"]',
110+
},
111+
]
112+
},
113+
114+
renderMarkdown(node, helpers) {
115+
return `=>${helpers.renderChildren(node)}<=`
116+
},
117+
118+
parseMarkdown(token, helpers) {
119+
return {
120+
type: this.name,
121+
content: helpers.applyMark('customMark', helpers.parseInline(token.tokens || [])),
122+
}
123+
},
124+
125+
markdownTokenizer: {
126+
name: 'customMark',
127+
level: 'inline',
128+
start(src) {
129+
return src.indexOf('=>')
130+
},
131+
tokenize(src, _tokens, lexer) {
132+
const rule = /^(=>)([\s\S]+?)(<=)/
133+
const match = rule.exec(src)
134+
135+
if (!match) {
136+
return undefined
137+
}
138+
139+
const innerContent = match[2].trim()
140+
141+
return {
142+
type: 'customMark',
143+
raw: match[0],
144+
text: innerContent,
145+
tokens: lexer.inlineTokens(innerContent),
146+
}
147+
},
148+
},
149+
})
150+
151+
export default () => {
152+
const [serializedContent, setSerializedContent] = useState('')
153+
const editor = useEditor({
154+
extensions: [Markdown, StarterKit, Image, TableKit, CustomNode, CustomAtom, CustomInline, CustomMark],
155+
content: `
156+
<p>In this demo, you can see how to define custom syntax for Markdown.</p>
157+
<div data-type="custom">
158+
<p>This is a custom node.</p>
159+
</div>
160+
<div data-type="custom">
161+
<p>We also support nested nodes.</p>
162+
163+
<div data-type="custom">
164+
<p>This is a custom node.</p>
165+
</div>
166+
</div>
167+
<div data-type="atom"></div>
168+
<p>
169+
This is a <span data-type="mark">paragraph</span> with a <span data-type="inline">custom inline node</span>.
170+
</p>
171+
<p>Feel free to edit this document to see the live-changes.</p>
172+
`,
173+
onUpdate: ({ editor: currentEditor }) => {
174+
setSerializedContent(currentEditor.getMarkdown())
175+
},
176+
onCreate: ({ editor: currentEditor }) => {
177+
setSerializedContent(currentEditor.getMarkdown())
178+
},
179+
})
180+
181+
return (
182+
<>
183+
<div className="grid">
184+
<EditorContent className="editor-wrapper" editor={editor} />
185+
<div className="preview">
186+
<pre>{serializedContent}</pre>
187+
</div>
188+
</div>
189+
</>
190+
)
191+
}

0 commit comments

Comments
 (0)