import type { Universe } from '@ephox/boss'; import { Arr, Fun, Optional } from '@ephox/katamari'; interface Bisect { readonly before: E[]; readonly after: E[]; } export interface LeftRight { readonly left: E; readonly right: E; } export interface BrokenPathSplits { readonly first: E; readonly second: E; } export interface BrokenPath { readonly first: E; readonly second: Optional; readonly splits: BrokenPathSplits[]; } const leftRight = (left: E, right: E): LeftRight => ({ left, right }); const brokenPath = (first: E, second: Optional, splits: BrokenPathSplits[]): BrokenPath => ({ first, second, splits }); const bisect = (universe: Universe, parent: E, child: E): Optional> => { const children = universe.property().children(parent); const index = Arr.findIndex(children, Fun.curry(universe.eq, child)); return index.map((ind) => { return { before: children.slice(0, ind), after: children.slice(ind + 1) }; }); }; /** * Clone parent to the RIGHT and move everything after child in the parent element into * a clone of the parent (placed after parent). */ const breakToRight = (universe: Universe, parent: E, child: E): Optional> => { return bisect(universe, parent, child).map((parts) => { const second = universe.create().clone(parent); universe.insert().appendAll(second, parts.after); universe.insert().after(parent, second); return leftRight(parent, second); }); }; /** * Clone parent to the LEFT and move everything before and including child into * the a clone of the parent (placed before parent) */ const breakToLeft = (universe: Universe, parent: E, child: E): Optional> => { return bisect(universe, parent, child).map((parts) => { const prior = universe.create().clone(parent); universe.insert().appendAll(prior, parts.before.concat([ child ])); universe.insert().appendAll(parent, parts.after); universe.insert().before(parent, prior); return leftRight(prior, parent); }); }; /* * Using the breaker, break from the child up to the top element defined by the predicate. * It returns three values: * first: the top level element that completed the break * second: the optional element representing second part of the top-level split if the breaking completed successfully to the top * splits: a list of (Element, Element) pairs that represent the splits that have occurred on the way to the top. */ const breakPath = (universe: Universe, item: E, isTop: (e: E) => boolean, breaker: (universe: Universe, parent: E, child: E) => Optional>): BrokenPath => { const next = (child: E, group: Optional, splits: BrokenPathSplits[]): BrokenPath => { const fallback = brokenPath(child, Optional.none(), splits); // Found the top, so stop. if (isTop(child)) { return brokenPath(child, group, splits); } else { // Split the child at parent, and keep going return universe.property().parent(child).bind((parent: E) => { return breaker(universe, parent, child).map((breakage) => { const extra = [{ first: breakage.left, second: breakage.right }]; // Our isTop is based on the left-side parent, so keep it regardless of split. const nextChild = isTop(parent) ? parent : breakage.left; return next(nextChild, Optional.some(breakage.right), splits.concat(extra)); }); }).getOr(fallback); } }; return next(item, Optional.none(), []); }; export { breakToLeft, breakToRight, breakPath };