feat: Add support for autolink (#2226)
* wip * WIP * add autolink implementation * refactoring * set keepOnSplit to false * refactoring * improve changed ranges detection * move some helpers into core Co-authored-by: Philipp Kühn <philippkuehn@MacBook-Pro-von-Philipp.local>
This commit is contained in:
18
packages/core/src/helpers/combineTransactionSteps.ts
Normal file
18
packages/core/src/helpers/combineTransactionSteps.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Node as ProseMirrorNode } from 'prosemirror-model'
|
||||
import { Transaction } from 'prosemirror-state'
|
||||
import { Transform } from 'prosemirror-transform'
|
||||
|
||||
/**
|
||||
* Returns a new `Transform` based on all steps of the passed transactions.
|
||||
*/
|
||||
export default function combineTransactionSteps(oldDoc: ProseMirrorNode, transactions: Transaction[]): Transform {
|
||||
const transform = new Transform(oldDoc)
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
transaction.steps.forEach(step => {
|
||||
transform.step(step)
|
||||
})
|
||||
})
|
||||
|
||||
return transform
|
||||
}
|
||||
82
packages/core/src/helpers/getChangedRanges.ts
Normal file
82
packages/core/src/helpers/getChangedRanges.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Transform, Step } from 'prosemirror-transform'
|
||||
import { Range } from '../types'
|
||||
import removeDuplicates from '../utilities/removeDuplicates'
|
||||
|
||||
export type ChangedRange = {
|
||||
oldRange: Range,
|
||||
newRange: Range,
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicated ranges and ranges that are
|
||||
* fully captured by other ranges.
|
||||
*/
|
||||
function simplifyChangedRanges(changes: ChangedRange[]): ChangedRange[] {
|
||||
const uniqueChanges = removeDuplicates(changes)
|
||||
|
||||
return uniqueChanges.length === 1
|
||||
? uniqueChanges
|
||||
: uniqueChanges.filter((change, index) => {
|
||||
const rest = uniqueChanges.filter((_, i) => i !== index)
|
||||
|
||||
return !rest.some(otherChange => {
|
||||
return change.oldRange.from >= otherChange.oldRange.from
|
||||
&& change.oldRange.to <= otherChange.oldRange.to
|
||||
&& change.newRange.from >= otherChange.newRange.from
|
||||
&& change.newRange.to <= otherChange.newRange.to
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of changed ranges
|
||||
* based on the first and last state of all steps.
|
||||
*/
|
||||
export default function getChangedRanges(transform: Transform): ChangedRange[] {
|
||||
const { mapping, steps } = transform
|
||||
const changes: ChangedRange[] = []
|
||||
|
||||
mapping.maps.forEach((stepMap, index) => {
|
||||
const ranges: Range[] = []
|
||||
|
||||
// This accounts for step changes where no range was actually altered
|
||||
// e.g. when setting a mark, node attribute, etc.
|
||||
// @ts-ignore
|
||||
if (!stepMap.ranges.length) {
|
||||
const { from, to } = steps[index] as Step & {
|
||||
from?: number,
|
||||
to?: number,
|
||||
}
|
||||
|
||||
if (from === undefined || to === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
ranges.push({ from, to })
|
||||
} else {
|
||||
stepMap.forEach((from, to) => {
|
||||
ranges.push({ from, to })
|
||||
})
|
||||
}
|
||||
|
||||
ranges.forEach(({ from, to }) => {
|
||||
const newStart = mapping.slice(index).map(from, -1)
|
||||
const newEnd = mapping.slice(index).map(to)
|
||||
const oldStart = mapping.invert().map(newStart, -1)
|
||||
const oldEnd = mapping.invert().map(newEnd)
|
||||
|
||||
changes.push({
|
||||
oldRange: {
|
||||
from: oldStart,
|
||||
to: oldEnd,
|
||||
},
|
||||
newRange: {
|
||||
from: newStart,
|
||||
to: newEnd,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return simplifyChangedRanges(changes)
|
||||
}
|
||||
@@ -1,16 +1,37 @@
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { Node as ProseMirrorNode } from 'prosemirror-model'
|
||||
import { MarkRange } from '../types'
|
||||
import getMarkRange from './getMarkRange'
|
||||
|
||||
export default function getMarksBetween(from: number, to: number, state: EditorState): MarkRange[] {
|
||||
export default function getMarksBetween(from: number, to: number, doc: ProseMirrorNode): MarkRange[] {
|
||||
const marks: MarkRange[] = []
|
||||
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
marks.push(...node.marks.map(mark => ({
|
||||
from: pos,
|
||||
to: pos + node.nodeSize,
|
||||
mark,
|
||||
})))
|
||||
})
|
||||
// get all inclusive marks on empty selection
|
||||
if (from === to) {
|
||||
doc
|
||||
.resolve(from)
|
||||
.marks()
|
||||
.forEach(mark => {
|
||||
const $pos = doc.resolve(from - 1)
|
||||
const range = getMarkRange($pos, mark.type)
|
||||
|
||||
if (!range) {
|
||||
return
|
||||
}
|
||||
|
||||
marks.push({
|
||||
mark,
|
||||
...range,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
doc.nodesBetween(from, to, (node, pos) => {
|
||||
marks.push(...node.marks.map(mark => ({
|
||||
from: pos,
|
||||
to: pos + node.nodeSize,
|
||||
mark,
|
||||
})))
|
||||
})
|
||||
}
|
||||
|
||||
return marks
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export { default as textPasteRule } from './pasteRules/textPasteRule'
|
||||
export { default as callOrReturn } from './utilities/callOrReturn'
|
||||
export { default as mergeAttributes } from './utilities/mergeAttributes'
|
||||
|
||||
export { default as combineTransactionSteps } from './helpers/combineTransactionSteps'
|
||||
export { default as defaultBlockAt } from './helpers/defaultBlockAt'
|
||||
export { default as getExtensionField } from './helpers/getExtensionField'
|
||||
export { default as findChildren } from './helpers/findChildren'
|
||||
@@ -32,6 +33,7 @@ export { default as findParentNodeClosestToPos } from './helpers/findParentNodeC
|
||||
export { default as generateHTML } from './helpers/generateHTML'
|
||||
export { default as generateJSON } from './helpers/generateJSON'
|
||||
export { default as generateText } from './helpers/generateText'
|
||||
export { default as getChangedRanges } from './helpers/getChangedRanges'
|
||||
export { default as getSchema } from './helpers/getSchema'
|
||||
export { default as getHTMLFromFragment } from './helpers/getHTMLFromFragment'
|
||||
export { default as getDebugJSON } from './helpers/getDebugJSON'
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function markInputRule(config: {
|
||||
const textStart = range.from + fullMatch.indexOf(captureGroup)
|
||||
const textEnd = textStart + captureGroup.length
|
||||
|
||||
const excludedMarks = getMarksBetween(range.from, range.to, state)
|
||||
const excludedMarks = getMarksBetween(range.from, range.to, state.doc)
|
||||
.filter(item => {
|
||||
// @ts-ignore
|
||||
const excluded = item.mark.type.excluded as MarkType[]
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function markPasteRule(config: {
|
||||
const textStart = range.from + fullMatch.indexOf(captureGroup)
|
||||
const textEnd = textStart + captureGroup.length
|
||||
|
||||
const excludedMarks = getMarksBetween(range.from, range.to, state)
|
||||
const excludedMarks = getMarksBetween(range.from, range.to, state.doc)
|
||||
.filter(item => {
|
||||
// @ts-ignore
|
||||
const excluded = item.mark.type.excluded as MarkType[]
|
||||
|
||||
15
packages/core/src/utilities/removeDuplicates.ts
Normal file
15
packages/core/src/utilities/removeDuplicates.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Removes duplicated values within an array.
|
||||
* Supports numbers, strings and objects.
|
||||
*/
|
||||
export default function removeDuplicates<T>(array: T[], by = JSON.stringify): T[] {
|
||||
const seen: Record<any, any> = {}
|
||||
|
||||
return array.filter(item => {
|
||||
const key = by(item)
|
||||
|
||||
return Object.prototype.hasOwnProperty.call(seen, key)
|
||||
? false
|
||||
: (seen[key] = true)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user