UNPKG

@tiptap/extension-blockquote

Version:

blockquote extension for tiptap

1 lines 8.78 kB
{"version":3,"sources":["../src/index.ts","../src/blockquote.tsx","../src/handleBackspace.ts"],"sourcesContent":["import { Blockquote } from './blockquote.jsx'\n\nexport * from './blockquote.jsx'\n\nexport default Blockquote\n","/** @jsxImportSource @tiptap/core */\nimport { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core'\n\nimport { handleBackspace } from './handleBackspace.js'\n\nexport interface BlockquoteOptions {\n /**\n * HTML attributes to add to the blockquote element\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n blockQuote: {\n /**\n * Set a blockquote node\n */\n setBlockquote: () => ReturnType\n /**\n * Toggle a blockquote node\n */\n toggleBlockquote: () => ReturnType\n /**\n * Unset a blockquote node\n */\n unsetBlockquote: () => ReturnType\n }\n }\n}\n\n/**\n * Matches a blockquote to a `>` as input.\n */\nexport const inputRegex = /^\\s*>\\s$/\n\n/**\n * This extension allows you to create blockquotes.\n * @see https://tiptap.dev/api/nodes/blockquote\n */\nexport const Blockquote = Node.create<BlockquoteOptions>({\n name: 'blockquote',\n\n addOptions() {\n return {\n HTMLAttributes: {},\n }\n },\n\n content: 'block+',\n\n group: 'block',\n\n defining: true,\n\n parseHTML() {\n return [{ tag: 'blockquote' }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return (\n <blockquote {...mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)}>\n <slot />\n </blockquote>\n )\n },\n\n parseMarkdown: (token, helpers) => {\n const parseBlockChildren = helpers.parseBlockChildren ?? helpers.parseChildren\n\n return helpers.createNode('blockquote', undefined, parseBlockChildren(token.tokens || []))\n },\n\n renderMarkdown: (node, h) => {\n if (!node.content) {\n return ''\n }\n\n // Use a single '>' prefix regardless of nesting level\n // Nested blockquotes will add their own '>' prefix recursively\n const prefix = '>'\n const result: string[] = []\n\n node.content.forEach((child, index) => {\n const childContent = h.renderChild?.(child, index) ?? h.renderChildren([child])\n const lines = childContent.split('\\n')\n\n const linesWithPrefix = lines.map(line => {\n // Don't add prefix to empty lines\n if (line.trim() === '') {\n return prefix\n }\n\n // Nested blockquotes will already have their own prefixes\n // We just need to add our own prefix at the start\n return `${prefix} ${line}`\n })\n\n result.push(linesWithPrefix.join('\\n'))\n })\n\n // Add separator lines between children\n return result.join(`\\n${prefix}\\n`)\n },\n\n addCommands() {\n return {\n setBlockquote:\n () =>\n ({ commands }) => {\n return commands.wrapIn(this.name)\n },\n toggleBlockquote:\n () =>\n ({ commands }) => {\n return commands.toggleWrap(this.name)\n },\n unsetBlockquote:\n () =>\n ({ commands }) => {\n return commands.lift(this.name)\n },\n }\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-Shift-b': () => this.editor.commands.toggleBlockquote(),\n Backspace: () => handleBackspace(this.editor, this.type),\n }\n },\n\n addInputRules() {\n return [\n wrappingInputRule({\n find: inputRegex,\n type: this.type,\n }),\n ]\n },\n})\n","import type { Editor } from '@tiptap/core'\nimport type { NodeType } from '@tiptap/pm/model'\nimport { TextSelection } from '@tiptap/pm/state'\n\n/**\n * Restructure the blockquote boundary at the caret.\n *\n * Two cases are handled in a single backspace:\n *\n * 1. Caret at the start of a non-first child of a blockquote — lift the\n * current child out, splitting the blockquote around it.\n * 2. Caret at the start of a top-level textblock whose previous sibling is\n * a blockquote with a textblock last child — merge the current\n * textblock's inline content into the blockquote's last textblock\n * instead of letting joinBackward pull the paragraph back inside.\n *\n * Returns true when the backspace was consumed.\n */\nexport const handleBackspace = (editor: Editor, type: NodeType): boolean => {\n const { state, view } = editor\n const { selection } = state\n if (!selection.empty) return false\n\n const { $from } = selection\n if ($from.parentOffset !== 0) return false\n\n const parentDepth = $from.depth - 1\n // At the very start of the document the caret can resolve to the top (doc)\n // level — for example a gap cursor before a leading image — where there is\n // no parent block. Bail out so backspace is a no-op instead of dereferencing\n // an undefined parent at a negative depth. (#7973)\n if (parentDepth < 0) return false\n\n const parent = $from.node(parentDepth)\n const index = $from.index(parentDepth)\n if (index === 0) return false\n\n // Non-first child of a blockquote: lift to split the blockquote around it.\n if (parent.type === type) {\n return editor.commands.lift(type.name)\n }\n\n // Previous sibling is a blockquote whose last child is a textblock:\n // merge the inline content in instead of letting joinBackward pull the\n // paragraph back inside the blockquote.\n const previous = parent.child(index - 1)\n if (previous.type !== type || !previous.lastChild?.isTextblock) {\n return false\n }\n\n const blockStart = $from.before()\n // `blockStart` sits in the shared parent at the position right after\n // the previous blockquote. In ProseMirror coordinates, each closing\n // token costs one position: step one back to land inside the blockquote\n // right after its last child, then one more to land inside that last\n // child at the end of its inline content.\n const insideBlockquoteEnd = blockStart - 1\n const targetPos = insideBlockquoteEnd - 1\n const { tr } = state\n tr.delete(blockStart, $from.after()).insert(targetPos, $from.parent.content)\n tr.setSelection(TextSelection.create(tr.doc, targetPos))\n view.dispatch(tr.scrollIntoView())\n return true\n}\n"],"mappings":"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","names":[]}