@tiptap/extension-list 3.6.7 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  // src/task-list/task-list.ts
2
- import { mergeAttributes, Node } from "@tiptap/core";
2
+ import { mergeAttributes, Node, parseIndentedBlocks } from "@tiptap/core";
3
3
  var TaskList = Node.create({
4
4
  name: "taskList",
5
5
  addOptions() {
@@ -23,6 +23,97 @@ var TaskList = Node.create({
23
23
  renderHTML({ HTMLAttributes }) {
24
24
  return ["ul", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { "data-type": this.name }), 0];
25
25
  },
26
+ parseMarkdown: (token, h) => {
27
+ return h.createNode("taskList", {}, h.parseChildren(token.items || []));
28
+ },
29
+ renderMarkdown: (node, h) => {
30
+ if (!node.content) {
31
+ return "";
32
+ }
33
+ return h.renderChildren(node.content, "\n");
34
+ },
35
+ markdownTokenizer: {
36
+ name: "taskList",
37
+ level: "block",
38
+ start(src) {
39
+ var _a;
40
+ const index = (_a = src.match(/^\s*[-+*]\s+\[([ xX])\]\s+/)) == null ? void 0 : _a.index;
41
+ return index !== void 0 ? index : -1;
42
+ },
43
+ tokenize(src, tokens, lexer) {
44
+ const parseTaskListContent = (content) => {
45
+ const nestedResult = parseIndentedBlocks(
46
+ content,
47
+ {
48
+ itemPattern: /^(\s*)([-+*])\s+\[([ xX])\]\s+(.*)$/,
49
+ extractItemData: (match) => ({
50
+ indentLevel: match[1].length,
51
+ mainContent: match[4],
52
+ checked: match[3].toLowerCase() === "x"
53
+ }),
54
+ createToken: (data, nestedTokens) => ({
55
+ type: "taskItem",
56
+ raw: "",
57
+ mainContent: data.mainContent,
58
+ indentLevel: data.indentLevel,
59
+ checked: data.checked,
60
+ text: data.mainContent,
61
+ tokens: lexer.inlineTokens(data.mainContent),
62
+ nestedTokens
63
+ }),
64
+ // Allow recursive nesting
65
+ customNestedParser: parseTaskListContent
66
+ },
67
+ lexer
68
+ );
69
+ if (nestedResult) {
70
+ return [
71
+ {
72
+ type: "taskList",
73
+ raw: nestedResult.raw,
74
+ items: nestedResult.items
75
+ }
76
+ ];
77
+ }
78
+ return lexer.blockTokens(content);
79
+ };
80
+ const result = parseIndentedBlocks(
81
+ src,
82
+ {
83
+ itemPattern: /^(\s*)([-+*])\s+\[([ xX])\]\s+(.*)$/,
84
+ extractItemData: (match) => ({
85
+ indentLevel: match[1].length,
86
+ mainContent: match[4],
87
+ checked: match[3].toLowerCase() === "x"
88
+ }),
89
+ createToken: (data, nestedTokens) => ({
90
+ type: "taskItem",
91
+ raw: "",
92
+ mainContent: data.mainContent,
93
+ indentLevel: data.indentLevel,
94
+ checked: data.checked,
95
+ text: data.mainContent,
96
+ tokens: lexer.inlineTokens(data.mainContent),
97
+ nestedTokens
98
+ }),
99
+ // Use the recursive parser for nested content
100
+ customNestedParser: parseTaskListContent
101
+ },
102
+ lexer
103
+ );
104
+ if (!result) {
105
+ return void 0;
106
+ }
107
+ return {
108
+ type: "taskList",
109
+ raw: result.raw,
110
+ items: result.items
111
+ };
112
+ }
113
+ },
114
+ markdownOptions: {
115
+ indentsContent: true
116
+ },
26
117
  addCommands() {
27
118
  return {
28
119
  toggleTaskList: () => ({ commands }) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/task-list/task-list.ts"],"sourcesContent":["import { mergeAttributes, Node } from '@tiptap/core'\n\nexport interface TaskListOptions {\n /**\n * The node type name for a task item.\n * @default 'taskItem'\n * @example 'myCustomTaskItem'\n */\n itemTypeName: string\n\n /**\n * The HTML attributes for a task list node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n taskList: {\n /**\n * Toggle a task list\n * @example editor.commands.toggleTaskList()\n */\n toggleTaskList: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to create task lists.\n * @see https://www.tiptap.dev/api/nodes/task-list\n */\nexport const TaskList = Node.create<TaskListOptions>({\n name: 'taskList',\n\n addOptions() {\n return {\n itemTypeName: 'taskItem',\n HTMLAttributes: {},\n }\n },\n\n group: 'block list',\n\n content() {\n return `${this.options.itemTypeName}+`\n },\n\n parseHTML() {\n return [\n {\n tag: `ul[data-type=\"${this.name}\"]`,\n priority: 51,\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['ul', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 'data-type': this.name }), 0]\n },\n\n addCommands() {\n return {\n toggleTaskList:\n () =>\n ({ commands }) => {\n return commands.toggleList(this.name, this.options.itemTypeName)\n },\n }\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-Shift-9': () => this.editor.commands.toggleTaskList(),\n }\n },\n})\n"],"mappings":";AAAA,SAAS,iBAAiB,YAAY;AAkC/B,IAAM,WAAW,KAAK,OAAwB;AAAA,EACnD,MAAM;AAAA,EAEN,aAAa;AACX,WAAO;AAAA,MACL,cAAc;AAAA,MACd,gBAAgB,CAAC;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,OAAO;AAAA,EAEP,UAAU;AACR,WAAO,GAAG,KAAK,QAAQ,YAAY;AAAA,EACrC;AAAA,EAEA,YAAY;AACV,WAAO;AAAA,MACL;AAAA,QACE,KAAK,iBAAiB,KAAK,IAAI;AAAA,QAC/B,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAAA,EAEA,WAAW,EAAE,eAAe,GAAG;AAC7B,WAAO,CAAC,MAAM,gBAAgB,KAAK,QAAQ,gBAAgB,gBAAgB,EAAE,aAAa,KAAK,KAAK,CAAC,GAAG,CAAC;AAAA,EAC3G;AAAA,EAEA,cAAc;AACZ,WAAO;AAAA,MACL,gBACE,MACA,CAAC,EAAE,SAAS,MAAM;AAChB,eAAO,SAAS,WAAW,KAAK,MAAM,KAAK,QAAQ,YAAY;AAAA,MACjE;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,uBAAuB;AACrB,WAAO;AAAA,MACL,eAAe,MAAM,KAAK,OAAO,SAAS,eAAe;AAAA,IAC3D;AAAA,EACF;AACF,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/task-list/task-list.ts"],"sourcesContent":["import { mergeAttributes, Node, parseIndentedBlocks } from '@tiptap/core'\n\nexport interface TaskListOptions {\n /**\n * The node type name for a task item.\n * @default 'taskItem'\n * @example 'myCustomTaskItem'\n */\n itemTypeName: string\n\n /**\n * The HTML attributes for a task list node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n taskList: {\n /**\n * Toggle a task list\n * @example editor.commands.toggleTaskList()\n */\n toggleTaskList: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to create task lists.\n * @see https://www.tiptap.dev/api/nodes/task-list\n */\nexport const TaskList = Node.create<TaskListOptions>({\n name: 'taskList',\n\n addOptions() {\n return {\n itemTypeName: 'taskItem',\n HTMLAttributes: {},\n }\n },\n\n group: 'block list',\n\n content() {\n return `${this.options.itemTypeName}+`\n },\n\n parseHTML() {\n return [\n {\n tag: `ul[data-type=\"${this.name}\"]`,\n priority: 51,\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['ul', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 'data-type': this.name }), 0]\n },\n\n parseMarkdown: (token, h) => {\n return h.createNode('taskList', {}, h.parseChildren(token.items || []))\n },\n\n renderMarkdown: (node, h) => {\n if (!node.content) {\n return ''\n }\n\n return h.renderChildren(node.content, '\\n')\n },\n\n markdownTokenizer: {\n name: 'taskList',\n level: 'block',\n start(src) {\n // Look for the start of a task list item\n const index = src.match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+/)?.index\n return index !== undefined ? index : -1\n },\n tokenize(src, tokens, lexer) {\n // Helper function to recursively parse task lists\n const parseTaskListContent = (content: string): any[] | undefined => {\n const nestedResult = parseIndentedBlocks(\n content,\n {\n itemPattern: /^(\\s*)([-+*])\\s+\\[([ xX])\\]\\s+(.*)$/,\n extractItemData: match => ({\n indentLevel: match[1].length,\n mainContent: match[4],\n checked: match[3].toLowerCase() === 'x',\n }),\n createToken: (data, nestedTokens) => ({\n type: 'taskItem',\n raw: '',\n mainContent: data.mainContent,\n indentLevel: data.indentLevel,\n checked: data.checked,\n text: data.mainContent,\n tokens: lexer.inlineTokens(data.mainContent),\n nestedTokens,\n }),\n // Allow recursive nesting\n customNestedParser: parseTaskListContent,\n },\n lexer,\n )\n\n if (nestedResult) {\n // Return as task list token\n return [\n {\n type: 'taskList',\n raw: nestedResult.raw,\n items: nestedResult.items,\n },\n ]\n }\n\n // Fall back to regular markdown parsing if not a task list\n return lexer.blockTokens(content)\n }\n\n const result = parseIndentedBlocks(\n src,\n {\n itemPattern: /^(\\s*)([-+*])\\s+\\[([ xX])\\]\\s+(.*)$/,\n extractItemData: match => ({\n indentLevel: match[1].length,\n mainContent: match[4],\n checked: match[3].toLowerCase() === 'x',\n }),\n createToken: (data, nestedTokens) => ({\n type: 'taskItem',\n raw: '',\n mainContent: data.mainContent,\n indentLevel: data.indentLevel,\n checked: data.checked,\n text: data.mainContent,\n tokens: lexer.inlineTokens(data.mainContent),\n nestedTokens,\n }),\n // Use the recursive parser for nested content\n customNestedParser: parseTaskListContent,\n },\n lexer,\n )\n\n if (!result) {\n return undefined\n }\n\n return {\n type: 'taskList',\n raw: result.raw,\n items: result.items,\n }\n },\n },\n\n markdownOptions: {\n indentsContent: true,\n },\n\n addCommands() {\n return {\n toggleTaskList:\n () =>\n ({ commands }) => {\n return commands.toggleList(this.name, this.options.itemTypeName)\n },\n }\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-Shift-9': () => this.editor.commands.toggleTaskList(),\n }\n },\n})\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/extension-list",
3
3
  "description": "List extension for tiptap",
4
- "version": "3.6.7",
4
+ "version": "3.7.0",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -87,12 +87,12 @@
87
87
  "dist"
88
88
  ],
89
89
  "devDependencies": {
90
- "@tiptap/core": "^3.6.7",
91
- "@tiptap/pm": "^3.6.7"
90
+ "@tiptap/core": "^3.7.0",
91
+ "@tiptap/pm": "^3.7.0"
92
92
  },
93
93
  "peerDependencies": {
94
- "@tiptap/pm": "^3.6.7",
95
- "@tiptap/core": "^3.6.7"
94
+ "@tiptap/core": "^3.7.0",
95
+ "@tiptap/pm": "^3.7.0"
96
96
  },
97
97
  "repository": {
98
98
  "type": "git",
@@ -81,6 +81,31 @@ export const BulletList = Node.create<BulletListOptions>({
81
81
  return ['ul', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
82
82
  },
83
83
 
84
+ markdownTokenName: 'list',
85
+
86
+ parseMarkdown: (token, helpers) => {
87
+ if (token.type !== 'list' || (token as any).ordered) {
88
+ return []
89
+ }
90
+
91
+ return {
92
+ type: 'bulletList',
93
+ content: token.items ? helpers.parseChildren(token.items) : [],
94
+ }
95
+ },
96
+
97
+ renderMarkdown: (node, h) => {
98
+ if (!node.content) {
99
+ return ''
100
+ }
101
+
102
+ return h.renderChildren(node.content, '\n')
103
+ },
104
+
105
+ markdownOptions: {
106
+ indentsContent: true,
107
+ },
108
+
84
109
  addCommands() {
85
110
  return {
86
111
  toggleBulletList:
@@ -1,4 +1,4 @@
1
- import { mergeAttributes, Node } from '@tiptap/core'
1
+ import { mergeAttributes, Node, renderNestedMarkdownContent } from '@tiptap/core'
2
2
 
3
3
  export interface ListItemOptions {
4
4
  /**
@@ -54,6 +54,86 @@ export const ListItem = Node.create<ListItemOptions>({
54
54
  return ['li', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
55
55
  },
56
56
 
57
+ markdownTokenName: 'list_item',
58
+
59
+ parseMarkdown: (token, helpers) => {
60
+ if (token.type !== 'list_item') {
61
+ return []
62
+ }
63
+
64
+ let content: any[] = []
65
+
66
+ if (token.tokens && token.tokens.length > 0) {
67
+ // Check if we have paragraph tokens (complex list items)
68
+ const hasParagraphTokens = token.tokens.some(t => t.type === 'paragraph')
69
+
70
+ if (hasParagraphTokens) {
71
+ // If we have paragraph tokens, parse them as block elements
72
+ content = helpers.parseChildren(token.tokens)
73
+ } else {
74
+ // Check if the first token is a text token with nested inline tokens
75
+ const firstToken = token.tokens[0]
76
+
77
+ if (firstToken && firstToken.type === 'text' && firstToken.tokens && firstToken.tokens.length > 0) {
78
+ // Parse the inline content from the text token
79
+ const inlineContent = helpers.parseInline(firstToken.tokens)
80
+
81
+ // Start with the paragraph containing the inline content
82
+ content = [
83
+ {
84
+ type: 'paragraph',
85
+ content: inlineContent,
86
+ },
87
+ ]
88
+
89
+ // If there are additional tokens after the first text token (like nested lists),
90
+ // parse them as block elements and add them
91
+ if (token.tokens.length > 1) {
92
+ const remainingTokens = token.tokens.slice(1)
93
+ const additionalContent = helpers.parseChildren(remainingTokens)
94
+ content.push(...additionalContent)
95
+ }
96
+ } else {
97
+ // Fallback: parse all tokens as block elements
98
+ content = helpers.parseChildren(token.tokens)
99
+ }
100
+ }
101
+ }
102
+
103
+ // Ensure we always have at least an empty paragraph
104
+ if (content.length === 0) {
105
+ content = [
106
+ {
107
+ type: 'paragraph',
108
+ content: [],
109
+ },
110
+ ]
111
+ }
112
+
113
+ return {
114
+ type: 'listItem',
115
+ content,
116
+ }
117
+ },
118
+
119
+ renderMarkdown: (node, h, ctx) => {
120
+ return renderNestedMarkdownContent(
121
+ node,
122
+ h,
123
+ (context: any) => {
124
+ if (context.parentType === 'bulletList') {
125
+ return '- '
126
+ }
127
+ if (context.parentType === 'orderedList') {
128
+ return `${context.index + 1}. `
129
+ }
130
+ // Fallback to bullet list for unknown parent types
131
+ return '- '
132
+ },
133
+ ctx,
134
+ )
135
+ },
136
+
57
137
  addKeyboardShortcuts() {
58
138
  return {
59
139
  Enter: () => this.editor.commands.splitListItem(this.name),
@@ -1,5 +1,7 @@
1
1
  import { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core'
2
2
 
3
+ import { buildNestedStructure, collectOrderedListItems, parseListItems } from './utils.js'
4
+
3
5
  const ListItemName = 'listItem'
4
6
  const TextStyleName = 'textStyle'
5
7
 
@@ -105,6 +107,76 @@ export const OrderedList = Node.create<OrderedListOptions>({
105
107
  : ['ol', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
106
108
  },
107
109
 
110
+ markdownTokenName: 'list',
111
+
112
+ parseMarkdown: (token, helpers) => {
113
+ if (token.type !== 'list' || !token.ordered) {
114
+ return []
115
+ }
116
+
117
+ const startValue = token.start || 1
118
+ const content = token.items ? parseListItems(token.items, helpers) : []
119
+
120
+ if (startValue !== 1) {
121
+ return {
122
+ type: 'orderedList',
123
+ attrs: { start: startValue },
124
+ content,
125
+ }
126
+ }
127
+
128
+ return {
129
+ type: 'orderedList',
130
+ content,
131
+ }
132
+ },
133
+
134
+ renderMarkdown: (node, h) => {
135
+ if (!node.content) {
136
+ return ''
137
+ }
138
+
139
+ return h.renderChildren(node.content, '\n')
140
+ },
141
+
142
+ markdownTokenizer: {
143
+ name: 'orderedList',
144
+ level: 'block',
145
+ start: (src: string) => {
146
+ const match = src.match(/^(\s*)(\d+)\.\s+/)
147
+ const index = match?.index
148
+ return index !== undefined ? index : -1
149
+ },
150
+ tokenize: (src: string, _tokens, lexer) => {
151
+ const lines = src.split('\n')
152
+ const [listItems, consumed] = collectOrderedListItems(lines)
153
+
154
+ if (listItems.length === 0) {
155
+ return undefined
156
+ }
157
+
158
+ const items = buildNestedStructure(listItems, 0, lexer)
159
+
160
+ if (items.length === 0) {
161
+ return undefined
162
+ }
163
+
164
+ const startValue = listItems[0]?.number || 1
165
+
166
+ return {
167
+ type: 'list',
168
+ ordered: true,
169
+ start: startValue,
170
+ items,
171
+ raw: lines.slice(0, consumed).join('\n'),
172
+ } as unknown as object
173
+ },
174
+ },
175
+
176
+ markdownOptions: {
177
+ indentsContent: true,
178
+ },
179
+
108
180
  addCommands() {
109
181
  return {
110
182
  toggleOrderedList:
@@ -0,0 +1,234 @@
1
+ import type { JSONContent, MarkdownLexerConfiguration, MarkdownParseHelpers, MarkdownToken } from '@tiptap/core'
2
+
3
+ /**
4
+ * Matches an ordered list item line with optional leading whitespace.
5
+ * Captures: (1) indentation spaces, (2) item number, (3) content after marker
6
+ * Example matches: "1. Item", " 2. Nested item", " 3. Deeply nested"
7
+ */
8
+ const ORDERED_LIST_ITEM_REGEX = /^(\s*)(\d+)\.\s+(.*)$/
9
+
10
+ /**
11
+ * Matches any line that starts with whitespace (indented content).
12
+ * Used to identify continuation content that belongs to a list item.
13
+ */
14
+ const INDENTED_LINE_REGEX = /^\s/
15
+
16
+ /**
17
+ * Represents a parsed ordered list item with indentation information
18
+ */
19
+ export interface OrderedListItem {
20
+ indent: number
21
+ number: number
22
+ content: string
23
+ raw: string
24
+ }
25
+
26
+ /**
27
+ * Collects all ordered list items from lines, parsing them into a flat array
28
+ * with indentation information. Stops collecting continuation content when
29
+ * encountering nested list items, allowing them to be processed separately.
30
+ *
31
+ * @param lines - Array of source lines to parse
32
+ * @returns Tuple of [listItems array, number of lines consumed]
33
+ */
34
+ export function collectOrderedListItems(lines: string[]): [OrderedListItem[], number] {
35
+ const listItems: OrderedListItem[] = []
36
+ let currentLineIndex = 0
37
+ let consumed = 0
38
+
39
+ while (currentLineIndex < lines.length) {
40
+ const line = lines[currentLineIndex]
41
+ const match = line.match(ORDERED_LIST_ITEM_REGEX)
42
+
43
+ if (!match) {
44
+ break
45
+ }
46
+
47
+ const [, indent, number, content] = match
48
+ const indentLevel = indent.length
49
+ let itemContent = content
50
+ let nextLineIndex = currentLineIndex + 1
51
+ const itemLines = [line]
52
+
53
+ // Collect continuation lines for this item (but NOT nested list items)
54
+ while (nextLineIndex < lines.length) {
55
+ const nextLine = lines[nextLineIndex]
56
+ const nextMatch = nextLine.match(ORDERED_LIST_ITEM_REGEX)
57
+
58
+ // If it's another list item (nested or not), stop collecting
59
+ if (nextMatch) {
60
+ break
61
+ }
62
+
63
+ // Check for continuation content (non-list content)
64
+ if (nextLine.trim() === '') {
65
+ // Empty line
66
+ itemLines.push(nextLine)
67
+ itemContent += '\n'
68
+ nextLineIndex += 1
69
+ } else if (nextLine.match(INDENTED_LINE_REGEX)) {
70
+ // Indented content - part of this item (but not a list item)
71
+ itemLines.push(nextLine)
72
+ itemContent += `\n${nextLine.slice(indentLevel + 2)}` // Remove list marker indent
73
+ nextLineIndex += 1
74
+ } else {
75
+ // Non-indented line means end of list
76
+ break
77
+ }
78
+ }
79
+
80
+ listItems.push({
81
+ indent: indentLevel,
82
+ number: parseInt(number, 10),
83
+ content: itemContent.trim(),
84
+ raw: itemLines.join('\n'),
85
+ })
86
+
87
+ consumed = nextLineIndex
88
+ currentLineIndex = nextLineIndex
89
+ }
90
+
91
+ return [listItems, consumed]
92
+ }
93
+
94
+ /**
95
+ * Recursively builds a nested structure from a flat array of list items
96
+ * based on their indentation levels. Creates proper markdown tokens with
97
+ * nested lists where appropriate.
98
+ *
99
+ * @param items - Flat array of list items with indentation info
100
+ * @param baseIndent - The indentation level to process at this recursion level
101
+ * @param lexer - Markdown lexer for parsing inline and block content
102
+ * @returns Array of list_item tokens with proper nesting
103
+ */
104
+ export function buildNestedStructure(
105
+ items: OrderedListItem[],
106
+ baseIndent: number,
107
+ lexer: MarkdownLexerConfiguration,
108
+ ): unknown[] {
109
+ const result: unknown[] = []
110
+ let currentIndex = 0
111
+
112
+ while (currentIndex < items.length) {
113
+ const item = items[currentIndex]
114
+
115
+ if (item.indent === baseIndent) {
116
+ // This item belongs at the current level
117
+ const contentLines = item.content.split('\n')
118
+ const mainText = contentLines[0]?.trim() || ''
119
+
120
+ const tokens = []
121
+
122
+ // Always wrap the main text in a paragraph token
123
+ if (mainText) {
124
+ tokens.push({
125
+ type: 'paragraph',
126
+ raw: mainText,
127
+ tokens: lexer.inlineTokens(mainText),
128
+ })
129
+ }
130
+
131
+ // Handle additional content after the main text
132
+ const additionalContent = contentLines.slice(1).join('\n').trim()
133
+ if (additionalContent) {
134
+ // Parse as block tokens (handles mixed unordered lists, etc.)
135
+ const blockTokens = lexer.blockTokens(additionalContent)
136
+ tokens.push(...blockTokens)
137
+ }
138
+
139
+ // Look ahead to find nested items at deeper indent levels
140
+ let lookAheadIndex = currentIndex + 1
141
+ const nestedItems = []
142
+
143
+ while (lookAheadIndex < items.length && items[lookAheadIndex].indent > baseIndent) {
144
+ nestedItems.push(items[lookAheadIndex])
145
+ lookAheadIndex += 1
146
+ }
147
+
148
+ // If we have nested items, recursively build their structure
149
+ if (nestedItems.length > 0) {
150
+ // Find the next indent level (immediate children)
151
+ const nextIndent = Math.min(...nestedItems.map(nestedItem => nestedItem.indent))
152
+
153
+ // Build the nested list recursively with all nested items
154
+ // The recursive call will handle further nesting
155
+ const nestedListItems = buildNestedStructure(nestedItems, nextIndent, lexer)
156
+
157
+ // Create a nested list token
158
+ tokens.push({
159
+ type: 'list',
160
+ ordered: true,
161
+ start: nestedItems[0].number,
162
+ items: nestedListItems,
163
+ raw: nestedItems.map(nestedItem => nestedItem.raw).join('\n'),
164
+ })
165
+ }
166
+
167
+ result.push({
168
+ type: 'list_item',
169
+ raw: item.raw,
170
+ tokens,
171
+ })
172
+
173
+ // Skip the nested items we just processed
174
+ currentIndex = lookAheadIndex
175
+ } else {
176
+ // This item has deeper indent than we're currently processing
177
+ // It should be handled by a recursive call
178
+ currentIndex += 1
179
+ }
180
+ }
181
+
182
+ return result
183
+ }
184
+
185
+ /**
186
+ * Parses markdown list item tokens into Tiptap JSONContent structure,
187
+ * ensuring text content is properly wrapped in paragraph nodes.
188
+ *
189
+ * @param items - Array of markdown tokens representing list items
190
+ * @param helpers - Markdown parse helpers for recursive parsing
191
+ * @returns Array of listItem JSONContent nodes
192
+ */
193
+ export function parseListItems(items: MarkdownToken[], helpers: MarkdownParseHelpers): JSONContent[] {
194
+ return items.map(item => {
195
+ if (item.type !== 'list_item') {
196
+ return helpers.parseChildren([item])[0]
197
+ }
198
+
199
+ // Parse the tokens within the list item
200
+ const content: JSONContent[] = []
201
+
202
+ if (item.tokens && item.tokens.length > 0) {
203
+ item.tokens.forEach(itemToken => {
204
+ // If it's already a proper block node (paragraph, list, etc.), parse it directly
205
+ if (
206
+ itemToken.type === 'paragraph' ||
207
+ itemToken.type === 'list' ||
208
+ itemToken.type === 'blockquote' ||
209
+ itemToken.type === 'code'
210
+ ) {
211
+ content.push(...helpers.parseChildren([itemToken]))
212
+ } else if (itemToken.type === 'text' && itemToken.tokens) {
213
+ // If it's inline text tokens, wrap them in a paragraph
214
+ const inlineContent = helpers.parseChildren([itemToken])
215
+ content.push({
216
+ type: 'paragraph',
217
+ content: inlineContent,
218
+ })
219
+ } else {
220
+ // For any other content, try to parse it
221
+ const parsed = helpers.parseChildren([itemToken])
222
+ if (parsed.length > 0) {
223
+ content.push(...parsed)
224
+ }
225
+ }
226
+ })
227
+ }
228
+
229
+ return {
230
+ type: 'listItem',
231
+ content,
232
+ }
233
+ })
234
+ }
@@ -1,5 +1,5 @@
1
1
  import type { KeyboardShortcutCommand } from '@tiptap/core'
2
- import { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core'
2
+ import { mergeAttributes, Node, renderNestedMarkdownContent, wrappingInputRule } from '@tiptap/core'
3
3
  import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
4
4
 
5
5
  export interface TaskItemOptions {
@@ -120,6 +120,38 @@ export const TaskItem = Node.create<TaskItemOptions>({
120
120
  ]
121
121
  },
122
122
 
123
+ parseMarkdown: (token, h) => {
124
+ // Parse the task item's text content into paragraph content
125
+ const content = []
126
+
127
+ // First, add the main paragraph content
128
+ if (token.tokens && token.tokens.length > 0) {
129
+ // If we have tokens, create a paragraph with the inline content
130
+ content.push(h.createNode('paragraph', {}, h.parseInline(token.tokens)))
131
+ } else if (token.text) {
132
+ // If we have raw text, create a paragraph with text node
133
+ content.push(h.createNode('paragraph', {}, [h.createNode('text', { text: token.text })]))
134
+ } else {
135
+ // Fallback: empty paragraph
136
+ content.push(h.createNode('paragraph', {}, []))
137
+ }
138
+
139
+ // Then, add any nested content (like nested task lists)
140
+ if (token.nestedTokens && token.nestedTokens.length > 0) {
141
+ const nestedContent = h.parseChildren(token.nestedTokens)
142
+ content.push(...nestedContent)
143
+ }
144
+
145
+ return h.createNode('taskItem', { checked: token.checked || false }, content)
146
+ },
147
+
148
+ renderMarkdown: (node, h) => {
149
+ const checkedChar = node.attrs?.checked ? 'x' : ' '
150
+ const prefix = `- [${checkedChar}] `
151
+
152
+ return renderNestedMarkdownContent(node, h, prefix)
153
+ },
154
+
123
155
  addKeyboardShortcuts() {
124
156
  const shortcuts: {
125
157
  [key: string]: KeyboardShortcutCommand