mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-10 19:23:49 +08:00
* 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
This commit is contained in:
parent
42e8755d87
commit
fe6a3e7491
15
demos/src/GuideContent/GenerateText/Vue/index.html
Normal file
15
demos/src/GuideContent/GenerateText/Vue/index.html
Normal file
@ -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
demos/src/GuideContent/GenerateText/Vue/index.spec.js
Normal file
7
demos/src/GuideContent/GenerateText/Vue/index.spec.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
context('/src/GuideContent/GenerateText/Vue/', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.visit('/src/GuideContent/GenerateText/Vue/')
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Write tests
|
||||||
|
})
|
59
demos/src/GuideContent/GenerateText/Vue/index.vue
Normal file
59
demos/src/GuideContent/GenerateText/Vue/index.vue
Normal file
@ -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>
|
@ -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. |
|
| `destroy()` | – | Stops the editor instance and unbinds all events. |
|
||||||
| `getHTML()` | – | Returns the current content as HTML. |
|
| `getHTML()` | – | Returns the current content as HTML. |
|
||||||
| `getJSON()` | – | Returns the current content as JSON. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `isEditable` | - | Returns whether the editor is editable. |
|
||||||
|
@ -11,7 +11,9 @@ import isActive from './helpers/isActive'
|
|||||||
import removeElement from './utilities/removeElement'
|
import removeElement from './utilities/removeElement'
|
||||||
import createDocument from './helpers/createDocument'
|
import createDocument from './helpers/createDocument'
|
||||||
import getHTMLFromFragment from './helpers/getHTMLFromFragment'
|
import getHTMLFromFragment from './helpers/getHTMLFromFragment'
|
||||||
|
import getText from './helpers/getText'
|
||||||
import isNodeEmpty from './helpers/isNodeEmpty'
|
import isNodeEmpty from './helpers/isNodeEmpty'
|
||||||
|
import getTextSeralizersFromSchema from './helpers/getTextSeralizersFromSchema'
|
||||||
import createStyleTag from './utilities/createStyleTag'
|
import createStyleTag from './utilities/createStyleTag'
|
||||||
import CommandManager from './CommandManager'
|
import CommandManager from './CommandManager'
|
||||||
import ExtensionManager from './ExtensionManager'
|
import ExtensionManager from './ExtensionManager'
|
||||||
@ -21,6 +23,7 @@ import {
|
|||||||
CanCommands,
|
CanCommands,
|
||||||
ChainedCommands,
|
ChainedCommands,
|
||||||
SingleCommands,
|
SingleCommands,
|
||||||
|
TextSerializer,
|
||||||
} from './types'
|
} from './types'
|
||||||
import * as extensions from './extensions'
|
import * as extensions from './extensions'
|
||||||
import style from './style'
|
import style from './style'
|
||||||
@ -394,6 +397,27 @@ export class Editor extends EventEmitter {
|
|||||||
return getHTMLFromFragment(this.state.doc, this.schema)
|
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.
|
* Check if there is no content.
|
||||||
*/
|
*/
|
||||||
|
@ -4,7 +4,12 @@ import { inputRules as inputRulesPlugin } from 'prosemirror-inputrules'
|
|||||||
import { EditorView, Decoration } from 'prosemirror-view'
|
import { EditorView, Decoration } from 'prosemirror-view'
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
import { Extensions, RawCommands, AnyConfig } from './types'
|
import {
|
||||||
|
Extensions,
|
||||||
|
RawCommands,
|
||||||
|
AnyConfig,
|
||||||
|
TextSerializer,
|
||||||
|
} from './types'
|
||||||
import getExtensionField from './helpers/getExtensionField'
|
import getExtensionField from './helpers/getExtensionField'
|
||||||
import getSchemaByResolvedExtensions from './helpers/getSchemaByResolvedExtensions'
|
import getSchemaByResolvedExtensions from './helpers/getSchemaByResolvedExtensions'
|
||||||
import getSchemaTypeByName from './helpers/getSchemaTypeByName'
|
import getSchemaTypeByName from './helpers/getSchemaTypeByName'
|
||||||
@ -330,31 +335,4 @@ export default class ExtensionManager {
|
|||||||
return [extension.name, nodeview]
|
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]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -379,12 +379,13 @@ declare module '@tiptap/core' {
|
|||||||
this: {
|
this: {
|
||||||
name: string,
|
name: string,
|
||||||
options: Options,
|
options: Options,
|
||||||
editor: Editor,
|
|
||||||
type: NodeType,
|
|
||||||
parent: ParentConfig<NodeConfig<Options>>['renderText'],
|
parent: ParentConfig<NodeConfig<Options>>['renderText'],
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
node: ProseMirrorNode,
|
node: ProseMirrorNode,
|
||||||
|
pos: number,
|
||||||
|
parent: ProseMirrorNode,
|
||||||
|
index: number,
|
||||||
}
|
}
|
||||||
) => string) | null,
|
) => string) | null,
|
||||||
|
|
||||||
|
@ -1,37 +1,6 @@
|
|||||||
import { Editor } from '@tiptap/core'
|
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||||
import { Extension } from '../Extension'
|
import { Extension } from '../Extension'
|
||||||
|
import getTextBetween from '../helpers/getTextBetween'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClipboardTextSerializer = Extension.create({
|
export const ClipboardTextSerializer = Extension.create({
|
||||||
name: 'editable',
|
name: 'editable',
|
||||||
@ -43,9 +12,15 @@ export const ClipboardTextSerializer = Extension.create({
|
|||||||
props: {
|
props: {
|
||||||
clipboardTextSerializer: () => {
|
clipboardTextSerializer: () => {
|
||||||
const { editor } = this
|
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,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
29
packages/core/src/helpers/generateText.ts
Normal file
29
packages/core/src/helpers/generateText.ts
Normal file
@ -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),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -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]
|
return [extension.name, schema]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
18
packages/core/src/helpers/getText.ts
Normal file
18
packages/core/src/helpers/getText.ts
Normal file
@ -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
packages/core/src/helpers/getTextBetween.ts
Normal file
45
packages/core/src/helpers/getTextBetween.ts
Normal file
@ -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
packages/core/src/helpers/getTextSeralizersFromSchema.ts
Normal file
9
packages/core/src/helpers/getTextSeralizersFromSchema.ts
Normal file
@ -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]))
|
||||||
|
}
|
@ -20,6 +20,7 @@ export { default as findParentNode } from './helpers/findParentNode'
|
|||||||
export { default as findParentNodeClosestToPos } from './helpers/findParentNodeClosestToPos'
|
export { default as findParentNodeClosestToPos } from './helpers/findParentNodeClosestToPos'
|
||||||
export { default as generateHTML } from './helpers/generateHTML'
|
export { default as generateHTML } from './helpers/generateHTML'
|
||||||
export { default as generateJSON } from './helpers/generateJSON'
|
export { default as generateJSON } from './helpers/generateJSON'
|
||||||
|
export { default as generateText } from './helpers/generateText'
|
||||||
export { default as getSchema } from './helpers/getSchema'
|
export { default as getSchema } from './helpers/getSchema'
|
||||||
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
|
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
|
||||||
export { default as getDebugJSON } from './helpers/getDebugJSON'
|
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 getMarksBetween } from './helpers/getMarksBetween'
|
||||||
export { default as getNodeAttributes } from './helpers/getNodeAttributes'
|
export { default as getNodeAttributes } from './helpers/getNodeAttributes'
|
||||||
export { default as getNodeType } from './helpers/getNodeType'
|
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 isActive } from './helpers/isActive'
|
||||||
export { default as isList } from './helpers/isList'
|
export { default as isList } from './helpers/isList'
|
||||||
export { default as isMarkActive } from './helpers/isMarkActive'
|
export { default as isMarkActive } from './helpers/isMarkActive'
|
||||||
|
@ -210,3 +210,10 @@ export type NodeWithPos = {
|
|||||||
node: ProseMirrorNode,
|
node: ProseMirrorNode,
|
||||||
pos: number,
|
pos: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TextSerializer = (props: {
|
||||||
|
node: ProseMirrorNode,
|
||||||
|
pos: number,
|
||||||
|
parent: ProseMirrorNode,
|
||||||
|
index: number,
|
||||||
|
}) => string
|
||||||
|
Loading…
Reference in New Issue
Block a user