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:
Philipp Kühn
2021-12-03 08:53:58 +01:00
committed by GitHub
parent 40a9404c94
commit 3d68981b47
15 changed files with 366 additions and 82 deletions

View 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
}

View 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)
}

View File

@@ -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
}