You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/morfo/resolver.ts

151 lines
4.7 KiB

/**
* Pure morfo → attrs resolver.
*
* Walks a `MorfoPart`'s `data` and `aria` declarations, evaluates conditions,
* resolves value sources against the supplied bindings, and returns a flat
* attribute map ready to render or to pass to `dom.apply`.
*
* No reactivity, no side-effects, no DOM. The runtime layer wraps these in
* effects; the Provider base class wraps them in a `$derived.by`.
*/
import type {
MorfoAriaEntry,
MorfoCondition,
MorfoData,
MorfoPart,
MorfoPrimitiveValueSource,
MorfoValueSource
} from './types';
export interface MorfoBindings {
props?: Record<string, unknown>;
states?: Record<string, unknown>;
parts?: Record<string, unknown>;
translations?: (key: string) => string | undefined;
}
export function findMorfoPart(parts: readonly MorfoPart[], target: string): MorfoPart | undefined {
for (const part of parts) {
if (part.kebab === target) return part;
if (part.parts) {
const nested = findMorfoPart(part.parts, target);
if (nested) return nested;
}
}
return undefined;
}
export function shouldEmitMorfoEntry(
condition: MorfoCondition | undefined,
bindings: MorfoBindings
): boolean {
if (!condition || condition === 'always') return true;
if (condition.when === 'part-present') return Boolean(bindings.parts?.[condition.part]);
if (condition.when === 'state-equals') return bindings.states?.[condition.state] === condition.value;
if (condition.when === 'prop-truthy') return Boolean(bindings.props?.[condition.prop]);
if (condition.when === 'prop-falsy') return !bindings.props?.[condition.prop];
return true;
}
function resolveMorfoPrimitiveSource(
source: MorfoPrimitiveValueSource,
bindings: MorfoBindings
): unknown {
if (source.kind === 'literal') return source.value;
if (source.kind === 'stateRef') return bindings.states?.[source.state];
if (source.kind === 'partRef') return bindings.parts?.[source.target];
if (source.kind === 'propRef') return bindings.props?.[source.prop];
if (source.kind === 'translationRef') return bindings.translations?.(source.key);
return undefined;
}
export function resolveMorfoSource(source: MorfoValueSource, bindings: MorfoBindings): unknown {
if (source.kind !== 'mapRef') {
return resolveMorfoPrimitiveSource(source, bindings);
}
const raw = resolveMorfoPrimitiveSource(source.source, bindings);
if (raw === undefined || raw === null) {
return source.fallback;
}
const mapped = source.map[String(raw)];
return mapped ?? source.fallback;
}
export function resolveMorfoDataValue(data: MorfoData, bindings: MorfoBindings): unknown {
const source = data.value;
if (!source) return undefined;
const raw = resolveMorfoSource(source, bindings);
if (!data.values || data.values.length === 0) {
return raw ? '' : undefined;
}
if (source.kind === 'stateRef' && typeof raw === 'boolean') {
if (raw) return source.state;
return data.values.find((value) => value !== source.state);
}
return raw;
}
export function resolveMorfoAriaValue(entry: MorfoAriaEntry, bindings: MorfoBindings): unknown {
const raw = resolveMorfoSource(entry.value, bindings);
if (entry.value.kind === 'stateRef') {
// ARIA boolean attrs require literal 'true'/'false' strings, not the
// HTML boolean attribute semantics. `aria-pressed="false"` is a valid
// state distinct from the attribute being absent — never remove it
// based on falsy values.
return Boolean(raw) ? 'true' : 'false';
}
return raw;
}
/**
* Resolves a part's full attribute map (role + data-* + aria-*) against the
* given bindings.
*
* The map ALWAYS includes every declared attr (data with a `value` source +
* every aria entry). Attrs whose condition is falsy or whose value source
* resolves to nothing are emitted as `undefined`, which `dom.apply` reads as
* "remove this attribute" and Svelte ignores during render. This is what
* lets the runtime clean up attrs that previously matched their condition
* but no longer do (e.g. `data-invalid` toggling from set to absent).
*
* Entries with no `value` source on `data` are skipped entirely — they are
* documentation in the morfo, not runtime-managed attrs.
*/
export function resolvePartAttrs(
partMeta: MorfoPart,
bindings: MorfoBindings
): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (partMeta.role) {
result.role = partMeta.role;
}
for (const data of partMeta.data) {
if (!data.value) continue;
if (!shouldEmitMorfoEntry(data.condition, bindings)) {
result[data.attr] = undefined;
continue;
}
result[data.attr] = resolveMorfoDataValue(data, bindings);
}
for (const aria of partMeta.aria) {
if (!shouldEmitMorfoEntry(aria.condition, bindings)) {
result[aria.attr] = undefined;
continue;
}
result[aria.attr] = resolveMorfoAriaValue(aria, bindings);
}
return result;
}

Powered by TurnKey Linux.