import { Mark, MarkType, Node, ResolvedPos } from "prosemirror-model"; import { Bias } from "./types"; /** * Find the closest ancestor of a position matching a predicate. */ export function closest( $pos: ResolvedPos, predicate: (node: Node) => boolean ): { node: N; pos: number; start: S; depth: number } | null { for (let i = $pos.depth; i > 0; i--) { const node = $pos.node(i); if (predicate(node)) { return { depth: i - 1, pos: i > 0 ? $pos.before(i) : 0, start: $pos.start(i) as S, node: node as N }; } } return null; } /** * Look for a node matching a predicate, by searching forwards through a * document from a position. */ export function findForward( $pos: ResolvedPos, predicate: (node: Node, pos: number) => boolean ): { $pos: ResolvedPos; node: Node; parent: Node } | null { let result: { $pos: ResolvedPos; node: Node; parent: Node } | null = null; $pos.doc.nodesBetween($pos.pos, $pos.doc.content.size, (node, pos, parent) => { if (pos < $pos.pos) { // Don't recurse into nodes that are completely before the position we're // interested in. if (pos + node.nodeSize < $pos.pos) { return false; } // Skip nodes that are before the position we're interested in. return; } if (predicate(node, pos)) { result = { $pos: $pos.doc.resolve(pos), node: node, parent: parent }; } // `false` needs to be returned repeatedly until `nodesBetween` bubbles up // to the root and exits. if (result !== null) { return false; } // Continue return; }); return result; } /** * Helper for iterating through the nodes in a document that changed compared to * the given previous document. Useful for avoiding duplicate work on each * transaction. */ export function changedDescendants(oldNode: Node, newNode: Node, offset: number, f: (node: Node, pos: number) => void) { const oldSize = oldNode.childCount; const curSize = newNode.childCount; outer: for (let i = 0, j = 0; i < curSize; i++) { const child = newNode.child(i); for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) { if (oldNode.child(scan) == child) { j = scan + 1; offset += child.nodeSize; continue outer; } } f(child, offset); if (j < oldSize && oldNode.child(j).sameMarkup(child)) { changedDescendants(oldNode.child(j), child, offset + 1, f); } else { // HACK // tslint:disable-next-line:no-any (child.nodesBetween as any)(0, child.content.size, f, offset + 1); } offset += child.nodeSize; } } /** * Finds the bounds of an adjacent mark of a particular type. */ export function adjacentMarkBounds( $pos: ResolvedPos, markType: MarkType, bias: Bias = Bias.BACKWARD ): { mark: Mark; from: number; to: number } | null { const posIndex = $pos.index(); const parent = $pos.parent; let searchIndex: { mark: Mark; index: number } | null = null; const indexPriority = $pos.textOffset > 0 ? [posIndex] : bias === Bias.BACKWARD ? [posIndex - 1, posIndex] : [posIndex, posIndex - 1]; loop: for (const index of indexPriority) { const sibling = parent.maybeChild(index); if (sibling != null) { for (const m of sibling.marks) { if (m.type === markType) { searchIndex = { mark: m, index }; break loop; } } } } // If the mark isn't applied to the node that the position points into, or a // node adjacent to it, it doesn't make sense to return a result. if (searchIndex === null) { return null; } const { mark, index } = searchIndex; let pos = $pos.start(); for (let i = 0; i < index; i++) { pos += parent.child(i).nodeSize; } let from = pos; for (let i = index - 1; i >= 0; i--) { const node = parent.child(i); if (mark.isInSet(node.marks)) { from -= node.nodeSize; } else { break; } } let to = pos; for (let i = index; i < parent.childCount; i++) { const node = parent.child(i); if (mark.isInSet(node.marks)) { to += node.nodeSize; } else { break; } } return { mark, from, to }; } export function contiguousTextRange($from: ResolvedPos, $to: ResolvedPos): { from: number; to: number } | null { if ($from.pos > $to.pos || $from.doc !== $to.doc) { return null; } const fromParent = $from.parent; const toParent = $to.parent; if (!fromParent.isTextblock) { return null; } const rawFromIndex = $from.index(); const rawToIndex = $to.index(); const fromIndex = rawFromIndex > 0 && $from.textOffset === 0 && $from.nodeAfter == null ? rawFromIndex - 1 : rawFromIndex; const toIndex = rawToIndex > 0 && $to.textOffset === 0 && $to.nodeAfter == null ? rawToIndex - 1 : rawToIndex; let boundedTo = $from.pos; if (fromParent === toParent && fromIndex === toIndex) { boundedTo = $to.pos; } else { let rollingTo = $from.start() + fromParent.child(fromIndex).nodeSize; for (let i = fromIndex + 1; i < fromParent.childCount; i++) { const node = fromParent.child(i); if (!node.isText) { break; } const end = rollingTo + node.nodeSize; if (end >= $to.pos) { rollingTo = Math.min(end, $to.pos); } } boundedTo = rollingTo; } return { from: $from.pos, to: boundedTo }; } export function grandParent($pos: ResolvedPos): { node: Node; index: number } { return { node: $pos.node($pos.depth - 1), index: $pos.index($pos.depth - 1) }; }