Skip to content

Commit

Permalink
feat: add getText() and generateText() methods (fix #1428) (#1875)
Browse files Browse the repository at this point in the history
* move getTextBetween method

* add getText method

* refactoring

* refactoring

* refactoring

* move renderText to schema, add generateText method

* add GenerateText demo

* docs: update

* remove demo from html page
  • Loading branch information
philippkuehn authored Sep 9, 2021
1 parent 42e8755 commit fe6a3e7
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 64 deletions.
15 changes: 15 additions & 0 deletions demos/src/GuideContent/GenerateText/Vue/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('GuideContent/GenerateText', source)
</script>
</body>
</html>
7 changes: 7 additions & 0 deletions demos/src/GuideContent/GenerateText/Vue/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
context('/src/GuideContent/GenerateText/Vue/', () => {
before(() => {
cy.visit('/src/GuideContent/GenerateText/Vue/')
})

// TODO: Write tests
})
59 changes: 59 additions & 0 deletions demos/src/GuideContent/GenerateText/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<template>
<pre><code>{{ output }}</code></pre>
</template>

<script>
import { generateText } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import HardBreak from '@tiptap/extension-hard-break'
const json = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This is a paragraph.',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here is another paragraph …',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: '… with an hard break.',
},
],
},
],
}
export default {
computed: {
output() {
return generateText(json, [
Document,
Paragraph,
Text,
HardBreak,
// other extensions …
], {
// define a custom block separator if you want to
blockSeparator: '\n\n',
})
},
},
}
</script>
1 change: 1 addition & 0 deletions docs/src/docPages/api/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<br>`attrs` Attributes of the node or mark | Returns if the currently selected node or mark is active. |
| `isEditable` | - | Returns whether the editor is editable. |
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,6 +23,7 @@ import {
CanCommands,
ChainedCommands,
SingleCommands,
TextSerializer,
} from './types'
import * as extensions from './extensions'
import style from './style'
Expand Down Expand Up @@ -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, TextSerializer>,
}): string {
const {
blockSeparator = '\n\n',
textSerializers = {},
} = options || {}

return getText(this.state.doc, {
blockSeparator,
textSerializers: {
...textSerializers,
...getTextSeralizersFromSchema(this.schema),
},
})
}

/**
* Check if there is no content.
*/
Expand Down
34 changes: 6 additions & 28 deletions packages/core/src/ExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<NodeConfig['renderText']>(extension, 'renderText', context)

if (!renderText) {
return []
}

const textSerializer = (props: { node: ProsemirrorNode }) => renderText(props)

return [extension.name, textSerializer]
}))
}

}
5 changes: 3 additions & 2 deletions packages/core/src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,13 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options>>['renderText'],
},
props: {
node: ProseMirrorNode,
pos: number,
parent: ProseMirrorNode,
index: number,
}
) => string) | null,

Expand Down
43 changes: 9 additions & 34 deletions packages/core/src/extensions/clipboardTextSerializer.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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,
})
},
},
}),
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/helpers/generateText.ts
Original file line number Diff line number Diff line change
@@ -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, TextSerializer>,
},
): 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),
},
})
}
6 changes: 6 additions & 0 deletions packages/core/src/helpers/getSchemaByResolvedExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export default function getSchemaByResolvedExtensions(extensions: Extensions): S
})
}

const renderText = getExtensionField<NodeConfig['renderText']>(extension, 'renderText', context)

if (renderText) {
schema.toText = renderText
}

return [extension.name, schema]
}))

Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/helpers/getText.ts
Original file line number Diff line number Diff line change
@@ -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<string, TextSerializer>,
},
) {
const range = {
from: 0,
to: node.content.size,
}

return getTextBetween(node, range, options)
}
45 changes: 45 additions & 0 deletions packages/core/src/helpers/getTextBetween.ts
Original file line number Diff line number Diff line change
@@ -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, TextSerializer>,
},
): 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
}
9 changes: 9 additions & 0 deletions packages/core/src/helpers/getTextSeralizersFromSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Schema } from 'prosemirror-model'
import { TextSerializer } from '../types'

export default function getTextSeralizersFromSchema(schema: Schema): Record<string, TextSerializer> {
return Object.fromEntries(Object
.entries(schema.nodes)
.filter(([, node]) => node.spec.toText)
.map(([name, node]) => [name, node.spec.toText]))
}
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down
Loading

0 comments on commit fe6a3e7

Please sign in to comment.