import { AnyExtension, JSONContent, Extension, Content, MarkdownToken } from '@tiptap/core'; import { marked } from 'marked'; import { Fragment, Node } from '@tiptap/pm/model'; declare class MarkdownManager { private markedInstance; private activeParseLexer; private registry; private nodeTypeRegistry; /** * Order in which extensions were registered. Used to resolve mark nesting * deterministically when several marks open on the same text node. * * The flattened extensions passed to the manager are pre-sorted by Tiptap's * extension priority (descending), which is also the order ProseMirror uses * to assign mark ranks. Recording that index here lets the serializer place * higher-priority / lower-rank marks (e.g. link with priority 1000) on the * outside without inspecting any rendered markdown output. */ private extensionRanks; private indentStyle; private indentSize; private baseExtensions; private extensions; /** Set of extension names whose `code` spec property is truthy (nodes and marks). */ private codeTypes; /** Lazy cache of tag names declared by the registered schema's parseDOM rules. */ private schemaParseDomTagsCache; /** * Create a MarkdownManager. * @param options.marked Optional marked instance to use (injected). * @param options.markedOptions Optional options to pass to marked.setOptions * @param options.indentation Indentation settings (style and size). * @param options.extensions An array of Tiptap extensions to register for markdown parsing and rendering. */ constructor(options?: { marked?: typeof marked; markedOptions?: Parameters[0]; indentation?: { style?: 'space' | 'tab'; size?: number; }; extensions: AnyExtension[]; }); /** Returns the underlying marked instance. */ get instance(): typeof marked; /** Returns the correct indentCharacter (space or tab) */ get indentCharacter(): string; /** Returns the correct indentString repeated X times */ get indentString(): string; /** Helper to quickly check whether a marked instance is available. */ hasMarked(): boolean; /** * Register a Tiptap extension (Node/Mark/Extension). This will read * `markdownName`, `parseMarkdown`, `renderMarkdown` and `priority` from the * extension config (using the same resolution used across the codebase). */ registerExtension(extension: AnyExtension): void; private createLexer; private createTokenizerHelpers; private tokenizeInline; /** * Register a custom tokenizer with marked.js for parsing non-standard markdown syntax. */ private registerTokenizer; /** Get registered handlers for a token type and try each until one succeeds. */ private getHandlersForToken; /** Get the first handler for a token type (for backwards compatibility). */ private getHandlerForToken; /** Get registered handlers for a node type (for rendering). */ private getHandlersForNodeType; /** * Serialize a ProseMirror-like JSON document (or node array) to a Markdown string * using registered renderers and fallback renderers. */ serialize(docOrContent: JSONContent): string; /** * Check if the markdown output represents an empty document. * Empty documents may contain only   entities or non-breaking space characters * which are used by the Paragraph extension to preserve blank lines. */ private isEmptyOutput; /** * Parse markdown string into Tiptap JSON document using registered extension handlers. */ parse(markdown: string): JSONContent; /** * Convert an array of marked tokens into Tiptap JSON nodes using registered extension handlers. */ private parseTokens; private createImplicitEmptyParagraphsFromSpace; private countParagraphSeparators; /** * Parse a single token into Tiptap JSON using the appropriate registered handler. */ private parseToken; private lastParseResult; /** * Parse a list token, handling mixed bullet and task list items by splitting them into separate lists. * This ensures that consecutive task items and bullet items are grouped and parsed as separate list nodes. * * @param token The list token to parse * @returns Array of parsed list nodes, or null if parsing fails */ private parseListToken; /** * Parse a token using registered handlers (extracted for reuse). */ private parseTokenWithHandlers; /** * Creates helper functions for parsing markdown tokens. * @returns An object containing helper functions for parsing. */ private createParseHelpers; /** * Escape special regex characters in a string. */ private escapeRegex; /** * Parse inline tokens (bold, italic, links, etc.) into text nodes with marks. * This is the complex part that handles mark nesting and boundaries. */ private parseInlineTokens; /** * Apply a mark to content nodes. */ private applyMarkToContent; /** * Check if a parse result represents a mark to be applied. */ private isMarkResult; /** * Normalize parse results to ensure they're valid JSONContent. */ private normalizeParseResult; /** * Fallback parsing for common tokens when no specific handler is registered. */ private parseFallbackToken; /** * Parse an HTML token from marked into JSONContent using the registered * extensions' `parseHTML` rules. Falls back to literal text when the HTML * has nothing for the schema to keep. * * @param token Marked HTML token (block or inline). * @example * parseHTMLToken({ type: 'html', raw: 'hi', block: false }) * // → text node with an italic mark */ private parseHTMLToken; /** * Returns true when the HTML contains an element the browser classifies as * `HTMLUnknownElement` – unless a registered extension declares the tag * name in its parseDOM rules, in which case it is treated as a known * custom element. * * Recognized but empty elements such as `` or ``, * and hyphenated custom elements like ``, are not considered * unrecognized. * * @param html Raw HTML string from a marked token. * @example * isUnrecognizedHtml('') // → true * isUnrecognizedHtml('') // → false (empty, but real tag) * isUnrecognizedHtml('hi') // → false * isUnrecognizedHtml('') // → false (valid custom element) * isUnrecognizedHtml('
') // → false */ private isUnrecognizedHtml; /** * Collect the lower-cased tag names declared by the registered extensions' * parseDOM rules, so custom node/mark elements that use non-hyphenated, * non-standard tag names are treated as recognized HTML. Result is cached for the * lifetime of the manager since extensions don't change after registration. * * @example * // After registering an extension with parseDOM [{ tag: 'something' }] * getSchemaParseDomTags().has('something') // → true */ private getSchemaParseDomTags; /** * Build a JSONContent that preserves the original HTML markup as literal * text. Used when the HTML would otherwise be silently dropped during * schema-aware parsing. * * @param html Raw HTML string to preserve verbatim. * @param isBlock Whether to wrap the text in a paragraph node (block tokens) * or return it as a bare text node (inline tokens). * @example * htmlAsLiteralText('', true) * // → { type: 'paragraph', content: [{ type: 'text', text: '' }] } */ private htmlAsLiteralText; /** * Encode HTML entities in text unless the node is inside a code context * (code mark or code-block parent) where literal characters should be preserved. * Also backslash-escape markdown-significant characters in non-code text to * prevent them from being misinterpreted as formatting delimiters. */ private encodeTextForMarkdown; /** * Backslash-escape characters that have special meaning in markdown inline * syntax. This prevents literal characters in text nodes from being * misinterpreted as formatting delimiters when the output is parsed again. * * The set covers the most common inline markdown syntax characters. * Characters inside code blocks/code marks are skipped by the caller * (`encodeTextForMarkdown`) via the existing `isInsideCode` guard. */ private escapeMarkdownSyntax; renderNodeToMarkdown(node: JSONContent, parentNode?: JSONContent, index?: number, level?: number, meta?: Record): string; /** * Render a node or an array of nodes. Parent type controls how children * are joined (which determines newline insertion between children). */ renderNodes(nodeOrNodes: JSONContent | JSONContent[], parentNode?: JSONContent, separator?: string, index?: number, level?: number): string; /** * Render an array of nodes while properly tracking mark boundaries. * This handles cases where marks span across multiple text nodes. */ private renderNodesWithMarkBoundaries; /** * Get the opening markdown syntax for a mark type. */ private getMarkOpening; /** * Get the closing markdown syntax for a mark type. */ private getMarkClosing; /** * Returns the inline HTML tags an extension exposes for overlap-boundary * reopen handling, if that mark explicitly opted into HTML reopen mode. */ private getHtmlReopenTags; /** * Check if two mark sets are equal (same types and matching attributes). */ private markSetsEqual; /** * Decide the order in which marks open on the current text node. * * The returned array is iterated head-first when prepending opening * delimiters, so the first entry becomes the innermost mark in the emitted * markdown and the last becomes the outermost. Two stable signals drive * the order — neither one inspects any rendered markdown: * * 1. Marks that end on this node must be inner relative to marks that * continue into the next node, otherwise the delimiters interleave * instead of nesting. * 2. Within each lifetime group, marks are sorted so that lower * registration ranks (i.e. higher Tiptap extension priorities) end up * outermost. ProseMirror assigns mark ranks in the same priority-aware * order Tiptap uses when building the schema, so link (priority 1000) * naturally wraps bold/italic without the serializer needing to peek * at how any particular mark renders. */ private getMarksToOpenForSerialization; } type ContentType = 'json' | 'html' | 'markdown'; declare module '@tiptap/core' { interface Editor { /** * Get the content of the editor as markdown. */ getMarkdown: () => string; /** * The markdown manager instance. */ markdown?: MarkdownManager; } interface EditorOptions { /** * The content type the content is provided as. * * @default 'json' */ contentType?: ContentType; } interface Storage { markdown: MarkdownExtensionStorage; } interface InsertContentOptions { /** * The content type the content is provided as. * * @default 'json' */ contentType?: ContentType; } interface InsertContentAtOptions { /** * The content type the content is provided as. * * @default 'json' */ contentType?: ContentType; } interface SetContentOptions { /** * The content type the content is provided as. * * @default 'json' */ contentType?: ContentType; } } type MarkdownExtensionOptions = { /** * Configure the indentation style and size for lists and code blocks. * - `style`: Choose between spaces or tabs. Default is 'space'. * - `size`: Number of spaces or tabs for indentation. Default is 2. */ indentation?: { style?: 'space' | 'tab'; size?: number; }; /** * Use a custom version of `marked` for markdown parsing and serialization. * If not provided, the default `marked` instance will be used. */ marked?: typeof marked; /** * Options to pass to `marked.setOptions()`. * See the [marked documentation](https://marked.js.org/using_advanced#options) for available options. */ markedOptions?: Parameters[0]; }; type MarkdownExtensionStorage = { manager: MarkdownManager; }; declare const Markdown: Extension; /** * Wraps each line of the content with the given prefix. * @param prefix The prefix to wrap each line with. * @param content The content to wrap. * @returns The content with each line wrapped with the prefix. */ declare function wrapInMarkdownBlock(prefix: string, content: string): string; /** * Identifies marks that need to be closed, based on the marks in the next node. * Compares both mark type and attributes — two marks of the same type with * different attributes are treated as distinct and need to be closed/reopened. */ declare function findMarksToClose(currentMarks: Map, nextNode: any): string[]; /** * Identifies marks that need to be opened (in current node but not active, or * active with different attributes). Two marks of the same type with different * attributes are treated as distinct — the old one must be closed and the new * one reopened. */ declare function findMarksToOpen(activeMarks: Map, currentMarks: Map): Array<{ type: string; mark: any; }>; /** * Determines which marks need to be closed at the end of the current text node. * This handles cases where marks end at node boundaries or when transitioning * to nodes with different mark sets. * Compares both mark type and attributes — two marks of the same type with * different attributes are treated as distinct and trigger a close/reopen. */ declare function findMarksToCloseAtEnd(activeMarks: Map, currentMarks: Map, nextNode: any, markSetsEqual: (a: Map, b: Map) => boolean): string[]; /** * Closes active marks before rendering a non-text node. * Returns the closing markdown syntax and clears the active marks. */ declare function closeMarksBeforeNode(activeMarks: Map, getMarkClosing: (markType: string, mark: any) => string): string; /** * Reopens marks after rendering a non-text node. * Returns the opening markdown syntax and updates the active marks. */ declare function reopenMarksAfterNode(marksToReopen: Map, activeMarks: Map, getMarkOpening: (markType: string, mark: any) => string): string; /** * Check if a markdown list item token is a task item and extract its state. * * @param item The list item token to check * @returns Object containing isTask flag, checked state, and indentation level * * @example * ```ts * isTaskItem({ raw: '- [ ] Task' }) // { isTask: true, checked: false, indentLevel: 0 } * isTaskItem({ raw: ' - [x] Done' }) // { isTask: true, checked: true, indentLevel: 2 } * isTaskItem({ raw: '- Regular' }) // { isTask: false, indentLevel: 0 } * ``` */ declare function isTaskItem(item: MarkdownToken): { isTask: boolean; checked?: boolean; indentLevel: number; }; /** * Assumes the content type based off the content. * @param content The content to assume the type for. * @param contentType The content type that should be prioritized. */ declare function assumeContentType(content: (Content | Fragment | Node) | string, contentType: ContentType): ContentType; export { Markdown, type MarkdownExtensionOptions, type MarkdownExtensionStorage, MarkdownManager, assumeContentType, closeMarksBeforeNode, findMarksToClose, findMarksToCloseAtEnd, findMarksToOpen, isTaskItem, reopenMarksAfterNode, wrapInMarkdownBlock };