import type { Universe } from '@ephox/boss'; import { Arr, Fun, Optional, Optionals } from '@ephox/katamari'; import type { WordDecisionItem } from '../words/WordDecision'; export interface ZoneDetails { readonly lang: string; readonly details: WordDecisionItem[]; } export interface LanguageZones { openInline: (optLang: Optional, elem: E) => void; closeInline: (optLang: Optional, elem: E) => void; addDetail: (detail: WordDecisionItem) => void; addEmpty: (empty: E) => void; openBoundary: (optLang: Optional, elem: E) => void; closeBoundary: (optLang: Optional, elem: E) => void; done: () => ZoneDetails[]; } const nu = (defaultLang: string): LanguageZones => { let stack: string[] = []; const zones: ZoneDetails[] = []; let zone: WordDecisionItem[] = []; let zoneLang = defaultLang; const push = (optLang: Optional) => { optLang.each((l) => { stack.push(l); }); }; const pop = (optLang: Optional) => { optLang.each((_l) => { stack = stack.slice(0, stack.length - 1); }); }; const topOfStack = () => { return Optional.from(stack[stack.length - 1]); }; const pushZone = () => { if (zone.length > 0) { // Intentionally, not a zone. These are details zones.push({ lang: zoneLang, details: zone }); } }; const spawn = (newLang: string) => { pushZone(); zone = []; zoneLang = newLang; }; const getLang = (optLang: Optional) => { return optLang.or(topOfStack()).getOr(defaultLang); }; const openInline = (optLang: Optional, _elem: E) => { const lang = getLang(optLang); // If the inline tag being opened is different from the current top of the stack, // then we don't want to create a new zone. if (lang !== zoneLang) { spawn(lang); } push(optLang); }; const closeInline = (optLang: Optional, _elem: E) => { pop(optLang); }; const addDetail = (detail: WordDecisionItem) => { const lang = getLang(Optional.none()); // If the top of the stack is not the same as zoneLang, then we need to spawn again. if (lang !== zoneLang) { spawn(lang); } zone.push(detail); }; const addEmpty = (_empty: E) => { const lang = getLang(Optional.none()); spawn(lang); }; const openBoundary = (optLang: Optional, _elem: E) => { push(optLang); const lang = getLang(optLang); spawn(lang); }; const closeBoundary = (optLang: Optional, _elem: E) => { pop(optLang); const lang = getLang(optLang); spawn(lang); }; const done = () => { pushZone(); return zones.slice(0); }; return { openInline, closeInline, addDetail, addEmpty, openBoundary, closeBoundary, done }; }; // Returns: Optional(string) of the LANG attribute of the closest ancestor element or None. // - uses Fun.never for isRoot parameter to search even the top HTML element // (regardless of 'classic'/iframe or 'inline'/div mode). // Note: there may be descendant elements with a different language const calculate = (universe: Universe, item: E): Optional => { const props = universe.property(); return props.getLanguage(item).orThunk(() => { const ancestors = universe.up().all(item, Fun.never); return Arr.findMap(ancestors, props.getLanguage); }); }; const strictBounder = (envLang: string, onlyLang: string) => { return (universe: Universe, item: E): boolean => { const itemLang = calculate(universe, item).getOr(envLang); return onlyLang !== itemLang; }; }; const softBounder = (optLang: Optional) => { return (universe: Universe, item: E): boolean => { const itemLang = calculate(universe, item); return !Optionals.equals(optLang, itemLang); }; }; export const LanguageZones = { nu, calculate, softBounder, strictBounder };