split helpers and utilities
This commit is contained in:
11
packages/core/src/helpers/generateHTML.ts
Normal file
11
packages/core/src/helpers/generateHTML.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Node } from 'prosemirror-model'
|
||||
import getSchema from './getSchema'
|
||||
import getHTMLFromFragment from './getHTMLFromFragment'
|
||||
import { Extensions } from '../types'
|
||||
|
||||
export default function generateHTML(doc: object, extensions: Extensions): string {
|
||||
const schema = getSchema(extensions)
|
||||
const contentNode = Node.fromJSON(schema, doc)
|
||||
|
||||
return getHTMLFromFragment(contentNode, schema)
|
||||
}
|
||||
72
packages/core/src/helpers/getAttributesFromExtensions.ts
Normal file
72
packages/core/src/helpers/getAttributesFromExtensions.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import splitExtensions from './splitExtensions'
|
||||
import {
|
||||
Extensions,
|
||||
GlobalAttributes,
|
||||
Attributes,
|
||||
Attribute,
|
||||
ExtensionAttribute,
|
||||
} from '../types'
|
||||
|
||||
/**
|
||||
* Get a list of all extension attributes defined in `addAttribute` and `addGlobalAttribute`.
|
||||
* @param extensions List of extensions
|
||||
*/
|
||||
export default function getAttributesFromExtensions(extensions: Extensions) {
|
||||
const extensionAttributes: ExtensionAttribute[] = []
|
||||
const { nodeExtensions, markExtensions } = splitExtensions(extensions)
|
||||
const nodeAndMarkExtensions = [...nodeExtensions, ...markExtensions]
|
||||
const defaultAttribute: Required<Attribute> = {
|
||||
default: null,
|
||||
rendered: true,
|
||||
renderHTML: null,
|
||||
parseHTML: null,
|
||||
}
|
||||
|
||||
extensions.forEach(extension => {
|
||||
const context = {
|
||||
options: extension.options,
|
||||
}
|
||||
|
||||
const globalAttributes = extension.config.addGlobalAttributes.bind(context)() as GlobalAttributes
|
||||
|
||||
globalAttributes.forEach(globalAttribute => {
|
||||
globalAttribute.types.forEach(type => {
|
||||
Object
|
||||
.entries(globalAttribute.attributes)
|
||||
.forEach(([name, attribute]) => {
|
||||
extensionAttributes.push({
|
||||
type,
|
||||
name,
|
||||
attribute: {
|
||||
...defaultAttribute,
|
||||
...attribute,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
nodeAndMarkExtensions.forEach(extension => {
|
||||
const context = {
|
||||
options: extension.options,
|
||||
}
|
||||
|
||||
const attributes = extension.config.addAttributes.bind(context)() as Attributes
|
||||
|
||||
Object
|
||||
.entries(attributes)
|
||||
.forEach(([name, attribute]) => {
|
||||
extensionAttributes.push({
|
||||
type: extension.config.name,
|
||||
name,
|
||||
attribute: {
|
||||
...defaultAttribute,
|
||||
...attribute,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return extensionAttributes
|
||||
}
|
||||
13
packages/core/src/helpers/getHTMLFromFragment.ts
Normal file
13
packages/core/src/helpers/getHTMLFromFragment.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Node, DOMSerializer, Schema } from 'prosemirror-model'
|
||||
|
||||
export default function getHTMLFromFragment(doc: Node, schema: Schema): string {
|
||||
const fragment = DOMSerializer
|
||||
.fromSchema(schema)
|
||||
.serializeFragment(doc.content)
|
||||
|
||||
const temporaryDocument = document.implementation.createHTMLDocument()
|
||||
const container = temporaryDocument.createElement('div')
|
||||
container.appendChild(fragment)
|
||||
|
||||
return container.innerHTML
|
||||
}
|
||||
25
packages/core/src/helpers/getMarkAttributes.ts
Normal file
25
packages/core/src/helpers/getMarkAttributes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { Mark, MarkType } from 'prosemirror-model'
|
||||
import getMarkType from './getMarkType'
|
||||
|
||||
export default function getMarkAttributes(state: EditorState, typeOrName: string | MarkType) {
|
||||
const type = getMarkType(typeOrName, state.schema)
|
||||
const { from, to, empty } = state.selection
|
||||
let marks: Mark[] = []
|
||||
|
||||
if (empty) {
|
||||
marks = state.selection.$head.marks()
|
||||
} else {
|
||||
state.doc.nodesBetween(from, to, node => {
|
||||
marks = [...marks, ...node.marks]
|
||||
})
|
||||
}
|
||||
|
||||
const mark = marks.find(markItem => markItem.type.name === type.name)
|
||||
|
||||
if (mark) {
|
||||
return { ...mark.attrs }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
44
packages/core/src/helpers/getMarkRange.ts
Normal file
44
packages/core/src/helpers/getMarkRange.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { MarkType, ResolvedPos } from 'prosemirror-model'
|
||||
|
||||
interface Range {
|
||||
from: number,
|
||||
to: number,
|
||||
}
|
||||
|
||||
export default function getMarkRange($pos: ResolvedPos, type: MarkType): Range | void {
|
||||
if (!$pos || !type) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = $pos.parent.childAfter($pos.parentOffset)
|
||||
|
||||
if (!start.node) {
|
||||
return
|
||||
}
|
||||
|
||||
const link = start.node.marks.find(mark => mark.type === type)
|
||||
|
||||
if (!link) {
|
||||
return
|
||||
}
|
||||
|
||||
let startIndex = $pos.index()
|
||||
let startPos = $pos.start() + start.offset
|
||||
let endIndex = startIndex + 1
|
||||
let endPos = startPos + start.node.nodeSize
|
||||
|
||||
while (startIndex > 0 && link.isInSet($pos.parent.child(startIndex - 1).marks)) {
|
||||
startIndex -= 1
|
||||
startPos -= $pos.parent.child(startIndex).nodeSize
|
||||
}
|
||||
|
||||
while (endIndex < $pos.parent.childCount && link.isInSet($pos.parent.child(endIndex).marks)) {
|
||||
endPos += $pos.parent.child(endIndex).nodeSize
|
||||
endIndex += 1
|
||||
}
|
||||
|
||||
return {
|
||||
from: startPos,
|
||||
to: endPos,
|
||||
}
|
||||
}
|
||||
9
packages/core/src/helpers/getMarkType.ts
Normal file
9
packages/core/src/helpers/getMarkType.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MarkType, Schema } from 'prosemirror-model'
|
||||
|
||||
export default function getMarkType(nameOrType: string | MarkType, schema: Schema): MarkType {
|
||||
if (typeof nameOrType === 'string') {
|
||||
return schema.marks[nameOrType]
|
||||
}
|
||||
|
||||
return nameOrType
|
||||
}
|
||||
23
packages/core/src/helpers/getNodeAttributes.ts
Normal file
23
packages/core/src/helpers/getNodeAttributes.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { Node, NodeType } from 'prosemirror-model'
|
||||
import getNodeType from './getNodeType'
|
||||
|
||||
export default function getNodeAttributes(state: EditorState, typeOrName: string | NodeType) {
|
||||
const type = getNodeType(typeOrName, state.schema)
|
||||
const { from, to } = state.selection
|
||||
let nodes: Node[] = []
|
||||
|
||||
state.doc.nodesBetween(from, to, node => {
|
||||
nodes = [...nodes, node]
|
||||
})
|
||||
|
||||
const node = nodes
|
||||
.reverse()
|
||||
.find(nodeItem => nodeItem.type.name === type.name)
|
||||
|
||||
if (node) {
|
||||
return { ...node.attrs }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
9
packages/core/src/helpers/getNodeType.ts
Normal file
9
packages/core/src/helpers/getNodeType.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NodeType, Schema } from 'prosemirror-model'
|
||||
|
||||
export default function getNodeType(nameOrType: string | NodeType, schema: Schema): NodeType {
|
||||
if (typeof nameOrType === 'string') {
|
||||
return schema.nodes[nameOrType]
|
||||
}
|
||||
|
||||
return nameOrType
|
||||
}
|
||||
20
packages/core/src/helpers/getRenderedAttributes.ts
Normal file
20
packages/core/src/helpers/getRenderedAttributes.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Node, Mark } from 'prosemirror-model'
|
||||
import { ExtensionAttribute, AnyObject } from '../types'
|
||||
import mergeAttributes from '../utilities/mergeAttributes'
|
||||
|
||||
export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): AnyObject {
|
||||
return extensionAttributes
|
||||
.filter(item => item.attribute.rendered)
|
||||
.map(item => {
|
||||
if (!item.attribute.renderHTML) {
|
||||
return {
|
||||
[item.name]: nodeOrMark.attrs[item.name],
|
||||
}
|
||||
}
|
||||
|
||||
return item.attribute.renderHTML(nodeOrMark.attrs) || {}
|
||||
})
|
||||
.reduce((attributes, attribute) => {
|
||||
return mergeAttributes(attributes, attribute)
|
||||
}, {})
|
||||
}
|
||||
94
packages/core/src/helpers/getSchema.ts
Normal file
94
packages/core/src/helpers/getSchema.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { NodeSpec, MarkSpec, Schema } from 'prosemirror-model'
|
||||
import { Extensions } from '../types'
|
||||
import splitExtensions from './splitExtensions'
|
||||
import getAttributesFromExtensions from './getAttributesFromExtensions'
|
||||
import getRenderedAttributes from './getRenderedAttributes'
|
||||
import isEmptyObject from '../utilities/isEmptyObject'
|
||||
import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
function cleanUpSchemaItem<T>(data: T) {
|
||||
return Object.fromEntries(Object.entries(data).filter(([key, value]) => {
|
||||
if (key === 'attrs' && isEmptyObject(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return value !== null && value !== undefined
|
||||
})) as T
|
||||
}
|
||||
|
||||
export default function getSchema(extensions: Extensions): Schema {
|
||||
const allAttributes = getAttributesFromExtensions(extensions)
|
||||
const { nodeExtensions, markExtensions } = splitExtensions(extensions)
|
||||
const topNode = nodeExtensions.find(extension => extension.config.topNode)?.config.name
|
||||
|
||||
const nodes = Object.fromEntries(nodeExtensions.map(extension => {
|
||||
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.config.name)
|
||||
const context = { options: extension.options }
|
||||
const schema: NodeSpec = cleanUpSchemaItem({
|
||||
content: callOrReturn(extension.config.content, context),
|
||||
marks: callOrReturn(extension.config.marks, context),
|
||||
group: callOrReturn(extension.config.group, context),
|
||||
inline: callOrReturn(extension.config.inline, context),
|
||||
atom: callOrReturn(extension.config.atom, context),
|
||||
selectable: callOrReturn(extension.config.selectable, context),
|
||||
draggable: callOrReturn(extension.config.draggable, context),
|
||||
code: callOrReturn(extension.config.code, context),
|
||||
defining: callOrReturn(extension.config.defining, context),
|
||||
isolating: callOrReturn(extension.config.isolating, context),
|
||||
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
|
||||
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
|
||||
})),
|
||||
})
|
||||
|
||||
if (extension.config.parseHTML) {
|
||||
schema.parseDOM = extension.config.parseHTML
|
||||
.bind(context)()
|
||||
?.map(parseRule => injectExtensionAttributesToParseRule(parseRule, extensionAttributes))
|
||||
}
|
||||
|
||||
if (extension.config.renderHTML) {
|
||||
schema.toDOM = node => (extension.config.renderHTML as Function)?.bind(context)({
|
||||
node,
|
||||
HTMLAttributes: getRenderedAttributes(node, extensionAttributes),
|
||||
})
|
||||
}
|
||||
|
||||
return [extension.config.name, schema]
|
||||
}))
|
||||
|
||||
const marks = Object.fromEntries(markExtensions.map(extension => {
|
||||
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.config.name)
|
||||
const context = { options: extension.options }
|
||||
const schema: MarkSpec = cleanUpSchemaItem({
|
||||
inclusive: callOrReturn(extension.config.inclusive, context),
|
||||
excludes: callOrReturn(extension.config.excludes, context),
|
||||
group: callOrReturn(extension.config.group, context),
|
||||
spanning: callOrReturn(extension.config.spanning, context),
|
||||
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
|
||||
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
|
||||
})),
|
||||
})
|
||||
|
||||
if (extension.config.parseHTML) {
|
||||
schema.parseDOM = extension.config.parseHTML
|
||||
.bind(context)()
|
||||
?.map(parseRule => injectExtensionAttributesToParseRule(parseRule, extensionAttributes))
|
||||
}
|
||||
|
||||
if (extension.config.renderHTML) {
|
||||
schema.toDOM = mark => (extension.config.renderHTML as Function)?.bind(context)({
|
||||
mark,
|
||||
HTMLAttributes: getRenderedAttributes(mark, extensionAttributes),
|
||||
})
|
||||
}
|
||||
|
||||
return [extension.config.name, schema]
|
||||
}))
|
||||
|
||||
return new Schema({
|
||||
topNode,
|
||||
nodes,
|
||||
marks,
|
||||
})
|
||||
}
|
||||
13
packages/core/src/helpers/getSchemaTypeByName.ts
Normal file
13
packages/core/src/helpers/getSchemaTypeByName.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Schema } from 'prosemirror-model'
|
||||
|
||||
export default function getSchemaTypeByName(name: string, schema: Schema) {
|
||||
if (schema.nodes[name]) {
|
||||
return schema.nodes[name]
|
||||
}
|
||||
|
||||
if (schema.marks[name]) {
|
||||
return schema.marks[name]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
13
packages/core/src/helpers/getSchemaTypeNameByName.ts
Normal file
13
packages/core/src/helpers/getSchemaTypeNameByName.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Schema } from 'prosemirror-model'
|
||||
|
||||
export default function getSchemaTypeNameByName(name: string, schema: Schema) {
|
||||
if (schema.nodes[name]) {
|
||||
return 'node'
|
||||
}
|
||||
|
||||
if (schema.marks[name]) {
|
||||
return 'mark'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ParseRule } from 'prosemirror-model'
|
||||
import { ExtensionAttribute } from '../types'
|
||||
import fromString from '../utilities/fromString'
|
||||
|
||||
/**
|
||||
* This function merges extension attributes into parserule attributes (`attrs` or `getAttrs`).
|
||||
* Cancels when `getAttrs` returned `false`.
|
||||
* @param parseRule ProseMirror ParseRule
|
||||
* @param extensionAttributes List of attributes to inject
|
||||
*/
|
||||
export default function injectExtensionAttributesToParseRule(parseRule: ParseRule, extensionAttributes: ExtensionAttribute[]): ParseRule {
|
||||
if (parseRule.style) {
|
||||
return parseRule
|
||||
}
|
||||
|
||||
return {
|
||||
...parseRule,
|
||||
getAttrs: node => {
|
||||
const oldAttributes = parseRule.getAttrs
|
||||
? parseRule.getAttrs(node)
|
||||
: parseRule.attrs
|
||||
|
||||
if (oldAttributes === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
const newAttributes = extensionAttributes
|
||||
.filter(item => item.attribute.rendered)
|
||||
.reduce((items, item) => {
|
||||
const attributes = item.attribute.parseHTML
|
||||
? item.attribute.parseHTML(node as HTMLElement) || {}
|
||||
: {
|
||||
[item.name]: fromString((node as HTMLElement).getAttribute(item.name)),
|
||||
}
|
||||
|
||||
const filteredAttributes = Object.fromEntries(Object.entries(attributes)
|
||||
.filter(([, value]) => value !== undefined && value !== null))
|
||||
|
||||
return {
|
||||
...items,
|
||||
...filteredAttributes,
|
||||
}
|
||||
}, {})
|
||||
|
||||
return { ...oldAttributes, ...newAttributes }
|
||||
},
|
||||
}
|
||||
}
|
||||
45
packages/core/src/helpers/isActive.ts
Normal file
45
packages/core/src/helpers/isActive.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { Node, Mark } from 'prosemirror-model'
|
||||
import nodeIsActive from './nodeIsActive'
|
||||
import markIsActive from './markIsActive'
|
||||
import objectIncludes from '../utilities/objectIncludes'
|
||||
import getSchemaTypeNameByName from './getSchemaTypeNameByName'
|
||||
|
||||
export default function isActive(state: EditorState, name: string | null, attributes: { [key: string ]: any } = {}): boolean {
|
||||
if (name) {
|
||||
const schemaType = getSchemaTypeNameByName(name, state.schema)
|
||||
|
||||
if (schemaType === 'node') {
|
||||
return nodeIsActive(state, state.schema.nodes[name], attributes)
|
||||
} if (schemaType === 'mark') {
|
||||
return markIsActive(state, state.schema.marks[name], attributes)
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
const { from, to, empty } = state.selection
|
||||
let nodes: Node[] = []
|
||||
let marks: Mark[] = []
|
||||
|
||||
if (empty) {
|
||||
marks = state.selection.$head.marks()
|
||||
}
|
||||
|
||||
state.doc.nodesBetween(from, to, node => {
|
||||
nodes = [...nodes, node]
|
||||
|
||||
if (!empty) {
|
||||
marks = [...marks, ...node.marks]
|
||||
}
|
||||
})
|
||||
|
||||
const anyNodeWithAttributes = nodes.find(node => objectIncludes(node.attrs, attributes))
|
||||
const anyMarkWithAttributes = marks.find(mark => objectIncludes(mark.attrs, attributes))
|
||||
|
||||
if (anyNodeWithAttributes || anyMarkWithAttributes) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
20
packages/core/src/helpers/isList.ts
Normal file
20
packages/core/src/helpers/isList.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Extensions } from '../types'
|
||||
import splitExtensions from './splitExtensions'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
export default function isList(name: string, extensions: Extensions) {
|
||||
const { nodeExtensions } = splitExtensions(extensions)
|
||||
const extension = nodeExtensions.find(item => item.config.name === name)
|
||||
|
||||
if (!extension) {
|
||||
return false
|
||||
}
|
||||
|
||||
const groups = callOrReturn(extension.config.group, { options: extension.options })
|
||||
|
||||
if (typeof groups !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return groups.split(' ').includes('list')
|
||||
}
|
||||
15
packages/core/src/helpers/markHasAttributes.ts
Normal file
15
packages/core/src/helpers/markHasAttributes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import getMarkAttributes from './getMarkAttributes'
|
||||
import isEmptyObject from '../utilities/isEmptyObject'
|
||||
import objectIncludes from '../utilities/objectIncludes'
|
||||
|
||||
export default function markHasAttributes(state: EditorState, type: MarkType, attributes: {}) {
|
||||
if (isEmptyObject(attributes)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const originalAttributes = getMarkAttributes(state, type)
|
||||
|
||||
return objectIncludes(originalAttributes, attributes)
|
||||
}
|
||||
20
packages/core/src/helpers/markIsActive.ts
Normal file
20
packages/core/src/helpers/markIsActive.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import markHasAttributes from './markHasAttributes'
|
||||
|
||||
export default function markIsActive(state: EditorState, type: MarkType, attributes = {}) {
|
||||
const {
|
||||
from,
|
||||
$from,
|
||||
to,
|
||||
empty,
|
||||
} = state.selection
|
||||
|
||||
const hasMark = empty
|
||||
? !!(type.isInSet(state.storedMarks || $from.marks()))
|
||||
: state.doc.rangeHasMark(from, to, type)
|
||||
|
||||
const hasAttributes = markHasAttributes(state, type, attributes)
|
||||
|
||||
return hasMark && hasAttributes
|
||||
}
|
||||
18
packages/core/src/helpers/nodeIsActive.ts
Normal file
18
packages/core/src/helpers/nodeIsActive.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { findParentNode, findSelectedNodeOfType } from 'prosemirror-utils'
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { Node, NodeType } from 'prosemirror-model'
|
||||
|
||||
export default function nodeIsActive(state: EditorState, type: NodeType, attributes = {}) {
|
||||
const predicate = (node: Node) => node.type === type
|
||||
const node = findSelectedNodeOfType(type)(state.selection)
|
||||
|| findParentNode(predicate)(state.selection)
|
||||
|
||||
if (!Object.keys(attributes).length || !node) {
|
||||
return !!node
|
||||
}
|
||||
|
||||
return node.node.hasMarkup(type, {
|
||||
...node.node.attrs,
|
||||
...attributes,
|
||||
})
|
||||
}
|
||||
16
packages/core/src/helpers/splitExtensions.ts
Normal file
16
packages/core/src/helpers/splitExtensions.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Extensions } from '../types'
|
||||
import { Extension } from '../Extension'
|
||||
import { Node } from '../Node'
|
||||
import { Mark } from '../Mark'
|
||||
|
||||
export default function splitExtensions(extensions: Extensions) {
|
||||
const baseExtensions = extensions.filter(extension => extension.type === 'extension') as Extension[]
|
||||
const nodeExtensions = extensions.filter(extension => extension.type === 'node') as Node[]
|
||||
const markExtensions = extensions.filter(extension => extension.type === 'mark') as Mark[]
|
||||
|
||||
return {
|
||||
baseExtensions,
|
||||
nodeExtensions,
|
||||
markExtensions,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user