import type { EditorDisplayDefaults, EditorDraftField, InputDefinition, InputDraftField, InputId, OverrideDiagnostic, ParseThemeOverride, ParsedThemeOverride, PreservedUnknownFields, Platform, Preset, SelectionId, SerializeThemeOverrideDraft, ThemeOverride, ThemeSelections, ThemeOverrideEditorDraft, TokenOverride, TokenPath, } from './types'; const PROTOTYPE_KEYS = new Set(['__proto__', 'constructor', 'prototype']); const EMPTY_PRESERVED_UNKNOWN: PreservedUnknownFields = { topLevel: {}, extensions: {}, inputs: {}, selections: {}, overrides: {}, }; export interface CreateThemeOverrideDraftOptions { readonly defaults: EditorDisplayDefaults; readonly preset?: Preset; readonly previewPlatform?: Platform; } export interface LoadThemeOverrideDraftOptions extends CreateThemeOverrideDraftOptions { readonly raw: unknown; readonly parse: ParseThemeOverride; } export type ThemeOverrideDraftAction = | { readonly type: 'set-preset'; readonly preset: Preset; } | { readonly type: 'set-input'; readonly inputId: InputId; readonly value: string; } | { readonly type: 'clear-input'; readonly inputId: InputId; } | { readonly type: 'set-selection'; readonly selectionId: SelectionId; readonly value: NonNullable; } | { readonly type: 'clear-selection'; readonly selectionId: SelectionId; } | { readonly type: 'set-override'; readonly tokenPath: TokenPath; readonly value: TokenOverride; } | { readonly type: 'clear-override'; readonly tokenPath: TokenPath; } | { readonly type: 'set-diagnostics'; readonly diagnostics: readonly OverrideDiagnostic[]; } | { readonly type: 'set-artifact-revision'; readonly revision: string | null; } | { readonly type: 'set-preview-platform'; readonly platform: Platform; }; function absentField(displayValue: TValue): EditorDraftField { return { displayValue, provenance: 'absent', }; } function incomingField( intentValue: TValue | undefined, fallback: TValue, ): EditorDraftField { if (intentValue === undefined) return absentField(fallback); return { displayValue: intentValue, intentValue, provenance: 'incoming', }; } function hasFallbackStrategy(definition: InputDefinition): boolean { return definition.strategy.kind === 'palette' && Boolean(definition.strategy.fallback); } function inputValues( inputs: Readonly>>, ): Readonly>> { return Object.fromEntries( Object.entries(inputs).map(([inputId, field]) => [inputId, field.displayValue]), ); } function createDefaultInputFields( defaults: EditorDisplayDefaults, preset: Preset, ): Readonly> { const definitions = [...defaults.listInputs()].sort( (left, right) => left.priority - right.priority, ); const fields: Partial> = {}; for (const definition of definitions) { const value = defaults.getDefaultInput(definition, preset); if (value !== undefined) { fields[definition.id] = absentField(value); } } for (const definition of definitions) { if (fields[definition.id]) continue; const value = defaults.deriveInput(definition, inputValues(fields), preset); if (value === undefined) { throw new Error( `Theme Editor cannot resolve a display default for input "${definition.id}".`, ); } fields[definition.id] = { ...absentField(value), relationship: 'derived', }; } return fields as Readonly>; } function refreshDerivedInputs( inputs: Readonly>, defaults: EditorDisplayDefaults, preset: Preset, ): Readonly> { let next = inputs; const definitions = [...defaults.listInputs()].sort( (left, right) => left.priority - right.priority, ); for (const definition of definitions) { const field = next[definition.id]; if (field.relationship !== 'derived') continue; const value = defaults.deriveInput(definition, inputValues(next), preset); if (value === undefined) { throw new Error(`Theme Editor cannot derive input "${definition.id}".`); } next = { ...next, [definition.id]: { ...field, displayValue: value, }, }; } return next; } export function createEmptyThemeOverrideDraft({ defaults, preset = 'poise', previewPlatform = 'web', }: CreateThemeOverrideDraftOptions): ThemeOverrideEditorDraft { return { loadState: 'empty', basePreset: absentField(preset), inputs: createDefaultInputFields(defaults, preset), selections: {}, overrides: {}, preservedUnknown: EMPTY_PRESERVED_UNKNOWN, diagnostics: [], previewPlatform, artifactRevision: null, }; } export function createParsedThemeOverrideDraft( parsed: ParsedThemeOverride, diagnostics: readonly OverrideDiagnostic[], { defaults, previewPlatform = 'web', }: Pick, ): ThemeOverrideEditorDraft { const preset = parsed.metadata.basePreset; let inputs = createDefaultInputFields(defaults, preset); const selections: Record> = {}; for (const [selectionId, value] of Object.entries(parsed.selections.known)) { selections[selectionId] = { displayValue: value, intentValue: value, provenance: 'incoming', } as EditorDraftField; } for (const definition of defaults.listInputs()) { const value = parsed.inputs.known[definition.id]; if (value === undefined) continue; inputs = { ...inputs, [definition.id]: { ...incomingField(value, value), ...(hasFallbackStrategy(definition) ? { relationship: 'pinned' as const } : {}), }, }; } inputs = refreshDerivedInputs(inputs, defaults, preset); const overrides = Object.fromEntries( Object.entries(parsed.overrides.known).map(([tokenPath, value]) => [ tokenPath, { displayValue: value, intentValue: value, provenance: 'incoming', }, ]), ) as ThemeOverrideEditorDraft['overrides']; return { loadState: 'parsed', basePreset: { displayValue: preset, intentValue: preset, provenance: 'incoming', }, inputs, selections: selections as ThemeOverrideEditorDraft['selections'], overrides, preservedUnknown: parsed.preservedUnknown, diagnostics, previewPlatform, artifactRevision: null, }; } export function loadThemeOverrideDraft({ raw, parse, defaults, preset = 'poise', previewPlatform = 'web', }: LoadThemeOverrideDraftOptions): ThemeOverrideEditorDraft { if (raw === undefined) { return createEmptyThemeOverrideDraft({ defaults, preset, previewPlatform }); } const result = parse(raw); if (result.valid) { return createParsedThemeOverrideDraft(result.parsed, result.diagnostics, { defaults, previewPlatform, }); } const loadState = result.diagnostics.some( ({ code }) => code === 'override.unsupported_schema_version', ) ? 'unsupported' : 'invalid'; return { ...createEmptyThemeOverrideDraft({ defaults, preset, previewPlatform }), loadState, diagnostics: result.diagnostics, }; } function touchedField(value: TValue): EditorDraftField { return { displayValue: value, intentValue: value, provenance: 'touched', }; } function clearedField( field: EditorDraftField, displayValue = field.displayValue, ): EditorDraftField { return { displayValue, provenance: 'cleared', }; } function refreshDefault( field: EditorDraftField, displayValue: TValue, ): EditorDraftField { return field.provenance === 'absent' || field.provenance === 'cleared' ? { displayValue, provenance: field.provenance, } : field; } function getInputDefinition(defaults: EditorDisplayDefaults, inputId: InputId): InputDefinition { const definition = defaults.listInputs().find(({ id }) => id === inputId); if (!definition) { throw new Error(`Theme Editor draft action references unknown input "${inputId}".`); } return definition; } export function reduceThemeOverrideDraft( draft: ThemeOverrideEditorDraft, action: ThemeOverrideDraftAction, defaults: EditorDisplayDefaults, ): ThemeOverrideEditorDraft { switch (action.type) { case 'set-preset': { if (action.preset === draft.basePreset.displayValue) return draft; const presetDefaults = createDefaultInputFields(defaults, action.preset); let inputs = Object.fromEntries( defaults.listInputs().map((definition) => { const current = draft.inputs[definition.id]; const presetDefault = presetDefaults[definition.id].displayValue; const refreshed = definition.strategy.kind === 'display-font' && (current.provenance === 'incoming' || current.provenance === 'touched') ? clearedField(current, presetDefault) : refreshDefault(current, presetDefault); return [ definition.id, { ...refreshed, ...(current.relationship ? { relationship: current.relationship } : {}), }, ]; }), ) as Readonly>; inputs = refreshDerivedInputs(inputs, defaults, action.preset); return { ...draft, basePreset: touchedField(action.preset), inputs, }; } case 'set-input': { const definition = getInputDefinition(defaults, action.inputId); const inputs = refreshDerivedInputs( { ...draft.inputs, [action.inputId]: { ...touchedField(action.value), ...(hasFallbackStrategy(definition) ? { relationship: 'pinned' as const } : {}), }, }, defaults, draft.basePreset.displayValue, ); return { ...draft, inputs, }; } case 'clear-input': { const preset = draft.basePreset.displayValue; const definition = getInputDefinition(defaults, action.inputId); const displayDefaults = createDefaultInputFields(defaults, preset); const field = draft.inputs[action.inputId]; const cleared = hasFallbackStrategy(definition) ? { ...clearedField(field), relationship: 'derived' as const, } : clearedField(field, displayDefaults[action.inputId].displayValue); const inputs = refreshDerivedInputs( { ...draft.inputs, [action.inputId]: cleared, }, defaults, preset, ); return { ...draft, inputs, }; } case 'set-selection': return { ...draft, selections: { ...draft.selections, [action.selectionId]: touchedField(action.value), }, }; case 'clear-selection': { const field = draft.selections[action.selectionId]; if (!field) return draft; return { ...draft, selections: { ...draft.selections, [action.selectionId]: { displayValue: undefined, provenance: 'cleared', }, }, }; } case 'set-override': return { ...draft, overrides: { ...draft.overrides, [action.tokenPath]: touchedField(action.value), }, }; case 'clear-override': { if (!Object.hasOwn(draft.overrides, action.tokenPath)) return draft; const field = draft.overrides[action.tokenPath]; return { ...draft, overrides: { ...draft.overrides, [action.tokenPath]: clearedField(field), }, }; } case 'set-diagnostics': return { ...draft, diagnostics: action.diagnostics, }; case 'set-artifact-revision': return { ...draft, artifactRevision: action.revision, }; case 'set-preview-platform': return { ...draft, previewPlatform: action.platform, }; } } function prototypeDiagnostic(intentPath: string): OverrideDiagnostic { return { code: 'override.prototype_key', severity: 'error', phase: 'intent', message: `Unsafe prototype key cannot be reserialized from "${intentPath}".`, intentPath, }; } function copySafeUnknown( value: Readonly>, intentPath: string, excluded: ReadonlySet, diagnostics: OverrideDiagnostic[], ): Record { const result: Record = {}; for (const [key, unknownValue] of Object.entries(value)) { if (excluded.has(key)) continue; if (PROTOTYPE_KEYS.has(key)) { diagnostics.push(prototypeDiagnostic(`${intentPath}.${key}`)); continue; } result[key] = unknownValue; } return result; } function carriesIntent>( field: TField, ): field is TField & { readonly intentValue: TValue; } { return ( (field.provenance === 'incoming' || field.provenance === 'touched') && field.intentValue !== undefined ); } export const serializeThemeOverrideDraft: SerializeThemeOverrideDraft = (draft) => { const diagnostics = draft.diagnostics.filter(({ severity }) => severity === 'error'); if ( (draft.loadState === 'unsupported' || draft.loadState === 'invalid') && diagnostics.length === 0 ) { diagnostics.push({ code: 'override.invalid_structure', severity: 'error', phase: 'intent', message: 'Invalid or unsupported override input cannot be serialized.', }); } const topLevel = copySafeUnknown( draft.preservedUnknown.topLevel, '$', new Set(['$extensions', 'inputs', 'selections', 'overrides']), diagnostics, ); const extensions = copySafeUnknown( draft.preservedUnknown.extensions, '$extensions', new Set(['com.forge.ui.themeOverride']), diagnostics, ); const inputs = copySafeUnknown( draft.preservedUnknown.inputs, 'inputs', new Set(Object.keys(draft.inputs)), diagnostics, ); const selections = copySafeUnknown( draft.preservedUnknown.selections, 'selections', new Set(Object.keys(draft.selections)), diagnostics, ); const overrides = copySafeUnknown( draft.preservedUnknown.overrides, 'overrides', new Set(), diagnostics, ); for (const [inputId, field] of Object.entries(draft.inputs)) { if (!carriesIntent(field)) continue; if (field.relationship === 'derived') continue; inputs[inputId] = field.intentValue; } for (const [selectionId, field] of Object.entries(draft.selections)) { if (carriesIntent(field)) { selections[selectionId] = field.intentValue; } } for (const [tokenPath, field] of Object.entries(draft.overrides)) { if (carriesIntent(field)) { overrides[tokenPath] = field.intentValue; } } if (diagnostics.length > 0) { return { valid: false, diagnostics, }; } const output: Record = { ...topLevel, $extensions: { ...extensions, 'com.forge.ui.themeOverride': { schemaVersion: '1.0.0', basePreset: draft.basePreset.displayValue, }, }, }; if (Object.keys(inputs).length > 0) output.inputs = inputs; if (Object.keys(selections).length > 0) output.selections = selections; if (Object.keys(overrides).length > 0) output.overrides = overrides; return { valid: true, override: output as unknown as ThemeOverride, }; };