@doist/typist
Advanced tools
+6
-0
@@ -0,1 +1,7 @@ | ||
| ## [13.0.1](https://github.com/Doist/typist/compare/v13.0.0...v13.0.1) (2026-06-11) | ||
| ### Bug Fixes | ||
| * replace wrapping empty textblock when inserting block-only Markdown content ([#1372](https://github.com/Doist/typist/issues/1372)) ([9ed9697](https://github.com/Doist/typist/commit/9ed969732647541c15c4548df4da20af92ccfe31)) | ||
| ## [13.0.0](https://github.com/Doist/typist/compare/v12.0.0...v13.0.0) (2026-06-09) | ||
@@ -2,0 +8,0 @@ |
@@ -20,3 +20,3 @@ import { parseHtmlToElement } from "../../../../helpers/dom.js"; | ||
| }; | ||
| const { from, to } = typeof position === "number" ? { | ||
| let { from, to } = typeof position === "number" ? { | ||
| from: position, | ||
@@ -30,2 +30,13 @@ to: position | ||
| const content = DOMParser.fromSchema(editor.schema).parseSlice(parseHtmlToElement(htmlContent), options.parseOptions); | ||
| let isOnlyBlockContent = content.content.childCount > 0; | ||
| content.content.forEach((node) => { | ||
| isOnlyBlockContent = isOnlyBlockContent ? node.isBlock : false; | ||
| }); | ||
| if (from === to && isOnlyBlockContent) { | ||
| const { parent } = tr.doc.resolve(from); | ||
| if (parent.isTextblock && !parent.type.spec.code && !parent.childCount) { | ||
| from -= 1; | ||
| to += 1; | ||
| } | ||
| } | ||
| tr.replaceRange(from, to, content); | ||
@@ -32,0 +43,0 @@ if (options.updateSelection) selectionToInsertionEnd(tr, tr.steps.length - 1, -1); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"insert-markdown-content-at.js","names":[],"sources":["../../../../../src/extensions/core/extra-editor-commands/commands/insert-markdown-content-at.ts"],"sourcesContent":["import { RawCommands, selectionToInsertionEnd } from '@tiptap/core'\nimport { DOMParser } from '@tiptap/pm/model'\n\nimport { parseHtmlToElement } from '../../../../helpers/dom'\nimport { getHTMLSerializerInstance } from '../../../../serializers/html/html'\n\nimport type { Range } from '@tiptap/core'\nimport type { ParseOptions } from '@tiptap/pm/model'\n\n/**\n * Augment the official `@tiptap/core` module with extra commands so that the compiler knows about\n * them. For this to work externally, a wildcard export needs to be added to the root `index.ts`.\n */\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n insertMarkdownContentAt: {\n /**\n * Inserts the provided Markdown content as HTML into the editor at a specific position.\n *\n * @param position The position or range the Markdown will be inserted in.\n * @param markdown The Markdown content to parse and insert as HTML.\n * @param options An optional object with the following parameters:\n * @param options.parseOptions The parse options to use when the HTML content is parsed by ProseMirror.\n * @param options.updateSelection Whether the selection should move to the newly inserted content.\n */\n insertMarkdownContentAt: (\n position: number | Range,\n markdown: string,\n options?: {\n parseOptions?: ParseOptions\n updateSelection?: boolean\n },\n ) => ReturnType\n }\n }\n}\n\n/**\n * Inserts the provided Markdown content as HTML into the editor at a specific position.\n *\n * The solution for this function was inspired by how ProseMirror pastes content from the clipboard,\n * and how Tiptap inserts content with the `insertContentAt` command.\n */\nfunction insertMarkdownContentAt(\n position: number | Range,\n markdown: string,\n options?: {\n parseOptions?: ParseOptions\n updateSelection?: boolean\n },\n): ReturnType<RawCommands['insertMarkdownContentAt']> {\n return ({ editor, tr, dispatch }) => {\n // Check if the transaction should be dispatched\n // ref: https://tiptap.dev/api/commands#dry-run-for-commands\n if (dispatch) {\n // Default values for command options must be set here\n // (they do not work if set in the function signature)\n options = {\n parseOptions: {},\n updateSelection: true,\n ...options,\n }\n\n // Get the start and end positions from the provided position\n const { from, to } =\n typeof position === 'number'\n ? { from: position, to: position }\n : { from: position.from, to: position.to }\n\n // Parse the Markdown to HTML and then then into ProseMirror nodes\n const htmlContent = getHTMLSerializerInstance(editor.schema).serialize(markdown)\n const content = DOMParser.fromSchema(editor.schema).parseSlice(\n parseHtmlToElement(htmlContent),\n options.parseOptions,\n )\n\n // Inserts the content into the editor while preserving the current selection\n tr.replaceRange(from, to, content)\n\n // Set the text cursor to the end of the inserted content\n if (options.updateSelection) {\n selectionToInsertionEnd(tr, tr.steps.length - 1, -1)\n }\n }\n\n return true\n }\n}\n\nexport { insertMarkdownContentAt }\n"],"mappings":";;;;;;;;;;;AA2CA,SAAS,wBACL,UACA,UACA,SAIkD;CAClD,QAAQ,EAAE,QAAQ,IAAI,eAAe;EAGjC,IAAI,UAAU;GAGV,UAAU;IACN,cAAc,CAAC;IACf,iBAAiB;IACjB,GAAG;GACP;GAGA,MAAM,EAAE,MAAM,OACV,OAAO,aAAa,WACd;IAAE,MAAM;IAAU,IAAI;GAAS,IAC/B;IAAE,MAAM,SAAS;IAAM,IAAI,SAAS;GAAG;GAGjD,MAAM,cAAc,0BAA0B,OAAO,MAAM,CAAC,CAAC,UAAU,QAAQ;GAC/E,MAAM,UAAU,UAAU,WAAW,OAAO,MAAM,CAAC,CAAC,WAChD,mBAAmB,WAAW,GAC9B,QAAQ,YACZ;GAGA,GAAG,aAAa,MAAM,IAAI,OAAO;GAGjC,IAAI,QAAQ,iBACR,wBAAwB,IAAI,GAAG,MAAM,SAAS,GAAG,EAAE;EAE3D;EAEA,OAAO;CACX;AACJ"} | ||
| {"version":3,"file":"insert-markdown-content-at.js","names":[],"sources":["../../../../../src/extensions/core/extra-editor-commands/commands/insert-markdown-content-at.ts"],"sourcesContent":["import { RawCommands, selectionToInsertionEnd } from '@tiptap/core'\nimport { DOMParser } from '@tiptap/pm/model'\n\nimport { parseHtmlToElement } from '../../../../helpers/dom'\nimport { getHTMLSerializerInstance } from '../../../../serializers/html/html'\n\nimport type { Range } from '@tiptap/core'\nimport type { ParseOptions } from '@tiptap/pm/model'\n\n/**\n * Augment the official `@tiptap/core` module with extra commands so that the compiler knows about\n * them. For this to work externally, a wildcard export needs to be added to the root `index.ts`.\n */\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n insertMarkdownContentAt: {\n /**\n * Inserts the provided Markdown content as HTML into the editor at a specific position.\n *\n * @param position The position or range the Markdown will be inserted in.\n * @param markdown The Markdown content to parse and insert as HTML.\n * @param options An optional object with the following parameters:\n * @param options.parseOptions The parse options to use when the HTML content is parsed by ProseMirror.\n * @param options.updateSelection Whether the selection should move to the newly inserted content.\n */\n insertMarkdownContentAt: (\n position: number | Range,\n markdown: string,\n options?: {\n parseOptions?: ParseOptions\n updateSelection?: boolean\n },\n ) => ReturnType\n }\n }\n}\n\n/**\n * Inserts the provided Markdown content as HTML into the editor at a specific position.\n *\n * The solution for this function was inspired by how ProseMirror pastes content from the clipboard,\n * and how Tiptap inserts content with the `insertContentAt` command.\n */\nfunction insertMarkdownContentAt(\n position: number | Range,\n markdown: string,\n options?: {\n parseOptions?: ParseOptions\n updateSelection?: boolean\n },\n): ReturnType<RawCommands['insertMarkdownContentAt']> {\n return ({ editor, tr, dispatch }) => {\n // Check if the transaction should be dispatched\n // ref: https://tiptap.dev/api/commands#dry-run-for-commands\n if (dispatch) {\n // Default values for command options must be set here\n // (they do not work if set in the function signature)\n options = {\n parseOptions: {},\n updateSelection: true,\n ...options,\n }\n\n // Get the start and end positions from the provided position\n let { from, to } =\n typeof position === 'number'\n ? { from: position, to: position }\n : { from: position.from, to: position.to }\n\n // Parse the Markdown to HTML and then then into ProseMirror nodes\n const htmlContent = getHTMLSerializerInstance(editor.schema).serialize(markdown)\n const content = DOMParser.fromSchema(editor.schema).parseSlice(\n parseHtmlToElement(htmlContent),\n options.parseOptions,\n )\n\n // Check if the parsed content is non-empty and composed of block nodes only (the\n // initial value guards against an empty insert deleting the wrapping textblock below)\n let isOnlyBlockContent = content.content.childCount > 0\n\n content.content.forEach((node) => {\n isOnlyBlockContent = isOnlyBlockContent ? node.isBlock : false\n })\n\n // Expand the insertion range to replace the wrapping empty textblock when only block\n // content is inserted at a cursor position, mirroring the official `insertContentAt`\n // command behaviour (e.g., a table pasted into an empty paragraph should replace the\n // paragraph, instead of keeping the empty paragraph below the table)\n if (from === to && isOnlyBlockContent) {\n const { parent } = tr.doc.resolve(from)\n\n const isEmptyTextBlock =\n parent.isTextblock && !parent.type.spec.code && !parent.childCount\n\n if (isEmptyTextBlock) {\n from -= 1\n to += 1\n }\n }\n\n // Inserts the content into the editor while preserving the current selection\n tr.replaceRange(from, to, content)\n\n // Set the text cursor to the end of the inserted content\n if (options.updateSelection) {\n selectionToInsertionEnd(tr, tr.steps.length - 1, -1)\n }\n }\n\n return true\n }\n}\n\nexport { insertMarkdownContentAt }\n"],"mappings":";;;;;;;;;;;AA2CA,SAAS,wBACL,UACA,UACA,SAIkD;CAClD,QAAQ,EAAE,QAAQ,IAAI,eAAe;EAGjC,IAAI,UAAU;GAGV,UAAU;IACN,cAAc,CAAC;IACf,iBAAiB;IACjB,GAAG;GACP;GAGA,IAAI,EAAE,MAAM,OACR,OAAO,aAAa,WACd;IAAE,MAAM;IAAU,IAAI;GAAS,IAC/B;IAAE,MAAM,SAAS;IAAM,IAAI,SAAS;GAAG;GAGjD,MAAM,cAAc,0BAA0B,OAAO,MAAM,CAAC,CAAC,UAAU,QAAQ;GAC/E,MAAM,UAAU,UAAU,WAAW,OAAO,MAAM,CAAC,CAAC,WAChD,mBAAmB,WAAW,GAC9B,QAAQ,YACZ;GAIA,IAAI,qBAAqB,QAAQ,QAAQ,aAAa;GAEtD,QAAQ,QAAQ,SAAS,SAAS;IAC9B,qBAAqB,qBAAqB,KAAK,UAAU;GAC7D,CAAC;GAMD,IAAI,SAAS,MAAM,oBAAoB;IACnC,MAAM,EAAE,WAAW,GAAG,IAAI,QAAQ,IAAI;IAKtC,IAFI,OAAO,eAAe,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,OAAO,YAEtC;KAClB,QAAQ;KACR,MAAM;IACV;GACJ;GAGA,GAAG,aAAa,MAAM,IAAI,OAAO;GAGjC,IAAI,QAAQ,iBACR,wBAAwB,IAAI,GAAG,MAAM,SAAS,GAAG,EAAE;EAE3D;EAEA,OAAO;CACX;AACJ"} |
+3
-3
| { | ||
| "name": "@doist/typist", | ||
| "description": "The mighty Tiptap-based rich-text editor React component that powers Doist products.", | ||
| "version": "13.0.0", | ||
| "version": "13.0.1", | ||
| "license": "MIT", | ||
@@ -107,3 +107,3 @@ "homepage": "https://typist.doist.dev/", | ||
| "devDependencies": { | ||
| "@doist/reactist": "30.1.4", | ||
| "@doist/reactist": "33.0.1", | ||
| "@mdx-js/react": "3.1.1", | ||
@@ -121,3 +121,3 @@ "@semantic-release/changelog": "6.0.3", | ||
| "@types/lodash-es": "4.17.12", | ||
| "@types/react": "18.3.30", | ||
| "@types/react": "18.3.31", | ||
| "@types/react-dom": "18.3.7", | ||
@@ -124,0 +124,0 @@ "@types/react-syntax-highlighter": "15.5.13", |
428491
0.5%3349
0.33%