{"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[0]\n}\n\nexport type MarkdownExtensionStorage = {\n manager: MarkdownManager\n}\n\nexport const Markdown = Extension.create({\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(\n this.editor.options.content,\n this.editor.options.contentType,\n )\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 (\n this.editor.options.content === undefined ||\n typeof this.editor.options.content !== 'string'\n ) {\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\n // If the parsed markdown produced no content, leave options.content\n // as-is (empty string) so that createDoc / DOMParser.parse can fill in\n // the required block node via ProseMirror's ContentMatch.fillBefore.\n if (json.content?.length) {\n this.editor.options.content = json\n }\n },\n})\n","import {\n type AnyExtension,\n type ExtendableConfig,\n type JSONContent,\n type MarkdownExtensionSpec,\n type MarkdownLexerConfiguration,\n type MarkdownParseHelpers,\n type MarkdownParseResult,\n type MarkdownRendererHelpers,\n type MarkdownToken,\n type MarkdownTokenizer,\n type RenderContext,\n callOrReturn,\n decodeHtmlEntities,\n encodeHtmlEntities,\n flattenExtensions,\n generateJSON,\n getExtensionField,\n getSchema,\n sortExtensions,\n} from '@tiptap/core'\nimport { type Lexer, type Token, type TokenizerExtension, type TokenizerThis, marked } from 'marked'\n\nimport {\n attrsEqual,\n closeMarksBeforeNode,\n findMarksToClose,\n findMarksToCloseAtEnd,\n findMarksToOpen,\n isTaskItem,\n reopenMarksAfterNode,\n wrapInMarkdownBlock,\n} from './utils.js'\n\n/**\n * Returns true when the element's tag is not a recognized standard HTML\n * element. Note: the calling code further cross-references this result\n * against schema parseDOM tags, so non-standard tags that are declared\n * by registered extensions are still treated as valid.\n *\n * Browsers expose this classification natively: any non-hyphenated tag that\n * is not part of the HTML/SVG/MathML namespaces is constructed as an\n * `HTMLUnknownElement`. Standard tags (``, `
`, …) and custom\n * elements (``, must contain a hyphen) are `HTMLElement` instances.\n */\nconst isHtmlUnknownElement = (element: Element): boolean => {\n const ctor = (window as any).HTMLUnknownElement\n return typeof ctor === 'function' && element instanceof ctor\n}\n\nexport class MarkdownManager {\n private markedInstance: typeof marked\n private activeParseLexer: Lexer | null = null\n private registry: Map\n private nodeTypeRegistry: Map\n /**\n * Order in which extensions were registered. Used to resolve mark nesting\n * deterministically when several marks open on the same text node.\n *\n * The flattened extensions passed to the manager are pre-sorted by Tiptap's\n * extension priority (descending), which is also the order ProseMirror uses\n * to assign mark ranks. Recording that index here lets the serializer place\n * higher-priority / lower-rank marks (e.g. link with priority 1000) on the\n * outside without inspecting any rendered markdown output.\n */\n private extensionRanks: Map = new Map()\n private indentStyle: 'space' | 'tab'\n private indentSize: number\n private baseExtensions: AnyExtension[] = []\n private extensions: AnyExtension[] = []\n /** Set of extension names whose `code` spec property is truthy (nodes and marks). */\n private codeTypes: Set = new Set()\n /** Lazy cache of tag names declared by the registered schema's parseDOM rules. */\n private schemaParseDomTagsCache: Set | null = null\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[0]\n indentation?: { style?: 'space' | 'tab'; size?: number }\n extensions: AnyExtension[]\n }) {\n this.markedInstance = options?.marked ?? marked\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. Sort by Tiptap priority\n // first (matching how the editor builds its schema) so the registration\n // index lines up with ProseMirror's mark rank — this is what the\n // serializer relies on to nest higher-priority marks like link outermost.\n if (options?.extensions) {\n this.baseExtensions = options.extensions\n const flattened = sortExtensions(flattenExtensions(options.extensions))\n flattened.forEach(ext => this.registerExtension(ext))\n }\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): void {\n // Keep track of all extensions for HTML parsing\n this.extensions.push(extension)\n\n // Track extensions that declare `code: true` so we can skip HTML entity\n // encoding inside code contexts without hardcoding specific type names.\n const isCode = callOrReturn(getExtensionField(extension, 'code'))\n\n const name = extension.name\n\n if (isCode) {\n this.codeTypes.add(name)\n }\n\n if (!this.extensionRanks.has(name)) {\n this.extensionRanks.set(name, this.extensionRanks.size)\n }\n const tokenName =\n (getExtensionField(\n extension,\n 'markdownTokenName',\n ) as ExtendableConfig['markdownTokenName']) || name\n const parseMarkdown = getExtensionField(extension, 'parseMarkdown') as\n | ExtendableConfig['parseMarkdown']\n | 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') ??\n null) as ExtendableConfig['markdownOptions']\n const isIndenting = markdownCfg?.indentsContent ?? false\n const htmlReopen = markdownCfg?.htmlReopen\n\n const spec: MarkdownExtensionSpec = {\n tokenName,\n nodeName: name,\n parseMarkdown,\n renderMarkdown,\n isIndenting,\n htmlReopen,\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 }\n\n private createLexer(): Lexer {\n return new this.markedInstance.Lexer()\n }\n\n private createTokenizerHelpers(lexer: Lexer): MarkdownLexerConfiguration {\n return {\n inlineTokens: (src: string) => lexer.inlineTokens(src),\n blockTokens: (src: string) => lexer.blockTokens(src),\n }\n }\n\n private tokenizeInline(src: string): MarkdownToken[] {\n return (this.activeParseLexer ?? this.createLexer()).inlineTokens(src) as MarkdownToken[]\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 const createTokenizerHelpers = this.createTokenizerHelpers.bind(this)\n const createLexer = this.createLexer.bind(this)\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, [], this.createTokenizerHelpers(this.createLexer()))\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(this: TokenizerThis, src, tokens) {\n const helper = this.lexer\n ? createTokenizerHelpers(this.lexer)\n : createTokenizerHelpers(createLexer())\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   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   entities or non-breaking space characters\n const cleanedOutput = markdown\n .replace(/ /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 const previousParseLexer = this.activeParseLexer\n const parseLexer = this.createLexer()\n\n this.activeParseLexer = parseLexer\n\n try {\n // Use a parse-scoped lexer so follow-up inline tokenization can reuse\n // the same configured lexer state without sharing it across parses.\n const tokens = parseLexer.lex(markdown) as MarkdownToken[]\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 } finally {\n this.activeParseLexer = previousParseLexer\n }\n }\n\n /**\n * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers.\n */\n private parseTokens(\n tokens: MarkdownToken[],\n parseImplicitEmptyParagraphs = false,\n ): JSONContent[] {\n const nonSpaceTokenIndexes = tokens.reduce((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(\n token,\n previousNonSpaceTokenIndex,\n nextNonSpaceTokenIndex,\n )\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(\n token: MarkdownToken,\n parseImplicitEmptyParagraphs = false,\n ): 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 & {\n type: 'taskItem'\n checked?: boolean\n indentLevel?: number\n }\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(\n ...nonEmptyLines.map(line => line.length - line.trimStart().length),\n )\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.tokenizeInline(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 tokenizeInline: (src: string) => this.tokenizeInline(src),\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 / / inner / / 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 – decode HTML entities so that e.g. `<` displays as `<` in the editor\n result.push({\n type: 'text',\n text: decodeHtmlEntities(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 // oxlint-disable-next-line prefer-string-starts-ends-with\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(\n result: any,\n ): 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: decodeHtmlEntities(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 an HTML token from marked into JSONContent using the registered\n * extensions' `parseHTML` rules. Falls back to literal text when the HTML\n * has nothing for the schema to keep.\n *\n * @param token Marked HTML token (block or inline).\n * @example\n * parseHTMLToken({ type: 'html', raw: 'hi', block: false })\n * // → text node with an italic mark\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 // If the HTML would parse to nothing meaningful, keep the original\n // characters as literal text instead of dropping them.\n if (this.isUnrecognizedHtml(html)) {\n return this.htmlAsLiteralText(html, !!token.block)\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 (\n parsed.content.length === 1 &&\n parsed.content[0].type === 'paragraph' &&\n parsed.content[0].content\n ) {\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 /**\n * Returns true when the HTML contains an element the browser classifies as\n * `HTMLUnknownElement` – unless a registered extension declares the tag\n * name in its parseDOM rules, in which case it is treated as a known\n * custom element.\n *\n * Recognized but empty elements such as `` or ``,\n * and hyphenated custom elements like ``, are not considered\n * unrecognized.\n *\n * @param html Raw HTML string from a marked token.\n * @example\n * isUnrecognizedHtml('') // → true\n * isUnrecognizedHtml('') // → false (empty, but real tag)\n * isUnrecognizedHtml('hi') // → false\n * isUnrecognizedHtml('') // → false (valid custom element)\n * isUnrecognizedHtml('
') // → false\n */\n private isUnrecognizedHtml(html: string): boolean {\n if (typeof window === 'undefined' || typeof window.DOMParser === 'undefined') {\n // Can't reliably detect without DOMParser, so assume it's recognized to avoid false positives\n return false\n }\n\n const dom = new window.DOMParser().parseFromString(`${html}`, 'text/html').body\n const elements = dom.querySelectorAll('*')\n\n if (elements.length === 0) {\n return false\n }\n\n const schemaTags = this.getSchemaParseDomTags()\n\n return Array.from(elements).some(el => {\n if (!isHtmlUnknownElement(el)) {\n return false\n }\n\n // If the tag is declared by a registered extension's parseDOM rule,\n // treat it as recognized even though the browser doesn't know it.\n const tagName = el.tagName.toLowerCase()\n\n return !schemaTags.has(tagName)\n })\n }\n\n /**\n * Collect the lower-cased tag names declared by the registered extensions'\n * parseDOM rules, so custom node/mark elements that use non-hyphenated,\n * non-standard tag names are treated as recognized HTML. Result is cached for the\n * lifetime of the manager since extensions don't change after registration.\n *\n * @example\n * // After registering an extension with parseDOM [{ tag: 'something' }]\n * getSchemaParseDomTags().has('something') // → true\n */\n private getSchemaParseDomTags(): Set {\n if (this.schemaParseDomTagsCache) {\n return this.schemaParseDomTagsCache\n }\n\n const tags = new Set()\n\n try {\n const schema = getSchema(this.baseExtensions)\n\n const collect = (spec: any) => {\n const parseDOM = spec?.parseDOM\n if (!Array.isArray(parseDOM)) {\n return\n }\n parseDOM.forEach((rule: any) => {\n if (typeof rule?.tag === 'string') {\n // Extract the bare tag name from selectors like \"something.example\"\n const match = rule.tag.match(/^[a-zA-Z][\\w-]*/)\n if (match) {\n tags.add(match[0].toLowerCase())\n }\n }\n })\n }\n\n Object.values(schema.nodes).forEach(type => collect((type as any).spec))\n Object.values(schema.marks).forEach(type => collect((type as any).spec))\n } catch {\n // If schema construction fails, leave the set empty – detection then\n // falls back to the HTMLUnknownElement check alone.\n }\n\n this.schemaParseDomTagsCache = tags\n return tags\n }\n\n /**\n * Build a JSONContent that preserves the original HTML markup as literal\n * text. Used when the HTML would otherwise be silently dropped during\n * schema-aware parsing.\n *\n * @param html Raw HTML string to preserve verbatim.\n * @param isBlock Whether to wrap the text in a paragraph node (block tokens)\n * or return it as a bare text node (inline tokens).\n * @example\n * htmlAsLiteralText('', true)\n * // → { type: 'paragraph', content: [{ type: 'text', text: '' }] }\n */\n private htmlAsLiteralText(html: string, isBlock: boolean): JSONContent | JSONContent[] | null {\n // Strip trailing whitespace/newlines that marked appends to block HTML\n // tokens so the rendered text doesn't end with stray blank lines.\n const text = html.replace(/\\s+$/, '')\n\n if (!text) {\n return null\n }\n\n if (isBlock) {\n return {\n type: 'paragraph',\n content: [{ type: 'text', text }],\n }\n }\n\n return { type: 'text', text }\n }\n\n /**\n * Encode HTML entities in text unless the node is inside a code context\n * (code mark or code-block parent) where literal characters should be preserved.\n */\n private encodeTextForMarkdown(text: string, node: JSONContent, parentNode?: JSONContent): string {\n const isInsideCode =\n (parentNode?.type != null && this.codeTypes.has(parentNode.type)) ||\n (node.marks || []).some(m => this.codeTypes.has(typeof m === 'string' ? m : m.type))\n\n return isInsideCode ? text : encodeHtmlEntities(text)\n }\n\n renderNodeToMarkdown(\n node: JSONContent,\n parentNode?: JSONContent,\n index = 0,\n level = 0,\n meta: Record = {},\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 this.encodeTextForMarkdown(node.text || '', node, parentNode)\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 =\n 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(\n (nodes as any).content as JSONContent[],\n node,\n separator || '',\n index,\n childLevel,\n )\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 = new Map()\n const reopenWithHtmlOnNextOpen = new Set()\n const markOpeningModes = 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 = this.encodeTextForMarkdown(node.text || '', node, parentNode)\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 = this.getMarksToOpenForSerialization(activeMarks, currentMarks, nextNode)\n const marksToClose = findMarksToClose(currentMarks, nextNode)\n\n // When marks simultaneously close (old) AND open (new) at this boundary, the naive\n // approach of appending old-close and prepending new-open produces interleaved\n // delimiters like `*456**` (italic open, text, bold close) instead of properly\n // nested `_456_**` (italic open, text, italic close, bold close).\n //\n // The fix: when both are present, defer old mark closings to the end of the node\n // (after the new marks also close), ensuring correct inner-before-outer order.\n //\n // If an already-active mark ends on this node while another mark opens on this same\n // node, we defer closing the active mark until the end of the node so nesting stays\n // valid (`**...++abc++**` instead of `**...++abc**++`).\n const activeMarksClosingHere = marksToClose.filter(markType => activeMarks.has(markType))\n const hasCrossedBoundary = activeMarksClosingHere.length > 0 && marksToOpen.length > 0\n\n let middleTrailingWhitespace = ''\n\n if (marksToClose.length > 0 && !hasCrossedBoundary) {\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\n if (!hasCrossedBoundary) {\n // Normal path: close marks that are ending here (no new marks opening simultaneously).\n // Reverse so the last-opened mark closes first (LIFO), preserving valid nesting.\n marksToClose\n .slice()\n .reverse()\n .forEach(markType => {\n if (!activeMarks.has(markType)) {\n return\n }\n\n const mark = currentMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(\n markType,\n mark,\n markOpeningModes.get(markType),\n )\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n if (activeMarks.has(markType)) {\n activeMarks.delete(markType)\n markOpeningModes.delete(markType)\n }\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 // Snapshot active mark types before opening new marks, so each new mark's delimiter\n // is chosen based on what is already active (not including itself).\n // When crossing a boundary, old marks are still in activeMarks here (not yet removed),\n // so new marks correctly see them as active context.\n marksToOpen.forEach(({ type, mark }) => {\n const openingMode = reopenWithHtmlOnNextOpen.has(type) ? 'html' : 'markdown'\n const openMarkdown = this.getMarkOpening(type, mark, openingMode)\n if (openMarkdown) {\n textContent = openMarkdown + textContent\n }\n markOpeningModes.set(type, openingMode)\n reopenWithHtmlOnNextOpen.delete(type)\n })\n\n if (!hasCrossedBoundary) {\n marksToOpen\n .slice()\n .reverse()\n .forEach(({ type, mark }) => {\n activeMarks.set(type, mark)\n })\n }\n\n // Add leading whitespace before the mark opening\n textContent = leadingWhitespace + textContent\n\n // Determine marks to close at the end of this node.\n // On a crossed boundary, we close new marks (inner) first, then old marks (outer),\n // ensuring correct nesting order. Both sets are removed from activeMarks so the\n // next node's marksToOpen will reopen whichever ones continue.\n let marksToCloseAtEnd: string[]\n if (hasCrossedBoundary) {\n const nextMarkTypes = new Set((nextNode?.marks || []).map((mark: any) => mark.type))\n\n marksToOpen.forEach(({ type }) => {\n if (nextMarkTypes.has(type) && this.getHtmlReopenTags(type)) {\n reopenWithHtmlOnNextOpen.add(type)\n }\n })\n\n // Sort the previously-active closures in LIFO order: the mark that\n // was opened last (innermost) must close first. activeMarks preserves\n // insertion order, so a higher indexOf means opened later = inner.\n const activeMarkKeys = Array.from(activeMarks.keys())\n const activeMarksClosingHereLifo = activeMarksClosingHere\n .slice()\n .sort((a, b) => activeMarkKeys.indexOf(b) - activeMarkKeys.indexOf(a))\n\n marksToCloseAtEnd = [\n ...marksToOpen.map(m => m.type), // inner (opened here) — close first\n ...activeMarksClosingHereLifo, // outer (were active before) — close last, LIFO\n ]\n } else {\n marksToCloseAtEnd = findMarksToCloseAtEnd(\n activeMarks,\n currentMarks,\n nextNode,\n this.markSetsEqual.bind(this),\n )\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) ?? currentMarks.get(markType)\n const closeMarkdown = this.getMarkClosing(markType, mark, markOpeningModes.get(markType))\n if (closeMarkdown) {\n textContent += closeMarkdown\n }\n activeMarks.delete(markType)\n markOpeningModes.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 // Only reopen marks that the node itself carries — marks don't skip over inline atoms.\n const nodeMarkTypes = new Set((node.marks || []).map((mark: { type: string }) => mark.type))\n const marksToReopen = new Map }>()\n const openingModesToReopen = new Map()\n activeMarks.forEach((mark, type) => {\n if (nodeMarkTypes.has(type)) {\n marksToReopen.set(type, mark)\n openingModesToReopen.set(type, markOpeningModes.get(type) ?? 'markdown')\n }\n })\n\n // Close all marks before the node\n const beforeMarkdown = closeMarksBeforeNode(activeMarks, (markType, mark) => {\n return this.getMarkClosing(markType, mark, markOpeningModes.get(markType))\n })\n markOpeningModes.clear()\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, (markType, mark) => {\n const openingMode = openingModesToReopen.get(markType) ?? 'markdown'\n markOpeningModes.set(markType, openingMode)\n return this.getMarkOpening(markType, mark, openingMode)\n })\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(\n markType: string,\n mark: any,\n openingMode: 'markdown' | 'html' = 'markdown',\n ): string {\n if (openingMode === 'html') {\n return this.getHtmlReopenTags(markType)?.open || ''\n }\n\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(\n markType: string,\n mark: any,\n openingMode: 'markdown' | 'html' = 'markdown',\n ): string {\n if (openingMode === 'html') {\n return this.getHtmlReopenTags(markType)?.close || ''\n }\n\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 * Returns the inline HTML tags an extension exposes for overlap-boundary\n * reopen handling, if that mark explicitly opted into HTML reopen mode.\n */\n private getHtmlReopenTags(markType: string): { open: string; close: string } | undefined {\n const handlers = this.getHandlersForNodeType(markType)\n const handler = handlers.length > 0 ? handlers[0] : undefined\n\n return handler?.htmlReopen\n }\n\n /**\n * Check if two mark sets are equal (same types and matching attributes).\n */\n private markSetsEqual(marks1: Map, marks2: Map): boolean {\n if (marks1.size !== marks2.size) {\n return false\n }\n\n return Array.from(marks1.entries()).every(([type, mark]) => {\n const otherMark = marks2.get(type)\n return otherMark && attrsEqual(mark.attrs, otherMark.attrs)\n })\n }\n\n /**\n * Decide the order in which marks open on the current text node.\n *\n * The returned array is iterated head-first when prepending opening\n * delimiters, so the first entry becomes the innermost mark in the emitted\n * markdown and the last becomes the outermost. Two stable signals drive\n * the order — neither one inspects any rendered markdown:\n *\n * 1. Marks that end on this node must be inner relative to marks that\n * continue into the next node, otherwise the delimiters interleave\n * instead of nesting.\n * 2. Within each lifetime group, marks are sorted so that lower\n * registration ranks (i.e. higher Tiptap extension priorities) end up\n * outermost. ProseMirror assigns mark ranks in the same priority-aware\n * order Tiptap uses when building the schema, so link (priority 1000)\n * naturally wraps bold/italic without the serializer needing to peek\n * at how any particular mark renders.\n */\n private getMarksToOpenForSerialization(\n activeMarks: Map,\n currentMarks: Map,\n nextNode: any,\n ) {\n const marksToOpen = findMarksToOpen(activeMarks, currentMarks)\n\n if (marksToOpen.length <= 1) {\n return marksToOpen\n }\n\n const nextMarks = nextNode?.marks || []\n\n // Helper: check if the next node has a mark with the same type AND\n // matching attributes. Two marks of the same type but with different\n // attributes are logically distinct and must not be treated as continuing.\n const continuesInNextNode = (markType: string, attrs: any) =>\n nextMarks.some((m: any) => m.type === markType && attrsEqual(m.attrs, attrs))\n\n // Higher rank → earlier in the array → innermost mark. Marks without a\n // recorded rank fall back to MAX_SAFE_INTEGER so they sort innermost,\n // matching the implicit \"registered last\" assumption for ad-hoc marks.\n const byRankInnerFirst = (a: { type: string }, b: { type: string }) => {\n const rankA = this.extensionRanks.get(a.type) ?? Number.MAX_SAFE_INTEGER\n const rankB = this.extensionRanks.get(b.type) ?? Number.MAX_SAFE_INTEGER\n\n if (rankA !== rankB) {\n return rankB - rankA\n }\n\n return a.type.localeCompare(b.type)\n }\n\n const endingHere = marksToOpen\n .filter(mark => !continuesInNextNode(mark.type, mark.mark.attrs))\n .sort(byRankInnerFirst)\n const continuing = marksToOpen\n .filter(mark => continuesInNextNode(mark.type, mark.mark.attrs))\n .sort(byRankInnerFirst)\n\n return [...endingHere, ...continuing]\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 * Compare two attribute objects for equality.\n * Handles null/undefined and asserts key presence in both objects so that\n * `{ foo: undefined }` and `{ bar: undefined }` are not treated as equal.\n */\nexport function attrsEqual(\n a: Record | null | undefined,\n b: Record | null | undefined,\n): boolean {\n if (a === b) {\n return true\n }\n if (!a || !b) {\n return false\n }\n\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n\n if (keysA.length !== keysB.length) {\n return false\n }\n\n return keysA.every(\n key => Object.prototype.hasOwnProperty.call(b, key) && Object.is(a[key], b[key]),\n )\n}\n\n/**\n * Identifies marks that need to be closed, based on the marks in the next node.\n * Compares both mark type and attributes — two marks of the same type with\n * different attributes are treated as distinct and need to be closed/reopened.\n */\nexport function findMarksToClose(currentMarks: Map, nextNode: any): string[] {\n const marksToClose: string[] = []\n\n Array.from(currentMarks.entries()).forEach(([markType, currentMark]) => {\n if (!nextNode) {\n marksToClose.push(markType)\n return\n }\n\n // Check if the next node has a mark of the same type with matching attributes\n const nextMark = (nextNode.marks || []).find(\n (mark: any) => mark.type === markType && attrsEqual(mark.attrs, currentMark.attrs),\n )\n\n if (!nextMark) {\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, or\n * active with different attributes). Two marks of the same type with different\n * attributes are treated as distinct — the old one must be closed and the new\n * one reopened.\n */\nexport function findMarksToOpen(\n activeMarks: Map,\n currentMarks: Map,\n): Array<{ type: string; mark: any }> {\n const marksToOpen: Array<{ type: string; mark: any }> = []\n Array.from(currentMarks.entries()).forEach(([markType, mark]) => {\n const activeMark = activeMarks.get(markType)\n\n // Open if the mark type is not active, or if the attributes differ\n if (!activeMark || !attrsEqual(activeMark.attrs, mark.attrs)) {\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 * Compares both mark type and attributes — two marks of the same type with\n * different attributes are treated as distinct and trigger a close/reopen.\n */\nexport function findMarksToCloseAtEnd(\n activeMarks: Map,\n currentMarks: Map,\n nextNode: any,\n markSetsEqual: (a: Map, b: Map) => boolean,\n): string[] {\n const isLastNode = !nextNode\n const nextNodeHasNoMarks = nextNode && (!nextNode.marks || nextNode.marks.length === 0)\n const nextNodeHasDifferentMarks =\n nextNode &&\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.marks) {\n Array.from(activeMarks.entries())\n .reverse()\n .forEach(([markType, activeMark]) => {\n // Check if nextNode has a mark of the same type with matching attrs\n const nextMark = nextNode.marks.find(\n (m: any) => m.type === markType && attrsEqual(m.attrs, activeMark.attrs),\n )\n if (!nextMark) {\n marksToCloseAtEnd.push(markType)\n }\n })\n } else if (isLastNode || nextNodeHasNoMarks) {\n // Close all active marks\n marksToCloseAtEnd.push(...Array.from(activeMarks.keys()).reverse())\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,\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,\n activeMarks: Map,\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): {\n isTask: boolean\n checked?: boolean\n indentLevel: number\n} {\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"]}