Skip to content

Commit

Permalink
feat(commands)!: Add insertMarkdownContentAt command (#439)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: With the introduction of `insertMarkdownContentAt`, the
API for `insertMarkdownContent` was changed to match the Tiptap's
implementation of `insertContent`/`insertContentAt`, which the
`insertMarkdown*` commands draw inspiration from.
  • Loading branch information
rfgamaral authored Sep 13, 2023
1 parent c66d6ea commit e87b892
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { RawCommands, selectionToInsertionEnd } from '@tiptap/core'
import { DOMParser } from 'prosemirror-model'

import { parseHtmlToElement } from '../../../../helpers/dom'
import { getHTMLSerializerInstance } from '../../../../serializers/html/html'

import type { Range } from '@tiptap/core'
import type { ParseOptions } from 'prosemirror-model'

/**
* Augment the official `@tiptap/core` module with extra commands so that the compiler knows about
* them. For this to work externally, a wildcard export needs to be added to the root `index.ts`.
*/
declare module '@tiptap/core' {
interface Commands<ReturnType> {
insertMarkdownContentAt: {
/**
* Inserts the provided Markdown content as HTML into the editor at a specific position.
*
* @param position The position or range the Markdown will be inserted in.
* @param markdown The Markdown content to parse and insert as HTML.
* @param options An optional object with the following parameters:
* @param options.parseOptions The parse options to use when the HTML content is parsed by ProseMirror.
* @param options.updateSelection Whether the selection should move to the newly inserted content.
*/
insertMarkdownContentAt: (
position: number | Range,
markdown: string,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
) => ReturnType
}
}
}

/**
* Inserts the provided Markdown content as HTML into the editor at a specific position.
*
* The solution for this function was inspired by how ProseMirror pastes content from the clipboard,
* and how Tiptap inserts content with the `insertContentAt` command.
*/
function insertMarkdownContentAt(
position: number | Range,
markdown: string,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
): ReturnType<RawCommands['insertMarkdownContentAt']> {
return ({ editor, tr, dispatch }) => {
// Check if the transaction should be dispatched
// ref: https://tiptap.dev/api/commands#dry-run-for-commands
if (dispatch) {
// Default values for command options must be set here
// (they do not work if set in the function signature)
options = {
parseOptions: {},
updateSelection: true,
...options,
}

// Get the start and end positions from the provided position
let { from, to } =
typeof position === 'number'
? { from: position, to: position }
: { from: position.from, to: position.to }

// If the selection is empty, and we're in an empty textblock, expand the start and end
// positions to include the whole textblock (not leaving empty paragraphs behind)
if (from === to) {
const { parent } = tr.doc.resolve(from)

if (parent.isTextblock && parent.childCount === 0) {
from -= 1
to += 1
}
}

// Parse the Markdown to HTML and then then into ProseMirror nodes
const htmlContent = getHTMLSerializerInstance(editor.schema).serialize(markdown)
const content = DOMParser.fromSchema(editor.schema).parse(
parseHtmlToElement(htmlContent),
options.parseOptions,
)

// Inserts the content into the editor while preserving the current selection
tr.replaceWith(from, to, content)

// Set the text cursor to the end of the inserted content
if (options.updateSelection) {
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
}
}

return true
}
}

export { insertMarkdownContentAt }
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { RawCommands } from '@tiptap/core'
import { DOMParser } from 'prosemirror-model'

import { parseHtmlToElement } from '../../../../helpers/dom'
import { getHTMLSerializerInstance } from '../../../../serializers/html/html'

import type { ParseOptions } from 'prosemirror-model'

Expand All @@ -14,42 +10,43 @@ declare module '@tiptap/core' {
interface Commands<ReturnType> {
insertMarkdownContent: {
/**
* Inserts the provided Markdown as HTML into the editor.
* Inserts the provided Markdown as HTML into the editor at the current position.
*
* @param markdown The Markdown to parse and insert as HTML.
* @param parseOptions The parse options for ProseMirror's DOMParser.
* @param markdown The Markdown content to parse and insert as HTML.
* @param options An optional object with the following parameters:
* @param options.parseOptions The parse options to use when the HTML content is parsed by ProseMirror.
* @param options.updateSelection Whether the selection should move to the newly inserted content.
*/
insertMarkdownContent: (markdown: string, parseOptions?: ParseOptions) => ReturnType
insertMarkdownContent: (
markdown: string,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
) => ReturnType
}
}
}

/**
* Inserts the provided Markdown as HTML into the editor.
* Inserts the provided Markdown as HTML into the editor at the current position.
*
* The solution for this function was inspired how ProseMirror pastes content from the clipboard,
* and how Tiptap inserts content with the `insertContentAt` command.
* The solution for this function was inspired by how Tiptap inserts content with the
* `insertContent` command.
*/
function insertMarkdownContent(
markdown: string,
parseOptions?: ParseOptions,
options?: {
parseOptions?: ParseOptions
updateSelection?: boolean
},
): ReturnType<RawCommands['insertMarkdownContent']> {
return ({ dispatch, editor, tr }) => {
// Check if the transaction should be dispatched
// ref: https://tiptap.dev/api/commands#dry-run-for-commands
if (dispatch) {
const htmlContent = getHTMLSerializerInstance(editor.schema).serialize(markdown)

// Inserts the HTML content into the editor while preserving the current selection
tr.replaceSelection(
DOMParser.fromSchema(editor.schema).parseSlice(
parseHtmlToElement(htmlContent),
parseOptions,
),
)
}

return true
return ({ commands, tr }) => {
return commands.insertMarkdownContentAt(
{ from: tr.selection.from, to: tr.selection.to },
markdown,
options,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Extension } from '@tiptap/core'
import { createParagraphEnd } from './commands/create-paragraph-end'
import { extendWordRange } from './commands/extend-word-range'
import { insertMarkdownContent } from './commands/insert-markdown-content'
import { insertMarkdownContentAt } from './commands/insert-markdown-content-at'

/**
* The `ExtraEditorCommands` extension is a collection of editor commands that provide additional
Expand All @@ -16,6 +17,7 @@ const ExtraEditorCommands = Extension.create({
createParagraphEnd,
extendWordRange,
insertMarkdownContent,
insertMarkdownContentAt,
}
},
})
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './constants/extension-priorities'
export * from './extensions/core/extra-editor-commands/commands/create-paragraph-end'
export * from './extensions/core/extra-editor-commands/commands/extend-word-range'
export * from './extensions/core/extra-editor-commands/commands/insert-markdown-content'
export * from './extensions/core/extra-editor-commands/commands/insert-markdown-content-at'
export { PlainTextKit } from './extensions/plain-text/plain-text-kit'
export type {
RichTextImageAttributes,
Expand Down
10 changes: 8 additions & 2 deletions stories/typist-editor/constants/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* @see https://jaspervdj.be/lorem-markdownum/
*/
const MARKDOWN_PLACEHOLDER = `# Volucrum operisque praepetis distulit
const MARKDOWN_PLACEHOLDER_LONG = `# Volucrum operisque praepetis distulit
## Et sponte tamen
Expand Down Expand Up @@ -48,4 +48,10 @@ python_cisc.smartSurfaceLeopard.encryptionOop(permalinkHardError - primary * tec
Erubuit cum caruisse et **passim** mentesque nulla, vox deus, est ut quis iracunda Propoetidas? Velis adeste parentis vincere, [opem](http://passis-locum.io/saepe), quos acceptaque atque.`

export { MARKDOWN_PLACEHOLDER }
const MARKDOWN_PLACEHOLDER_SHORT = `> ### Duobus concita cum
>
> Simul saevo subcrescit **aetherias non** Pisaeae Mater inquit miserabilis
attolle muneris mundi *vivacisque accepisse*. Petit ad nullo omni carbasa,
peccavimus [essem](http://incidere.com/) monte fluxerunt caeruleos subito.`

export { MARKDOWN_PLACEHOLDER_LONG, MARKDOWN_PLACEHOLDER_SHORT }
23 changes: 21 additions & 2 deletions stories/typist-editor/plain-text-functions.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { useCallback, useRef } from 'react'
import { Button } from '@doist/reactist'

import { action } from '@storybook/addon-actions'
import { Selection } from '@tiptap/pm/state'

import { TypistEditor, TypistEditorRef } from '../../src'

import { DEFAULT_ARG_TYPES } from './constants/defaults'
import { MARKDOWN_PLACEHOLDER } from './constants/markdown'
import { MARKDOWN_PLACEHOLDER_LONG, MARKDOWN_PLACEHOLDER_SHORT } from './constants/markdown'
import { TypistEditorDecorator } from './decorators/typist-editor-decorator/typist-editor-decorator'
import { Default } from './plain-text.stories'

Expand Down Expand Up @@ -41,7 +42,19 @@ export const Commands: StoryObj<typeof TypistEditor> = {
?.getEditor()
.chain()
.focus()
.insertMarkdownContent(MARKDOWN_PLACEHOLDER)
.insertMarkdownContent(MARKDOWN_PLACEHOLDER_LONG)
.run()
}, [])

const handleInsertMarkdownContentAtClick = useCallback(() => {
typistEditorRef.current
?.getEditor()
.chain()
.focus()
.insertMarkdownContentAt(
Selection.atEnd(typistEditorRef.current?.getEditor().state.doc),
MARKDOWN_PLACEHOLDER_SHORT,
)
.run()
}, [])

Expand All @@ -65,6 +78,12 @@ export const Commands: StoryObj<typeof TypistEditor> = {
>
insertMarkdownContent
</Button>
<Button
variant="secondary"
onClick={handleInsertMarkdownContentAtClick}
>
insertMarkdownContentAt
</Button>
</>
)
}}
Expand Down
23 changes: 21 additions & 2 deletions stories/typist-editor/rich-text-functions.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { useCallback, useRef } from 'react'
import { Button } from '@doist/reactist'

import { action } from '@storybook/addon-actions'
import { Selection } from '@tiptap/pm/state'

import { TypistEditor, TypistEditorRef } from '../../src'

import { DEFAULT_ARG_TYPES } from './constants/defaults'
import { MARKDOWN_PLACEHOLDER } from './constants/markdown'
import { MARKDOWN_PLACEHOLDER_LONG, MARKDOWN_PLACEHOLDER_SHORT } from './constants/markdown'
import { TypistEditorDecorator } from './decorators/typist-editor-decorator/typist-editor-decorator'
import { Default } from './rich-text.stories'

Expand Down Expand Up @@ -41,7 +42,19 @@ export const Commands: StoryObj<typeof TypistEditor> = {
?.getEditor()
.chain()
.focus()
.insertMarkdownContent(MARKDOWN_PLACEHOLDER)
.insertMarkdownContent(MARKDOWN_PLACEHOLDER_LONG)
.run()
}, [])

const handleInsertMarkdownContentAtClick = useCallback(() => {
typistEditorRef.current
?.getEditor()
.chain()
.focus()
.insertMarkdownContentAt(
Selection.atEnd(typistEditorRef.current?.getEditor().state.doc),
MARKDOWN_PLACEHOLDER_SHORT,
)
.run()
}, [])

Expand All @@ -65,6 +78,12 @@ export const Commands: StoryObj<typeof TypistEditor> = {
>
insertMarkdownContent
</Button>
<Button
variant="secondary"
onClick={handleInsertMarkdownContentAtClick}
>
insertMarkdownContentAt
</Button>
</>
)
}}
Expand Down

0 comments on commit e87b892

Please sign in to comment.