import { InputRule, callOrReturn } from '@tiptap/core'
import { Blockquote as BlockquoteTiptap } from '@tiptap/extension-blockquote'
import { canJoin, findWrapping } from '@tiptap/pm/transform'
import { Plugin } from 'prosemirror-state'
import { exitAfterBlock, exitBeforeBlock } from './helpers/index.js'
const inputRegex = /^\s*>\s$/
export const Blockquote = BlockquoteTiptap.extend({
addOptions () {
return {
levels: 1
}
},
addInputRules () {
return [
wrappingInputRule({
find: inputRegex,
type: this.type,
depth: this.options.levels
})
]
},
addKeyboardShortcuts () {
const thisExitAfterBlock = exitAfterBlock.bind(this)
const thisExitBeforeBlock = exitBeforeBlock.bind(this)
return {
'Mod-Shift-9': () => this.editor.commands.toggleBlockquote(),
ArrowUp: () => thisExitBeforeBlock(),
ArrowLeft: () => thisExitBeforeBlock(),
ArrowDown: () => thisExitAfterBlock(),
ArrowRight: () => thisExitAfterBlock()
}
},
addProseMirrorPlugins () {
return [
new Plugin({
props: {
transformPastedHTML: (html) => {
const { body } = new window.DOMParser().parseFromString(html, 'text/html')
const blockquote = body.querySelector('blockquote')
if (blockquote) {
removeNestedBlockquotes(blockquote)
}
return body.innerHTML
}
}
})
]
}
})
function removeNestedBlockquotes (blockquote) {
const childNodes = Array.from(blockquote.childNodes)
childNodes.forEach(child => {
if (child.nodeName === 'BLOCKQUOTE') {
removeNestedBlockquotes(child)
while (child.firstChild) {
blockquote.insertBefore(child.firstChild, child)
}
blockquote.removeChild(child)
}
})
}
function wrappingInputRule (config) {
return new InputRule({
find: config.find,
handler: ({ state, range, match }) => {
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
const tr = state.tr.delete(range.from, range.to)
const $start = tr.doc.resolve(range.from)
const blockRange = $start.blockRange()
const wrapping = blockRange && findWrapping(blockRange, config.type, attributes)
const { selection, storedMarks } = state
let depth = 0
// Traverse up the node hierarchy to count the blockquote nesting level
for (let i = selection.$from.depth; i > 0; i--) {
const node = selection.$from.node(i)
if (node.type.name === 'blockquote') {
depth++
}
}
if (depth >= config.depth || !wrapping) {
return null
}
tr.wrap(blockRange, wrapping)
if (config.keepMarks && config.editor) {
const { splittableMarks } = config.editor.extensionManager
const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks())
if (marks) {
const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name))
tr.ensureMarks(filteredMarks)
}
}
const before = tr.doc.resolve(range.from - 1).nodeBefore
if (
before &&
before.type === config.type &&
canJoin(tr.doc, range.from - 1) &&
(!config.joinPredicate || config.joinPredicate(match, before))
) {
tr.join(range.from - 1)
}
}
})
}