diff --git a/demos/src/GuideContent/GenerateText/Vue/index.html b/demos/src/GuideContent/GenerateText/Vue/index.html
new file mode 100644
index 00000000..3a14b395
--- /dev/null
+++ b/demos/src/GuideContent/GenerateText/Vue/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/src/GuideContent/GenerateText/Vue/index.spec.js b/demos/src/GuideContent/GenerateText/Vue/index.spec.js
new file mode 100644
index 00000000..af2cc5ed
--- /dev/null
+++ b/demos/src/GuideContent/GenerateText/Vue/index.spec.js
@@ -0,0 +1,7 @@
+context('/src/GuideContent/GenerateText/Vue/', () => {
+ before(() => {
+ cy.visit('/src/GuideContent/GenerateText/Vue/')
+ })
+
+ // TODO: Write tests
+})
diff --git a/demos/src/GuideContent/GenerateText/Vue/index.vue b/demos/src/GuideContent/GenerateText/Vue/index.vue
new file mode 100644
index 00000000..a3532c0d
--- /dev/null
+++ b/demos/src/GuideContent/GenerateText/Vue/index.vue
@@ -0,0 +1,59 @@
+
+ {{ output }}
+
+
+
diff --git a/docs/src/docPages/api/editor.md b/docs/src/docPages/api/editor.md
index 0b104de7..057cde27 100644
--- a/docs/src/docPages/api/editor.md
+++ b/docs/src/docPages/api/editor.md
@@ -21,6 +21,7 @@ Don’t confuse methods with [commands](/api/commands). Commands are used to cha
| `destroy()` | – | Stops the editor instance and unbinds all events. |
| `getHTML()` | – | Returns the current content as HTML. |
| `getJSON()` | – | Returns the current content as JSON. |
+| `getText()` | – | Returns the current content as text. |
| `getAttributes()` | `name` Name of the node or mark | Get attributes of the currently selected node or mark. |
| `isActive()` | `name` Name of the node or mark
`attrs` Attributes of the node or mark | Returns if the currently selected node or mark is active. |
| `isEditable` | - | Returns whether the editor is editable. |
diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts
index 4a35edfb..a6d122b8 100644
--- a/packages/core/src/Editor.ts
+++ b/packages/core/src/Editor.ts
@@ -11,7 +11,9 @@ import isActive from './helpers/isActive'
import removeElement from './utilities/removeElement'
import createDocument from './helpers/createDocument'
import getHTMLFromFragment from './helpers/getHTMLFromFragment'
+import getText from './helpers/getText'
import isNodeEmpty from './helpers/isNodeEmpty'
+import getTextSeralizersFromSchema from './helpers/getTextSeralizersFromSchema'
import createStyleTag from './utilities/createStyleTag'
import CommandManager from './CommandManager'
import ExtensionManager from './ExtensionManager'
@@ -21,6 +23,7 @@ import {
CanCommands,
ChainedCommands,
SingleCommands,
+ TextSerializer,
} from './types'
import * as extensions from './extensions'
import style from './style'
@@ -394,6 +397,27 @@ export class Editor extends EventEmitter {
return getHTMLFromFragment(this.state.doc, this.schema)
}
+ /**
+ * Get the document as text.
+ */
+ public getText(options?: {
+ blockSeparator?: string,
+ textSerializers?: Record,
+ }): string {
+ const {
+ blockSeparator = '\n\n',
+ textSerializers = {},
+ } = options || {}
+
+ return getText(this.state.doc, {
+ blockSeparator,
+ textSerializers: {
+ ...textSerializers,
+ ...getTextSeralizersFromSchema(this.schema),
+ },
+ })
+ }
+
/**
* Check if there is no content.
*/
diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts
index 07c96d66..78bf15fd 100644
--- a/packages/core/src/ExtensionManager.ts
+++ b/packages/core/src/ExtensionManager.ts
@@ -4,7 +4,12 @@ import { inputRules as inputRulesPlugin } from 'prosemirror-inputrules'
import { EditorView, Decoration } from 'prosemirror-view'
import { Plugin } from 'prosemirror-state'
import { Editor } from './Editor'
-import { Extensions, RawCommands, AnyConfig } from './types'
+import {
+ Extensions,
+ RawCommands,
+ AnyConfig,
+ TextSerializer,
+} from './types'
import getExtensionField from './helpers/getExtensionField'
import getSchemaByResolvedExtensions from './helpers/getSchemaByResolvedExtensions'
import getSchemaTypeByName from './helpers/getSchemaTypeByName'
@@ -330,31 +335,4 @@ export default class ExtensionManager {
return [extension.name, nodeview]
}))
}
-
- get textSerializers() {
- const { editor } = this
- const { nodeExtensions } = splitExtensions(this.extensions)
-
- return Object.fromEntries(nodeExtensions
- .filter(extension => !!getExtensionField(extension, 'renderText'))
- .map(extension => {
- const context = {
- name: extension.name,
- options: extension.options,
- editor,
- type: getNodeType(extension.name, this.schema),
- }
-
- const renderText = getExtensionField(extension, 'renderText', context)
-
- if (!renderText) {
- return []
- }
-
- const textSerializer = (props: { node: ProsemirrorNode }) => renderText(props)
-
- return [extension.name, textSerializer]
- }))
- }
-
}
diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts
index 82236614..ce901592 100644
--- a/packages/core/src/Node.ts
+++ b/packages/core/src/Node.ts
@@ -379,12 +379,13 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- editor: Editor,
- type: NodeType,
parent: ParentConfig>['renderText'],
},
props: {
node: ProseMirrorNode,
+ pos: number,
+ parent: ProseMirrorNode,
+ index: number,
}
) => string) | null,
diff --git a/packages/core/src/extensions/clipboardTextSerializer.ts b/packages/core/src/extensions/clipboardTextSerializer.ts
index e9208671..8a861821 100644
--- a/packages/core/src/extensions/clipboardTextSerializer.ts
+++ b/packages/core/src/extensions/clipboardTextSerializer.ts
@@ -1,37 +1,6 @@
-import { Editor } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
import { Extension } from '../Extension'
-
-const textBetween = (
- editor: Editor,
- from: number,
- to: number,
- blockSeparator?: string,
- leafText?: string,
-): string => {
- let text = ''
- let separated = true
-
- editor.state.doc.nodesBetween(from, to, (node, pos) => {
- const textSerializer = editor.extensionManager.textSerializers[node.type.name]
-
- if (textSerializer) {
- text += textSerializer({ node })
- separated = !blockSeparator
- } else if (node.isText) {
- text += node?.text?.slice(Math.max(from, pos) - pos, to - pos)
- separated = !blockSeparator
- } else if (node.isLeaf && leafText) {
- text += leafText
- separated = !blockSeparator
- } else if (!separated && node.isBlock) {
- text += blockSeparator
- separated = true
- }
- }, 0)
-
- return text
-}
+import getTextBetween from '../helpers/getTextBetween'
export const ClipboardTextSerializer = Extension.create({
name: 'editable',
@@ -43,9 +12,15 @@ export const ClipboardTextSerializer = Extension.create({
props: {
clipboardTextSerializer: () => {
const { editor } = this
- const { from, to } = editor.state.selection
+ const { state, extensionManager } = editor
+ const { doc, selection } = state
+ const { from, to } = selection
+ const { textSerializers } = extensionManager
+ const range = { from, to }
- return textBetween(editor, from, to, '\n')
+ return getTextBetween(doc, range, {
+ textSerializers,
+ })
},
},
}),
diff --git a/packages/core/src/helpers/generateText.ts b/packages/core/src/helpers/generateText.ts
new file mode 100644
index 00000000..300f745b
--- /dev/null
+++ b/packages/core/src/helpers/generateText.ts
@@ -0,0 +1,29 @@
+import { Node } from 'prosemirror-model'
+import getSchema from './getSchema'
+import { Extensions, JSONContent, TextSerializer } from '../types'
+import getTextSeralizersFromSchema from './getTextSeralizersFromSchema'
+import getText from './getText'
+
+export default function generateText(
+ doc: JSONContent,
+ extensions: Extensions,
+ options?: {
+ blockSeparator?: string,
+ textSerializers?: Record,
+ },
+): string {
+ const {
+ blockSeparator = '\n\n',
+ textSerializers = {},
+ } = options || {}
+ const schema = getSchema(extensions)
+ const contentNode = Node.fromJSON(schema, doc)
+
+ return getText(contentNode, {
+ blockSeparator,
+ textSerializers: {
+ ...textSerializers,
+ ...getTextSeralizersFromSchema(schema),
+ },
+ })
+}
diff --git a/packages/core/src/helpers/getSchemaByResolvedExtensions.ts b/packages/core/src/helpers/getSchemaByResolvedExtensions.ts
index 62c017db..fb1d9f21 100644
--- a/packages/core/src/helpers/getSchemaByResolvedExtensions.ts
+++ b/packages/core/src/helpers/getSchemaByResolvedExtensions.ts
@@ -77,6 +77,12 @@ export default function getSchemaByResolvedExtensions(extensions: Extensions): S
})
}
+ const renderText = getExtensionField(extension, 'renderText', context)
+
+ if (renderText) {
+ schema.toText = renderText
+ }
+
return [extension.name, schema]
}))
diff --git a/packages/core/src/helpers/getText.ts b/packages/core/src/helpers/getText.ts
new file mode 100644
index 00000000..94b2489b
--- /dev/null
+++ b/packages/core/src/helpers/getText.ts
@@ -0,0 +1,18 @@
+import { TextSerializer } from '../types'
+import { Node as ProseMirrorNode } from 'prosemirror-model'
+import getTextBetween from './getTextBetween'
+
+export default function getText(
+ node: ProseMirrorNode,
+ options?: {
+ blockSeparator?: string,
+ textSerializers?: Record,
+ },
+) {
+ const range = {
+ from: 0,
+ to: node.content.size,
+ }
+
+ return getTextBetween(node, range, options)
+}
diff --git a/packages/core/src/helpers/getTextBetween.ts b/packages/core/src/helpers/getTextBetween.ts
new file mode 100644
index 00000000..cd1233b1
--- /dev/null
+++ b/packages/core/src/helpers/getTextBetween.ts
@@ -0,0 +1,45 @@
+import { Range, TextSerializer } from '../types'
+import { Node as ProseMirrorNode } from 'prosemirror-model'
+
+export default function getTextBetween(
+ startNode: ProseMirrorNode,
+ range: Range,
+ options?: {
+ blockSeparator?: string,
+ textSerializers?: Record,
+ },
+): string {
+ const { from, to } = range
+ const {
+ blockSeparator = '\n\n',
+ textSerializers = {},
+ } = options || {}
+ let text = ''
+ let separated = true
+
+ startNode.nodesBetween(from, to, (node, pos, parent, index) => {
+ const textSerializer = textSerializers?.[node.type.name]
+
+ if (textSerializer) {
+ if (node.isBlock && !separated) {
+ text += blockSeparator
+ separated = true
+ }
+
+ text += textSerializer({
+ node,
+ pos,
+ parent,
+ index,
+ })
+ } else if (node.isText) {
+ text += node?.text?.slice(Math.max(from, pos) - pos, to - pos)
+ separated = false
+ } else if (node.isBlock && !separated) {
+ text += blockSeparator
+ separated = true
+ }
+ })
+
+ return text
+}
diff --git a/packages/core/src/helpers/getTextSeralizersFromSchema.ts b/packages/core/src/helpers/getTextSeralizersFromSchema.ts
new file mode 100644
index 00000000..fc9b16ef
--- /dev/null
+++ b/packages/core/src/helpers/getTextSeralizersFromSchema.ts
@@ -0,0 +1,9 @@
+import { Schema } from 'prosemirror-model'
+import { TextSerializer } from '../types'
+
+export default function getTextSeralizersFromSchema(schema: Schema): Record {
+ return Object.fromEntries(Object
+ .entries(schema.nodes)
+ .filter(([, node]) => node.spec.toText)
+ .map(([name, node]) => [name, node.spec.toText]))
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index aa0f4342..184b9dd6 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -20,6 +20,7 @@ export { default as findParentNode } from './helpers/findParentNode'
export { default as findParentNodeClosestToPos } from './helpers/findParentNodeClosestToPos'
export { default as generateHTML } from './helpers/generateHTML'
export { default as generateJSON } from './helpers/generateJSON'
+export { default as generateText } from './helpers/generateText'
export { default as getSchema } from './helpers/getSchema'
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
export { default as getDebugJSON } from './helpers/getDebugJSON'
@@ -30,6 +31,8 @@ export { default as getMarkType } from './helpers/getMarkType'
export { default as getMarksBetween } from './helpers/getMarksBetween'
export { default as getNodeAttributes } from './helpers/getNodeAttributes'
export { default as getNodeType } from './helpers/getNodeType'
+export { default as getText } from './helpers/getText'
+export { default as getTextBetween } from './helpers/getTextBetween'
export { default as isActive } from './helpers/isActive'
export { default as isList } from './helpers/isList'
export { default as isMarkActive } from './helpers/isMarkActive'
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index b3fcba20..b039d458 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -210,3 +210,10 @@ export type NodeWithPos = {
node: ProseMirrorNode,
pos: number,
}
+
+export type TextSerializer = (props: {
+ node: ProseMirrorNode,
+ pos: number,
+ parent: ProseMirrorNode,
+ index: number,
+}) => string