import type { Universe } from '@ephox/boss'; import { Fun, Optional, Unicode } from '@ephox/katamari'; import * as Spot from '../api/data/Spot'; import type { SpanWrapRange, SpotPoint } from '../api/data/Types'; import * as Injection from '../api/general/Injection'; import * as Wrapper from './Wrapper'; import { Wraps } from './Wraps'; interface SpanWrapPoint { readonly cursor: SpotPoint; readonly temporary: boolean; readonly wrappers: E[]; } const point = (universe: Universe, start: E, soffset: number, _finish: E, _foffset: number, exclusions: (e: E) => boolean): Optional> => { const scanned = scan(universe, start, soffset, exclusions); const cursor = scanned.cursor; const range = Spot.points( Spot.point(cursor.element, cursor.offset), Spot.point(cursor.element, cursor.offset) ); return Optional.some>({ range, temporary: scanned.temporary, wrappers: scanned.wrappers }); }; const temporary = (universe: Universe, start: E, soffset: number): SpanWrapPoint => { const doc: D = universe.property().document(start); const span = universe.create().nu('span', doc); const cursor = universe.create().text(Unicode.zeroWidth, doc); universe.insert().append(span, cursor); const injectAt = universe.property().isEmptyTag(start) ? universe.property().parent(start) : Optional.some(start); injectAt.each((z) => { Injection.atStartOf(universe, z, soffset, span); }); return { cursor: Spot.point(cursor, 1), wrappers: [ span ], temporary: true }; }; /* * The point approach needs to reuse a temporary span (if we already have one) or create one if we don't. */ const scan = (universe: Universe, start: E, soffset: number, exclusions: (e: E) => boolean): SpanWrapPoint => { return universe.property().parent(start).bind((parent): Optional> => { const cursor = Spot.point(start, soffset); const canReuse = isSpan(universe, exclusions)(parent) && universe.property().children(parent).length === 1 && isUnicode(universe, start); return canReuse ? Optional.some>({ cursor, temporary: false, wrappers: [ parent ] }) : Optional.none(); }).getOrThunk(() => { return temporary(universe, start, soffset); }); }; const isUnicode = (universe: Universe, element: E): boolean => { return universe.property().isText(element) && universe.property().getText(element) === Unicode.zeroWidth; }; const isSpan = (universe: Universe, exclusions: (e: E) => boolean) => (elem: E): boolean => universe.property().name(elem) === 'span' && exclusions(elem) === false; const wrap = (universe: Universe, start: E, soffset: number, finish: E, foffset: number, exclusions: (e: E) => boolean): Optional> => { const doc = universe.property().document(start); const nuSpan = () => { return Wraps(universe, universe.create().nu('span', doc)); }; const wrappers = Wrapper.reuse(universe, start, soffset, finish, foffset, isSpan(universe, exclusions), nuSpan); return Optional.from(wrappers[wrappers.length - 1]).map((lastSpan): SpanWrapRange => { const lastOffset = universe.property().children(lastSpan).length; const range = Spot.points( Spot.point(wrappers[0], 0), Spot.point(lastSpan, lastOffset) ); return { wrappers, temporary: false, range }; }); }; const isCollapsed = (universe: Universe, start: E, soffset: number, finish: E, foffset: number): boolean => { return universe.eq(start, finish) && soffset === foffset; }; const spans = (universe: Universe, start: E, soffset: number, finish: E, foffset: number, exclusions: (e: E) => boolean = Fun.never): Optional> => { const wrapper = isCollapsed(universe, start, soffset, finish, foffset) ? point : wrap; return wrapper(universe, start, soffset, finish, foffset, exclusions); }; export { spans };