import { Adt, Arr, Fun, Obj, type Optional } from '@ephox/katamari'; import type { AlloySpec } from '../api/component/SpecTypes'; import type { CompositeSketchDetail } from '../api/ui/Sketcher'; import type { ConfiguredPart } from '../parts/AlloyParts'; interface Replacement { readonly name: () => string; readonly required: () => boolean; readonly used: () => boolean; readonly replace: () => UiSubstitutesAdt; } type ValueThunkFn = (detail: D, spec?: Record, partValidated?: Record) => R; type SingleSubstitution = (required: boolean, valueThunk: ValueThunkFn) => T; type MultipleSubstitution = (required: boolean, valueThunk: ValueThunkFn) => T; export interface UiSubstitutesAdt { fold: ( single: SingleSubstitution, multiple: MultipleSubstitution ) => T; match: (branches: { single: SingleSubstitution; multiple: MultipleSubstitution; }) => T; log: (label: string) => void; } const _placeholder = 'placeholder'; const adt: { single: SingleSubstitution; multiple: MultipleSubstitution; } = Adt.generate([ { single: [ 'required', 'valueThunk' ] }, { multiple: [ 'required', 'valueThunks' ] } ]); const isSubstituted = (spec: any): spec is ConfiguredPart => Obj.has(spec, 'uiType'); const subPlaceholder = (owner: Optional, detail: D, compSpec: ConfiguredPart, placeholders: Record): UiSubstitutesAdt => { if (owner.exists((o) => o !== compSpec.owner)) { return adt.single(true, Fun.constant(compSpec)); } // Ignore having to find something for the time being. return Obj.get(placeholders as any, compSpec.name).fold(() => { throw new Error('Unknown placeholder component: ' + compSpec.name + '\nKnown: [' + Obj.keys(placeholders) + ']\nNamespace: ' + owner.getOr('none') + '\nSpec: ' + JSON.stringify(compSpec, null, 2) ); }, (newSpec) => // Must return a single/multiple type newSpec.replace() ); }; const scan = (owner: Optional, detail: D, compSpec: AlloySpec, placeholders: Record): UiSubstitutesAdt => { if (isSubstituted(compSpec) && compSpec.uiType === _placeholder) { return subPlaceholder(owner, detail, compSpec, placeholders); } else { return adt.single(false, Fun.constant(compSpec)); } }; const substitute = (owner: Optional, detail: D, compSpec: AlloySpec, placeholders: Record): AlloySpec[] => { const base = scan(owner, detail, compSpec, placeholders); return base.fold( (req, valueThunk) => { const value = isSubstituted(compSpec) ? valueThunk(detail, compSpec.config, compSpec.validated) : valueThunk(detail); const childSpecs = Obj.get(value as any, 'components').getOr([]); const substituted = Arr.bind(childSpecs, (c) => substitute(owner, detail, c, placeholders)); return [ { ...value, components: substituted } ]; }, (req, valuesThunk) => { if (isSubstituted(compSpec)) { const values = valuesThunk(detail, compSpec.config, compSpec.validated); // Allow a preprocessing step for groups before returning the components const preprocessor = compSpec.validated.preprocess.getOr(Fun.identity); return preprocessor(values); } else { return valuesThunk(detail); } } ); }; const substituteAll = (owner: Optional, detail: D, components: AlloySpec[], placeholders: Record): AlloySpec[] => Arr.bind(components, (c) => substitute(owner, detail, c, placeholders)); const oneReplace = (label: string, replacements: UiSubstitutesAdt): Replacement => { let called = false; const used = () => called; const replace = () => { if (called) { throw new Error('Trying to use the same placeholder more than once: ' + label); } called = true; return replacements; }; const required = () => replacements.fold((req, _) => req, (req, _) => req); return { name: Fun.constant(label), required, used, replace }; }; const substitutePlaces = (owner: Optional, detail: D, components: AlloySpec[], placeholders: Record): AlloySpec[] => { const ps = Obj.map(placeholders, (ph, name) => oneReplace(name, ph)); const outcome = substituteAll(owner, detail, components, ps); Obj.each(ps, (p: Replacement) => { if (p.used() === false && p.required()) { throw new Error( 'Placeholder: ' + p.name() + ' was not found in components list\nNamespace: ' + owner.getOr('none') + '\nComponents: ' + JSON.stringify(detail.components, null, 2) ); } }); return outcome; }; const singleReplace = (detail: D, p: UiSubstitutesAdt): AlloySpec[] => p.fold((req, valueThunk) => [ valueThunk(detail) ], (req, valuesThunk) => valuesThunk(detail)); const single = adt.single; const multiple = adt.multiple; const placeholder = Fun.constant(_placeholder); export { single, multiple, placeholder, substituteAll, substitutePlaces, singleReplace };