import type { Universe } from '@ephox/boss'; import { Adt, Arr, Optional } from '@ephox/katamari'; import { Descent, Gather, Spot, type SpotPoint, type Transition } from '@ephox/phoenix'; import * as Structure from '../api/general/Structure'; export interface Clump { readonly start: E; readonly soffset: number; readonly finish: E; readonly foffset: number; } interface ClumpsScan { fold: ( none: (last: E, mode: Transition) => T, running: (next: E, mode: Transition) => T, split: (boundary: E, last: E, mode: Transition) => T, finished: (element: E, mode: Transition) => T ) => T; match: (branches: { none: (last: E, mode: Transition) => T; running: (next: E, mode: Transition) => T; split: (boundary: E, last: E, mode: Transition) => T; finished: (element: E, mode: Transition) => T; }) => T; log: (label: string) => void; } const adt: { none: (last: E, mode: Transition) => ClumpsScan; running: (next: E, mode: Transition) => ClumpsScan; split: (boundary: E, last: E, mode: Transition) => ClumpsScan; finished: (element: E, mode: Transition) => ClumpsScan; } = Adt.generate([ { none: [ 'last', 'mode' ] }, { running: [ 'next', 'mode' ] }, { split: [ 'boundary', 'last', 'mode' ] }, { finished: [ 'element', 'mode' ] } ]); interface ClumpRange { start: E; finish: E; } const clump = (start: E, soffset: number, finish: E, foffset: number): Clump => ({ start, soffset, finish, foffset }); const descendBlock = (universe: Universe, isRoot: (e: E) => boolean, block: E): SpotPoint => { const leaf = Descent.toLeaf(universe, block, 0); if (!skip(universe, leaf.element)) { return leaf; } else { return skipToRight(universe, isRoot, leaf.element).map((next) => { return Spot.point(next, 0); }).getOr(leaf); } }; const isBlock = (universe: Universe, item: E): boolean => { return Structure.isFrame(universe, item) || Structure.isBlock(universe, item) || Arr.contains([ 'li' ], universe.property().name(item)); }; const skipToRight = (universe: Universe, isRoot: (e: E) => boolean, item: E): Optional => { return Gather.seekRight(universe, item, (i) => { return !skip(universe, i) && !isBlock(universe, i); }, isRoot); }; const skip = (universe: Universe, item: E): boolean => { if (!universe.property().isText(item)) { return false; } return universe.property().parent(item).exists((p) => { // Text nodes of these children should be ignored when adding tags. // Dupe from phoenix OrphanText. We'll need a better solution for this. return Arr.contains([ 'table', 'tbody', 'thead', 'tfoot', 'tr', 'ul', 'ol' ], universe.property().name(p)); }); }; const walk = (universe: Universe, isRoot: (e: E) => boolean, mode: Transition, element: E, target: E): ClumpsScan => { const next = Gather.walk(universe, element, mode, Gather.walkers().right()); return next.fold(() => { return adt.none(element, Gather.sidestep); }, (n) => { if (universe.eq(n.item, target)) { return adt.finished(target, n.mode); } else if (isBlock(universe, n.item)) { return adt.split(n.item, element, n.mode); } else { return adt.running(n.item, n.mode); } }); }; /* * Resuming is used to find a new starting point for the next clump. It will aim * to hit the target or a leaf. Note, resuming should not start again from the same * boundary that the previous clump finished within. */ const resume = (universe: Universe, isRoot: (e: E) => boolean, boundary: E, target: E): Optional => { // I have to sidestep here so I don't descend down the same boundary. const next = skipToRight(universe, isRoot, boundary); return next.fold(() => { return Optional.none(); }, (n) => { if (universe.eq(n, target)) { return Optional.some(target); } else if (isParent(universe, boundary, n)) { return resume(universe, isRoot, n, target); } else if (isBlock(universe, n)) { const leaf = descendBlock(universe, isRoot, n); return Optional.some(leaf.element); } return Optional.some(n); }); }; const isParent = (universe: Universe, child: E, parent: E): boolean => { return universe.property().parent(child).exists((p) => { return universe.eq(p, parent); }); }; const scan = (universe: Universe, isRoot: (e: E) => boolean, mode: Transition, beginning: E, element: E, target: E): ClumpRange[] => { // Keep walking the tree. const step = walk(universe, isRoot, mode, element, target); return step.fold((last, _mode) => { // Hit the last element in the tree, so just stop. return [{ start: beginning, finish: last }]; }, (next, mode) => { // Keep going. We haven't finished this clump yet. return scan(universe, isRoot, mode, beginning, next, target); }, (boundary, last, _mode) => { const current = { start: beginning, finish: last }; // Logic .. if this boundary was a parent, then sidestep. const resumption = isParent(universe, element, boundary) ? resume(universe, isRoot, boundary, target) : (() => { const leaf = descendBlock(universe, isRoot, boundary); return !universe.eq(leaf.element, boundary) ? Optional.some(leaf.element) : Gather.walk(universe, boundary, Gather.advance, Gather.walkers().right()).map((g) => g.item); })(); // We have hit a boundary, so stop the current clump, and start a new from the next starting point. return resumption.fold(() => { // There was no new starting point, so just return the newly created clump return [ current ]; }, (n) => { if (universe.eq(n, target)) { return [ current ].concat({ start: target, finish: target }); } // There was a new starting point, so scan for more clumps and accumulate the result. return [ current ].concat(scan(universe, isRoot, Gather.advance, n, n, target)); }); }, (elem, _mode) => { // We hit the final destination, so finish our current clump return [{ start: beginning, finish: elem }]; }); }; const getEnd = (universe: Universe, item: E): number => { return universe.property().isText(item) ? universe.property().getText(item).length : universe.property().children(item).length; }; const drop = (universe: Universe, item: E, offset: number): E => { if (isBlock(universe, item)) { const children = universe.property().children(item); if (offset >= 0 && offset < children.length) { return drop(universe, children[offset], 0); } else if (offset === children.length) { return drop(universe, children[children.length - 1], 0); } else { return item; } } else { return item; } }; const skipInBlock = (universe: Universe, isRoot: (e: E) => boolean, item: E, offset: number): E => { const dropped = drop(universe, item, offset); if (!skip(universe, dropped)) { return dropped; } return skipToRight(universe, isRoot, dropped).getOr(dropped); }; const doCollect = (universe: Universe, isRoot: (e: E) => boolean, start: E, soffset: number, finish: E, foffset: number): Clump[] => { // We can't wrap block elements, so descend if we start on a block. const droppedStart = skipInBlock(universe, isRoot, start, soffset); const droppedFinish = skipInBlock(universe, isRoot, finish, foffset); // If the dropped start should be skipped, find the thing to the right of it. const raw = scan(universe, isRoot, Gather.sidestep, droppedStart, droppedStart, droppedFinish); return Arr.map(raw, (r) => { // Incorporate any offsets that were required. const soff = universe.eq(r.start, start) ? soffset : 0; const foff = universe.eq(r.finish, finish) ? foffset : getEnd(universe, r.finish); return clump(r.start, soff, r.finish, foff); }); }; const single = (universe: Universe, isRoot: (e: E) => boolean, item: E, soffset: number, foffset: number): Clump[] => { // If we aren't on blocks, just span a clump from start to finish. if (!isBlock(universe, item)) { return [ clump(item, soffset, item, foffset) ]; } // Jump to the leaves and try again if we have changed. const start = Descent.toLeaf(universe, item, soffset); const finish = Descent.toLeaf(universe, item, foffset); const changed = !universe.eq(start.element, item) || !universe.eq(finish.element, item); return changed ? collect(universe, isRoot, start.element, start.offset, finish.element, finish.offset) : [ clump(start.element, start.offset, finish.element, finish.offset) ]; }; const collect = (universe: Universe, isRoot: (e: E) => boolean, start: E, soffset: number, finish: E, foffset: number): Clump[] => { return universe.eq(start, finish) ? single(universe, isRoot, start, soffset, foffset) : doCollect(universe, isRoot, start, soffset, finish, foffset); }; export { collect };