feat: Integrate input rules and paste rules into the core (#1997)
* refactoring * improve link regex * WIP: add new markPasteRule und linkify to image mark * move copy of inputrule to core * trigger codeblock inputrule on enter * refactoring * add regex match to markpasterulematch * refactoring * improve link regex * WIP: add new markPasteRule und linkify to image mark * move copy of inputrule to core * trigger codeblock inputrule on enter * refactoring * add regex match to markpasterulematch * update linkify * wip * wip * log * wip * remove debug code * wip * wip * wip * wip * wip * wip * wip * wip * rename matcher * add data to ExtendedRegExpMatchArray * remove logging * add code option to marks, prevent inputrules in code mark * remove link regex * fix codeblock inputrule on enter * refactoring * refactoring * refactoring * refactoring * fix position bug * add test * export InputRule and PasteRule * clean up link demo * fix types
This commit is contained in:
@@ -1,50 +1,70 @@
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import getMarksBetween from '../helpers/getMarksBetween'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
|
||||
export default function (regexp: RegExp, markType: MarkType, getAttributes?: Function): InputRule {
|
||||
return new InputRule(regexp, (state, match, start, end) => {
|
||||
const attributes = getAttributes instanceof Function
|
||||
? getAttributes(match)
|
||||
: getAttributes
|
||||
const { tr } = state
|
||||
const captureGroup = match[match.length - 1]
|
||||
const fullMatch = match[0]
|
||||
let markEnd = end
|
||||
/**
|
||||
* Build an input rule that adds a mark when the
|
||||
* matched text is typed into it.
|
||||
*/
|
||||
export default function markInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: MarkType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match)
|
||||
|
||||
if (captureGroup) {
|
||||
const startSpaces = fullMatch.search(/\S/)
|
||||
const textStart = start + fullMatch.indexOf(captureGroup)
|
||||
const textEnd = textStart + captureGroup.length
|
||||
|
||||
const excludedMarks = getMarksBetween(start, end, state)
|
||||
.filter(item => {
|
||||
// TODO: PR to add excluded to MarkType
|
||||
// @ts-ignore
|
||||
const { excluded } = item.mark.type
|
||||
return excluded.find((type: MarkType) => type.name === markType.name)
|
||||
})
|
||||
.filter(item => item.to > textStart)
|
||||
|
||||
if (excludedMarks.length) {
|
||||
return null
|
||||
if (attributes === false || attributes === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (textEnd < end) {
|
||||
tr.delete(textEnd, end)
|
||||
const { tr } = state
|
||||
const captureGroup = match[match.length - 1]
|
||||
const fullMatch = match[0]
|
||||
let markEnd = range.to
|
||||
|
||||
if (captureGroup) {
|
||||
const startSpaces = fullMatch.search(/\S/)
|
||||
const textStart = range.from + fullMatch.indexOf(captureGroup)
|
||||
const textEnd = textStart + captureGroup.length
|
||||
|
||||
const excludedMarks = getMarksBetween(range.from, range.to, state)
|
||||
.filter(item => {
|
||||
// TODO: PR to add excluded to MarkType
|
||||
// @ts-ignore
|
||||
const { excluded } = item.mark.type
|
||||
|
||||
return excluded.find((type: MarkType) => type.name === config.type.name)
|
||||
})
|
||||
.filter(item => item.to > textStart)
|
||||
|
||||
if (excludedMarks.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (textEnd < range.to) {
|
||||
tr.delete(textEnd, range.to)
|
||||
}
|
||||
|
||||
if (textStart > range.from) {
|
||||
tr.delete(range.from + startSpaces, textStart)
|
||||
}
|
||||
|
||||
markEnd = range.from + startSpaces + captureGroup.length
|
||||
|
||||
tr.addMark(range.from + startSpaces, markEnd, config.type.create(attributes || {}))
|
||||
|
||||
tr.removeStoredMark(config.type)
|
||||
}
|
||||
|
||||
if (textStart > start) {
|
||||
tr.delete(start + startSpaces, textStart)
|
||||
}
|
||||
|
||||
markEnd = start + startSpaces + captureGroup.length
|
||||
|
||||
tr.addMark(start + startSpaces, markEnd, markType.create(attributes))
|
||||
|
||||
tr.removeStoredMark(markType)
|
||||
}
|
||||
|
||||
return tr
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { NodeType } from 'prosemirror-model'
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
export default function (regexp: RegExp, type: NodeType, getAttributes?: (match: any) => any): InputRule {
|
||||
return new InputRule(regexp, (state, match, start, end) => {
|
||||
const attributes = getAttributes instanceof Function
|
||||
? getAttributes(match)
|
||||
: getAttributes
|
||||
const { tr } = state
|
||||
/**
|
||||
* Build an input rule that adds a node when the
|
||||
* matched text is typed into it.
|
||||
*/
|
||||
export default function nodeInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: NodeType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||
const { tr } = state
|
||||
const start = range.from
|
||||
let end = range.to
|
||||
|
||||
if (match[1]) {
|
||||
const offset = match[0].lastIndexOf(match[1])
|
||||
let matchStart = start + offset
|
||||
if (matchStart > end) {
|
||||
matchStart = end
|
||||
} else {
|
||||
end = matchStart + match[1].length
|
||||
if (match[1]) {
|
||||
const offset = match[0].lastIndexOf(match[1])
|
||||
let matchStart = start + offset
|
||||
|
||||
if (matchStart > end) {
|
||||
matchStart = end
|
||||
} else {
|
||||
end = matchStart + match[1].length
|
||||
}
|
||||
|
||||
// insert last typed character
|
||||
const lastChar = match[0][match[0].length - 1]
|
||||
tr.insertText(lastChar, start + match[0].length - 1)
|
||||
|
||||
// insert node from input rule
|
||||
tr.replaceWith(matchStart, end, config.type.create(attributes))
|
||||
} else if (match[0]) {
|
||||
tr.replaceWith(start, end, config.type.create(attributes))
|
||||
}
|
||||
|
||||
// insert last typed character
|
||||
const lastChar = match[0][match[0].length - 1]
|
||||
tr.insertText(lastChar, start + match[0].length - 1)
|
||||
|
||||
// insert node from input rule
|
||||
tr.replaceWith(matchStart, end, type.create(attributes))
|
||||
} else if (match[0]) {
|
||||
tr.replaceWith(start, end, type.create(attributes))
|
||||
}
|
||||
|
||||
return tr
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
35
packages/core/src/inputRules/textInputRule.ts
Normal file
35
packages/core/src/inputRules/textInputRule.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
|
||||
/**
|
||||
* Build an input rule that replaces text when the
|
||||
* matched text is typed into it.
|
||||
*/
|
||||
export default function textInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
replace: string,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
let insert = config.replace
|
||||
let start = range.from
|
||||
const end = range.to
|
||||
|
||||
if (match[1]) {
|
||||
const offset = match[0].lastIndexOf(match[1])
|
||||
|
||||
insert += match[0].slice(offset + match[1].length)
|
||||
start += offset
|
||||
|
||||
const cutOff = start - end
|
||||
|
||||
if (cutOff > 0) {
|
||||
insert = match[0].slice(offset - cutOff, offset) + insert
|
||||
start = end
|
||||
}
|
||||
}
|
||||
|
||||
state.tr.insertText(insert, start, end)
|
||||
},
|
||||
})
|
||||
}
|
||||
37
packages/core/src/inputRules/textblockTypeInputRule.ts
Normal file
37
packages/core/src/inputRules/textblockTypeInputRule.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { NodeType } from 'prosemirror-model'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
/**
|
||||
* Build an input rule that changes the type of a textblock when the
|
||||
* matched text is typed into it. When using a regular expresion you’ll
|
||||
* probably want the regexp to start with `^`, so that the pattern can
|
||||
* only occur at the start of a textblock.
|
||||
*/
|
||||
export default function textblockTypeInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: NodeType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const $start = state.doc.resolve(range.from)
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||
|
||||
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), config.type)) {
|
||||
return null
|
||||
}
|
||||
|
||||
state.tr
|
||||
.delete(range.from, range.to)
|
||||
.setBlockType(range.from, range.from, config.type, attributes)
|
||||
},
|
||||
})
|
||||
}
|
||||
59
packages/core/src/inputRules/wrappingInputRule.ts
Normal file
59
packages/core/src/inputRules/wrappingInputRule.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||
import { NodeType, Node as ProseMirrorNode } from 'prosemirror-model'
|
||||
import { findWrapping, canJoin } from 'prosemirror-transform'
|
||||
import { ExtendedRegExpMatchArray } from '../types'
|
||||
import callOrReturn from '../utilities/callOrReturn'
|
||||
|
||||
/**
|
||||
* Build an input rule for automatically wrapping a textblock when a
|
||||
* given string is typed. When using a regular expresion you’ll
|
||||
* probably want the regexp to start with `^`, so that the pattern can
|
||||
* only occur at the start of a textblock.
|
||||
*
|
||||
* `type` is the type of node to wrap in.
|
||||
*
|
||||
* By default, if there’s a node with the same type above the newly
|
||||
* wrapped node, the rule will try to join those
|
||||
* two nodes. You can pass a join predicate, which takes a regular
|
||||
* expression match and the node before the wrapped node, and can
|
||||
* return a boolean to indicate whether a join should happen.
|
||||
*/
|
||||
export default function wrappingInputRule(config: {
|
||||
find: InputRuleFinder,
|
||||
type: NodeType,
|
||||
getAttributes?:
|
||||
| Record<string, any>
|
||||
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||
| false
|
||||
| null
|
||||
,
|
||||
joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean,
|
||||
}) {
|
||||
return new InputRule({
|
||||
find: config.find,
|
||||
handler: ({ state, range, match }) => {
|
||||
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||
const tr = state.tr.delete(range.from, range.to)
|
||||
const $start = tr.doc.resolve(range.from)
|
||||
const blockRange = $start.blockRange()
|
||||
const wrapping = blockRange && findWrapping(blockRange, config.type, attributes)
|
||||
|
||||
if (!wrapping) {
|
||||
return null
|
||||
}
|
||||
|
||||
tr.wrap(blockRange, wrapping)
|
||||
|
||||
const before = tr.doc.resolve(range.from - 1).nodeBefore
|
||||
|
||||
if (
|
||||
before
|
||||
&& before.type === config.type
|
||||
&& canJoin(tr.doc, range.from - 1)
|
||||
&& (!config.joinPredicate || config.joinPredicate(match, before))
|
||||
) {
|
||||
tr.join(range.from - 1)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user