import { type InsertContentAtOptions as MarkdownInsertContentAtOptions, type InsertContentOptions as MarkdownInsertContentOptions, type SetContentOptions as MarkdownSetContentOptions, commands, Extension, } from '@tiptap/core' import type { marked } from 'marked' import MarkdownManager from './MarkdownManager.js' import type { ContentType } from './types.js' import { assumeContentType } from './utils.js' 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 } } export 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] } export type MarkdownExtensionStorage = { manager: MarkdownManager } export const Markdown = Extension.create({ name: 'markdown', addOptions() { return { indentation: { style: 'space', size: 2 }, marked: undefined, markedOptions: {}, } }, addCommands() { return { setContent: (content, options?: MarkdownSetContentOptions) => { // if no contentType is specified, we assume the content is in JSON format OR HTML format if (!options?.contentType) { return commands.setContent(content, options) } const actualContentType = assumeContentType(content, options?.contentType) if (actualContentType !== 'markdown' || !this.editor.markdown) { return commands.setContent(content, options) } const mdContent = this.editor.markdown.parse(content as string) return commands.setContent(mdContent, options) }, insertContent: (value, options?: MarkdownInsertContentOptions) => { // if no contentType is specified, we assume the content is in JSON format OR HTML format if (!options?.contentType) { return commands.insertContent(value, options) } const actualContentType = assumeContentType(value, options?.contentType) if (actualContentType !== 'markdown' || !this.editor.markdown) { return commands.insertContent(value, options) } const mdContent = this.editor.markdown.parse(value as string) return commands.insertContent(mdContent, options) }, insertContentAt: (position, value, options?: MarkdownInsertContentAtOptions) => { // if no contentType is specified, we assume the content is in JSON format OR HTML format if (!options?.contentType) { return commands.insertContentAt(position, value, options) } const actualContentType = assumeContentType(value, options?.contentType) if (actualContentType !== 'markdown' || !this.editor.markdown) { return commands.insertContentAt(position, value, options) } const mdContent = this.editor.markdown.parse(value as string) return commands.insertContentAt(position, mdContent, options) }, } }, addStorage() { return { manager: new MarkdownManager({ indentation: this.options.indentation, marked: this.options.marked, markedOptions: this.options.markedOptions, extensions: [], }), } }, onBeforeCreate() { if (this.editor.markdown) { console.error( '[tiptap][markdown]: There is already a `markdown` property on the editor instance. This might lead to unexpected behavior.', ) return } this.storage.manager = new MarkdownManager({ indentation: this.options.indentation, marked: this.options.marked, markedOptions: this.options.markedOptions, extensions: this.editor.extensionManager.baseExtensions, }) this.editor.markdown = this.storage.manager // add a `getMarkdown()` method to the editor this.editor.getMarkdown = () => { return this.storage.manager.serialize(this.editor.getJSON()) } if (!this.editor.options.contentType) { return } const assumedType = assumeContentType( this.editor.options.content, this.editor.options.contentType, ) if (assumedType !== 'markdown') { return } if (!this.editor.markdown) { throw new Error( '[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.', ) } if ( this.editor.options.content === undefined || typeof this.editor.options.content !== 'string' ) { throw new Error( '[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.', ) } const json = this.editor.markdown.parse(this.editor.options.content as string) // If the parsed markdown produced no content, leave options.content // as-is (empty string) so that createDoc / DOMParser.parse can fill in // the required block node via ProseMirror's ContentMatch.fillBefore. if (json.content?.length) { this.editor.options.content = json } }, })