@tiptap/markdown 3.20.0 → 3.20.2

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.
package/dist/index.cjs CHANGED
@@ -289,7 +289,7 @@ var MarkdownManager = class {
289
289
  throw new Error("No marked instance available for parsing");
290
290
  }
291
291
  const tokens = this.markedInstance.lexer(markdown);
292
- const content = this.parseTokens(tokens);
292
+ const content = this.parseTokens(tokens, true);
293
293
  return {
294
294
  type: "doc",
295
295
  content
@@ -298,13 +298,48 @@ var MarkdownManager = class {
298
298
  /**
299
299
  * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.
300
300
  */
301
- parseTokens(tokens) {
302
- return tokens.map((token) => this.parseToken(token)).filter((parsed) => parsed !== null).flatMap((parsed) => Array.isArray(parsed) ? parsed : [parsed]);
301
+ parseTokens(tokens, parseImplicitEmptyParagraphs = false) {
302
+ const nonSpaceTokenIndexes = tokens.reduce((indexes, token, index) => {
303
+ if (token.type !== "space") {
304
+ indexes.push(index);
305
+ }
306
+ return indexes;
307
+ }, []);
308
+ let previousNonSpaceTokenIndex = -1;
309
+ let nextNonSpaceTokenPointer = 0;
310
+ return tokens.flatMap((token, index) => {
311
+ var _a;
312
+ while (nextNonSpaceTokenPointer < nonSpaceTokenIndexes.length && nonSpaceTokenIndexes[nextNonSpaceTokenPointer] < index) {
313
+ previousNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer];
314
+ nextNonSpaceTokenPointer += 1;
315
+ }
316
+ if (parseImplicitEmptyParagraphs && token.type === "space") {
317
+ const nextNonSpaceTokenIndex = (_a = nonSpaceTokenIndexes[nextNonSpaceTokenPointer]) != null ? _a : -1;
318
+ return this.createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex);
319
+ }
320
+ const parsed = this.parseToken(token, parseImplicitEmptyParagraphs);
321
+ if (parsed === null) {
322
+ return [];
323
+ }
324
+ return Array.isArray(parsed) ? parsed : [parsed];
325
+ });
326
+ }
327
+ createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex) {
328
+ const separatorCount = this.countParagraphSeparators(token.raw || "");
329
+ if (separatorCount === 0) {
330
+ return [];
331
+ }
332
+ const isBoundarySpace = previousNonSpaceTokenIndex === -1 || nextNonSpaceTokenIndex === -1;
333
+ const emptyParagraphCount = Math.max(separatorCount - (isBoundarySpace ? 0 : 1), 0);
334
+ return Array.from({ length: emptyParagraphCount }, () => ({ type: "paragraph", content: [] }));
335
+ }
336
+ countParagraphSeparators(raw) {
337
+ return (raw.replace(/\r\n/g, "\n").match(/\n\n/g) || []).length;
303
338
  }
304
339
  /**
305
340
  * Parse a single token into Tiptap JSON using the appropriate registered handler.
306
341
  */
307
- parseToken(token) {
342
+ parseToken(token, parseImplicitEmptyParagraphs = false) {
308
343
  if (!token.type) {
309
344
  return null;
310
345
  }
@@ -330,7 +365,7 @@ var MarkdownManager = class {
330
365
  this.lastParseResult = null;
331
366
  return toReturn;
332
367
  }
333
- return this.parseFallbackToken(token);
368
+ return this.parseFallbackToken(token, parseImplicitEmptyParagraphs);
334
369
  }
335
370
  /**
336
371
  * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists.
@@ -458,6 +493,7 @@ var MarkdownManager = class {
458
493
  return {
459
494
  parseInline: (tokens) => this.parseInlineTokens(tokens),
460
495
  parseChildren: (tokens) => this.parseTokens(tokens),
496
+ parseBlockChildren: (tokens) => this.parseTokens(tokens, true),
461
497
  createTextNode: (text, marks) => {
462
498
  const node = {
463
499
  type: "text",
@@ -616,7 +652,7 @@ var MarkdownManager = class {
616
652
  /**
617
653
  * Fallback parsing for common tokens when no specific handler is registered.
618
654
  */
619
- parseFallbackToken(token) {
655
+ parseFallbackToken(token, parseImplicitEmptyParagraphs = false) {
620
656
  switch (token.type) {
621
657
  case "paragraph":
622
658
  return {
@@ -640,7 +676,7 @@ var MarkdownManager = class {
640
676
  return null;
641
677
  default:
642
678
  if (token.tokens) {
643
- return this.parseTokens(token.tokens);
679
+ return this.parseTokens(token.tokens, parseImplicitEmptyParagraphs);
644
680
  }
645
681
  return null;
646
682
  }
@@ -687,7 +723,7 @@ var MarkdownManager = class {
687
723
  throw new Error(`Failed to parse HTML in markdown: ${error}`);
688
724
  }
689
725
  }
690
- renderNodeToMarkdown(node, parentNode, index = 0, level = 0) {
726
+ renderNodeToMarkdown(node, parentNode, index = 0, level = 0, meta = {}) {
691
727
  var _a;
692
728
  if (node.type === "text") {
693
729
  return node.text || "";
@@ -699,6 +735,7 @@ var MarkdownManager = class {
699
735
  if (!handler) {
700
736
  return "";
701
737
  }
738
+ const previousNode = Array.isArray(parentNode == null ? void 0 : parentNode.content) && index > 0 ? parentNode.content[index - 1] : void 0;
702
739
  const helpers = {
703
740
  renderChildren: (nodes, separator) => {
704
741
  const childLevel = handler.isIndenting ? level + 1 : level;
@@ -707,6 +744,10 @@ var MarkdownManager = class {
707
744
  }
708
745
  return this.renderNodes(nodes, node, separator || "", index, childLevel);
709
746
  },
747
+ renderChild: (childNode, childIndex) => {
748
+ const childLevel = handler.isIndenting ? level + 1 : level;
749
+ return this.renderNodeToMarkdown(childNode, node, childIndex, childLevel);
750
+ },
710
751
  indent: (content) => {
711
752
  return this.indentString + content;
712
753
  },
@@ -716,8 +757,10 @@ var MarkdownManager = class {
716
757
  index,
717
758
  level,
718
759
  parentType: parentNode == null ? void 0 : parentNode.type,
760
+ previousNode,
719
761
  meta: {
720
- parentAttrs: parentNode == null ? void 0 : parentNode.attrs
762
+ parentAttrs: parentNode == null ? void 0 : parentNode.attrs,
763
+ ...meta
721
764
  }
722
765
  };
723
766
  const rendered = ((_a = handler.renderMarkdown) == null ? void 0 : _a.call(handler, node, helpers, context)) || "";
@@ -844,6 +887,7 @@ var MarkdownManager = class {
844
887
  syntheticNode,
845
888
  {
846
889
  renderChildren: () => placeholder,
890
+ renderChild: () => placeholder,
847
891
  indent: (content) => content,
848
892
  wrapInBlock: (prefix, content) => prefix + content
849
893
  },
@@ -875,6 +919,7 @@ var MarkdownManager = class {
875
919
  syntheticNode,
876
920
  {
877
921
  renderChildren: () => placeholder,
922
+ renderChild: () => placeholder,
878
923
  indent: (content) => content,
879
924
  wrapInBlock: (prefix, content) => prefix + content
880
925
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/tiptap/tiptap/packages/markdown/dist/index.cjs","../src/Extension.ts","../src/MarkdownManager.ts","../src/utils.ts"],"names":[],"mappings":"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","file":"/home/runner/work/tiptap/tiptap/packages/markdown/dist/index.cjs","sourcesContent":[null,"import {\n type InsertContentAtOptions as MarkdownInsertContentAtOptions,\n type InsertContentOptions as MarkdownInsertContentOptions,\n type SetContentOptions as MarkdownSetContentOptions,\n commands,\n Extension,\n} from '@tiptap/core'\nimport type { marked } from 'marked'\n\nimport MarkdownManager from './MarkdownManager.js'\nimport type { ContentType } from './types.js'\nimport { assumeContentType } from './utils.js'\n\ndeclare module '@tiptap/core' {\n interface Editor {\n /**\n * Get the content of the editor as markdown.\n */\n getMarkdown: () => string\n\n /**\n * The markdown manager instance.\n */\n markdown?: MarkdownManager\n }\n\n interface EditorOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface Storage {\n markdown: MarkdownExtensionStorage\n }\n\n interface InsertContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface InsertContentAtOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface SetContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n}\n\nexport type MarkdownExtensionOptions = {\n /**\n * Configure the indentation style and size for lists and code blocks.\n * - `style`: Choose between spaces or tabs. Default is 'space'.\n * - `size`: Number of spaces or tabs for indentation. Default is 2.\n */\n indentation?: { style?: 'space' | 'tab'; size?: number }\n\n /**\n * Use a custom version of `marked` for markdown parsing and serialization.\n * If not provided, the default `marked` instance will be used.\n */\n marked?: typeof marked\n\n /**\n * Options to pass to `marked.setOptions()`.\n * See the [marked documentation](https://marked.js.org/using_advanced#options) for available options.\n */\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n}\n\nexport type MarkdownExtensionStorage = {\n manager: MarkdownManager\n}\n\nexport const Markdown = Extension.create<MarkdownExtensionOptions, MarkdownExtensionStorage>({\n name: 'markdown',\n\n addOptions() {\n return {\n indentation: { style: 'space', size: 2 },\n marked: undefined,\n markedOptions: {},\n }\n },\n\n addCommands() {\n return {\n setContent: (content, options?: MarkdownSetContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.setContent(content, options)\n }\n\n const actualContentType = assumeContentType(content, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.setContent(content, options)\n }\n\n const mdContent = this.editor.markdown.parse(content as string)\n return commands.setContent(mdContent, options)\n },\n\n insertContent: (value, options?: MarkdownInsertContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContent(value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContent(value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContent(mdContent, options)\n },\n\n insertContentAt: (position, value, options?: MarkdownInsertContentAtOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContentAt(position, value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContentAt(position, value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContentAt(position, mdContent, options)\n },\n }\n },\n\n addStorage() {\n return {\n manager: new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: [],\n }),\n }\n },\n\n onBeforeCreate() {\n if (this.editor.markdown) {\n console.error(\n '[tiptap][markdown]: There is already a `markdown` property on the editor instance. This might lead to unexpected behavior.',\n )\n return\n }\n\n this.storage.manager = new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: this.editor.extensionManager.baseExtensions,\n })\n\n this.editor.markdown = this.storage.manager\n\n // add a `getMarkdown()` method to the editor\n this.editor.getMarkdown = () => {\n return this.storage.manager.serialize(this.editor.getJSON())\n }\n\n if (!this.editor.options.contentType) {\n return\n }\n\n const assumedType = assumeContentType(this.editor.options.content, this.editor.options.contentType)\n if (assumedType !== 'markdown') {\n return\n }\n\n if (!this.editor.markdown) {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the Markdown extension is not added to the editor. Please add the Markdown extension to use this feature.',\n )\n }\n\n if (this.editor.options.content === undefined || typeof this.editor.options.content !== 'string') {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the initial content is not a string. Please provide the initial content as a markdown string.',\n )\n }\n\n const json = this.editor.markdown.parse(this.editor.options.content as string)\n this.editor.options.content = json\n },\n})\n","import {\n type AnyExtension,\n type ExtendableConfig,\n type JSONContent,\n type MarkdownExtensionSpec,\n type MarkdownParseHelpers,\n type MarkdownParseResult,\n type MarkdownRendererHelpers,\n type MarkdownToken,\n type MarkdownTokenizer,\n type RenderContext,\n flattenExtensions,\n generateJSON,\n getExtensionField,\n} from '@tiptap/core'\nimport { type Lexer, type Token, type TokenizerExtension, marked } from 'marked'\n\nimport {\n closeMarksBeforeNode,\n findMarksToClose,\n findMarksToCloseAtEnd,\n findMarksToOpen,\n isTaskItem,\n reopenMarksAfterNode,\n wrapInMarkdownBlock,\n} from './utils.js'\n\nexport class MarkdownManager {\n private markedInstance: typeof marked\n private lexer: Lexer\n private registry: Map<string, MarkdownExtensionSpec[]>\n private nodeTypeRegistry: Map<string, MarkdownExtensionSpec[]>\n private indentStyle: 'space' | 'tab'\n private indentSize: number\n private baseExtensions: AnyExtension[] = []\n private extensions: AnyExtension[] = []\n\n /**\n * Create a MarkdownManager.\n * @param options.marked Optional marked instance to use (injected).\n * @param options.markedOptions Optional options to pass to marked.setOptions\n * @param options.indentation Indentation settings (style and size).\n * @param options.extensions An array of Tiptap extensions to register for markdown parsing and rendering.\n */\n constructor(options?: {\n marked?: typeof marked\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n indentation?: { style?: 'space' | 'tab'; size?: number }\n extensions: AnyExtension[]\n }) {\n this.markedInstance = options?.marked ?? marked\n this.lexer = new this.markedInstance.Lexer()\n this.indentStyle = options?.indentation?.style ?? 'space'\n this.indentSize = options?.indentation?.size ?? 2\n this.baseExtensions = options?.extensions || []\n\n if (options?.markedOptions && typeof this.markedInstance.setOptions === 'function') {\n this.markedInstance.setOptions(options.markedOptions)\n }\n\n this.registry = new Map()\n this.nodeTypeRegistry = new Map()\n\n // If extensions were provided, register them now\n if (options?.extensions) {\n this.baseExtensions = options.extensions\n const flattened = flattenExtensions(options.extensions)\n flattened.forEach(ext => this.registerExtension(ext, false))\n }\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include all tokenizers\n }\n\n /** Returns the underlying marked instance. */\n get instance(): typeof marked {\n return this.markedInstance\n }\n\n /** Returns the correct indentCharacter (space or tab) */\n get indentCharacter(): string {\n return this.indentStyle === 'space' ? ' ' : '\\t'\n }\n\n /** Returns the correct indentString repeated X times */\n get indentString(): string {\n return this.indentCharacter.repeat(this.indentSize)\n }\n\n /** Helper to quickly check whether a marked instance is available. */\n hasMarked(): boolean {\n return !!this.markedInstance\n }\n\n /**\n * Register a Tiptap extension (Node/Mark/Extension). This will read\n * `markdownName`, `parseMarkdown`, `renderMarkdown` and `priority` from the\n * extension config (using the same resolution used across the codebase).\n */\n registerExtension(extension: AnyExtension, recreateLexer: boolean = true): void {\n // Keep track of all extensions for HTML parsing\n this.extensions.push(extension)\n\n const name = extension.name\n const tokenName =\n (getExtensionField(extension, 'markdownTokenName') as ExtendableConfig['markdownTokenName']) || name\n const parseMarkdown = getExtensionField(extension, 'parseMarkdown') as ExtendableConfig['parseMarkdown'] | undefined\n const renderMarkdown = getExtensionField(extension, 'renderMarkdown') as\n | ExtendableConfig['renderMarkdown']\n | undefined\n const tokenizer = getExtensionField(extension, 'markdownTokenizer') as\n | ExtendableConfig['markdownTokenizer']\n | undefined\n\n // Read the `markdown` object from the extension config. This allows\n // extensions to provide `markdown: { name?, parseName?, renderName?, parse?, render?, match? }`.\n const markdownCfg = (getExtensionField(extension, 'markdownOptions') ?? null) as ExtendableConfig['markdownOptions']\n const isIndenting = markdownCfg?.indentsContent ?? false\n\n const spec: MarkdownExtensionSpec = {\n tokenName,\n nodeName: name,\n parseMarkdown,\n renderMarkdown,\n isIndenting,\n tokenizer,\n }\n\n // Add to parse registry using parseName\n if (tokenName && parseMarkdown) {\n const parseExisting = this.registry.get(tokenName) || []\n parseExisting.push(spec)\n this.registry.set(tokenName, parseExisting)\n }\n\n // Add to render registry using renderName (node type)\n if (renderMarkdown) {\n const renderExisting = this.nodeTypeRegistry.get(name) || []\n renderExisting.push(spec)\n this.nodeTypeRegistry.set(name, renderExisting)\n }\n\n // Register custom tokenizer with marked.js\n if (tokenizer && this.hasMarked()) {\n this.registerTokenizer(tokenizer)\n\n if (recreateLexer) {\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include new tokenizer\n }\n }\n }\n\n /**\n * Register a custom tokenizer with marked.js for parsing non-standard markdown syntax.\n */\n private registerTokenizer(tokenizer: MarkdownTokenizer): void {\n if (!this.hasMarked()) {\n return\n }\n\n const { name, start, level = 'inline', tokenize } = tokenizer\n\n // Helper functions that use a fresh lexer instance with all registered extensions\n const tokenizeInline = (src: string) => {\n return this.lexer.inlineTokens(src)\n }\n\n const tokenizeBlock = (src: string) => {\n return this.lexer.blockTokens(src)\n }\n\n const helper = {\n inlineTokens: tokenizeInline,\n blockTokens: tokenizeBlock,\n }\n\n let startCb: (src: string) => number\n\n if (!start) {\n startCb = (src: string) => {\n // For other tokenizers, try to find a match and return its position\n const result = tokenize(src, [], helper)\n if (result && result.raw) {\n const index = src.indexOf(result.raw)\n return index\n }\n return -1\n }\n } else {\n startCb = typeof start === 'function' ? start : (src: string) => src.indexOf(start)\n }\n\n // Create marked.js extension with proper types\n const markedExtension: TokenizerExtension = {\n name,\n level,\n start: startCb,\n tokenizer: (src, tokens) => {\n const result = tokenize(src, tokens, helper)\n\n if (result && result.type) {\n return {\n ...result,\n type: result.type || name,\n raw: result.raw || '',\n tokens: (result.tokens || []) as Token[],\n }\n }\n\n return undefined\n },\n childTokens: [],\n }\n\n // Register with marked.js - use extensions array to control priority\n this.markedInstance.use({\n extensions: [markedExtension],\n })\n }\n\n /** Get registered handlers for a token type and try each until one succeeds. */\n private getHandlersForToken(type: string): MarkdownExtensionSpec[] {\n try {\n return this.registry.get(type) || []\n } catch {\n return []\n }\n }\n\n /** Get the first handler for a token type (for backwards compatibility). */\n private getHandlerForToken(type: string): MarkdownExtensionSpec | undefined {\n // First try the markdown token registry (for parsing)\n const markdownHandlers = this.getHandlersForToken(type)\n if (markdownHandlers.length > 0) {\n return markdownHandlers[0]\n }\n\n // Then try the node type registry (for rendering)\n const nodeTypeHandlers = this.getHandlersForNodeType(type)\n return nodeTypeHandlers.length > 0 ? nodeTypeHandlers[0] : undefined\n }\n\n /** Get registered handlers for a node type (for rendering). */\n private getHandlersForNodeType(type: string): MarkdownExtensionSpec[] {\n try {\n return this.nodeTypeRegistry.get(type) || []\n } catch {\n return []\n }\n }\n\n /**\n * Serialize a ProseMirror-like JSON document (or node array) to a Markdown string\n * using registered renderers and fallback renderers.\n */\n serialize(docOrContent: JSONContent): string {\n if (!docOrContent) {\n return ''\n }\n\n const result = this.renderNodes(docOrContent, docOrContent)\n // Return empty string if result is only whitespace entities or non-breaking spaces\n return this.isEmptyOutput(result) ? '' : result\n }\n\n /**\n * Check if the markdown output represents an empty document.\n * Empty documents may contain only &nbsp; entities or non-breaking space characters\n * which are used by the Paragraph extension to preserve blank lines.\n */\n private isEmptyOutput(markdown: string): boolean {\n if (!markdown || markdown.trim() === '') {\n return true\n }\n\n // Check if the output is only &nbsp; entities or non-breaking space characters\n const cleanedOutput = markdown\n .replace(/&nbsp;/g, '')\n .replace(/\\u00A0/g, '')\n .trim()\n return cleanedOutput === ''\n }\n\n /**\n * Parse markdown string into Tiptap JSON document using registered extension handlers.\n */\n parse(markdown: string): JSONContent {\n if (!this.hasMarked()) {\n throw new Error('No marked instance available for parsing')\n }\n\n // Use marked to tokenize the markdown\n const tokens = this.markedInstance.lexer(markdown)\n\n // Convert tokens to Tiptap JSON\n const content = this.parseTokens(tokens)\n\n // Return a document node containing the parsed content\n return {\n type: 'doc',\n content,\n }\n }\n\n /**\n * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.\n */\n private parseTokens(tokens: MarkdownToken[]): JSONContent[] {\n return tokens\n .map(token => this.parseToken(token))\n .filter((parsed): parsed is JSONContent | JSONContent[] => parsed !== null)\n .flatMap(parsed => (Array.isArray(parsed) ? parsed : [parsed]))\n }\n\n /**\n * Parse a single token into Tiptap JSON using the appropriate registered handler.\n */\n private parseToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n // Special handling for 'list' tokens that may contain mixed bullet/task items\n if (token.type === 'list') {\n return this.parseListToken(token)\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token)\n }\n\n private lastParseResult: JSONContent | JSONContent[] | null = null\n\n /**\n * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists.\n * This ensures that consecutive task items and bullet items are grouped and parsed as separate list nodes.\n *\n * @param token The list token to parse\n * @returns Array of parsed list nodes, or null if parsing fails\n */\n private parseListToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.items || token.items.length === 0) {\n // No items, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n const hasTask = token.items.some(item => isTaskItem(item).isTask)\n const hasNonTask = token.items.some(item => !isTaskItem(item).isTask)\n\n if (!hasTask || !hasNonTask || this.getHandlersForToken('taskList').length === 0) {\n // Not mixed or no taskList extension, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n // Mixed list with taskList extension available: split into separate lists\n type TaskListItemToken = MarkdownToken & { type: 'taskItem'; checked?: boolean; indentLevel?: number }\n const groups: { type: 'list' | 'taskList'; items: (MarkdownToken | TaskListItemToken)[] }[] = []\n let currentGroup: (MarkdownToken | TaskListItemToken)[] = []\n let currentType: 'list' | 'taskList' | null = null\n\n for (let i = 0; i < token.items.length; i += 1) {\n const item = token.items[i]\n const { isTask, checked, indentLevel } = isTaskItem(item)\n let processedItem = item\n\n if (isTask) {\n // Transform list_item into taskItem token\n const raw = item.raw || item.text || ''\n\n // Split raw content by lines to separate main content from nested\n const lines = raw.split('\\n')\n\n // Extract main content from the first line\n const firstLineMatch = lines[0].match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+(.*)$/)\n const mainContent = firstLineMatch ? firstLineMatch[2] : ''\n\n // Parse nested content from remaining lines\n let nestedTokens: MarkdownToken[] = []\n if (lines.length > 1) {\n // Join all lines after the first\n const nestedRaw = lines.slice(1).join('\\n')\n\n // Only parse if there's actual content\n if (nestedRaw.trim()) {\n // Find minimum indentation of non-empty lines\n const nestedLines = lines.slice(1)\n const nonEmptyLines = nestedLines.filter(line => line.trim())\n if (nonEmptyLines.length > 0) {\n const minIndent = Math.min(...nonEmptyLines.map(line => line.length - line.trimStart().length))\n // Remove common indentation while preserving structure\n const trimmedLines = nestedLines.map(line => {\n if (!line.trim()) {\n return '' // Keep empty lines\n }\n return line.slice(minIndent)\n })\n const nestedContent = trimmedLines.join('\\n').trim()\n // Use the lexer to parse nested content\n if (nestedContent) {\n // Use the full lexer pipeline to ensure inline tokens are populated\n nestedTokens = this.markedInstance.lexer(`${nestedContent}\\n`)\n }\n }\n }\n }\n\n processedItem = {\n type: 'taskItem',\n raw: '',\n mainContent,\n indentLevel,\n checked: checked ?? false,\n text: mainContent,\n tokens: this.lexer.inlineTokens(mainContent),\n nestedTokens,\n }\n }\n\n const itemType: 'list' | 'taskList' = isTask ? 'taskList' : 'list'\n\n if (currentType !== itemType) {\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n currentGroup = [processedItem]\n currentType = itemType\n } else {\n currentGroup.push(processedItem)\n }\n }\n\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n\n // Parse each group as a separate token\n const results: JSONContent[] = []\n for (let i = 0; i < groups.length; i += 1) {\n const group = groups[i]\n const subToken = { ...token, type: group.type, items: group.items }\n const parsed = this.parseToken(subToken)\n if (parsed) {\n if (Array.isArray(parsed)) {\n results.push(...parsed)\n } else {\n results.push(parsed)\n }\n }\n }\n\n return results.length > 0 ? results : null\n }\n\n /**\n * Parse a token using registered handlers (extracted for reuse).\n */\n private parseTokenWithHandlers(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token)\n }\n\n /**\n * Creates helper functions for parsing markdown tokens.\n * @returns An object containing helper functions for parsing.\n */\n private createParseHelpers(): MarkdownParseHelpers {\n return {\n parseInline: (tokens: MarkdownToken[]) => this.parseInlineTokens(tokens),\n parseChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens),\n createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => {\n const node = {\n type: 'text',\n text,\n marks: marks || undefined,\n }\n\n return node\n },\n createNode: (type: string, attrs?: any, content?: JSONContent[]) => {\n const node = {\n type,\n attrs: attrs || undefined,\n content: content || undefined,\n }\n\n if (!attrs || Object.keys(attrs).length === 0) {\n delete node.attrs\n }\n\n return node\n },\n applyMark: (markType: string, content: JSONContent[], attrs?: any) => ({\n mark: markType,\n content,\n attrs: attrs && Object.keys(attrs).length > 0 ? attrs : undefined,\n }),\n }\n }\n\n /**\n * Escape special regex characters in a string.\n */\n private escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n\n /**\n * Parse inline tokens (bold, italic, links, etc.) into text nodes with marks.\n * This is the complex part that handles mark nesting and boundaries.\n */\n private parseInlineTokens(tokens: MarkdownToken[]): JSONContent[] {\n const result: JSONContent[] = []\n\n // Process tokens sequentially using an index so we can lookahead and\n // merge split inline HTML fragments like: text / <em> / inner / </em> / text\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i]\n\n if (token.type === 'text') {\n // Create text node\n result.push({\n type: 'text',\n text: token.text || '',\n })\n } else if (token.type === 'html') {\n // Handle possible split inline HTML by attempting to detect an\n // opening tag and searching forward for a matching closing tag.\n const raw = (token.raw ?? token.text ?? '').toString()\n\n // Quick checks for opening vs. closing tag\n const isClosing = /^<\\/[\\s]*[\\w-]+/i.test(raw)\n const openMatch = raw.match(/^<[\\s]*([\\w-]+)(\\s|>|\\/|$)/i)\n\n if (!isClosing && openMatch && !/\\/>$/.test(raw)) {\n // Try to find the corresponding closing html token for this tag\n const tagName = openMatch[1]\n const escapedTagName = this.escapeRegex(tagName)\n const closingRegex = new RegExp(`^<\\\\/\\\\s*${escapedTagName}\\\\b`, 'i')\n let foundIndex = -1\n\n // Collect intermediate raw parts to reconstruct full HTML fragment\n const parts: string[] = [raw]\n for (let j = i + 1; j < tokens.length; j += 1) {\n const t = tokens[j]\n const tRaw = (t.raw ?? t.text ?? '').toString()\n parts.push(tRaw)\n if (t.type === 'html' && closingRegex.test(tRaw)) {\n foundIndex = j\n break\n }\n }\n\n if (foundIndex !== -1) {\n // Merge opening + inner + closing into one html fragment and parse\n const mergedRaw = parts.join('')\n const mergedToken = {\n type: 'html',\n raw: mergedRaw,\n text: mergedRaw,\n block: false,\n } as unknown as MarkdownToken\n\n const parsed = this.parseHTMLToken(mergedToken)\n if (parsed) {\n const normalized = this.normalizeParseResult(parsed as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n\n // Advance i to the closing token\n i = foundIndex\n continue\n }\n }\n\n // Fallback: single html token parse\n const parsedSingle = this.parseHTMLToken(token)\n if (parsedSingle) {\n const normalized = this.normalizeParseResult(parsedSingle as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.type) {\n // Handle inline marks (bold, italic, etc.)\n const markHandler = this.getHandlerForToken(token.type)\n if (markHandler && markHandler.parseMarkdown) {\n const helpers = this.createParseHelpers()\n const parsed = markHandler.parseMarkdown(token, helpers)\n\n if (this.isMarkResult(parsed)) {\n // This is a mark result - apply the mark to the content\n const markedContent = this.applyMarkToContent(parsed.mark, parsed.content, parsed.attrs)\n result.push(...markedContent)\n } else {\n // Regular inline node\n const normalized = this.normalizeParseResult(parsed)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.tokens) {\n // Fallback: try to parse children if they exist\n result.push(...this.parseInlineTokens(token.tokens))\n }\n }\n }\n\n return result\n }\n\n /**\n * Apply a mark to content nodes.\n */\n private applyMarkToContent(markType: string, content: JSONContent[], attrs?: any): JSONContent[] {\n return content.map(node => {\n if (node.type === 'text') {\n // Add the mark to existing marks or create new marks array\n const existingMarks = node.marks || []\n const newMark = attrs ? { type: markType, attrs } : { type: markType }\n return {\n ...node,\n marks: [...existingMarks, newMark],\n }\n }\n\n // For non-text nodes, recursively apply to content\n return {\n ...node,\n content: node.content ? this.applyMarkToContent(markType, node.content, attrs) : undefined,\n }\n })\n } /**\n * Check if a parse result represents a mark to be applied.\n */\n private isMarkResult(result: any): result is { mark: string; content: JSONContent[]; attrs?: any } {\n return result && typeof result === 'object' && 'mark' in result\n }\n\n /**\n * Normalize parse results to ensure they're valid JSONContent.\n */\n private normalizeParseResult(result: MarkdownParseResult): JSONContent | JSONContent[] | null {\n if (!result) {\n return null\n }\n\n if (this.isMarkResult(result)) {\n // This shouldn't happen at the top level, but handle it gracefully\n return result.content\n }\n\n return result as JSONContent | JSONContent[]\n }\n\n /**\n * Fallback parsing for common tokens when no specific handler is registered.\n */\n private parseFallbackToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n switch (token.type) {\n case 'paragraph':\n return {\n type: 'paragraph',\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'heading':\n return {\n type: 'heading',\n attrs: { level: token.depth || 1 },\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'text':\n return {\n type: 'text',\n text: token.text || '',\n }\n\n case 'html':\n // Parse HTML using extensions' parseHTML methods\n return this.parseHTMLToken(token)\n\n case 'space':\n return null\n\n default:\n // Unknown token type - try to parse children if they exist\n if (token.tokens) {\n return this.parseTokens(token.tokens)\n }\n return null\n }\n }\n\n /**\n * Parse HTML tokens using extensions' parseHTML methods.\n * This allows HTML within markdown to be parsed according to extension rules.\n */\n private parseHTMLToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n const html = token.text || token.raw || ''\n\n if (!html.trim()) {\n return null\n }\n\n // Check if we're in a server-side environment (no window object)\n // If so, fall back to treating HTML as plain text to avoid runtime errors\n if (typeof window === 'undefined') {\n // For block-level HTML, wrap in a paragraph to maintain valid document structure\n if (token.block) {\n return {\n type: 'paragraph',\n content: [\n {\n type: 'text',\n text: html,\n },\n ],\n }\n }\n // For inline HTML, return plain text\n return {\n type: 'text',\n text: html,\n }\n }\n\n // Use generateJSON to parse the HTML using extensions' parseHTML rules\n try {\n const parsed = generateJSON(html, this.baseExtensions)\n\n // If the result is a doc node, extract its content\n if (parsed.type === 'doc' && parsed.content) {\n // For block-level HTML, return the content array\n if (token.block) {\n return parsed.content\n }\n\n // For inline HTML, we need to flatten the content appropriately\n // If there's only one paragraph with content, unwrap it\n if (parsed.content.length === 1 && parsed.content[0].type === 'paragraph' && parsed.content[0].content) {\n return parsed.content[0].content\n }\n\n return parsed.content\n }\n\n return parsed as JSONContent\n } catch (error) {\n throw new Error(`Failed to parse HTML in markdown: ${error}`)\n }\n }\n\n renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index = 0, level = 0): string {\n // if node is a text node, we simply return it's text content\n // marks are handled at the array level in renderNodesWithMarkBoundaries\n if (node.type === 'text') {\n return node.text || ''\n }\n\n if (!node.type) {\n return ''\n }\n\n const handler = this.getHandlerForToken(node.type)\n if (!handler) {\n return ''\n }\n\n const helpers: MarkdownRendererHelpers = {\n renderChildren: (nodes, separator) => {\n const childLevel = handler.isIndenting ? level + 1 : level\n\n if (!Array.isArray(nodes) && (nodes as any).content) {\n return this.renderNodes((nodes as any).content as JSONContent[], node, separator || '', index, childLevel)\n }\n\n return this.renderNodes(nodes, node, separator || '', index, childLevel)\n },\n indent: content => {\n return this.indentString + content\n },\n wrapInBlock: wrapInMarkdownBlock,\n }\n\n const context: RenderContext = {\n index,\n level,\n parentType: parentNode?.type,\n meta: {\n parentAttrs: parentNode?.attrs,\n },\n }\n\n // First render the node itself (this will render children recursively)\n const rendered = handler.renderMarkdown?.(node, helpers, context) || ''\n\n return rendered\n }\n\n /**\n * Render a node or an array of nodes. Parent type controls how children\n * are joined (which determines newline insertion between children).\n */\n renderNodes(\n nodeOrNodes: JSONContent | JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n index = 0,\n level = 0,\n ): string {\n // if we have just one node, call renderNodeToMarkdown directly\n if (!Array.isArray(nodeOrNodes)) {\n if (!nodeOrNodes.type) {\n return ''\n }\n\n return this.renderNodeToMarkdown(nodeOrNodes, parentNode, index, level)\n }\n\n return this.renderNodesWithMarkBoundaries(nodeOrNodes, parentNode, separator, level)\n }\n\n /**\n * Render an array of nodes while properly tracking mark boundaries.\n * This handles cases where marks span across multiple text nodes.\n */\n private renderNodesWithMarkBoundaries(\n nodes: JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n level = 0,\n ): string {\n const result: string[] = []\n const activeMarks: Map<string, any> = new Map()\n nodes.forEach((node, i) => {\n // Lookahead to the next node to determine if marks need to be closed\n const nextNode = i < nodes.length - 1 ? nodes[i + 1] : null\n\n if (!node.type) {\n return\n }\n\n if (node.type === 'text') {\n let textContent = node.text || ''\n const currentMarks = new Map((node.marks || []).map(mark => [mark.type, mark]))\n\n // Find marks that need to be closed and opened\n const marksToOpen = findMarksToOpen(activeMarks, currentMarks)\n const marksToClose = findMarksToClose(currentMarks, nextNode)\n\n let middleTrailingWhitespace = ''\n\n if (marksToClose.length > 0) {\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n const middleTrailingMatch = textContent.match(/(\\s+)$/)\n if (middleTrailingMatch) {\n middleTrailingWhitespace = middleTrailingMatch[1]\n textContent = textContent.slice(0, -middleTrailingWhitespace.length)\n }\n }\n // Close marks that are ending here\n marksToClose.forEach(markType => {\n const mark = currentMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n // deleting closed marks from active marks\n if (activeMarks.has(markType)) {\n activeMarks.delete(markType)\n }\n })\n\n // Open new marks (should be at the beginning)\n // Extract leading whitespace before opening marks to prevent invalid markdown like \"** text**\"\n let leadingWhitespace = ''\n if (marksToOpen.length > 0) {\n const leadingMatch = textContent.match(/^(\\s+)/)\n if (leadingMatch) {\n leadingWhitespace = leadingMatch[1]\n textContent = textContent.slice(leadingWhitespace.length)\n }\n }\n\n marksToOpen.forEach(({ type, mark }) => {\n const openMarkdown = this.getMarkOpening(type, mark)\n if (openMarkdown) {\n textContent = openMarkdown + textContent\n }\n if (!marksToClose.includes(type)) {\n activeMarks.set(type, mark)\n }\n })\n\n // Add leading whitespace before the mark opening\n textContent = leadingWhitespace + textContent\n\n // Close marks at the end of this node if needed\n const marksToCloseAtEnd = findMarksToCloseAtEnd(\n activeMarks,\n currentMarks,\n nextNode,\n this.markSetsEqual.bind(this),\n )\n\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n let trailingWhitespace = ''\n if (marksToCloseAtEnd.length > 0) {\n const trailingMatch = textContent.match(/(\\s+)$/)\n if (trailingMatch) {\n trailingWhitespace = trailingMatch[1]\n textContent = textContent.slice(0, -trailingWhitespace.length)\n }\n }\n\n marksToCloseAtEnd.forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n activeMarks.delete(markType)\n })\n\n // Add trailing whitespace after the mark closing\n textContent += trailingWhitespace\n textContent += middleTrailingWhitespace\n\n result.push(textContent)\n } else {\n // For non-text nodes, close all active marks before rendering, then reopen after\n const marksToReopen = new Map(activeMarks)\n\n // Close all marks before the node\n const beforeMarkdown = closeMarksBeforeNode(activeMarks, this.getMarkClosing.bind(this))\n\n // Render the node\n const nodeContent = this.renderNodeToMarkdown(node, parentNode, i, level)\n\n // Reopen marks after the node, but NOT after a hard break\n // Hard breaks should terminate marks (they create a line break where marks don't continue)\n const afterMarkdown =\n node.type === 'hardBreak'\n ? ''\n : reopenMarksAfterNode(marksToReopen, activeMarks, this.getMarkOpening.bind(this))\n\n result.push(beforeMarkdown + nodeContent + afterMarkdown)\n }\n })\n\n return result.join(separator)\n }\n\n /**\n * Get the opening markdown syntax for a mark type.\n */\n private getMarkOpening(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n // For most marks, we can extract the opening syntax by rendering a simple case\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the opening part (everything before placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n return placeholderIndex >= 0 ? rendered.substring(0, placeholderIndex) : ''\n } catch (err) {\n throw new Error(`Failed to get mark opening for ${markType}: ${err}`)\n }\n }\n\n /**\n * Get the closing markdown syntax for a mark type.\n */\n private getMarkClosing(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the closing part (everything after placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n const placeholderEnd = placeholderIndex + placeholder.length\n return placeholderIndex >= 0 ? rendered.substring(placeholderEnd) : ''\n } catch (err) {\n throw new Error(`Failed to get mark closing for ${markType}: ${err}`)\n }\n }\n\n /**\n * Check if two mark sets are equal.\n */\n private markSetsEqual(marks1: Map<string, any>, marks2: Map<string, any>): boolean {\n if (marks1.size !== marks2.size) {\n return false\n }\n\n return Array.from(marks1.keys()).every(type => marks2.has(type))\n }\n}\n\nexport default MarkdownManager\n","import type { Content, MarkdownToken } from '@tiptap/core'\nimport type { Fragment, Node } from '@tiptap/pm/model'\n\nimport type { ContentType } from './types.js'\n\n/**\n * Wraps each line of the content with the given prefix.\n * @param prefix The prefix to wrap each line with.\n * @param content The content to wrap.\n * @returns The content with each line wrapped with the prefix.\n */\nexport function wrapInMarkdownBlock(prefix: string, content: string) {\n // split content lines\n const lines = content.split('\\n')\n\n // add empty strings between every line\n const output = lines\n // add empty lines between each block\n .flatMap(line => [line, ''])\n // add the prefix to each line\n .map(line => `${prefix}${line}`)\n .join('\\n')\n\n return output.slice(0, output.length - 1)\n}\n\n/**\n * Identifies marks that need to be closed, based on the marks in the next node.\n */\nexport function findMarksToClose(currentMarks: Map<string, any>, nextNode: any): string[] {\n const marksToClose: string[] = []\n\n Array.from(currentMarks.keys()).forEach(markType => {\n if (!nextNode || !nextNode.marks || !nextNode.marks.map((mark: any) => mark.type).includes(markType)) {\n marksToClose.push(markType)\n }\n })\n return marksToClose\n}\n\n/**\n * Identifies marks that need to be opened (in current node but not active).\n */\nexport function findMarksToOpen(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n): Array<{ type: string; mark: any }> {\n const marksToOpen: Array<{ type: string; mark: any }> = []\n Array.from(currentMarks.entries()).forEach(([markType, mark]) => {\n if (!activeMarks.has(markType)) {\n marksToOpen.push({ type: markType, mark })\n }\n })\n return marksToOpen\n}\n\n/**\n * Determines which marks need to be closed at the end of the current text node.\n * This handles cases where marks end at node boundaries or when transitioning\n * to nodes with different mark sets.\n */\nexport function findMarksToCloseAtEnd(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n nextNode: any,\n markSetsEqual: (a: Map<string, any>, b: Map<string, any>) => boolean,\n): string[] {\n const isLastNode = !nextNode\n const nextNodeHasNoMarks = nextNode && nextNode.type === 'text' && (!nextNode.marks || nextNode.marks.length === 0)\n const nextNodeHasDifferentMarks =\n nextNode &&\n nextNode.type === 'text' &&\n nextNode.marks &&\n !markSetsEqual(currentMarks, new Map(nextNode.marks.map((mark: any) => [mark.type, mark])))\n\n const marksToCloseAtEnd: string[] = []\n if (isLastNode || nextNodeHasNoMarks || nextNodeHasDifferentMarks) {\n if (nextNode && nextNode.type === 'text' && nextNode.marks) {\n const nextMarks = new Map(nextNode.marks.map((mark: any) => [mark.type, mark]))\n Array.from(activeMarks.keys()).forEach(markType => {\n if (!nextMarks.has(markType)) {\n marksToCloseAtEnd.push(markType)\n }\n })\n } else if (isLastNode || nextNodeHasNoMarks) {\n // Close all active marks\n marksToCloseAtEnd.push(...Array.from(activeMarks.keys()))\n }\n }\n\n return marksToCloseAtEnd\n}\n\n/**\n * Closes active marks before rendering a non-text node.\n * Returns the closing markdown syntax and clears the active marks.\n */\nexport function closeMarksBeforeNode(\n activeMarks: Map<string, any>,\n getMarkClosing: (markType: string, mark: any) => string,\n): string {\n let beforeMarkdown = ''\n Array.from(activeMarks.keys())\n .reverse()\n .forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = getMarkClosing(markType, mark)\n if (closeMarkdown) {\n beforeMarkdown = closeMarkdown + beforeMarkdown\n }\n })\n activeMarks.clear()\n return beforeMarkdown\n}\n\n/**\n * Reopens marks after rendering a non-text node.\n * Returns the opening markdown syntax and updates the active marks.\n */\nexport function reopenMarksAfterNode(\n marksToReopen: Map<string, any>,\n activeMarks: Map<string, any>,\n getMarkOpening: (markType: string, mark: any) => string,\n): string {\n let afterMarkdown = ''\n Array.from(marksToReopen.entries()).forEach(([markType, mark]) => {\n const openMarkdown = getMarkOpening(markType, mark)\n if (openMarkdown) {\n afterMarkdown += openMarkdown\n }\n activeMarks.set(markType, mark)\n })\n return afterMarkdown\n}\n\n/**\n * Check if a markdown list item token is a task item and extract its state.\n *\n * @param item The list item token to check\n * @returns Object containing isTask flag, checked state, and indentation level\n *\n * @example\n * ```ts\n * isTaskItem({ raw: '- [ ] Task' }) // { isTask: true, checked: false, indentLevel: 0 }\n * isTaskItem({ raw: ' - [x] Done' }) // { isTask: true, checked: true, indentLevel: 2 }\n * isTaskItem({ raw: '- Regular' }) // { isTask: false, indentLevel: 0 }\n * ```\n */\nexport function isTaskItem(item: MarkdownToken): { isTask: boolean; checked?: boolean; indentLevel: number } {\n const raw = item.raw || item.text || ''\n\n // Match patterns like \"- [ ] \" or \" - [x] \"\n const match = raw.match(/^(\\s*)[-+*]\\s+\\[([ xX])\\]\\s+/)\n\n if (match) {\n return { isTask: true, checked: match[2].toLowerCase() === 'x', indentLevel: match[1].length }\n }\n return { isTask: false, indentLevel: 0 }\n}\n\n/**\n * Assumes the content type based off the content.\n * @param content The content to assume the type for.\n * @param contentType The content type that should be prioritized.\n */\nexport function assumeContentType(\n content: (Content | Fragment | Node) | string,\n contentType: ContentType,\n): ContentType {\n // if not a string, we assume it will be a json content object\n if (typeof content !== 'string') {\n return 'json'\n }\n\n // otherwise we let the content type be what it is\n return contentType\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/tiptap/tiptap/packages/markdown/dist/index.cjs","../src/Extension.ts","../src/MarkdownManager.ts","../src/utils.ts"],"names":[],"mappings":"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","file":"/home/runner/work/tiptap/tiptap/packages/markdown/dist/index.cjs","sourcesContent":[null,"import {\n type InsertContentAtOptions as MarkdownInsertContentAtOptions,\n type InsertContentOptions as MarkdownInsertContentOptions,\n type SetContentOptions as MarkdownSetContentOptions,\n commands,\n Extension,\n} from '@tiptap/core'\nimport type { marked } from 'marked'\n\nimport MarkdownManager from './MarkdownManager.js'\nimport type { ContentType } from './types.js'\nimport { assumeContentType } from './utils.js'\n\ndeclare module '@tiptap/core' {\n interface Editor {\n /**\n * Get the content of the editor as markdown.\n */\n getMarkdown: () => string\n\n /**\n * The markdown manager instance.\n */\n markdown?: MarkdownManager\n }\n\n interface EditorOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface Storage {\n markdown: MarkdownExtensionStorage\n }\n\n interface InsertContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface InsertContentAtOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface SetContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n}\n\nexport type MarkdownExtensionOptions = {\n /**\n * Configure the indentation style and size for lists and code blocks.\n * - `style`: Choose between spaces or tabs. Default is 'space'.\n * - `size`: Number of spaces or tabs for indentation. Default is 2.\n */\n indentation?: { style?: 'space' | 'tab'; size?: number }\n\n /**\n * Use a custom version of `marked` for markdown parsing and serialization.\n * If not provided, the default `marked` instance will be used.\n */\n marked?: typeof marked\n\n /**\n * Options to pass to `marked.setOptions()`.\n * See the [marked documentation](https://marked.js.org/using_advanced#options) for available options.\n */\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n}\n\nexport type MarkdownExtensionStorage = {\n manager: MarkdownManager\n}\n\nexport const Markdown = Extension.create<MarkdownExtensionOptions, MarkdownExtensionStorage>({\n name: 'markdown',\n\n addOptions() {\n return {\n indentation: { style: 'space', size: 2 },\n marked: undefined,\n markedOptions: {},\n }\n },\n\n addCommands() {\n return {\n setContent: (content, options?: MarkdownSetContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.setContent(content, options)\n }\n\n const actualContentType = assumeContentType(content, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.setContent(content, options)\n }\n\n const mdContent = this.editor.markdown.parse(content as string)\n return commands.setContent(mdContent, options)\n },\n\n insertContent: (value, options?: MarkdownInsertContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContent(value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContent(value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContent(mdContent, options)\n },\n\n insertContentAt: (position, value, options?: MarkdownInsertContentAtOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContentAt(position, value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContentAt(position, value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContentAt(position, mdContent, options)\n },\n }\n },\n\n addStorage() {\n return {\n manager: new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: [],\n }),\n }\n },\n\n onBeforeCreate() {\n if (this.editor.markdown) {\n console.error(\n '[tiptap][markdown]: There is already a `markdown` property on the editor instance. This might lead to unexpected behavior.',\n )\n return\n }\n\n this.storage.manager = new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: this.editor.extensionManager.baseExtensions,\n })\n\n this.editor.markdown = this.storage.manager\n\n // add a `getMarkdown()` method to the editor\n this.editor.getMarkdown = () => {\n return this.storage.manager.serialize(this.editor.getJSON())\n }\n\n if (!this.editor.options.contentType) {\n return\n }\n\n const assumedType = assumeContentType(this.editor.options.content, this.editor.options.contentType)\n if (assumedType !== 'markdown') {\n return\n }\n\n if (!this.editor.markdown) {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the Markdown extension is not added to the editor. Please add the Markdown extension to use this feature.',\n )\n }\n\n if (this.editor.options.content === undefined || typeof this.editor.options.content !== 'string') {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the initial content is not a string. Please provide the initial content as a markdown string.',\n )\n }\n\n const json = this.editor.markdown.parse(this.editor.options.content as string)\n this.editor.options.content = json\n },\n})\n","import {\n type AnyExtension,\n type ExtendableConfig,\n type JSONContent,\n type MarkdownExtensionSpec,\n type MarkdownParseHelpers,\n type MarkdownParseResult,\n type MarkdownRendererHelpers,\n type MarkdownToken,\n type MarkdownTokenizer,\n type RenderContext,\n flattenExtensions,\n generateJSON,\n getExtensionField,\n} from '@tiptap/core'\nimport { type Lexer, type Token, type TokenizerExtension, marked } from 'marked'\n\nimport {\n closeMarksBeforeNode,\n findMarksToClose,\n findMarksToCloseAtEnd,\n findMarksToOpen,\n isTaskItem,\n reopenMarksAfterNode,\n wrapInMarkdownBlock,\n} from './utils.js'\n\nexport class MarkdownManager {\n private markedInstance: typeof marked\n private lexer: Lexer\n private registry: Map<string, MarkdownExtensionSpec[]>\n private nodeTypeRegistry: Map<string, MarkdownExtensionSpec[]>\n private indentStyle: 'space' | 'tab'\n private indentSize: number\n private baseExtensions: AnyExtension[] = []\n private extensions: AnyExtension[] = []\n\n /**\n * Create a MarkdownManager.\n * @param options.marked Optional marked instance to use (injected).\n * @param options.markedOptions Optional options to pass to marked.setOptions\n * @param options.indentation Indentation settings (style and size).\n * @param options.extensions An array of Tiptap extensions to register for markdown parsing and rendering.\n */\n constructor(options?: {\n marked?: typeof marked\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n indentation?: { style?: 'space' | 'tab'; size?: number }\n extensions: AnyExtension[]\n }) {\n this.markedInstance = options?.marked ?? marked\n this.lexer = new this.markedInstance.Lexer()\n this.indentStyle = options?.indentation?.style ?? 'space'\n this.indentSize = options?.indentation?.size ?? 2\n this.baseExtensions = options?.extensions || []\n\n if (options?.markedOptions && typeof this.markedInstance.setOptions === 'function') {\n this.markedInstance.setOptions(options.markedOptions)\n }\n\n this.registry = new Map()\n this.nodeTypeRegistry = new Map()\n\n // If extensions were provided, register them now\n if (options?.extensions) {\n this.baseExtensions = options.extensions\n const flattened = flattenExtensions(options.extensions)\n flattened.forEach(ext => this.registerExtension(ext, false))\n }\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include all tokenizers\n }\n\n /** Returns the underlying marked instance. */\n get instance(): typeof marked {\n return this.markedInstance\n }\n\n /** Returns the correct indentCharacter (space or tab) */\n get indentCharacter(): string {\n return this.indentStyle === 'space' ? ' ' : '\\t'\n }\n\n /** Returns the correct indentString repeated X times */\n get indentString(): string {\n return this.indentCharacter.repeat(this.indentSize)\n }\n\n /** Helper to quickly check whether a marked instance is available. */\n hasMarked(): boolean {\n return !!this.markedInstance\n }\n\n /**\n * Register a Tiptap extension (Node/Mark/Extension). This will read\n * `markdownName`, `parseMarkdown`, `renderMarkdown` and `priority` from the\n * extension config (using the same resolution used across the codebase).\n */\n registerExtension(extension: AnyExtension, recreateLexer: boolean = true): void {\n // Keep track of all extensions for HTML parsing\n this.extensions.push(extension)\n\n const name = extension.name\n const tokenName =\n (getExtensionField(extension, 'markdownTokenName') as ExtendableConfig['markdownTokenName']) || name\n const parseMarkdown = getExtensionField(extension, 'parseMarkdown') as ExtendableConfig['parseMarkdown'] | undefined\n const renderMarkdown = getExtensionField(extension, 'renderMarkdown') as\n | ExtendableConfig['renderMarkdown']\n | undefined\n const tokenizer = getExtensionField(extension, 'markdownTokenizer') as\n | ExtendableConfig['markdownTokenizer']\n | undefined\n\n // Read the `markdown` object from the extension config. This allows\n // extensions to provide `markdown: { name?, parseName?, renderName?, parse?, render?, match? }`.\n const markdownCfg = (getExtensionField(extension, 'markdownOptions') ?? null) as ExtendableConfig['markdownOptions']\n const isIndenting = markdownCfg?.indentsContent ?? false\n\n const spec: MarkdownExtensionSpec = {\n tokenName,\n nodeName: name,\n parseMarkdown,\n renderMarkdown,\n isIndenting,\n tokenizer,\n }\n\n // Add to parse registry using parseName\n if (tokenName && parseMarkdown) {\n const parseExisting = this.registry.get(tokenName) || []\n parseExisting.push(spec)\n this.registry.set(tokenName, parseExisting)\n }\n\n // Add to render registry using renderName (node type)\n if (renderMarkdown) {\n const renderExisting = this.nodeTypeRegistry.get(name) || []\n renderExisting.push(spec)\n this.nodeTypeRegistry.set(name, renderExisting)\n }\n\n // Register custom tokenizer with marked.js\n if (tokenizer && this.hasMarked()) {\n this.registerTokenizer(tokenizer)\n\n if (recreateLexer) {\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include new tokenizer\n }\n }\n }\n\n /**\n * Register a custom tokenizer with marked.js for parsing non-standard markdown syntax.\n */\n private registerTokenizer(tokenizer: MarkdownTokenizer): void {\n if (!this.hasMarked()) {\n return\n }\n\n const { name, start, level = 'inline', tokenize } = tokenizer\n\n // Helper functions that use a fresh lexer instance with all registered extensions\n const tokenizeInline = (src: string) => {\n return this.lexer.inlineTokens(src)\n }\n\n const tokenizeBlock = (src: string) => {\n return this.lexer.blockTokens(src)\n }\n\n const helper = {\n inlineTokens: tokenizeInline,\n blockTokens: tokenizeBlock,\n }\n\n let startCb: (src: string) => number\n\n if (!start) {\n startCb = (src: string) => {\n // For other tokenizers, try to find a match and return its position\n const result = tokenize(src, [], helper)\n if (result && result.raw) {\n const index = src.indexOf(result.raw)\n return index\n }\n return -1\n }\n } else {\n startCb = typeof start === 'function' ? start : (src: string) => src.indexOf(start)\n }\n\n // Create marked.js extension with proper types\n const markedExtension: TokenizerExtension = {\n name,\n level,\n start: startCb,\n tokenizer: (src, tokens) => {\n const result = tokenize(src, tokens, helper)\n\n if (result && result.type) {\n return {\n ...result,\n type: result.type || name,\n raw: result.raw || '',\n tokens: (result.tokens || []) as Token[],\n }\n }\n\n return undefined\n },\n childTokens: [],\n }\n\n // Register with marked.js - use extensions array to control priority\n this.markedInstance.use({\n extensions: [markedExtension],\n })\n }\n\n /** Get registered handlers for a token type and try each until one succeeds. */\n private getHandlersForToken(type: string): MarkdownExtensionSpec[] {\n try {\n return this.registry.get(type) || []\n } catch {\n return []\n }\n }\n\n /** Get the first handler for a token type (for backwards compatibility). */\n private getHandlerForToken(type: string): MarkdownExtensionSpec | undefined {\n // First try the markdown token registry (for parsing)\n const markdownHandlers = this.getHandlersForToken(type)\n if (markdownHandlers.length > 0) {\n return markdownHandlers[0]\n }\n\n // Then try the node type registry (for rendering)\n const nodeTypeHandlers = this.getHandlersForNodeType(type)\n return nodeTypeHandlers.length > 0 ? nodeTypeHandlers[0] : undefined\n }\n\n /** Get registered handlers for a node type (for rendering). */\n private getHandlersForNodeType(type: string): MarkdownExtensionSpec[] {\n try {\n return this.nodeTypeRegistry.get(type) || []\n } catch {\n return []\n }\n }\n\n /**\n * Serialize a ProseMirror-like JSON document (or node array) to a Markdown string\n * using registered renderers and fallback renderers.\n */\n serialize(docOrContent: JSONContent): string {\n if (!docOrContent) {\n return ''\n }\n\n const result = this.renderNodes(docOrContent, docOrContent)\n // Return empty string if result is only whitespace entities or non-breaking spaces\n return this.isEmptyOutput(result) ? '' : result\n }\n\n /**\n * Check if the markdown output represents an empty document.\n * Empty documents may contain only &nbsp; entities or non-breaking space characters\n * which are used by the Paragraph extension to preserve blank lines.\n */\n private isEmptyOutput(markdown: string): boolean {\n if (!markdown || markdown.trim() === '') {\n return true\n }\n\n // Check if the output is only &nbsp; entities or non-breaking space characters\n const cleanedOutput = markdown\n .replace(/&nbsp;/g, '')\n .replace(/\\u00A0/g, '')\n .trim()\n return cleanedOutput === ''\n }\n\n /**\n * Parse markdown string into Tiptap JSON document using registered extension handlers.\n */\n parse(markdown: string): JSONContent {\n if (!this.hasMarked()) {\n throw new Error('No marked instance available for parsing')\n }\n\n // Use marked to tokenize the markdown\n const tokens = this.markedInstance.lexer(markdown)\n\n // Convert tokens to Tiptap JSON\n const content = this.parseTokens(tokens, true)\n\n // Return a document node containing the parsed content\n return {\n type: 'doc',\n content,\n }\n }\n\n /**\n * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.\n */\n private parseTokens(tokens: MarkdownToken[], parseImplicitEmptyParagraphs = false): JSONContent[] {\n const nonSpaceTokenIndexes = tokens.reduce<number[]>((indexes, token, index) => {\n if (token.type !== 'space') {\n indexes.push(index)\n }\n\n return indexes\n }, [])\n\n let previousNonSpaceTokenIndex = -1\n let nextNonSpaceTokenPointer = 0\n\n return tokens.flatMap((token, index) => {\n while (\n nextNonSpaceTokenPointer < nonSpaceTokenIndexes.length &&\n nonSpaceTokenIndexes[nextNonSpaceTokenPointer] < index\n ) {\n previousNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer]\n nextNonSpaceTokenPointer += 1\n }\n\n if (parseImplicitEmptyParagraphs && token.type === 'space') {\n const nextNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer] ?? -1\n\n return this.createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex)\n }\n\n const parsed = this.parseToken(token, parseImplicitEmptyParagraphs)\n\n if (parsed === null) {\n return []\n }\n\n return Array.isArray(parsed) ? parsed : [parsed]\n })\n }\n\n private createImplicitEmptyParagraphsFromSpace(\n token: MarkdownToken,\n previousNonSpaceTokenIndex: number,\n nextNonSpaceTokenIndex: number,\n ): JSONContent[] {\n const separatorCount = this.countParagraphSeparators(token.raw || '')\n\n if (separatorCount === 0) {\n return []\n }\n\n const isBoundarySpace = previousNonSpaceTokenIndex === -1 || nextNonSpaceTokenIndex === -1\n const emptyParagraphCount = Math.max(separatorCount - (isBoundarySpace ? 0 : 1), 0)\n\n return Array.from({ length: emptyParagraphCount }, () => ({ type: 'paragraph', content: [] }))\n }\n\n private countParagraphSeparators(raw: string): number {\n return (raw.replace(/\\r\\n/g, '\\n').match(/\\n\\n/g) || []).length\n }\n\n /**\n * Parse a single token into Tiptap JSON using the appropriate registered handler.\n */\n private parseToken(token: MarkdownToken, parseImplicitEmptyParagraphs = false): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n // Special handling for 'list' tokens that may contain mixed bullet/task items\n if (token.type === 'list') {\n return this.parseListToken(token)\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token, parseImplicitEmptyParagraphs)\n }\n\n private lastParseResult: JSONContent | JSONContent[] | null = null\n\n /**\n * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists.\n * This ensures that consecutive task items and bullet items are grouped and parsed as separate list nodes.\n *\n * @param token The list token to parse\n * @returns Array of parsed list nodes, or null if parsing fails\n */\n private parseListToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.items || token.items.length === 0) {\n // No items, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n const hasTask = token.items.some(item => isTaskItem(item).isTask)\n const hasNonTask = token.items.some(item => !isTaskItem(item).isTask)\n\n if (!hasTask || !hasNonTask || this.getHandlersForToken('taskList').length === 0) {\n // Not mixed or no taskList extension, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n // Mixed list with taskList extension available: split into separate lists\n type TaskListItemToken = MarkdownToken & { type: 'taskItem'; checked?: boolean; indentLevel?: number }\n const groups: { type: 'list' | 'taskList'; items: (MarkdownToken | TaskListItemToken)[] }[] = []\n let currentGroup: (MarkdownToken | TaskListItemToken)[] = []\n let currentType: 'list' | 'taskList' | null = null\n\n for (let i = 0; i < token.items.length; i += 1) {\n const item = token.items[i]\n const { isTask, checked, indentLevel } = isTaskItem(item)\n let processedItem = item\n\n if (isTask) {\n // Transform list_item into taskItem token\n const raw = item.raw || item.text || ''\n\n // Split raw content by lines to separate main content from nested\n const lines = raw.split('\\n')\n\n // Extract main content from the first line\n const firstLineMatch = lines[0].match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+(.*)$/)\n const mainContent = firstLineMatch ? firstLineMatch[2] : ''\n\n // Parse nested content from remaining lines\n let nestedTokens: MarkdownToken[] = []\n if (lines.length > 1) {\n // Join all lines after the first\n const nestedRaw = lines.slice(1).join('\\n')\n\n // Only parse if there's actual content\n if (nestedRaw.trim()) {\n // Find minimum indentation of non-empty lines\n const nestedLines = lines.slice(1)\n const nonEmptyLines = nestedLines.filter(line => line.trim())\n if (nonEmptyLines.length > 0) {\n const minIndent = Math.min(...nonEmptyLines.map(line => line.length - line.trimStart().length))\n // Remove common indentation while preserving structure\n const trimmedLines = nestedLines.map(line => {\n if (!line.trim()) {\n return '' // Keep empty lines\n }\n return line.slice(minIndent)\n })\n const nestedContent = trimmedLines.join('\\n').trim()\n // Use the lexer to parse nested content\n if (nestedContent) {\n // Use the full lexer pipeline to ensure inline tokens are populated\n nestedTokens = this.markedInstance.lexer(`${nestedContent}\\n`)\n }\n }\n }\n }\n\n processedItem = {\n type: 'taskItem',\n raw: '',\n mainContent,\n indentLevel,\n checked: checked ?? false,\n text: mainContent,\n tokens: this.lexer.inlineTokens(mainContent),\n nestedTokens,\n }\n }\n\n const itemType: 'list' | 'taskList' = isTask ? 'taskList' : 'list'\n\n if (currentType !== itemType) {\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n currentGroup = [processedItem]\n currentType = itemType\n } else {\n currentGroup.push(processedItem)\n }\n }\n\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n\n // Parse each group as a separate token\n const results: JSONContent[] = []\n for (let i = 0; i < groups.length; i += 1) {\n const group = groups[i]\n const subToken = { ...token, type: group.type, items: group.items }\n const parsed = this.parseToken(subToken)\n if (parsed) {\n if (Array.isArray(parsed)) {\n results.push(...parsed)\n } else {\n results.push(parsed)\n }\n }\n }\n\n return results.length > 0 ? results : null\n }\n\n /**\n * Parse a token using registered handlers (extracted for reuse).\n */\n private parseTokenWithHandlers(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token)\n }\n\n /**\n * Creates helper functions for parsing markdown tokens.\n * @returns An object containing helper functions for parsing.\n */\n private createParseHelpers(): MarkdownParseHelpers {\n return {\n parseInline: (tokens: MarkdownToken[]) => this.parseInlineTokens(tokens),\n parseChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens),\n parseBlockChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens, true),\n createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => {\n const node = {\n type: 'text',\n text,\n marks: marks || undefined,\n }\n\n return node\n },\n createNode: (type: string, attrs?: any, content?: JSONContent[]) => {\n const node = {\n type,\n attrs: attrs || undefined,\n content: content || undefined,\n }\n\n if (!attrs || Object.keys(attrs).length === 0) {\n delete node.attrs\n }\n\n return node\n },\n applyMark: (markType: string, content: JSONContent[], attrs?: any) => ({\n mark: markType,\n content,\n attrs: attrs && Object.keys(attrs).length > 0 ? attrs : undefined,\n }),\n }\n }\n\n /**\n * Escape special regex characters in a string.\n */\n private escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n\n /**\n * Parse inline tokens (bold, italic, links, etc.) into text nodes with marks.\n * This is the complex part that handles mark nesting and boundaries.\n */\n private parseInlineTokens(tokens: MarkdownToken[]): JSONContent[] {\n const result: JSONContent[] = []\n\n // Process tokens sequentially using an index so we can lookahead and\n // merge split inline HTML fragments like: text / <em> / inner / </em> / text\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i]\n\n if (token.type === 'text') {\n // Create text node\n result.push({\n type: 'text',\n text: token.text || '',\n })\n } else if (token.type === 'html') {\n // Handle possible split inline HTML by attempting to detect an\n // opening tag and searching forward for a matching closing tag.\n const raw = (token.raw ?? token.text ?? '').toString()\n\n // Quick checks for opening vs. closing tag\n const isClosing = /^<\\/[\\s]*[\\w-]+/i.test(raw)\n const openMatch = raw.match(/^<[\\s]*([\\w-]+)(\\s|>|\\/|$)/i)\n\n if (!isClosing && openMatch && !/\\/>$/.test(raw)) {\n // Try to find the corresponding closing html token for this tag\n const tagName = openMatch[1]\n const escapedTagName = this.escapeRegex(tagName)\n const closingRegex = new RegExp(`^<\\\\/\\\\s*${escapedTagName}\\\\b`, 'i')\n let foundIndex = -1\n\n // Collect intermediate raw parts to reconstruct full HTML fragment\n const parts: string[] = [raw]\n for (let j = i + 1; j < tokens.length; j += 1) {\n const t = tokens[j]\n const tRaw = (t.raw ?? t.text ?? '').toString()\n parts.push(tRaw)\n if (t.type === 'html' && closingRegex.test(tRaw)) {\n foundIndex = j\n break\n }\n }\n\n if (foundIndex !== -1) {\n // Merge opening + inner + closing into one html fragment and parse\n const mergedRaw = parts.join('')\n const mergedToken = {\n type: 'html',\n raw: mergedRaw,\n text: mergedRaw,\n block: false,\n } as unknown as MarkdownToken\n\n const parsed = this.parseHTMLToken(mergedToken)\n if (parsed) {\n const normalized = this.normalizeParseResult(parsed as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n\n // Advance i to the closing token\n i = foundIndex\n continue\n }\n }\n\n // Fallback: single html token parse\n const parsedSingle = this.parseHTMLToken(token)\n if (parsedSingle) {\n const normalized = this.normalizeParseResult(parsedSingle as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.type) {\n // Handle inline marks (bold, italic, etc.)\n const markHandler = this.getHandlerForToken(token.type)\n if (markHandler && markHandler.parseMarkdown) {\n const helpers = this.createParseHelpers()\n const parsed = markHandler.parseMarkdown(token, helpers)\n\n if (this.isMarkResult(parsed)) {\n // This is a mark result - apply the mark to the content\n const markedContent = this.applyMarkToContent(parsed.mark, parsed.content, parsed.attrs)\n result.push(...markedContent)\n } else {\n // Regular inline node\n const normalized = this.normalizeParseResult(parsed)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.tokens) {\n // Fallback: try to parse children if they exist\n result.push(...this.parseInlineTokens(token.tokens))\n }\n }\n }\n\n return result\n }\n\n /**\n * Apply a mark to content nodes.\n */\n private applyMarkToContent(markType: string, content: JSONContent[], attrs?: any): JSONContent[] {\n return content.map(node => {\n if (node.type === 'text') {\n // Add the mark to existing marks or create new marks array\n const existingMarks = node.marks || []\n const newMark = attrs ? { type: markType, attrs } : { type: markType }\n return {\n ...node,\n marks: [...existingMarks, newMark],\n }\n }\n\n // For non-text nodes, recursively apply to content\n return {\n ...node,\n content: node.content ? this.applyMarkToContent(markType, node.content, attrs) : undefined,\n }\n })\n } /**\n * Check if a parse result represents a mark to be applied.\n */\n private isMarkResult(result: any): result is { mark: string; content: JSONContent[]; attrs?: any } {\n return result && typeof result === 'object' && 'mark' in result\n }\n\n /**\n * Normalize parse results to ensure they're valid JSONContent.\n */\n private normalizeParseResult(result: MarkdownParseResult): JSONContent | JSONContent[] | null {\n if (!result) {\n return null\n }\n\n if (this.isMarkResult(result)) {\n // This shouldn't happen at the top level, but handle it gracefully\n return result.content\n }\n\n return result as JSONContent | JSONContent[]\n }\n\n /**\n * Fallback parsing for common tokens when no specific handler is registered.\n */\n private parseFallbackToken(\n token: MarkdownToken,\n parseImplicitEmptyParagraphs = false,\n ): JSONContent | JSONContent[] | null {\n switch (token.type) {\n case 'paragraph':\n return {\n type: 'paragraph',\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'heading':\n return {\n type: 'heading',\n attrs: { level: token.depth || 1 },\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'text':\n return {\n type: 'text',\n text: token.text || '',\n }\n\n case 'html':\n // Parse HTML using extensions' parseHTML methods\n return this.parseHTMLToken(token)\n\n case 'space':\n return null\n\n default:\n // Unknown token type - try to parse children if they exist\n if (token.tokens) {\n return this.parseTokens(token.tokens, parseImplicitEmptyParagraphs)\n }\n return null\n }\n }\n\n /**\n * Parse HTML tokens using extensions' parseHTML methods.\n * This allows HTML within markdown to be parsed according to extension rules.\n */\n private parseHTMLToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n const html = token.text || token.raw || ''\n\n if (!html.trim()) {\n return null\n }\n\n // Check if we're in a server-side environment (no window object)\n // If so, fall back to treating HTML as plain text to avoid runtime errors\n if (typeof window === 'undefined') {\n // For block-level HTML, wrap in a paragraph to maintain valid document structure\n if (token.block) {\n return {\n type: 'paragraph',\n content: [\n {\n type: 'text',\n text: html,\n },\n ],\n }\n }\n // For inline HTML, return plain text\n return {\n type: 'text',\n text: html,\n }\n }\n\n // Use generateJSON to parse the HTML using extensions' parseHTML rules\n try {\n const parsed = generateJSON(html, this.baseExtensions)\n\n // If the result is a doc node, extract its content\n if (parsed.type === 'doc' && parsed.content) {\n // For block-level HTML, return the content array\n if (token.block) {\n return parsed.content\n }\n\n // For inline HTML, we need to flatten the content appropriately\n // If there's only one paragraph with content, unwrap it\n if (parsed.content.length === 1 && parsed.content[0].type === 'paragraph' && parsed.content[0].content) {\n return parsed.content[0].content\n }\n\n return parsed.content\n }\n\n return parsed as JSONContent\n } catch (error) {\n throw new Error(`Failed to parse HTML in markdown: ${error}`)\n }\n }\n\n renderNodeToMarkdown(\n node: JSONContent,\n parentNode?: JSONContent,\n index = 0,\n level = 0,\n meta: Record<string, any> = {},\n ): string {\n // if node is a text node, we simply return it's text content\n // marks are handled at the array level in renderNodesWithMarkBoundaries\n if (node.type === 'text') {\n return node.text || ''\n }\n\n if (!node.type) {\n return ''\n }\n\n const handler = this.getHandlerForToken(node.type)\n if (!handler) {\n return ''\n }\n\n const previousNode = Array.isArray(parentNode?.content) && index > 0 ? parentNode.content[index - 1] : undefined\n const helpers: MarkdownRendererHelpers = {\n renderChildren: (nodes, separator) => {\n const childLevel = handler.isIndenting ? level + 1 : level\n\n if (!Array.isArray(nodes) && (nodes as any).content) {\n return this.renderNodes((nodes as any).content as JSONContent[], node, separator || '', index, childLevel)\n }\n\n return this.renderNodes(nodes, node, separator || '', index, childLevel)\n },\n renderChild: (childNode, childIndex) => {\n const childLevel = handler.isIndenting ? level + 1 : level\n\n return this.renderNodeToMarkdown(childNode, node, childIndex, childLevel)\n },\n indent: content => {\n return this.indentString + content\n },\n wrapInBlock: wrapInMarkdownBlock,\n }\n\n const context: RenderContext = {\n index,\n level,\n parentType: parentNode?.type,\n previousNode,\n meta: {\n parentAttrs: parentNode?.attrs,\n ...meta,\n },\n }\n\n // First render the node itself (this will render children recursively)\n const rendered = handler.renderMarkdown?.(node, helpers, context) || ''\n\n return rendered\n }\n\n /**\n * Render a node or an array of nodes. Parent type controls how children\n * are joined (which determines newline insertion between children).\n */\n renderNodes(\n nodeOrNodes: JSONContent | JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n index = 0,\n level = 0,\n ): string {\n // if we have just one node, call renderNodeToMarkdown directly\n if (!Array.isArray(nodeOrNodes)) {\n if (!nodeOrNodes.type) {\n return ''\n }\n\n return this.renderNodeToMarkdown(nodeOrNodes, parentNode, index, level)\n }\n\n return this.renderNodesWithMarkBoundaries(nodeOrNodes, parentNode, separator, level)\n }\n\n /**\n * Render an array of nodes while properly tracking mark boundaries.\n * This handles cases where marks span across multiple text nodes.\n */\n private renderNodesWithMarkBoundaries(\n nodes: JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n level = 0,\n ): string {\n const result: string[] = []\n const activeMarks: Map<string, any> = new Map()\n nodes.forEach((node, i) => {\n // Lookahead to the next node to determine if marks need to be closed\n const nextNode = i < nodes.length - 1 ? nodes[i + 1] : null\n\n if (!node.type) {\n return\n }\n\n if (node.type === 'text') {\n let textContent = node.text || ''\n const currentMarks = new Map((node.marks || []).map(mark => [mark.type, mark]))\n\n // Find marks that need to be closed and opened\n const marksToOpen = findMarksToOpen(activeMarks, currentMarks)\n const marksToClose = findMarksToClose(currentMarks, nextNode)\n\n let middleTrailingWhitespace = ''\n\n if (marksToClose.length > 0) {\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n const middleTrailingMatch = textContent.match(/(\\s+)$/)\n if (middleTrailingMatch) {\n middleTrailingWhitespace = middleTrailingMatch[1]\n textContent = textContent.slice(0, -middleTrailingWhitespace.length)\n }\n }\n // Close marks that are ending here\n marksToClose.forEach(markType => {\n const mark = currentMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n // deleting closed marks from active marks\n if (activeMarks.has(markType)) {\n activeMarks.delete(markType)\n }\n })\n\n // Open new marks (should be at the beginning)\n // Extract leading whitespace before opening marks to prevent invalid markdown like \"** text**\"\n let leadingWhitespace = ''\n if (marksToOpen.length > 0) {\n const leadingMatch = textContent.match(/^(\\s+)/)\n if (leadingMatch) {\n leadingWhitespace = leadingMatch[1]\n textContent = textContent.slice(leadingWhitespace.length)\n }\n }\n\n marksToOpen.forEach(({ type, mark }) => {\n const openMarkdown = this.getMarkOpening(type, mark)\n if (openMarkdown) {\n textContent = openMarkdown + textContent\n }\n if (!marksToClose.includes(type)) {\n activeMarks.set(type, mark)\n }\n })\n\n // Add leading whitespace before the mark opening\n textContent = leadingWhitespace + textContent\n\n // Close marks at the end of this node if needed\n const marksToCloseAtEnd = findMarksToCloseAtEnd(\n activeMarks,\n currentMarks,\n nextNode,\n this.markSetsEqual.bind(this),\n )\n\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n let trailingWhitespace = ''\n if (marksToCloseAtEnd.length > 0) {\n const trailingMatch = textContent.match(/(\\s+)$/)\n if (trailingMatch) {\n trailingWhitespace = trailingMatch[1]\n textContent = textContent.slice(0, -trailingWhitespace.length)\n }\n }\n\n marksToCloseAtEnd.forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n activeMarks.delete(markType)\n })\n\n // Add trailing whitespace after the mark closing\n textContent += trailingWhitespace\n textContent += middleTrailingWhitespace\n\n result.push(textContent)\n } else {\n // For non-text nodes, close all active marks before rendering, then reopen after\n const marksToReopen = new Map(activeMarks)\n\n // Close all marks before the node\n const beforeMarkdown = closeMarksBeforeNode(activeMarks, this.getMarkClosing.bind(this))\n\n // Render the node\n const nodeContent = this.renderNodeToMarkdown(node, parentNode, i, level)\n\n // Reopen marks after the node, but NOT after a hard break\n // Hard breaks should terminate marks (they create a line break where marks don't continue)\n const afterMarkdown =\n node.type === 'hardBreak'\n ? ''\n : reopenMarksAfterNode(marksToReopen, activeMarks, this.getMarkOpening.bind(this))\n\n result.push(beforeMarkdown + nodeContent + afterMarkdown)\n }\n })\n\n return result.join(separator)\n }\n\n /**\n * Get the opening markdown syntax for a mark type.\n */\n private getMarkOpening(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n // For most marks, we can extract the opening syntax by rendering a simple case\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n renderChild: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the opening part (everything before placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n return placeholderIndex >= 0 ? rendered.substring(0, placeholderIndex) : ''\n } catch (err) {\n throw new Error(`Failed to get mark opening for ${markType}: ${err}`)\n }\n }\n\n /**\n * Get the closing markdown syntax for a mark type.\n */\n private getMarkClosing(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n renderChild: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the closing part (everything after placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n const placeholderEnd = placeholderIndex + placeholder.length\n return placeholderIndex >= 0 ? rendered.substring(placeholderEnd) : ''\n } catch (err) {\n throw new Error(`Failed to get mark closing for ${markType}: ${err}`)\n }\n }\n\n /**\n * Check if two mark sets are equal.\n */\n private markSetsEqual(marks1: Map<string, any>, marks2: Map<string, any>): boolean {\n if (marks1.size !== marks2.size) {\n return false\n }\n\n return Array.from(marks1.keys()).every(type => marks2.has(type))\n }\n}\n\nexport default MarkdownManager\n","import type { Content, MarkdownToken } from '@tiptap/core'\nimport type { Fragment, Node } from '@tiptap/pm/model'\n\nimport type { ContentType } from './types.js'\n\n/**\n * Wraps each line of the content with the given prefix.\n * @param prefix The prefix to wrap each line with.\n * @param content The content to wrap.\n * @returns The content with each line wrapped with the prefix.\n */\nexport function wrapInMarkdownBlock(prefix: string, content: string) {\n // split content lines\n const lines = content.split('\\n')\n\n // add empty strings between every line\n const output = lines\n // add empty lines between each block\n .flatMap(line => [line, ''])\n // add the prefix to each line\n .map(line => `${prefix}${line}`)\n .join('\\n')\n\n return output.slice(0, output.length - 1)\n}\n\n/**\n * Identifies marks that need to be closed, based on the marks in the next node.\n */\nexport function findMarksToClose(currentMarks: Map<string, any>, nextNode: any): string[] {\n const marksToClose: string[] = []\n\n Array.from(currentMarks.keys()).forEach(markType => {\n if (!nextNode || !nextNode.marks || !nextNode.marks.map((mark: any) => mark.type).includes(markType)) {\n marksToClose.push(markType)\n }\n })\n return marksToClose\n}\n\n/**\n * Identifies marks that need to be opened (in current node but not active).\n */\nexport function findMarksToOpen(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n): Array<{ type: string; mark: any }> {\n const marksToOpen: Array<{ type: string; mark: any }> = []\n Array.from(currentMarks.entries()).forEach(([markType, mark]) => {\n if (!activeMarks.has(markType)) {\n marksToOpen.push({ type: markType, mark })\n }\n })\n return marksToOpen\n}\n\n/**\n * Determines which marks need to be closed at the end of the current text node.\n * This handles cases where marks end at node boundaries or when transitioning\n * to nodes with different mark sets.\n */\nexport function findMarksToCloseAtEnd(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n nextNode: any,\n markSetsEqual: (a: Map<string, any>, b: Map<string, any>) => boolean,\n): string[] {\n const isLastNode = !nextNode\n const nextNodeHasNoMarks = nextNode && nextNode.type === 'text' && (!nextNode.marks || nextNode.marks.length === 0)\n const nextNodeHasDifferentMarks =\n nextNode &&\n nextNode.type === 'text' &&\n nextNode.marks &&\n !markSetsEqual(currentMarks, new Map(nextNode.marks.map((mark: any) => [mark.type, mark])))\n\n const marksToCloseAtEnd: string[] = []\n if (isLastNode || nextNodeHasNoMarks || nextNodeHasDifferentMarks) {\n if (nextNode && nextNode.type === 'text' && nextNode.marks) {\n const nextMarks = new Map(nextNode.marks.map((mark: any) => [mark.type, mark]))\n Array.from(activeMarks.keys()).forEach(markType => {\n if (!nextMarks.has(markType)) {\n marksToCloseAtEnd.push(markType)\n }\n })\n } else if (isLastNode || nextNodeHasNoMarks) {\n // Close all active marks\n marksToCloseAtEnd.push(...Array.from(activeMarks.keys()))\n }\n }\n\n return marksToCloseAtEnd\n}\n\n/**\n * Closes active marks before rendering a non-text node.\n * Returns the closing markdown syntax and clears the active marks.\n */\nexport function closeMarksBeforeNode(\n activeMarks: Map<string, any>,\n getMarkClosing: (markType: string, mark: any) => string,\n): string {\n let beforeMarkdown = ''\n Array.from(activeMarks.keys())\n .reverse()\n .forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = getMarkClosing(markType, mark)\n if (closeMarkdown) {\n beforeMarkdown = closeMarkdown + beforeMarkdown\n }\n })\n activeMarks.clear()\n return beforeMarkdown\n}\n\n/**\n * Reopens marks after rendering a non-text node.\n * Returns the opening markdown syntax and updates the active marks.\n */\nexport function reopenMarksAfterNode(\n marksToReopen: Map<string, any>,\n activeMarks: Map<string, any>,\n getMarkOpening: (markType: string, mark: any) => string,\n): string {\n let afterMarkdown = ''\n Array.from(marksToReopen.entries()).forEach(([markType, mark]) => {\n const openMarkdown = getMarkOpening(markType, mark)\n if (openMarkdown) {\n afterMarkdown += openMarkdown\n }\n activeMarks.set(markType, mark)\n })\n return afterMarkdown\n}\n\n/**\n * Check if a markdown list item token is a task item and extract its state.\n *\n * @param item The list item token to check\n * @returns Object containing isTask flag, checked state, and indentation level\n *\n * @example\n * ```ts\n * isTaskItem({ raw: '- [ ] Task' }) // { isTask: true, checked: false, indentLevel: 0 }\n * isTaskItem({ raw: ' - [x] Done' }) // { isTask: true, checked: true, indentLevel: 2 }\n * isTaskItem({ raw: '- Regular' }) // { isTask: false, indentLevel: 0 }\n * ```\n */\nexport function isTaskItem(item: MarkdownToken): { isTask: boolean; checked?: boolean; indentLevel: number } {\n const raw = item.raw || item.text || ''\n\n // Match patterns like \"- [ ] \" or \" - [x] \"\n const match = raw.match(/^(\\s*)[-+*]\\s+\\[([ xX])\\]\\s+/)\n\n if (match) {\n return { isTask: true, checked: match[2].toLowerCase() === 'x', indentLevel: match[1].length }\n }\n return { isTask: false, indentLevel: 0 }\n}\n\n/**\n * Assumes the content type based off the content.\n * @param content The content to assume the type for.\n * @param contentType The content type that should be prioritized.\n */\nexport function assumeContentType(\n content: (Content | Fragment | Node) | string,\n contentType: ContentType,\n): ContentType {\n // if not a string, we assume it will be a json content object\n if (typeof content !== 'string') {\n return 'json'\n }\n\n // otherwise we let the content type be what it is\n return contentType\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -70,6 +70,8 @@ declare class MarkdownManager {
70
70
  * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.
71
71
  */
72
72
  private parseTokens;
73
+ private createImplicitEmptyParagraphsFromSpace;
74
+ private countParagraphSeparators;
73
75
  /**
74
76
  * Parse a single token into Tiptap JSON using the appropriate registered handler.
75
77
  */
@@ -121,7 +123,7 @@ declare class MarkdownManager {
121
123
  * This allows HTML within markdown to be parsed according to extension rules.
122
124
  */
123
125
  private parseHTMLToken;
124
- renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index?: number, level?: number): string;
126
+ renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index?: number, level?: number, meta?: Record<string, any>): string;
125
127
  /**
126
128
  * Render a node or an array of nodes. Parent type controls how children
127
129
  * are joined (which determines newline insertion between children).
package/dist/index.d.ts CHANGED
@@ -70,6 +70,8 @@ declare class MarkdownManager {
70
70
  * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.
71
71
  */
72
72
  private parseTokens;
73
+ private createImplicitEmptyParagraphsFromSpace;
74
+ private countParagraphSeparators;
73
75
  /**
74
76
  * Parse a single token into Tiptap JSON using the appropriate registered handler.
75
77
  */
@@ -121,7 +123,7 @@ declare class MarkdownManager {
121
123
  * This allows HTML within markdown to be parsed according to extension rules.
122
124
  */
123
125
  private parseHTMLToken;
124
- renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index?: number, level?: number): string;
126
+ renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index?: number, level?: number, meta?: Record<string, any>): string;
125
127
  /**
126
128
  * Render a node or an array of nodes. Parent type controls how children
127
129
  * are joined (which determines newline insertion between children).
package/dist/index.js CHANGED
@@ -289,7 +289,7 @@ var MarkdownManager = class {
289
289
  throw new Error("No marked instance available for parsing");
290
290
  }
291
291
  const tokens = this.markedInstance.lexer(markdown);
292
- const content = this.parseTokens(tokens);
292
+ const content = this.parseTokens(tokens, true);
293
293
  return {
294
294
  type: "doc",
295
295
  content
@@ -298,13 +298,48 @@ var MarkdownManager = class {
298
298
  /**
299
299
  * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.
300
300
  */
301
- parseTokens(tokens) {
302
- return tokens.map((token) => this.parseToken(token)).filter((parsed) => parsed !== null).flatMap((parsed) => Array.isArray(parsed) ? parsed : [parsed]);
301
+ parseTokens(tokens, parseImplicitEmptyParagraphs = false) {
302
+ const nonSpaceTokenIndexes = tokens.reduce((indexes, token, index) => {
303
+ if (token.type !== "space") {
304
+ indexes.push(index);
305
+ }
306
+ return indexes;
307
+ }, []);
308
+ let previousNonSpaceTokenIndex = -1;
309
+ let nextNonSpaceTokenPointer = 0;
310
+ return tokens.flatMap((token, index) => {
311
+ var _a;
312
+ while (nextNonSpaceTokenPointer < nonSpaceTokenIndexes.length && nonSpaceTokenIndexes[nextNonSpaceTokenPointer] < index) {
313
+ previousNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer];
314
+ nextNonSpaceTokenPointer += 1;
315
+ }
316
+ if (parseImplicitEmptyParagraphs && token.type === "space") {
317
+ const nextNonSpaceTokenIndex = (_a = nonSpaceTokenIndexes[nextNonSpaceTokenPointer]) != null ? _a : -1;
318
+ return this.createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex);
319
+ }
320
+ const parsed = this.parseToken(token, parseImplicitEmptyParagraphs);
321
+ if (parsed === null) {
322
+ return [];
323
+ }
324
+ return Array.isArray(parsed) ? parsed : [parsed];
325
+ });
326
+ }
327
+ createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex) {
328
+ const separatorCount = this.countParagraphSeparators(token.raw || "");
329
+ if (separatorCount === 0) {
330
+ return [];
331
+ }
332
+ const isBoundarySpace = previousNonSpaceTokenIndex === -1 || nextNonSpaceTokenIndex === -1;
333
+ const emptyParagraphCount = Math.max(separatorCount - (isBoundarySpace ? 0 : 1), 0);
334
+ return Array.from({ length: emptyParagraphCount }, () => ({ type: "paragraph", content: [] }));
335
+ }
336
+ countParagraphSeparators(raw) {
337
+ return (raw.replace(/\r\n/g, "\n").match(/\n\n/g) || []).length;
303
338
  }
304
339
  /**
305
340
  * Parse a single token into Tiptap JSON using the appropriate registered handler.
306
341
  */
307
- parseToken(token) {
342
+ parseToken(token, parseImplicitEmptyParagraphs = false) {
308
343
  if (!token.type) {
309
344
  return null;
310
345
  }
@@ -330,7 +365,7 @@ var MarkdownManager = class {
330
365
  this.lastParseResult = null;
331
366
  return toReturn;
332
367
  }
333
- return this.parseFallbackToken(token);
368
+ return this.parseFallbackToken(token, parseImplicitEmptyParagraphs);
334
369
  }
335
370
  /**
336
371
  * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists.
@@ -458,6 +493,7 @@ var MarkdownManager = class {
458
493
  return {
459
494
  parseInline: (tokens) => this.parseInlineTokens(tokens),
460
495
  parseChildren: (tokens) => this.parseTokens(tokens),
496
+ parseBlockChildren: (tokens) => this.parseTokens(tokens, true),
461
497
  createTextNode: (text, marks) => {
462
498
  const node = {
463
499
  type: "text",
@@ -616,7 +652,7 @@ var MarkdownManager = class {
616
652
  /**
617
653
  * Fallback parsing for common tokens when no specific handler is registered.
618
654
  */
619
- parseFallbackToken(token) {
655
+ parseFallbackToken(token, parseImplicitEmptyParagraphs = false) {
620
656
  switch (token.type) {
621
657
  case "paragraph":
622
658
  return {
@@ -640,7 +676,7 @@ var MarkdownManager = class {
640
676
  return null;
641
677
  default:
642
678
  if (token.tokens) {
643
- return this.parseTokens(token.tokens);
679
+ return this.parseTokens(token.tokens, parseImplicitEmptyParagraphs);
644
680
  }
645
681
  return null;
646
682
  }
@@ -687,7 +723,7 @@ var MarkdownManager = class {
687
723
  throw new Error(`Failed to parse HTML in markdown: ${error}`);
688
724
  }
689
725
  }
690
- renderNodeToMarkdown(node, parentNode, index = 0, level = 0) {
726
+ renderNodeToMarkdown(node, parentNode, index = 0, level = 0, meta = {}) {
691
727
  var _a;
692
728
  if (node.type === "text") {
693
729
  return node.text || "";
@@ -699,6 +735,7 @@ var MarkdownManager = class {
699
735
  if (!handler) {
700
736
  return "";
701
737
  }
738
+ const previousNode = Array.isArray(parentNode == null ? void 0 : parentNode.content) && index > 0 ? parentNode.content[index - 1] : void 0;
702
739
  const helpers = {
703
740
  renderChildren: (nodes, separator) => {
704
741
  const childLevel = handler.isIndenting ? level + 1 : level;
@@ -707,6 +744,10 @@ var MarkdownManager = class {
707
744
  }
708
745
  return this.renderNodes(nodes, node, separator || "", index, childLevel);
709
746
  },
747
+ renderChild: (childNode, childIndex) => {
748
+ const childLevel = handler.isIndenting ? level + 1 : level;
749
+ return this.renderNodeToMarkdown(childNode, node, childIndex, childLevel);
750
+ },
710
751
  indent: (content) => {
711
752
  return this.indentString + content;
712
753
  },
@@ -716,8 +757,10 @@ var MarkdownManager = class {
716
757
  index,
717
758
  level,
718
759
  parentType: parentNode == null ? void 0 : parentNode.type,
760
+ previousNode,
719
761
  meta: {
720
- parentAttrs: parentNode == null ? void 0 : parentNode.attrs
762
+ parentAttrs: parentNode == null ? void 0 : parentNode.attrs,
763
+ ...meta
721
764
  }
722
765
  };
723
766
  const rendered = ((_a = handler.renderMarkdown) == null ? void 0 : _a.call(handler, node, helpers, context)) || "";
@@ -844,6 +887,7 @@ var MarkdownManager = class {
844
887
  syntheticNode,
845
888
  {
846
889
  renderChildren: () => placeholder,
890
+ renderChild: () => placeholder,
847
891
  indent: (content) => content,
848
892
  wrapInBlock: (prefix, content) => prefix + content
849
893
  },
@@ -875,6 +919,7 @@ var MarkdownManager = class {
875
919
  syntheticNode,
876
920
  {
877
921
  renderChildren: () => placeholder,
922
+ renderChild: () => placeholder,
878
923
  indent: (content) => content,
879
924
  wrapInBlock: (prefix, content) => prefix + content
880
925
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/Extension.ts","../src/MarkdownManager.ts","../src/utils.ts"],"sourcesContent":["import {\n type InsertContentAtOptions as MarkdownInsertContentAtOptions,\n type InsertContentOptions as MarkdownInsertContentOptions,\n type SetContentOptions as MarkdownSetContentOptions,\n commands,\n Extension,\n} from '@tiptap/core'\nimport type { marked } from 'marked'\n\nimport MarkdownManager from './MarkdownManager.js'\nimport type { ContentType } from './types.js'\nimport { assumeContentType } from './utils.js'\n\ndeclare module '@tiptap/core' {\n interface Editor {\n /**\n * Get the content of the editor as markdown.\n */\n getMarkdown: () => string\n\n /**\n * The markdown manager instance.\n */\n markdown?: MarkdownManager\n }\n\n interface EditorOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface Storage {\n markdown: MarkdownExtensionStorage\n }\n\n interface InsertContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface InsertContentAtOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface SetContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n}\n\nexport type MarkdownExtensionOptions = {\n /**\n * Configure the indentation style and size for lists and code blocks.\n * - `style`: Choose between spaces or tabs. Default is 'space'.\n * - `size`: Number of spaces or tabs for indentation. Default is 2.\n */\n indentation?: { style?: 'space' | 'tab'; size?: number }\n\n /**\n * Use a custom version of `marked` for markdown parsing and serialization.\n * If not provided, the default `marked` instance will be used.\n */\n marked?: typeof marked\n\n /**\n * Options to pass to `marked.setOptions()`.\n * See the [marked documentation](https://marked.js.org/using_advanced#options) for available options.\n */\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n}\n\nexport type MarkdownExtensionStorage = {\n manager: MarkdownManager\n}\n\nexport const Markdown = Extension.create<MarkdownExtensionOptions, MarkdownExtensionStorage>({\n name: 'markdown',\n\n addOptions() {\n return {\n indentation: { style: 'space', size: 2 },\n marked: undefined,\n markedOptions: {},\n }\n },\n\n addCommands() {\n return {\n setContent: (content, options?: MarkdownSetContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.setContent(content, options)\n }\n\n const actualContentType = assumeContentType(content, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.setContent(content, options)\n }\n\n const mdContent = this.editor.markdown.parse(content as string)\n return commands.setContent(mdContent, options)\n },\n\n insertContent: (value, options?: MarkdownInsertContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContent(value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContent(value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContent(mdContent, options)\n },\n\n insertContentAt: (position, value, options?: MarkdownInsertContentAtOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContentAt(position, value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContentAt(position, value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContentAt(position, mdContent, options)\n },\n }\n },\n\n addStorage() {\n return {\n manager: new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: [],\n }),\n }\n },\n\n onBeforeCreate() {\n if (this.editor.markdown) {\n console.error(\n '[tiptap][markdown]: There is already a `markdown` property on the editor instance. This might lead to unexpected behavior.',\n )\n return\n }\n\n this.storage.manager = new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: this.editor.extensionManager.baseExtensions,\n })\n\n this.editor.markdown = this.storage.manager\n\n // add a `getMarkdown()` method to the editor\n this.editor.getMarkdown = () => {\n return this.storage.manager.serialize(this.editor.getJSON())\n }\n\n if (!this.editor.options.contentType) {\n return\n }\n\n const assumedType = assumeContentType(this.editor.options.content, this.editor.options.contentType)\n if (assumedType !== 'markdown') {\n return\n }\n\n if (!this.editor.markdown) {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the Markdown extension is not added to the editor. Please add the Markdown extension to use this feature.',\n )\n }\n\n if (this.editor.options.content === undefined || typeof this.editor.options.content !== 'string') {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the initial content is not a string. Please provide the initial content as a markdown string.',\n )\n }\n\n const json = this.editor.markdown.parse(this.editor.options.content as string)\n this.editor.options.content = json\n },\n})\n","import {\n type AnyExtension,\n type ExtendableConfig,\n type JSONContent,\n type MarkdownExtensionSpec,\n type MarkdownParseHelpers,\n type MarkdownParseResult,\n type MarkdownRendererHelpers,\n type MarkdownToken,\n type MarkdownTokenizer,\n type RenderContext,\n flattenExtensions,\n generateJSON,\n getExtensionField,\n} from '@tiptap/core'\nimport { type Lexer, type Token, type TokenizerExtension, marked } from 'marked'\n\nimport {\n closeMarksBeforeNode,\n findMarksToClose,\n findMarksToCloseAtEnd,\n findMarksToOpen,\n isTaskItem,\n reopenMarksAfterNode,\n wrapInMarkdownBlock,\n} from './utils.js'\n\nexport class MarkdownManager {\n private markedInstance: typeof marked\n private lexer: Lexer\n private registry: Map<string, MarkdownExtensionSpec[]>\n private nodeTypeRegistry: Map<string, MarkdownExtensionSpec[]>\n private indentStyle: 'space' | 'tab'\n private indentSize: number\n private baseExtensions: AnyExtension[] = []\n private extensions: AnyExtension[] = []\n\n /**\n * Create a MarkdownManager.\n * @param options.marked Optional marked instance to use (injected).\n * @param options.markedOptions Optional options to pass to marked.setOptions\n * @param options.indentation Indentation settings (style and size).\n * @param options.extensions An array of Tiptap extensions to register for markdown parsing and rendering.\n */\n constructor(options?: {\n marked?: typeof marked\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n indentation?: { style?: 'space' | 'tab'; size?: number }\n extensions: AnyExtension[]\n }) {\n this.markedInstance = options?.marked ?? marked\n this.lexer = new this.markedInstance.Lexer()\n this.indentStyle = options?.indentation?.style ?? 'space'\n this.indentSize = options?.indentation?.size ?? 2\n this.baseExtensions = options?.extensions || []\n\n if (options?.markedOptions && typeof this.markedInstance.setOptions === 'function') {\n this.markedInstance.setOptions(options.markedOptions)\n }\n\n this.registry = new Map()\n this.nodeTypeRegistry = new Map()\n\n // If extensions were provided, register them now\n if (options?.extensions) {\n this.baseExtensions = options.extensions\n const flattened = flattenExtensions(options.extensions)\n flattened.forEach(ext => this.registerExtension(ext, false))\n }\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include all tokenizers\n }\n\n /** Returns the underlying marked instance. */\n get instance(): typeof marked {\n return this.markedInstance\n }\n\n /** Returns the correct indentCharacter (space or tab) */\n get indentCharacter(): string {\n return this.indentStyle === 'space' ? ' ' : '\\t'\n }\n\n /** Returns the correct indentString repeated X times */\n get indentString(): string {\n return this.indentCharacter.repeat(this.indentSize)\n }\n\n /** Helper to quickly check whether a marked instance is available. */\n hasMarked(): boolean {\n return !!this.markedInstance\n }\n\n /**\n * Register a Tiptap extension (Node/Mark/Extension). This will read\n * `markdownName`, `parseMarkdown`, `renderMarkdown` and `priority` from the\n * extension config (using the same resolution used across the codebase).\n */\n registerExtension(extension: AnyExtension, recreateLexer: boolean = true): void {\n // Keep track of all extensions for HTML parsing\n this.extensions.push(extension)\n\n const name = extension.name\n const tokenName =\n (getExtensionField(extension, 'markdownTokenName') as ExtendableConfig['markdownTokenName']) || name\n const parseMarkdown = getExtensionField(extension, 'parseMarkdown') as ExtendableConfig['parseMarkdown'] | undefined\n const renderMarkdown = getExtensionField(extension, 'renderMarkdown') as\n | ExtendableConfig['renderMarkdown']\n | undefined\n const tokenizer = getExtensionField(extension, 'markdownTokenizer') as\n | ExtendableConfig['markdownTokenizer']\n | undefined\n\n // Read the `markdown` object from the extension config. This allows\n // extensions to provide `markdown: { name?, parseName?, renderName?, parse?, render?, match? }`.\n const markdownCfg = (getExtensionField(extension, 'markdownOptions') ?? null) as ExtendableConfig['markdownOptions']\n const isIndenting = markdownCfg?.indentsContent ?? false\n\n const spec: MarkdownExtensionSpec = {\n tokenName,\n nodeName: name,\n parseMarkdown,\n renderMarkdown,\n isIndenting,\n tokenizer,\n }\n\n // Add to parse registry using parseName\n if (tokenName && parseMarkdown) {\n const parseExisting = this.registry.get(tokenName) || []\n parseExisting.push(spec)\n this.registry.set(tokenName, parseExisting)\n }\n\n // Add to render registry using renderName (node type)\n if (renderMarkdown) {\n const renderExisting = this.nodeTypeRegistry.get(name) || []\n renderExisting.push(spec)\n this.nodeTypeRegistry.set(name, renderExisting)\n }\n\n // Register custom tokenizer with marked.js\n if (tokenizer && this.hasMarked()) {\n this.registerTokenizer(tokenizer)\n\n if (recreateLexer) {\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include new tokenizer\n }\n }\n }\n\n /**\n * Register a custom tokenizer with marked.js for parsing non-standard markdown syntax.\n */\n private registerTokenizer(tokenizer: MarkdownTokenizer): void {\n if (!this.hasMarked()) {\n return\n }\n\n const { name, start, level = 'inline', tokenize } = tokenizer\n\n // Helper functions that use a fresh lexer instance with all registered extensions\n const tokenizeInline = (src: string) => {\n return this.lexer.inlineTokens(src)\n }\n\n const tokenizeBlock = (src: string) => {\n return this.lexer.blockTokens(src)\n }\n\n const helper = {\n inlineTokens: tokenizeInline,\n blockTokens: tokenizeBlock,\n }\n\n let startCb: (src: string) => number\n\n if (!start) {\n startCb = (src: string) => {\n // For other tokenizers, try to find a match and return its position\n const result = tokenize(src, [], helper)\n if (result && result.raw) {\n const index = src.indexOf(result.raw)\n return index\n }\n return -1\n }\n } else {\n startCb = typeof start === 'function' ? start : (src: string) => src.indexOf(start)\n }\n\n // Create marked.js extension with proper types\n const markedExtension: TokenizerExtension = {\n name,\n level,\n start: startCb,\n tokenizer: (src, tokens) => {\n const result = tokenize(src, tokens, helper)\n\n if (result && result.type) {\n return {\n ...result,\n type: result.type || name,\n raw: result.raw || '',\n tokens: (result.tokens || []) as Token[],\n }\n }\n\n return undefined\n },\n childTokens: [],\n }\n\n // Register with marked.js - use extensions array to control priority\n this.markedInstance.use({\n extensions: [markedExtension],\n })\n }\n\n /** Get registered handlers for a token type and try each until one succeeds. */\n private getHandlersForToken(type: string): MarkdownExtensionSpec[] {\n try {\n return this.registry.get(type) || []\n } catch {\n return []\n }\n }\n\n /** Get the first handler for a token type (for backwards compatibility). */\n private getHandlerForToken(type: string): MarkdownExtensionSpec | undefined {\n // First try the markdown token registry (for parsing)\n const markdownHandlers = this.getHandlersForToken(type)\n if (markdownHandlers.length > 0) {\n return markdownHandlers[0]\n }\n\n // Then try the node type registry (for rendering)\n const nodeTypeHandlers = this.getHandlersForNodeType(type)\n return nodeTypeHandlers.length > 0 ? nodeTypeHandlers[0] : undefined\n }\n\n /** Get registered handlers for a node type (for rendering). */\n private getHandlersForNodeType(type: string): MarkdownExtensionSpec[] {\n try {\n return this.nodeTypeRegistry.get(type) || []\n } catch {\n return []\n }\n }\n\n /**\n * Serialize a ProseMirror-like JSON document (or node array) to a Markdown string\n * using registered renderers and fallback renderers.\n */\n serialize(docOrContent: JSONContent): string {\n if (!docOrContent) {\n return ''\n }\n\n const result = this.renderNodes(docOrContent, docOrContent)\n // Return empty string if result is only whitespace entities or non-breaking spaces\n return this.isEmptyOutput(result) ? '' : result\n }\n\n /**\n * Check if the markdown output represents an empty document.\n * Empty documents may contain only &nbsp; entities or non-breaking space characters\n * which are used by the Paragraph extension to preserve blank lines.\n */\n private isEmptyOutput(markdown: string): boolean {\n if (!markdown || markdown.trim() === '') {\n return true\n }\n\n // Check if the output is only &nbsp; entities or non-breaking space characters\n const cleanedOutput = markdown\n .replace(/&nbsp;/g, '')\n .replace(/\\u00A0/g, '')\n .trim()\n return cleanedOutput === ''\n }\n\n /**\n * Parse markdown string into Tiptap JSON document using registered extension handlers.\n */\n parse(markdown: string): JSONContent {\n if (!this.hasMarked()) {\n throw new Error('No marked instance available for parsing')\n }\n\n // Use marked to tokenize the markdown\n const tokens = this.markedInstance.lexer(markdown)\n\n // Convert tokens to Tiptap JSON\n const content = this.parseTokens(tokens)\n\n // Return a document node containing the parsed content\n return {\n type: 'doc',\n content,\n }\n }\n\n /**\n * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.\n */\n private parseTokens(tokens: MarkdownToken[]): JSONContent[] {\n return tokens\n .map(token => this.parseToken(token))\n .filter((parsed): parsed is JSONContent | JSONContent[] => parsed !== null)\n .flatMap(parsed => (Array.isArray(parsed) ? parsed : [parsed]))\n }\n\n /**\n * Parse a single token into Tiptap JSON using the appropriate registered handler.\n */\n private parseToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n // Special handling for 'list' tokens that may contain mixed bullet/task items\n if (token.type === 'list') {\n return this.parseListToken(token)\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token)\n }\n\n private lastParseResult: JSONContent | JSONContent[] | null = null\n\n /**\n * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists.\n * This ensures that consecutive task items and bullet items are grouped and parsed as separate list nodes.\n *\n * @param token The list token to parse\n * @returns Array of parsed list nodes, or null if parsing fails\n */\n private parseListToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.items || token.items.length === 0) {\n // No items, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n const hasTask = token.items.some(item => isTaskItem(item).isTask)\n const hasNonTask = token.items.some(item => !isTaskItem(item).isTask)\n\n if (!hasTask || !hasNonTask || this.getHandlersForToken('taskList').length === 0) {\n // Not mixed or no taskList extension, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n // Mixed list with taskList extension available: split into separate lists\n type TaskListItemToken = MarkdownToken & { type: 'taskItem'; checked?: boolean; indentLevel?: number }\n const groups: { type: 'list' | 'taskList'; items: (MarkdownToken | TaskListItemToken)[] }[] = []\n let currentGroup: (MarkdownToken | TaskListItemToken)[] = []\n let currentType: 'list' | 'taskList' | null = null\n\n for (let i = 0; i < token.items.length; i += 1) {\n const item = token.items[i]\n const { isTask, checked, indentLevel } = isTaskItem(item)\n let processedItem = item\n\n if (isTask) {\n // Transform list_item into taskItem token\n const raw = item.raw || item.text || ''\n\n // Split raw content by lines to separate main content from nested\n const lines = raw.split('\\n')\n\n // Extract main content from the first line\n const firstLineMatch = lines[0].match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+(.*)$/)\n const mainContent = firstLineMatch ? firstLineMatch[2] : ''\n\n // Parse nested content from remaining lines\n let nestedTokens: MarkdownToken[] = []\n if (lines.length > 1) {\n // Join all lines after the first\n const nestedRaw = lines.slice(1).join('\\n')\n\n // Only parse if there's actual content\n if (nestedRaw.trim()) {\n // Find minimum indentation of non-empty lines\n const nestedLines = lines.slice(1)\n const nonEmptyLines = nestedLines.filter(line => line.trim())\n if (nonEmptyLines.length > 0) {\n const minIndent = Math.min(...nonEmptyLines.map(line => line.length - line.trimStart().length))\n // Remove common indentation while preserving structure\n const trimmedLines = nestedLines.map(line => {\n if (!line.trim()) {\n return '' // Keep empty lines\n }\n return line.slice(minIndent)\n })\n const nestedContent = trimmedLines.join('\\n').trim()\n // Use the lexer to parse nested content\n if (nestedContent) {\n // Use the full lexer pipeline to ensure inline tokens are populated\n nestedTokens = this.markedInstance.lexer(`${nestedContent}\\n`)\n }\n }\n }\n }\n\n processedItem = {\n type: 'taskItem',\n raw: '',\n mainContent,\n indentLevel,\n checked: checked ?? false,\n text: mainContent,\n tokens: this.lexer.inlineTokens(mainContent),\n nestedTokens,\n }\n }\n\n const itemType: 'list' | 'taskList' = isTask ? 'taskList' : 'list'\n\n if (currentType !== itemType) {\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n currentGroup = [processedItem]\n currentType = itemType\n } else {\n currentGroup.push(processedItem)\n }\n }\n\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n\n // Parse each group as a separate token\n const results: JSONContent[] = []\n for (let i = 0; i < groups.length; i += 1) {\n const group = groups[i]\n const subToken = { ...token, type: group.type, items: group.items }\n const parsed = this.parseToken(subToken)\n if (parsed) {\n if (Array.isArray(parsed)) {\n results.push(...parsed)\n } else {\n results.push(parsed)\n }\n }\n }\n\n return results.length > 0 ? results : null\n }\n\n /**\n * Parse a token using registered handlers (extracted for reuse).\n */\n private parseTokenWithHandlers(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token)\n }\n\n /**\n * Creates helper functions for parsing markdown tokens.\n * @returns An object containing helper functions for parsing.\n */\n private createParseHelpers(): MarkdownParseHelpers {\n return {\n parseInline: (tokens: MarkdownToken[]) => this.parseInlineTokens(tokens),\n parseChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens),\n createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => {\n const node = {\n type: 'text',\n text,\n marks: marks || undefined,\n }\n\n return node\n },\n createNode: (type: string, attrs?: any, content?: JSONContent[]) => {\n const node = {\n type,\n attrs: attrs || undefined,\n content: content || undefined,\n }\n\n if (!attrs || Object.keys(attrs).length === 0) {\n delete node.attrs\n }\n\n return node\n },\n applyMark: (markType: string, content: JSONContent[], attrs?: any) => ({\n mark: markType,\n content,\n attrs: attrs && Object.keys(attrs).length > 0 ? attrs : undefined,\n }),\n }\n }\n\n /**\n * Escape special regex characters in a string.\n */\n private escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n\n /**\n * Parse inline tokens (bold, italic, links, etc.) into text nodes with marks.\n * This is the complex part that handles mark nesting and boundaries.\n */\n private parseInlineTokens(tokens: MarkdownToken[]): JSONContent[] {\n const result: JSONContent[] = []\n\n // Process tokens sequentially using an index so we can lookahead and\n // merge split inline HTML fragments like: text / <em> / inner / </em> / text\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i]\n\n if (token.type === 'text') {\n // Create text node\n result.push({\n type: 'text',\n text: token.text || '',\n })\n } else if (token.type === 'html') {\n // Handle possible split inline HTML by attempting to detect an\n // opening tag and searching forward for a matching closing tag.\n const raw = (token.raw ?? token.text ?? '').toString()\n\n // Quick checks for opening vs. closing tag\n const isClosing = /^<\\/[\\s]*[\\w-]+/i.test(raw)\n const openMatch = raw.match(/^<[\\s]*([\\w-]+)(\\s|>|\\/|$)/i)\n\n if (!isClosing && openMatch && !/\\/>$/.test(raw)) {\n // Try to find the corresponding closing html token for this tag\n const tagName = openMatch[1]\n const escapedTagName = this.escapeRegex(tagName)\n const closingRegex = new RegExp(`^<\\\\/\\\\s*${escapedTagName}\\\\b`, 'i')\n let foundIndex = -1\n\n // Collect intermediate raw parts to reconstruct full HTML fragment\n const parts: string[] = [raw]\n for (let j = i + 1; j < tokens.length; j += 1) {\n const t = tokens[j]\n const tRaw = (t.raw ?? t.text ?? '').toString()\n parts.push(tRaw)\n if (t.type === 'html' && closingRegex.test(tRaw)) {\n foundIndex = j\n break\n }\n }\n\n if (foundIndex !== -1) {\n // Merge opening + inner + closing into one html fragment and parse\n const mergedRaw = parts.join('')\n const mergedToken = {\n type: 'html',\n raw: mergedRaw,\n text: mergedRaw,\n block: false,\n } as unknown as MarkdownToken\n\n const parsed = this.parseHTMLToken(mergedToken)\n if (parsed) {\n const normalized = this.normalizeParseResult(parsed as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n\n // Advance i to the closing token\n i = foundIndex\n continue\n }\n }\n\n // Fallback: single html token parse\n const parsedSingle = this.parseHTMLToken(token)\n if (parsedSingle) {\n const normalized = this.normalizeParseResult(parsedSingle as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.type) {\n // Handle inline marks (bold, italic, etc.)\n const markHandler = this.getHandlerForToken(token.type)\n if (markHandler && markHandler.parseMarkdown) {\n const helpers = this.createParseHelpers()\n const parsed = markHandler.parseMarkdown(token, helpers)\n\n if (this.isMarkResult(parsed)) {\n // This is a mark result - apply the mark to the content\n const markedContent = this.applyMarkToContent(parsed.mark, parsed.content, parsed.attrs)\n result.push(...markedContent)\n } else {\n // Regular inline node\n const normalized = this.normalizeParseResult(parsed)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.tokens) {\n // Fallback: try to parse children if they exist\n result.push(...this.parseInlineTokens(token.tokens))\n }\n }\n }\n\n return result\n }\n\n /**\n * Apply a mark to content nodes.\n */\n private applyMarkToContent(markType: string, content: JSONContent[], attrs?: any): JSONContent[] {\n return content.map(node => {\n if (node.type === 'text') {\n // Add the mark to existing marks or create new marks array\n const existingMarks = node.marks || []\n const newMark = attrs ? { type: markType, attrs } : { type: markType }\n return {\n ...node,\n marks: [...existingMarks, newMark],\n }\n }\n\n // For non-text nodes, recursively apply to content\n return {\n ...node,\n content: node.content ? this.applyMarkToContent(markType, node.content, attrs) : undefined,\n }\n })\n } /**\n * Check if a parse result represents a mark to be applied.\n */\n private isMarkResult(result: any): result is { mark: string; content: JSONContent[]; attrs?: any } {\n return result && typeof result === 'object' && 'mark' in result\n }\n\n /**\n * Normalize parse results to ensure they're valid JSONContent.\n */\n private normalizeParseResult(result: MarkdownParseResult): JSONContent | JSONContent[] | null {\n if (!result) {\n return null\n }\n\n if (this.isMarkResult(result)) {\n // This shouldn't happen at the top level, but handle it gracefully\n return result.content\n }\n\n return result as JSONContent | JSONContent[]\n }\n\n /**\n * Fallback parsing for common tokens when no specific handler is registered.\n */\n private parseFallbackToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n switch (token.type) {\n case 'paragraph':\n return {\n type: 'paragraph',\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'heading':\n return {\n type: 'heading',\n attrs: { level: token.depth || 1 },\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'text':\n return {\n type: 'text',\n text: token.text || '',\n }\n\n case 'html':\n // Parse HTML using extensions' parseHTML methods\n return this.parseHTMLToken(token)\n\n case 'space':\n return null\n\n default:\n // Unknown token type - try to parse children if they exist\n if (token.tokens) {\n return this.parseTokens(token.tokens)\n }\n return null\n }\n }\n\n /**\n * Parse HTML tokens using extensions' parseHTML methods.\n * This allows HTML within markdown to be parsed according to extension rules.\n */\n private parseHTMLToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n const html = token.text || token.raw || ''\n\n if (!html.trim()) {\n return null\n }\n\n // Check if we're in a server-side environment (no window object)\n // If so, fall back to treating HTML as plain text to avoid runtime errors\n if (typeof window === 'undefined') {\n // For block-level HTML, wrap in a paragraph to maintain valid document structure\n if (token.block) {\n return {\n type: 'paragraph',\n content: [\n {\n type: 'text',\n text: html,\n },\n ],\n }\n }\n // For inline HTML, return plain text\n return {\n type: 'text',\n text: html,\n }\n }\n\n // Use generateJSON to parse the HTML using extensions' parseHTML rules\n try {\n const parsed = generateJSON(html, this.baseExtensions)\n\n // If the result is a doc node, extract its content\n if (parsed.type === 'doc' && parsed.content) {\n // For block-level HTML, return the content array\n if (token.block) {\n return parsed.content\n }\n\n // For inline HTML, we need to flatten the content appropriately\n // If there's only one paragraph with content, unwrap it\n if (parsed.content.length === 1 && parsed.content[0].type === 'paragraph' && parsed.content[0].content) {\n return parsed.content[0].content\n }\n\n return parsed.content\n }\n\n return parsed as JSONContent\n } catch (error) {\n throw new Error(`Failed to parse HTML in markdown: ${error}`)\n }\n }\n\n renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index = 0, level = 0): string {\n // if node is a text node, we simply return it's text content\n // marks are handled at the array level in renderNodesWithMarkBoundaries\n if (node.type === 'text') {\n return node.text || ''\n }\n\n if (!node.type) {\n return ''\n }\n\n const handler = this.getHandlerForToken(node.type)\n if (!handler) {\n return ''\n }\n\n const helpers: MarkdownRendererHelpers = {\n renderChildren: (nodes, separator) => {\n const childLevel = handler.isIndenting ? level + 1 : level\n\n if (!Array.isArray(nodes) && (nodes as any).content) {\n return this.renderNodes((nodes as any).content as JSONContent[], node, separator || '', index, childLevel)\n }\n\n return this.renderNodes(nodes, node, separator || '', index, childLevel)\n },\n indent: content => {\n return this.indentString + content\n },\n wrapInBlock: wrapInMarkdownBlock,\n }\n\n const context: RenderContext = {\n index,\n level,\n parentType: parentNode?.type,\n meta: {\n parentAttrs: parentNode?.attrs,\n },\n }\n\n // First render the node itself (this will render children recursively)\n const rendered = handler.renderMarkdown?.(node, helpers, context) || ''\n\n return rendered\n }\n\n /**\n * Render a node or an array of nodes. Parent type controls how children\n * are joined (which determines newline insertion between children).\n */\n renderNodes(\n nodeOrNodes: JSONContent | JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n index = 0,\n level = 0,\n ): string {\n // if we have just one node, call renderNodeToMarkdown directly\n if (!Array.isArray(nodeOrNodes)) {\n if (!nodeOrNodes.type) {\n return ''\n }\n\n return this.renderNodeToMarkdown(nodeOrNodes, parentNode, index, level)\n }\n\n return this.renderNodesWithMarkBoundaries(nodeOrNodes, parentNode, separator, level)\n }\n\n /**\n * Render an array of nodes while properly tracking mark boundaries.\n * This handles cases where marks span across multiple text nodes.\n */\n private renderNodesWithMarkBoundaries(\n nodes: JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n level = 0,\n ): string {\n const result: string[] = []\n const activeMarks: Map<string, any> = new Map()\n nodes.forEach((node, i) => {\n // Lookahead to the next node to determine if marks need to be closed\n const nextNode = i < nodes.length - 1 ? nodes[i + 1] : null\n\n if (!node.type) {\n return\n }\n\n if (node.type === 'text') {\n let textContent = node.text || ''\n const currentMarks = new Map((node.marks || []).map(mark => [mark.type, mark]))\n\n // Find marks that need to be closed and opened\n const marksToOpen = findMarksToOpen(activeMarks, currentMarks)\n const marksToClose = findMarksToClose(currentMarks, nextNode)\n\n let middleTrailingWhitespace = ''\n\n if (marksToClose.length > 0) {\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n const middleTrailingMatch = textContent.match(/(\\s+)$/)\n if (middleTrailingMatch) {\n middleTrailingWhitespace = middleTrailingMatch[1]\n textContent = textContent.slice(0, -middleTrailingWhitespace.length)\n }\n }\n // Close marks that are ending here\n marksToClose.forEach(markType => {\n const mark = currentMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n // deleting closed marks from active marks\n if (activeMarks.has(markType)) {\n activeMarks.delete(markType)\n }\n })\n\n // Open new marks (should be at the beginning)\n // Extract leading whitespace before opening marks to prevent invalid markdown like \"** text**\"\n let leadingWhitespace = ''\n if (marksToOpen.length > 0) {\n const leadingMatch = textContent.match(/^(\\s+)/)\n if (leadingMatch) {\n leadingWhitespace = leadingMatch[1]\n textContent = textContent.slice(leadingWhitespace.length)\n }\n }\n\n marksToOpen.forEach(({ type, mark }) => {\n const openMarkdown = this.getMarkOpening(type, mark)\n if (openMarkdown) {\n textContent = openMarkdown + textContent\n }\n if (!marksToClose.includes(type)) {\n activeMarks.set(type, mark)\n }\n })\n\n // Add leading whitespace before the mark opening\n textContent = leadingWhitespace + textContent\n\n // Close marks at the end of this node if needed\n const marksToCloseAtEnd = findMarksToCloseAtEnd(\n activeMarks,\n currentMarks,\n nextNode,\n this.markSetsEqual.bind(this),\n )\n\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n let trailingWhitespace = ''\n if (marksToCloseAtEnd.length > 0) {\n const trailingMatch = textContent.match(/(\\s+)$/)\n if (trailingMatch) {\n trailingWhitespace = trailingMatch[1]\n textContent = textContent.slice(0, -trailingWhitespace.length)\n }\n }\n\n marksToCloseAtEnd.forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n activeMarks.delete(markType)\n })\n\n // Add trailing whitespace after the mark closing\n textContent += trailingWhitespace\n textContent += middleTrailingWhitespace\n\n result.push(textContent)\n } else {\n // For non-text nodes, close all active marks before rendering, then reopen after\n const marksToReopen = new Map(activeMarks)\n\n // Close all marks before the node\n const beforeMarkdown = closeMarksBeforeNode(activeMarks, this.getMarkClosing.bind(this))\n\n // Render the node\n const nodeContent = this.renderNodeToMarkdown(node, parentNode, i, level)\n\n // Reopen marks after the node, but NOT after a hard break\n // Hard breaks should terminate marks (they create a line break where marks don't continue)\n const afterMarkdown =\n node.type === 'hardBreak'\n ? ''\n : reopenMarksAfterNode(marksToReopen, activeMarks, this.getMarkOpening.bind(this))\n\n result.push(beforeMarkdown + nodeContent + afterMarkdown)\n }\n })\n\n return result.join(separator)\n }\n\n /**\n * Get the opening markdown syntax for a mark type.\n */\n private getMarkOpening(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n // For most marks, we can extract the opening syntax by rendering a simple case\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the opening part (everything before placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n return placeholderIndex >= 0 ? rendered.substring(0, placeholderIndex) : ''\n } catch (err) {\n throw new Error(`Failed to get mark opening for ${markType}: ${err}`)\n }\n }\n\n /**\n * Get the closing markdown syntax for a mark type.\n */\n private getMarkClosing(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the closing part (everything after placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n const placeholderEnd = placeholderIndex + placeholder.length\n return placeholderIndex >= 0 ? rendered.substring(placeholderEnd) : ''\n } catch (err) {\n throw new Error(`Failed to get mark closing for ${markType}: ${err}`)\n }\n }\n\n /**\n * Check if two mark sets are equal.\n */\n private markSetsEqual(marks1: Map<string, any>, marks2: Map<string, any>): boolean {\n if (marks1.size !== marks2.size) {\n return false\n }\n\n return Array.from(marks1.keys()).every(type => marks2.has(type))\n }\n}\n\nexport default MarkdownManager\n","import type { Content, MarkdownToken } from '@tiptap/core'\nimport type { Fragment, Node } from '@tiptap/pm/model'\n\nimport type { ContentType } from './types.js'\n\n/**\n * Wraps each line of the content with the given prefix.\n * @param prefix The prefix to wrap each line with.\n * @param content The content to wrap.\n * @returns The content with each line wrapped with the prefix.\n */\nexport function wrapInMarkdownBlock(prefix: string, content: string) {\n // split content lines\n const lines = content.split('\\n')\n\n // add empty strings between every line\n const output = lines\n // add empty lines between each block\n .flatMap(line => [line, ''])\n // add the prefix to each line\n .map(line => `${prefix}${line}`)\n .join('\\n')\n\n return output.slice(0, output.length - 1)\n}\n\n/**\n * Identifies marks that need to be closed, based on the marks in the next node.\n */\nexport function findMarksToClose(currentMarks: Map<string, any>, nextNode: any): string[] {\n const marksToClose: string[] = []\n\n Array.from(currentMarks.keys()).forEach(markType => {\n if (!nextNode || !nextNode.marks || !nextNode.marks.map((mark: any) => mark.type).includes(markType)) {\n marksToClose.push(markType)\n }\n })\n return marksToClose\n}\n\n/**\n * Identifies marks that need to be opened (in current node but not active).\n */\nexport function findMarksToOpen(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n): Array<{ type: string; mark: any }> {\n const marksToOpen: Array<{ type: string; mark: any }> = []\n Array.from(currentMarks.entries()).forEach(([markType, mark]) => {\n if (!activeMarks.has(markType)) {\n marksToOpen.push({ type: markType, mark })\n }\n })\n return marksToOpen\n}\n\n/**\n * Determines which marks need to be closed at the end of the current text node.\n * This handles cases where marks end at node boundaries or when transitioning\n * to nodes with different mark sets.\n */\nexport function findMarksToCloseAtEnd(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n nextNode: any,\n markSetsEqual: (a: Map<string, any>, b: Map<string, any>) => boolean,\n): string[] {\n const isLastNode = !nextNode\n const nextNodeHasNoMarks = nextNode && nextNode.type === 'text' && (!nextNode.marks || nextNode.marks.length === 0)\n const nextNodeHasDifferentMarks =\n nextNode &&\n nextNode.type === 'text' &&\n nextNode.marks &&\n !markSetsEqual(currentMarks, new Map(nextNode.marks.map((mark: any) => [mark.type, mark])))\n\n const marksToCloseAtEnd: string[] = []\n if (isLastNode || nextNodeHasNoMarks || nextNodeHasDifferentMarks) {\n if (nextNode && nextNode.type === 'text' && nextNode.marks) {\n const nextMarks = new Map(nextNode.marks.map((mark: any) => [mark.type, mark]))\n Array.from(activeMarks.keys()).forEach(markType => {\n if (!nextMarks.has(markType)) {\n marksToCloseAtEnd.push(markType)\n }\n })\n } else if (isLastNode || nextNodeHasNoMarks) {\n // Close all active marks\n marksToCloseAtEnd.push(...Array.from(activeMarks.keys()))\n }\n }\n\n return marksToCloseAtEnd\n}\n\n/**\n * Closes active marks before rendering a non-text node.\n * Returns the closing markdown syntax and clears the active marks.\n */\nexport function closeMarksBeforeNode(\n activeMarks: Map<string, any>,\n getMarkClosing: (markType: string, mark: any) => string,\n): string {\n let beforeMarkdown = ''\n Array.from(activeMarks.keys())\n .reverse()\n .forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = getMarkClosing(markType, mark)\n if (closeMarkdown) {\n beforeMarkdown = closeMarkdown + beforeMarkdown\n }\n })\n activeMarks.clear()\n return beforeMarkdown\n}\n\n/**\n * Reopens marks after rendering a non-text node.\n * Returns the opening markdown syntax and updates the active marks.\n */\nexport function reopenMarksAfterNode(\n marksToReopen: Map<string, any>,\n activeMarks: Map<string, any>,\n getMarkOpening: (markType: string, mark: any) => string,\n): string {\n let afterMarkdown = ''\n Array.from(marksToReopen.entries()).forEach(([markType, mark]) => {\n const openMarkdown = getMarkOpening(markType, mark)\n if (openMarkdown) {\n afterMarkdown += openMarkdown\n }\n activeMarks.set(markType, mark)\n })\n return afterMarkdown\n}\n\n/**\n * Check if a markdown list item token is a task item and extract its state.\n *\n * @param item The list item token to check\n * @returns Object containing isTask flag, checked state, and indentation level\n *\n * @example\n * ```ts\n * isTaskItem({ raw: '- [ ] Task' }) // { isTask: true, checked: false, indentLevel: 0 }\n * isTaskItem({ raw: ' - [x] Done' }) // { isTask: true, checked: true, indentLevel: 2 }\n * isTaskItem({ raw: '- Regular' }) // { isTask: false, indentLevel: 0 }\n * ```\n */\nexport function isTaskItem(item: MarkdownToken): { isTask: boolean; checked?: boolean; indentLevel: number } {\n const raw = item.raw || item.text || ''\n\n // Match patterns like \"- [ ] \" or \" - [x] \"\n const match = raw.match(/^(\\s*)[-+*]\\s+\\[([ xX])\\]\\s+/)\n\n if (match) {\n return { isTask: true, checked: match[2].toLowerCase() === 'x', indentLevel: match[1].length }\n }\n return { isTask: false, indentLevel: 0 }\n}\n\n/**\n * Assumes the content type based off the content.\n * @param content The content to assume the type for.\n * @param contentType The content type that should be prioritized.\n */\nexport function assumeContentType(\n content: (Content | Fragment | Node) | string,\n contentType: ContentType,\n): ContentType {\n // if not a string, we assume it will be a json content object\n if (typeof content !== 'string') {\n return 'json'\n }\n\n // otherwise we let the content type be what it is\n return contentType\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/Extension.ts","../src/MarkdownManager.ts","../src/utils.ts"],"sourcesContent":["import {\n type InsertContentAtOptions as MarkdownInsertContentAtOptions,\n type InsertContentOptions as MarkdownInsertContentOptions,\n type SetContentOptions as MarkdownSetContentOptions,\n commands,\n Extension,\n} from '@tiptap/core'\nimport type { marked } from 'marked'\n\nimport MarkdownManager from './MarkdownManager.js'\nimport type { ContentType } from './types.js'\nimport { assumeContentType } from './utils.js'\n\ndeclare module '@tiptap/core' {\n interface Editor {\n /**\n * Get the content of the editor as markdown.\n */\n getMarkdown: () => string\n\n /**\n * The markdown manager instance.\n */\n markdown?: MarkdownManager\n }\n\n interface EditorOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface Storage {\n markdown: MarkdownExtensionStorage\n }\n\n interface InsertContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface InsertContentAtOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n\n interface SetContentOptions {\n /**\n * The content type the content is provided as.\n *\n * @default 'json'\n */\n contentType?: ContentType\n }\n}\n\nexport type MarkdownExtensionOptions = {\n /**\n * Configure the indentation style and size for lists and code blocks.\n * - `style`: Choose between spaces or tabs. Default is 'space'.\n * - `size`: Number of spaces or tabs for indentation. Default is 2.\n */\n indentation?: { style?: 'space' | 'tab'; size?: number }\n\n /**\n * Use a custom version of `marked` for markdown parsing and serialization.\n * If not provided, the default `marked` instance will be used.\n */\n marked?: typeof marked\n\n /**\n * Options to pass to `marked.setOptions()`.\n * See the [marked documentation](https://marked.js.org/using_advanced#options) for available options.\n */\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n}\n\nexport type MarkdownExtensionStorage = {\n manager: MarkdownManager\n}\n\nexport const Markdown = Extension.create<MarkdownExtensionOptions, MarkdownExtensionStorage>({\n name: 'markdown',\n\n addOptions() {\n return {\n indentation: { style: 'space', size: 2 },\n marked: undefined,\n markedOptions: {},\n }\n },\n\n addCommands() {\n return {\n setContent: (content, options?: MarkdownSetContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.setContent(content, options)\n }\n\n const actualContentType = assumeContentType(content, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.setContent(content, options)\n }\n\n const mdContent = this.editor.markdown.parse(content as string)\n return commands.setContent(mdContent, options)\n },\n\n insertContent: (value, options?: MarkdownInsertContentOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContent(value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContent(value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContent(mdContent, options)\n },\n\n insertContentAt: (position, value, options?: MarkdownInsertContentAtOptions) => {\n // if no contentType is specified, we assume the content is in JSON format OR HTML format\n if (!options?.contentType) {\n return commands.insertContentAt(position, value, options)\n }\n\n const actualContentType = assumeContentType(value, options?.contentType)\n\n if (actualContentType !== 'markdown' || !this.editor.markdown) {\n return commands.insertContentAt(position, value, options)\n }\n\n const mdContent = this.editor.markdown.parse(value as string)\n return commands.insertContentAt(position, mdContent, options)\n },\n }\n },\n\n addStorage() {\n return {\n manager: new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: [],\n }),\n }\n },\n\n onBeforeCreate() {\n if (this.editor.markdown) {\n console.error(\n '[tiptap][markdown]: There is already a `markdown` property on the editor instance. This might lead to unexpected behavior.',\n )\n return\n }\n\n this.storage.manager = new MarkdownManager({\n indentation: this.options.indentation,\n marked: this.options.marked,\n markedOptions: this.options.markedOptions,\n extensions: this.editor.extensionManager.baseExtensions,\n })\n\n this.editor.markdown = this.storage.manager\n\n // add a `getMarkdown()` method to the editor\n this.editor.getMarkdown = () => {\n return this.storage.manager.serialize(this.editor.getJSON())\n }\n\n if (!this.editor.options.contentType) {\n return\n }\n\n const assumedType = assumeContentType(this.editor.options.content, this.editor.options.contentType)\n if (assumedType !== 'markdown') {\n return\n }\n\n if (!this.editor.markdown) {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the Markdown extension is not added to the editor. Please add the Markdown extension to use this feature.',\n )\n }\n\n if (this.editor.options.content === undefined || typeof this.editor.options.content !== 'string') {\n throw new Error(\n '[tiptap][markdown]: The `contentType` option is set to \"markdown\", but the initial content is not a string. Please provide the initial content as a markdown string.',\n )\n }\n\n const json = this.editor.markdown.parse(this.editor.options.content as string)\n this.editor.options.content = json\n },\n})\n","import {\n type AnyExtension,\n type ExtendableConfig,\n type JSONContent,\n type MarkdownExtensionSpec,\n type MarkdownParseHelpers,\n type MarkdownParseResult,\n type MarkdownRendererHelpers,\n type MarkdownToken,\n type MarkdownTokenizer,\n type RenderContext,\n flattenExtensions,\n generateJSON,\n getExtensionField,\n} from '@tiptap/core'\nimport { type Lexer, type Token, type TokenizerExtension, marked } from 'marked'\n\nimport {\n closeMarksBeforeNode,\n findMarksToClose,\n findMarksToCloseAtEnd,\n findMarksToOpen,\n isTaskItem,\n reopenMarksAfterNode,\n wrapInMarkdownBlock,\n} from './utils.js'\n\nexport class MarkdownManager {\n private markedInstance: typeof marked\n private lexer: Lexer\n private registry: Map<string, MarkdownExtensionSpec[]>\n private nodeTypeRegistry: Map<string, MarkdownExtensionSpec[]>\n private indentStyle: 'space' | 'tab'\n private indentSize: number\n private baseExtensions: AnyExtension[] = []\n private extensions: AnyExtension[] = []\n\n /**\n * Create a MarkdownManager.\n * @param options.marked Optional marked instance to use (injected).\n * @param options.markedOptions Optional options to pass to marked.setOptions\n * @param options.indentation Indentation settings (style and size).\n * @param options.extensions An array of Tiptap extensions to register for markdown parsing and rendering.\n */\n constructor(options?: {\n marked?: typeof marked\n markedOptions?: Parameters<typeof marked.setOptions>[0]\n indentation?: { style?: 'space' | 'tab'; size?: number }\n extensions: AnyExtension[]\n }) {\n this.markedInstance = options?.marked ?? marked\n this.lexer = new this.markedInstance.Lexer()\n this.indentStyle = options?.indentation?.style ?? 'space'\n this.indentSize = options?.indentation?.size ?? 2\n this.baseExtensions = options?.extensions || []\n\n if (options?.markedOptions && typeof this.markedInstance.setOptions === 'function') {\n this.markedInstance.setOptions(options.markedOptions)\n }\n\n this.registry = new Map()\n this.nodeTypeRegistry = new Map()\n\n // If extensions were provided, register them now\n if (options?.extensions) {\n this.baseExtensions = options.extensions\n const flattened = flattenExtensions(options.extensions)\n flattened.forEach(ext => this.registerExtension(ext, false))\n }\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include all tokenizers\n }\n\n /** Returns the underlying marked instance. */\n get instance(): typeof marked {\n return this.markedInstance\n }\n\n /** Returns the correct indentCharacter (space or tab) */\n get indentCharacter(): string {\n return this.indentStyle === 'space' ? ' ' : '\\t'\n }\n\n /** Returns the correct indentString repeated X times */\n get indentString(): string {\n return this.indentCharacter.repeat(this.indentSize)\n }\n\n /** Helper to quickly check whether a marked instance is available. */\n hasMarked(): boolean {\n return !!this.markedInstance\n }\n\n /**\n * Register a Tiptap extension (Node/Mark/Extension). This will read\n * `markdownName`, `parseMarkdown`, `renderMarkdown` and `priority` from the\n * extension config (using the same resolution used across the codebase).\n */\n registerExtension(extension: AnyExtension, recreateLexer: boolean = true): void {\n // Keep track of all extensions for HTML parsing\n this.extensions.push(extension)\n\n const name = extension.name\n const tokenName =\n (getExtensionField(extension, 'markdownTokenName') as ExtendableConfig['markdownTokenName']) || name\n const parseMarkdown = getExtensionField(extension, 'parseMarkdown') as ExtendableConfig['parseMarkdown'] | undefined\n const renderMarkdown = getExtensionField(extension, 'renderMarkdown') as\n | ExtendableConfig['renderMarkdown']\n | undefined\n const tokenizer = getExtensionField(extension, 'markdownTokenizer') as\n | ExtendableConfig['markdownTokenizer']\n | undefined\n\n // Read the `markdown` object from the extension config. This allows\n // extensions to provide `markdown: { name?, parseName?, renderName?, parse?, render?, match? }`.\n const markdownCfg = (getExtensionField(extension, 'markdownOptions') ?? null) as ExtendableConfig['markdownOptions']\n const isIndenting = markdownCfg?.indentsContent ?? false\n\n const spec: MarkdownExtensionSpec = {\n tokenName,\n nodeName: name,\n parseMarkdown,\n renderMarkdown,\n isIndenting,\n tokenizer,\n }\n\n // Add to parse registry using parseName\n if (tokenName && parseMarkdown) {\n const parseExisting = this.registry.get(tokenName) || []\n parseExisting.push(spec)\n this.registry.set(tokenName, parseExisting)\n }\n\n // Add to render registry using renderName (node type)\n if (renderMarkdown) {\n const renderExisting = this.nodeTypeRegistry.get(name) || []\n renderExisting.push(spec)\n this.nodeTypeRegistry.set(name, renderExisting)\n }\n\n // Register custom tokenizer with marked.js\n if (tokenizer && this.hasMarked()) {\n this.registerTokenizer(tokenizer)\n\n if (recreateLexer) {\n this.lexer = new this.markedInstance.Lexer() // Reset lexer to include new tokenizer\n }\n }\n }\n\n /**\n * Register a custom tokenizer with marked.js for parsing non-standard markdown syntax.\n */\n private registerTokenizer(tokenizer: MarkdownTokenizer): void {\n if (!this.hasMarked()) {\n return\n }\n\n const { name, start, level = 'inline', tokenize } = tokenizer\n\n // Helper functions that use a fresh lexer instance with all registered extensions\n const tokenizeInline = (src: string) => {\n return this.lexer.inlineTokens(src)\n }\n\n const tokenizeBlock = (src: string) => {\n return this.lexer.blockTokens(src)\n }\n\n const helper = {\n inlineTokens: tokenizeInline,\n blockTokens: tokenizeBlock,\n }\n\n let startCb: (src: string) => number\n\n if (!start) {\n startCb = (src: string) => {\n // For other tokenizers, try to find a match and return its position\n const result = tokenize(src, [], helper)\n if (result && result.raw) {\n const index = src.indexOf(result.raw)\n return index\n }\n return -1\n }\n } else {\n startCb = typeof start === 'function' ? start : (src: string) => src.indexOf(start)\n }\n\n // Create marked.js extension with proper types\n const markedExtension: TokenizerExtension = {\n name,\n level,\n start: startCb,\n tokenizer: (src, tokens) => {\n const result = tokenize(src, tokens, helper)\n\n if (result && result.type) {\n return {\n ...result,\n type: result.type || name,\n raw: result.raw || '',\n tokens: (result.tokens || []) as Token[],\n }\n }\n\n return undefined\n },\n childTokens: [],\n }\n\n // Register with marked.js - use extensions array to control priority\n this.markedInstance.use({\n extensions: [markedExtension],\n })\n }\n\n /** Get registered handlers for a token type and try each until one succeeds. */\n private getHandlersForToken(type: string): MarkdownExtensionSpec[] {\n try {\n return this.registry.get(type) || []\n } catch {\n return []\n }\n }\n\n /** Get the first handler for a token type (for backwards compatibility). */\n private getHandlerForToken(type: string): MarkdownExtensionSpec | undefined {\n // First try the markdown token registry (for parsing)\n const markdownHandlers = this.getHandlersForToken(type)\n if (markdownHandlers.length > 0) {\n return markdownHandlers[0]\n }\n\n // Then try the node type registry (for rendering)\n const nodeTypeHandlers = this.getHandlersForNodeType(type)\n return nodeTypeHandlers.length > 0 ? nodeTypeHandlers[0] : undefined\n }\n\n /** Get registered handlers for a node type (for rendering). */\n private getHandlersForNodeType(type: string): MarkdownExtensionSpec[] {\n try {\n return this.nodeTypeRegistry.get(type) || []\n } catch {\n return []\n }\n }\n\n /**\n * Serialize a ProseMirror-like JSON document (or node array) to a Markdown string\n * using registered renderers and fallback renderers.\n */\n serialize(docOrContent: JSONContent): string {\n if (!docOrContent) {\n return ''\n }\n\n const result = this.renderNodes(docOrContent, docOrContent)\n // Return empty string if result is only whitespace entities or non-breaking spaces\n return this.isEmptyOutput(result) ? '' : result\n }\n\n /**\n * Check if the markdown output represents an empty document.\n * Empty documents may contain only &nbsp; entities or non-breaking space characters\n * which are used by the Paragraph extension to preserve blank lines.\n */\n private isEmptyOutput(markdown: string): boolean {\n if (!markdown || markdown.trim() === '') {\n return true\n }\n\n // Check if the output is only &nbsp; entities or non-breaking space characters\n const cleanedOutput = markdown\n .replace(/&nbsp;/g, '')\n .replace(/\\u00A0/g, '')\n .trim()\n return cleanedOutput === ''\n }\n\n /**\n * Parse markdown string into Tiptap JSON document using registered extension handlers.\n */\n parse(markdown: string): JSONContent {\n if (!this.hasMarked()) {\n throw new Error('No marked instance available for parsing')\n }\n\n // Use marked to tokenize the markdown\n const tokens = this.markedInstance.lexer(markdown)\n\n // Convert tokens to Tiptap JSON\n const content = this.parseTokens(tokens, true)\n\n // Return a document node containing the parsed content\n return {\n type: 'doc',\n content,\n }\n }\n\n /**\n * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.\n */\n private parseTokens(tokens: MarkdownToken[], parseImplicitEmptyParagraphs = false): JSONContent[] {\n const nonSpaceTokenIndexes = tokens.reduce<number[]>((indexes, token, index) => {\n if (token.type !== 'space') {\n indexes.push(index)\n }\n\n return indexes\n }, [])\n\n let previousNonSpaceTokenIndex = -1\n let nextNonSpaceTokenPointer = 0\n\n return tokens.flatMap((token, index) => {\n while (\n nextNonSpaceTokenPointer < nonSpaceTokenIndexes.length &&\n nonSpaceTokenIndexes[nextNonSpaceTokenPointer] < index\n ) {\n previousNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer]\n nextNonSpaceTokenPointer += 1\n }\n\n if (parseImplicitEmptyParagraphs && token.type === 'space') {\n const nextNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer] ?? -1\n\n return this.createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex)\n }\n\n const parsed = this.parseToken(token, parseImplicitEmptyParagraphs)\n\n if (parsed === null) {\n return []\n }\n\n return Array.isArray(parsed) ? parsed : [parsed]\n })\n }\n\n private createImplicitEmptyParagraphsFromSpace(\n token: MarkdownToken,\n previousNonSpaceTokenIndex: number,\n nextNonSpaceTokenIndex: number,\n ): JSONContent[] {\n const separatorCount = this.countParagraphSeparators(token.raw || '')\n\n if (separatorCount === 0) {\n return []\n }\n\n const isBoundarySpace = previousNonSpaceTokenIndex === -1 || nextNonSpaceTokenIndex === -1\n const emptyParagraphCount = Math.max(separatorCount - (isBoundarySpace ? 0 : 1), 0)\n\n return Array.from({ length: emptyParagraphCount }, () => ({ type: 'paragraph', content: [] }))\n }\n\n private countParagraphSeparators(raw: string): number {\n return (raw.replace(/\\r\\n/g, '\\n').match(/\\n\\n/g) || []).length\n }\n\n /**\n * Parse a single token into Tiptap JSON using the appropriate registered handler.\n */\n private parseToken(token: MarkdownToken, parseImplicitEmptyParagraphs = false): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n // Special handling for 'list' tokens that may contain mixed bullet/task items\n if (token.type === 'list') {\n return this.parseListToken(token)\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token, parseImplicitEmptyParagraphs)\n }\n\n private lastParseResult: JSONContent | JSONContent[] | null = null\n\n /**\n * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists.\n * This ensures that consecutive task items and bullet items are grouped and parsed as separate list nodes.\n *\n * @param token The list token to parse\n * @returns Array of parsed list nodes, or null if parsing fails\n */\n private parseListToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.items || token.items.length === 0) {\n // No items, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n const hasTask = token.items.some(item => isTaskItem(item).isTask)\n const hasNonTask = token.items.some(item => !isTaskItem(item).isTask)\n\n if (!hasTask || !hasNonTask || this.getHandlersForToken('taskList').length === 0) {\n // Not mixed or no taskList extension, parse normally\n return this.parseTokenWithHandlers(token)\n }\n\n // Mixed list with taskList extension available: split into separate lists\n type TaskListItemToken = MarkdownToken & { type: 'taskItem'; checked?: boolean; indentLevel?: number }\n const groups: { type: 'list' | 'taskList'; items: (MarkdownToken | TaskListItemToken)[] }[] = []\n let currentGroup: (MarkdownToken | TaskListItemToken)[] = []\n let currentType: 'list' | 'taskList' | null = null\n\n for (let i = 0; i < token.items.length; i += 1) {\n const item = token.items[i]\n const { isTask, checked, indentLevel } = isTaskItem(item)\n let processedItem = item\n\n if (isTask) {\n // Transform list_item into taskItem token\n const raw = item.raw || item.text || ''\n\n // Split raw content by lines to separate main content from nested\n const lines = raw.split('\\n')\n\n // Extract main content from the first line\n const firstLineMatch = lines[0].match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+(.*)$/)\n const mainContent = firstLineMatch ? firstLineMatch[2] : ''\n\n // Parse nested content from remaining lines\n let nestedTokens: MarkdownToken[] = []\n if (lines.length > 1) {\n // Join all lines after the first\n const nestedRaw = lines.slice(1).join('\\n')\n\n // Only parse if there's actual content\n if (nestedRaw.trim()) {\n // Find minimum indentation of non-empty lines\n const nestedLines = lines.slice(1)\n const nonEmptyLines = nestedLines.filter(line => line.trim())\n if (nonEmptyLines.length > 0) {\n const minIndent = Math.min(...nonEmptyLines.map(line => line.length - line.trimStart().length))\n // Remove common indentation while preserving structure\n const trimmedLines = nestedLines.map(line => {\n if (!line.trim()) {\n return '' // Keep empty lines\n }\n return line.slice(minIndent)\n })\n const nestedContent = trimmedLines.join('\\n').trim()\n // Use the lexer to parse nested content\n if (nestedContent) {\n // Use the full lexer pipeline to ensure inline tokens are populated\n nestedTokens = this.markedInstance.lexer(`${nestedContent}\\n`)\n }\n }\n }\n }\n\n processedItem = {\n type: 'taskItem',\n raw: '',\n mainContent,\n indentLevel,\n checked: checked ?? false,\n text: mainContent,\n tokens: this.lexer.inlineTokens(mainContent),\n nestedTokens,\n }\n }\n\n const itemType: 'list' | 'taskList' = isTask ? 'taskList' : 'list'\n\n if (currentType !== itemType) {\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n currentGroup = [processedItem]\n currentType = itemType\n } else {\n currentGroup.push(processedItem)\n }\n }\n\n if (currentGroup.length > 0) {\n groups.push({ type: currentType!, items: currentGroup })\n }\n\n // Parse each group as a separate token\n const results: JSONContent[] = []\n for (let i = 0; i < groups.length; i += 1) {\n const group = groups[i]\n const subToken = { ...token, type: group.type, items: group.items }\n const parsed = this.parseToken(subToken)\n if (parsed) {\n if (Array.isArray(parsed)) {\n results.push(...parsed)\n } else {\n results.push(parsed)\n }\n }\n }\n\n return results.length > 0 ? results : null\n }\n\n /**\n * Parse a token using registered handlers (extracted for reuse).\n */\n private parseTokenWithHandlers(token: MarkdownToken): JSONContent | JSONContent[] | null {\n if (!token.type) {\n return null\n }\n\n const handlers = this.getHandlersForToken(token.type)\n const helpers = this.createParseHelpers()\n\n // Try each handler until one returns a valid result\n const result = handlers.find(handler => {\n if (!handler.parseMarkdown) {\n return false\n }\n\n const parseResult = handler.parseMarkdown(token, helpers)\n const normalized = this.normalizeParseResult(parseResult)\n\n // Check if this handler returned a valid result (not null/empty array)\n if (normalized && (!Array.isArray(normalized) || normalized.length > 0)) {\n // Store result for return\n this.lastParseResult = normalized\n return true\n }\n\n return false\n })\n\n // If a handler worked, return its result\n if (result && this.lastParseResult) {\n const toReturn = this.lastParseResult\n this.lastParseResult = null // Clean up\n return toReturn\n }\n\n // If no handler worked, try fallback parsing\n return this.parseFallbackToken(token)\n }\n\n /**\n * Creates helper functions for parsing markdown tokens.\n * @returns An object containing helper functions for parsing.\n */\n private createParseHelpers(): MarkdownParseHelpers {\n return {\n parseInline: (tokens: MarkdownToken[]) => this.parseInlineTokens(tokens),\n parseChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens),\n parseBlockChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens, true),\n createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => {\n const node = {\n type: 'text',\n text,\n marks: marks || undefined,\n }\n\n return node\n },\n createNode: (type: string, attrs?: any, content?: JSONContent[]) => {\n const node = {\n type,\n attrs: attrs || undefined,\n content: content || undefined,\n }\n\n if (!attrs || Object.keys(attrs).length === 0) {\n delete node.attrs\n }\n\n return node\n },\n applyMark: (markType: string, content: JSONContent[], attrs?: any) => ({\n mark: markType,\n content,\n attrs: attrs && Object.keys(attrs).length > 0 ? attrs : undefined,\n }),\n }\n }\n\n /**\n * Escape special regex characters in a string.\n */\n private escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n\n /**\n * Parse inline tokens (bold, italic, links, etc.) into text nodes with marks.\n * This is the complex part that handles mark nesting and boundaries.\n */\n private parseInlineTokens(tokens: MarkdownToken[]): JSONContent[] {\n const result: JSONContent[] = []\n\n // Process tokens sequentially using an index so we can lookahead and\n // merge split inline HTML fragments like: text / <em> / inner / </em> / text\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i]\n\n if (token.type === 'text') {\n // Create text node\n result.push({\n type: 'text',\n text: token.text || '',\n })\n } else if (token.type === 'html') {\n // Handle possible split inline HTML by attempting to detect an\n // opening tag and searching forward for a matching closing tag.\n const raw = (token.raw ?? token.text ?? '').toString()\n\n // Quick checks for opening vs. closing tag\n const isClosing = /^<\\/[\\s]*[\\w-]+/i.test(raw)\n const openMatch = raw.match(/^<[\\s]*([\\w-]+)(\\s|>|\\/|$)/i)\n\n if (!isClosing && openMatch && !/\\/>$/.test(raw)) {\n // Try to find the corresponding closing html token for this tag\n const tagName = openMatch[1]\n const escapedTagName = this.escapeRegex(tagName)\n const closingRegex = new RegExp(`^<\\\\/\\\\s*${escapedTagName}\\\\b`, 'i')\n let foundIndex = -1\n\n // Collect intermediate raw parts to reconstruct full HTML fragment\n const parts: string[] = [raw]\n for (let j = i + 1; j < tokens.length; j += 1) {\n const t = tokens[j]\n const tRaw = (t.raw ?? t.text ?? '').toString()\n parts.push(tRaw)\n if (t.type === 'html' && closingRegex.test(tRaw)) {\n foundIndex = j\n break\n }\n }\n\n if (foundIndex !== -1) {\n // Merge opening + inner + closing into one html fragment and parse\n const mergedRaw = parts.join('')\n const mergedToken = {\n type: 'html',\n raw: mergedRaw,\n text: mergedRaw,\n block: false,\n } as unknown as MarkdownToken\n\n const parsed = this.parseHTMLToken(mergedToken)\n if (parsed) {\n const normalized = this.normalizeParseResult(parsed as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n\n // Advance i to the closing token\n i = foundIndex\n continue\n }\n }\n\n // Fallback: single html token parse\n const parsedSingle = this.parseHTMLToken(token)\n if (parsedSingle) {\n const normalized = this.normalizeParseResult(parsedSingle as any)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.type) {\n // Handle inline marks (bold, italic, etc.)\n const markHandler = this.getHandlerForToken(token.type)\n if (markHandler && markHandler.parseMarkdown) {\n const helpers = this.createParseHelpers()\n const parsed = markHandler.parseMarkdown(token, helpers)\n\n if (this.isMarkResult(parsed)) {\n // This is a mark result - apply the mark to the content\n const markedContent = this.applyMarkToContent(parsed.mark, parsed.content, parsed.attrs)\n result.push(...markedContent)\n } else {\n // Regular inline node\n const normalized = this.normalizeParseResult(parsed)\n if (Array.isArray(normalized)) {\n result.push(...normalized)\n } else if (normalized) {\n result.push(normalized)\n }\n }\n } else if (token.tokens) {\n // Fallback: try to parse children if they exist\n result.push(...this.parseInlineTokens(token.tokens))\n }\n }\n }\n\n return result\n }\n\n /**\n * Apply a mark to content nodes.\n */\n private applyMarkToContent(markType: string, content: JSONContent[], attrs?: any): JSONContent[] {\n return content.map(node => {\n if (node.type === 'text') {\n // Add the mark to existing marks or create new marks array\n const existingMarks = node.marks || []\n const newMark = attrs ? { type: markType, attrs } : { type: markType }\n return {\n ...node,\n marks: [...existingMarks, newMark],\n }\n }\n\n // For non-text nodes, recursively apply to content\n return {\n ...node,\n content: node.content ? this.applyMarkToContent(markType, node.content, attrs) : undefined,\n }\n })\n } /**\n * Check if a parse result represents a mark to be applied.\n */\n private isMarkResult(result: any): result is { mark: string; content: JSONContent[]; attrs?: any } {\n return result && typeof result === 'object' && 'mark' in result\n }\n\n /**\n * Normalize parse results to ensure they're valid JSONContent.\n */\n private normalizeParseResult(result: MarkdownParseResult): JSONContent | JSONContent[] | null {\n if (!result) {\n return null\n }\n\n if (this.isMarkResult(result)) {\n // This shouldn't happen at the top level, but handle it gracefully\n return result.content\n }\n\n return result as JSONContent | JSONContent[]\n }\n\n /**\n * Fallback parsing for common tokens when no specific handler is registered.\n */\n private parseFallbackToken(\n token: MarkdownToken,\n parseImplicitEmptyParagraphs = false,\n ): JSONContent | JSONContent[] | null {\n switch (token.type) {\n case 'paragraph':\n return {\n type: 'paragraph',\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'heading':\n return {\n type: 'heading',\n attrs: { level: token.depth || 1 },\n content: token.tokens ? this.parseInlineTokens(token.tokens) : [],\n }\n\n case 'text':\n return {\n type: 'text',\n text: token.text || '',\n }\n\n case 'html':\n // Parse HTML using extensions' parseHTML methods\n return this.parseHTMLToken(token)\n\n case 'space':\n return null\n\n default:\n // Unknown token type - try to parse children if they exist\n if (token.tokens) {\n return this.parseTokens(token.tokens, parseImplicitEmptyParagraphs)\n }\n return null\n }\n }\n\n /**\n * Parse HTML tokens using extensions' parseHTML methods.\n * This allows HTML within markdown to be parsed according to extension rules.\n */\n private parseHTMLToken(token: MarkdownToken): JSONContent | JSONContent[] | null {\n const html = token.text || token.raw || ''\n\n if (!html.trim()) {\n return null\n }\n\n // Check if we're in a server-side environment (no window object)\n // If so, fall back to treating HTML as plain text to avoid runtime errors\n if (typeof window === 'undefined') {\n // For block-level HTML, wrap in a paragraph to maintain valid document structure\n if (token.block) {\n return {\n type: 'paragraph',\n content: [\n {\n type: 'text',\n text: html,\n },\n ],\n }\n }\n // For inline HTML, return plain text\n return {\n type: 'text',\n text: html,\n }\n }\n\n // Use generateJSON to parse the HTML using extensions' parseHTML rules\n try {\n const parsed = generateJSON(html, this.baseExtensions)\n\n // If the result is a doc node, extract its content\n if (parsed.type === 'doc' && parsed.content) {\n // For block-level HTML, return the content array\n if (token.block) {\n return parsed.content\n }\n\n // For inline HTML, we need to flatten the content appropriately\n // If there's only one paragraph with content, unwrap it\n if (parsed.content.length === 1 && parsed.content[0].type === 'paragraph' && parsed.content[0].content) {\n return parsed.content[0].content\n }\n\n return parsed.content\n }\n\n return parsed as JSONContent\n } catch (error) {\n throw new Error(`Failed to parse HTML in markdown: ${error}`)\n }\n }\n\n renderNodeToMarkdown(\n node: JSONContent,\n parentNode?: JSONContent,\n index = 0,\n level = 0,\n meta: Record<string, any> = {},\n ): string {\n // if node is a text node, we simply return it's text content\n // marks are handled at the array level in renderNodesWithMarkBoundaries\n if (node.type === 'text') {\n return node.text || ''\n }\n\n if (!node.type) {\n return ''\n }\n\n const handler = this.getHandlerForToken(node.type)\n if (!handler) {\n return ''\n }\n\n const previousNode = Array.isArray(parentNode?.content) && index > 0 ? parentNode.content[index - 1] : undefined\n const helpers: MarkdownRendererHelpers = {\n renderChildren: (nodes, separator) => {\n const childLevel = handler.isIndenting ? level + 1 : level\n\n if (!Array.isArray(nodes) && (nodes as any).content) {\n return this.renderNodes((nodes as any).content as JSONContent[], node, separator || '', index, childLevel)\n }\n\n return this.renderNodes(nodes, node, separator || '', index, childLevel)\n },\n renderChild: (childNode, childIndex) => {\n const childLevel = handler.isIndenting ? level + 1 : level\n\n return this.renderNodeToMarkdown(childNode, node, childIndex, childLevel)\n },\n indent: content => {\n return this.indentString + content\n },\n wrapInBlock: wrapInMarkdownBlock,\n }\n\n const context: RenderContext = {\n index,\n level,\n parentType: parentNode?.type,\n previousNode,\n meta: {\n parentAttrs: parentNode?.attrs,\n ...meta,\n },\n }\n\n // First render the node itself (this will render children recursively)\n const rendered = handler.renderMarkdown?.(node, helpers, context) || ''\n\n return rendered\n }\n\n /**\n * Render a node or an array of nodes. Parent type controls how children\n * are joined (which determines newline insertion between children).\n */\n renderNodes(\n nodeOrNodes: JSONContent | JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n index = 0,\n level = 0,\n ): string {\n // if we have just one node, call renderNodeToMarkdown directly\n if (!Array.isArray(nodeOrNodes)) {\n if (!nodeOrNodes.type) {\n return ''\n }\n\n return this.renderNodeToMarkdown(nodeOrNodes, parentNode, index, level)\n }\n\n return this.renderNodesWithMarkBoundaries(nodeOrNodes, parentNode, separator, level)\n }\n\n /**\n * Render an array of nodes while properly tracking mark boundaries.\n * This handles cases where marks span across multiple text nodes.\n */\n private renderNodesWithMarkBoundaries(\n nodes: JSONContent[],\n parentNode?: JSONContent,\n separator = '',\n level = 0,\n ): string {\n const result: string[] = []\n const activeMarks: Map<string, any> = new Map()\n nodes.forEach((node, i) => {\n // Lookahead to the next node to determine if marks need to be closed\n const nextNode = i < nodes.length - 1 ? nodes[i + 1] : null\n\n if (!node.type) {\n return\n }\n\n if (node.type === 'text') {\n let textContent = node.text || ''\n const currentMarks = new Map((node.marks || []).map(mark => [mark.type, mark]))\n\n // Find marks that need to be closed and opened\n const marksToOpen = findMarksToOpen(activeMarks, currentMarks)\n const marksToClose = findMarksToClose(currentMarks, nextNode)\n\n let middleTrailingWhitespace = ''\n\n if (marksToClose.length > 0) {\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n const middleTrailingMatch = textContent.match(/(\\s+)$/)\n if (middleTrailingMatch) {\n middleTrailingWhitespace = middleTrailingMatch[1]\n textContent = textContent.slice(0, -middleTrailingWhitespace.length)\n }\n }\n // Close marks that are ending here\n marksToClose.forEach(markType => {\n const mark = currentMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n // deleting closed marks from active marks\n if (activeMarks.has(markType)) {\n activeMarks.delete(markType)\n }\n })\n\n // Open new marks (should be at the beginning)\n // Extract leading whitespace before opening marks to prevent invalid markdown like \"** text**\"\n let leadingWhitespace = ''\n if (marksToOpen.length > 0) {\n const leadingMatch = textContent.match(/^(\\s+)/)\n if (leadingMatch) {\n leadingWhitespace = leadingMatch[1]\n textContent = textContent.slice(leadingWhitespace.length)\n }\n }\n\n marksToOpen.forEach(({ type, mark }) => {\n const openMarkdown = this.getMarkOpening(type, mark)\n if (openMarkdown) {\n textContent = openMarkdown + textContent\n }\n if (!marksToClose.includes(type)) {\n activeMarks.set(type, mark)\n }\n })\n\n // Add leading whitespace before the mark opening\n textContent = leadingWhitespace + textContent\n\n // Close marks at the end of this node if needed\n const marksToCloseAtEnd = findMarksToCloseAtEnd(\n activeMarks,\n currentMarks,\n nextNode,\n this.markSetsEqual.bind(this),\n )\n\n // Extract trailing whitespace before closing marks to prevent invalid markdown like \"**text **\"\n let trailingWhitespace = ''\n if (marksToCloseAtEnd.length > 0) {\n const trailingMatch = textContent.match(/(\\s+)$/)\n if (trailingMatch) {\n trailingWhitespace = trailingMatch[1]\n textContent = textContent.slice(0, -trailingWhitespace.length)\n }\n }\n\n marksToCloseAtEnd.forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark)\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n activeMarks.delete(markType)\n })\n\n // Add trailing whitespace after the mark closing\n textContent += trailingWhitespace\n textContent += middleTrailingWhitespace\n\n result.push(textContent)\n } else {\n // For non-text nodes, close all active marks before rendering, then reopen after\n const marksToReopen = new Map(activeMarks)\n\n // Close all marks before the node\n const beforeMarkdown = closeMarksBeforeNode(activeMarks, this.getMarkClosing.bind(this))\n\n // Render the node\n const nodeContent = this.renderNodeToMarkdown(node, parentNode, i, level)\n\n // Reopen marks after the node, but NOT after a hard break\n // Hard breaks should terminate marks (they create a line break where marks don't continue)\n const afterMarkdown =\n node.type === 'hardBreak'\n ? ''\n : reopenMarksAfterNode(marksToReopen, activeMarks, this.getMarkOpening.bind(this))\n\n result.push(beforeMarkdown + nodeContent + afterMarkdown)\n }\n })\n\n return result.join(separator)\n }\n\n /**\n * Get the opening markdown syntax for a mark type.\n */\n private getMarkOpening(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n // For most marks, we can extract the opening syntax by rendering a simple case\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n renderChild: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the opening part (everything before placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n return placeholderIndex >= 0 ? rendered.substring(0, placeholderIndex) : ''\n } catch (err) {\n throw new Error(`Failed to get mark opening for ${markType}: ${err}`)\n }\n }\n\n /**\n * Get the closing markdown syntax for a mark type.\n */\n private getMarkClosing(markType: string, mark: any): string {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n if (!handler || !handler.renderMarkdown) {\n return ''\n }\n\n // Use a unique placeholder that's extremely unlikely to appear in real content\n const placeholder = '\\uE000__TIPTAP_MARKDOWN_PLACEHOLDER__\\uE001'\n\n const syntheticNode: JSONContent = {\n type: markType,\n attrs: mark.attrs || {},\n content: [{ type: 'text', text: placeholder }],\n }\n\n try {\n const rendered = handler.renderMarkdown(\n syntheticNode,\n {\n renderChildren: () => placeholder,\n renderChild: () => placeholder,\n indent: (content: string) => content,\n wrapInBlock: (prefix: string, content: string) => prefix + content,\n },\n { index: 0, level: 0, parentType: 'text', meta: {} },\n )\n\n // Extract the closing part (everything after placeholder)\n const placeholderIndex = rendered.indexOf(placeholder)\n const placeholderEnd = placeholderIndex + placeholder.length\n return placeholderIndex >= 0 ? rendered.substring(placeholderEnd) : ''\n } catch (err) {\n throw new Error(`Failed to get mark closing for ${markType}: ${err}`)\n }\n }\n\n /**\n * Check if two mark sets are equal.\n */\n private markSetsEqual(marks1: Map<string, any>, marks2: Map<string, any>): boolean {\n if (marks1.size !== marks2.size) {\n return false\n }\n\n return Array.from(marks1.keys()).every(type => marks2.has(type))\n }\n}\n\nexport default MarkdownManager\n","import type { Content, MarkdownToken } from '@tiptap/core'\nimport type { Fragment, Node } from '@tiptap/pm/model'\n\nimport type { ContentType } from './types.js'\n\n/**\n * Wraps each line of the content with the given prefix.\n * @param prefix The prefix to wrap each line with.\n * @param content The content to wrap.\n * @returns The content with each line wrapped with the prefix.\n */\nexport function wrapInMarkdownBlock(prefix: string, content: string) {\n // split content lines\n const lines = content.split('\\n')\n\n // add empty strings between every line\n const output = lines\n // add empty lines between each block\n .flatMap(line => [line, ''])\n // add the prefix to each line\n .map(line => `${prefix}${line}`)\n .join('\\n')\n\n return output.slice(0, output.length - 1)\n}\n\n/**\n * Identifies marks that need to be closed, based on the marks in the next node.\n */\nexport function findMarksToClose(currentMarks: Map<string, any>, nextNode: any): string[] {\n const marksToClose: string[] = []\n\n Array.from(currentMarks.keys()).forEach(markType => {\n if (!nextNode || !nextNode.marks || !nextNode.marks.map((mark: any) => mark.type).includes(markType)) {\n marksToClose.push(markType)\n }\n })\n return marksToClose\n}\n\n/**\n * Identifies marks that need to be opened (in current node but not active).\n */\nexport function findMarksToOpen(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n): Array<{ type: string; mark: any }> {\n const marksToOpen: Array<{ type: string; mark: any }> = []\n Array.from(currentMarks.entries()).forEach(([markType, mark]) => {\n if (!activeMarks.has(markType)) {\n marksToOpen.push({ type: markType, mark })\n }\n })\n return marksToOpen\n}\n\n/**\n * Determines which marks need to be closed at the end of the current text node.\n * This handles cases where marks end at node boundaries or when transitioning\n * to nodes with different mark sets.\n */\nexport function findMarksToCloseAtEnd(\n activeMarks: Map<string, any>,\n currentMarks: Map<string, any>,\n nextNode: any,\n markSetsEqual: (a: Map<string, any>, b: Map<string, any>) => boolean,\n): string[] {\n const isLastNode = !nextNode\n const nextNodeHasNoMarks = nextNode && nextNode.type === 'text' && (!nextNode.marks || nextNode.marks.length === 0)\n const nextNodeHasDifferentMarks =\n nextNode &&\n nextNode.type === 'text' &&\n nextNode.marks &&\n !markSetsEqual(currentMarks, new Map(nextNode.marks.map((mark: any) => [mark.type, mark])))\n\n const marksToCloseAtEnd: string[] = []\n if (isLastNode || nextNodeHasNoMarks || nextNodeHasDifferentMarks) {\n if (nextNode && nextNode.type === 'text' && nextNode.marks) {\n const nextMarks = new Map(nextNode.marks.map((mark: any) => [mark.type, mark]))\n Array.from(activeMarks.keys()).forEach(markType => {\n if (!nextMarks.has(markType)) {\n marksToCloseAtEnd.push(markType)\n }\n })\n } else if (isLastNode || nextNodeHasNoMarks) {\n // Close all active marks\n marksToCloseAtEnd.push(...Array.from(activeMarks.keys()))\n }\n }\n\n return marksToCloseAtEnd\n}\n\n/**\n * Closes active marks before rendering a non-text node.\n * Returns the closing markdown syntax and clears the active marks.\n */\nexport function closeMarksBeforeNode(\n activeMarks: Map<string, any>,\n getMarkClosing: (markType: string, mark: any) => string,\n): string {\n let beforeMarkdown = ''\n Array.from(activeMarks.keys())\n .reverse()\n .forEach(markType => {\n const mark = activeMarks.get(markType)\n const closeMarkdown = getMarkClosing(markType, mark)\n if (closeMarkdown) {\n beforeMarkdown = closeMarkdown + beforeMarkdown\n }\n })\n activeMarks.clear()\n return beforeMarkdown\n}\n\n/**\n * Reopens marks after rendering a non-text node.\n * Returns the opening markdown syntax and updates the active marks.\n */\nexport function reopenMarksAfterNode(\n marksToReopen: Map<string, any>,\n activeMarks: Map<string, any>,\n getMarkOpening: (markType: string, mark: any) => string,\n): string {\n let afterMarkdown = ''\n Array.from(marksToReopen.entries()).forEach(([markType, mark]) => {\n const openMarkdown = getMarkOpening(markType, mark)\n if (openMarkdown) {\n afterMarkdown += openMarkdown\n }\n activeMarks.set(markType, mark)\n })\n return afterMarkdown\n}\n\n/**\n * Check if a markdown list item token is a task item and extract its state.\n *\n * @param item The list item token to check\n * @returns Object containing isTask flag, checked state, and indentation level\n *\n * @example\n * ```ts\n * isTaskItem({ raw: '- [ ] Task' }) // { isTask: true, checked: false, indentLevel: 0 }\n * isTaskItem({ raw: ' - [x] Done' }) // { isTask: true, checked: true, indentLevel: 2 }\n * isTaskItem({ raw: '- Regular' }) // { isTask: false, indentLevel: 0 }\n * ```\n */\nexport function isTaskItem(item: MarkdownToken): { isTask: boolean; checked?: boolean; indentLevel: number } {\n const raw = item.raw || item.text || ''\n\n // Match patterns like \"- [ ] \" or \" - [x] \"\n const match = raw.match(/^(\\s*)[-+*]\\s+\\[([ xX])\\]\\s+/)\n\n if (match) {\n return { isTask: true, checked: match[2].toLowerCase() === 'x', indentLevel: match[1].length }\n }\n return { isTask: false, indentLevel: 0 }\n}\n\n/**\n * Assumes the content type based off the content.\n * @param content The content to assume the type for.\n * @param contentType The content type that should be prioritized.\n */\nexport function assumeContentType(\n content: (Content | Fragment | Node) | string,\n contentType: ContentType,\n): ContentType {\n // if not a string, we assume it will be a json content object\n if (typeof content !== 'string') {\n return 'json'\n }\n\n // otherwise we let the content type be what it is\n return contentType\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/markdown",
3
3
  "description": "markdown parser and serializer for tiptap",
4
- "version": "3.20.0",
4
+ "version": "3.20.2",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -37,12 +37,12 @@
37
37
  "marked": "^17.0.1"
38
38
  },
39
39
  "devDependencies": {
40
- "@tiptap/core": "^3.20.0",
41
- "@tiptap/pm": "^3.20.0"
40
+ "@tiptap/core": "^3.20.2",
41
+ "@tiptap/pm": "^3.20.2"
42
42
  },
43
43
  "peerDependencies": {
44
- "@tiptap/core": "^3.20.0",
45
- "@tiptap/pm": "^3.20.0"
44
+ "@tiptap/core": "^3.20.2",
45
+ "@tiptap/pm": "^3.20.2"
46
46
  },
47
47
  "repository": {
48
48
  "type": "git",
@@ -291,7 +291,7 @@ export class MarkdownManager {
291
291
  const tokens = this.markedInstance.lexer(markdown)
292
292
 
293
293
  // Convert tokens to Tiptap JSON
294
- const content = this.parseTokens(tokens)
294
+ const content = this.parseTokens(tokens, true)
295
295
 
296
296
  // Return a document node containing the parsed content
297
297
  return {
@@ -303,17 +303,68 @@ export class MarkdownManager {
303
303
  /**
304
304
  * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.
305
305
  */
306
- private parseTokens(tokens: MarkdownToken[]): JSONContent[] {
307
- return tokens
308
- .map(token => this.parseToken(token))
309
- .filter((parsed): parsed is JSONContent | JSONContent[] => parsed !== null)
310
- .flatMap(parsed => (Array.isArray(parsed) ? parsed : [parsed]))
306
+ private parseTokens(tokens: MarkdownToken[], parseImplicitEmptyParagraphs = false): JSONContent[] {
307
+ const nonSpaceTokenIndexes = tokens.reduce<number[]>((indexes, token, index) => {
308
+ if (token.type !== 'space') {
309
+ indexes.push(index)
310
+ }
311
+
312
+ return indexes
313
+ }, [])
314
+
315
+ let previousNonSpaceTokenIndex = -1
316
+ let nextNonSpaceTokenPointer = 0
317
+
318
+ return tokens.flatMap((token, index) => {
319
+ while (
320
+ nextNonSpaceTokenPointer < nonSpaceTokenIndexes.length &&
321
+ nonSpaceTokenIndexes[nextNonSpaceTokenPointer] < index
322
+ ) {
323
+ previousNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer]
324
+ nextNonSpaceTokenPointer += 1
325
+ }
326
+
327
+ if (parseImplicitEmptyParagraphs && token.type === 'space') {
328
+ const nextNonSpaceTokenIndex = nonSpaceTokenIndexes[nextNonSpaceTokenPointer] ?? -1
329
+
330
+ return this.createImplicitEmptyParagraphsFromSpace(token, previousNonSpaceTokenIndex, nextNonSpaceTokenIndex)
331
+ }
332
+
333
+ const parsed = this.parseToken(token, parseImplicitEmptyParagraphs)
334
+
335
+ if (parsed === null) {
336
+ return []
337
+ }
338
+
339
+ return Array.isArray(parsed) ? parsed : [parsed]
340
+ })
341
+ }
342
+
343
+ private createImplicitEmptyParagraphsFromSpace(
344
+ token: MarkdownToken,
345
+ previousNonSpaceTokenIndex: number,
346
+ nextNonSpaceTokenIndex: number,
347
+ ): JSONContent[] {
348
+ const separatorCount = this.countParagraphSeparators(token.raw || '')
349
+
350
+ if (separatorCount === 0) {
351
+ return []
352
+ }
353
+
354
+ const isBoundarySpace = previousNonSpaceTokenIndex === -1 || nextNonSpaceTokenIndex === -1
355
+ const emptyParagraphCount = Math.max(separatorCount - (isBoundarySpace ? 0 : 1), 0)
356
+
357
+ return Array.from({ length: emptyParagraphCount }, () => ({ type: 'paragraph', content: [] }))
358
+ }
359
+
360
+ private countParagraphSeparators(raw: string): number {
361
+ return (raw.replace(/\r\n/g, '\n').match(/\n\n/g) || []).length
311
362
  }
312
363
 
313
364
  /**
314
365
  * Parse a single token into Tiptap JSON using the appropriate registered handler.
315
366
  */
316
- private parseToken(token: MarkdownToken): JSONContent | JSONContent[] | null {
367
+ private parseToken(token: MarkdownToken, parseImplicitEmptyParagraphs = false): JSONContent | JSONContent[] | null {
317
368
  if (!token.type) {
318
369
  return null
319
370
  }
@@ -353,7 +404,7 @@ export class MarkdownManager {
353
404
  }
354
405
 
355
406
  // If no handler worked, try fallback parsing
356
- return this.parseFallbackToken(token)
407
+ return this.parseFallbackToken(token, parseImplicitEmptyParagraphs)
357
408
  }
358
409
 
359
410
  private lastParseResult: JSONContent | JSONContent[] | null = null
@@ -527,6 +578,7 @@ export class MarkdownManager {
527
578
  return {
528
579
  parseInline: (tokens: MarkdownToken[]) => this.parseInlineTokens(tokens),
529
580
  parseChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens),
581
+ parseBlockChildren: (tokens: MarkdownToken[]) => this.parseTokens(tokens, true),
530
582
  createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => {
531
583
  const node = {
532
584
  type: 'text',
@@ -723,7 +775,10 @@ export class MarkdownManager {
723
775
  /**
724
776
  * Fallback parsing for common tokens when no specific handler is registered.
725
777
  */
726
- private parseFallbackToken(token: MarkdownToken): JSONContent | JSONContent[] | null {
778
+ private parseFallbackToken(
779
+ token: MarkdownToken,
780
+ parseImplicitEmptyParagraphs = false,
781
+ ): JSONContent | JSONContent[] | null {
727
782
  switch (token.type) {
728
783
  case 'paragraph':
729
784
  return {
@@ -754,7 +809,7 @@ export class MarkdownManager {
754
809
  default:
755
810
  // Unknown token type - try to parse children if they exist
756
811
  if (token.tokens) {
757
- return this.parseTokens(token.tokens)
812
+ return this.parseTokens(token.tokens, parseImplicitEmptyParagraphs)
758
813
  }
759
814
  return null
760
815
  }
@@ -819,7 +874,13 @@ export class MarkdownManager {
819
874
  }
820
875
  }
821
876
 
822
- renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index = 0, level = 0): string {
877
+ renderNodeToMarkdown(
878
+ node: JSONContent,
879
+ parentNode?: JSONContent,
880
+ index = 0,
881
+ level = 0,
882
+ meta: Record<string, any> = {},
883
+ ): string {
823
884
  // if node is a text node, we simply return it's text content
824
885
  // marks are handled at the array level in renderNodesWithMarkBoundaries
825
886
  if (node.type === 'text') {
@@ -835,6 +896,7 @@ export class MarkdownManager {
835
896
  return ''
836
897
  }
837
898
 
899
+ const previousNode = Array.isArray(parentNode?.content) && index > 0 ? parentNode.content[index - 1] : undefined
838
900
  const helpers: MarkdownRendererHelpers = {
839
901
  renderChildren: (nodes, separator) => {
840
902
  const childLevel = handler.isIndenting ? level + 1 : level
@@ -845,6 +907,11 @@ export class MarkdownManager {
845
907
 
846
908
  return this.renderNodes(nodes, node, separator || '', index, childLevel)
847
909
  },
910
+ renderChild: (childNode, childIndex) => {
911
+ const childLevel = handler.isIndenting ? level + 1 : level
912
+
913
+ return this.renderNodeToMarkdown(childNode, node, childIndex, childLevel)
914
+ },
848
915
  indent: content => {
849
916
  return this.indentString + content
850
917
  },
@@ -855,8 +922,10 @@ export class MarkdownManager {
855
922
  index,
856
923
  level,
857
924
  parentType: parentNode?.type,
925
+ previousNode,
858
926
  meta: {
859
927
  parentAttrs: parentNode?.attrs,
928
+ ...meta,
860
929
  },
861
930
  }
862
931
 
@@ -1045,6 +1114,7 @@ export class MarkdownManager {
1045
1114
  syntheticNode,
1046
1115
  {
1047
1116
  renderChildren: () => placeholder,
1117
+ renderChild: () => placeholder,
1048
1118
  indent: (content: string) => content,
1049
1119
  wrapInBlock: (prefix: string, content: string) => prefix + content,
1050
1120
  },
@@ -1083,6 +1153,7 @@ export class MarkdownManager {
1083
1153
  syntheticNode,
1084
1154
  {
1085
1155
  renderChildren: () => placeholder,
1156
+ renderChild: () => placeholder,
1086
1157
  indent: (content: string) => content,
1087
1158
  wrapInBlock: (prefix: string, content: string) => prefix + content,
1088
1159
  },