diff --git a/packages/common/types/string/PartiallyKnownString.ts b/packages/common/types/string/PartiallyKnownString.ts new file mode 100644 index 00000000..25b0019d --- /dev/null +++ b/packages/common/types/string/PartiallyKnownString.ts @@ -0,0 +1,27 @@ +/** + * Produces a `string` type while preserving autocomplete/autosuggest + * functionality for a known string (union). + * + * @see {@link https://www.totaltypescript.com/tips/create-autocomplete-helper-which-allows-for-arbitrary-values} + * + * @example + * ```ts + * let foo: PartiallyKnownString<'a' | 'b' | 'zed'>; + * + * // Each of these will be suggested by a TypeScript-supporting editor: + * foo = 'a'; + * foo = 'b'; + * foo = 'zed'; + * + * // ... but any string is valid: + * foo = 'lmnop'; + * ``` + */ +// prettier-ignore +export type PartiallyKnownString = + [string] extends [Known] + ? string + : ( + | Known + | (string & { /* Type hack! */ }) + ); diff --git a/packages/xforms-engine/src/body/BodyDefinition.ts b/packages/xforms-engine/src/body/BodyDefinition.ts index a5136a77..8ebd272f 100644 --- a/packages/xforms-engine/src/body/BodyDefinition.ts +++ b/packages/xforms-engine/src/body/BodyDefinition.ts @@ -1,5 +1,7 @@ import type { XFormDefinition } from '../XFormDefinition.ts'; import { DependencyContext } from '../expression/DependencyContext.ts'; +import type { ParsedTokenList } from '../lib/TokenListParser.ts'; +import { TokenListParser } from '../lib/TokenListParser.ts'; import { RepeatElementDefinition } from './RepeatElementDefinition.ts'; import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts'; import { ControlDefinition } from './control/ControlDefinition.ts'; @@ -15,14 +17,18 @@ export interface BodyElementParentContext { readonly element: Element; } +// prettier-ignore +export type ControlElementDefinition = + | AnySelectDefinition + | InputDefinition; + type SupportedBodyElementDefinition = // eslint-disable-next-line @typescript-eslint/sort-type-constituents | RepeatElementDefinition | LogicalGroupDefinition | PresentationGroupDefinition | StructuralGroupDefinition - | InputDefinition - | AnySelectDefinition; + | ControlElementDefinition; // eslint-disable-next-line @typescript-eslint/no-explicit-any type BodyElementDefinitionConstructor = new (...args: any[]) => SupportedBodyElementDefinition; @@ -135,6 +141,10 @@ class BodyElementMap extends Map } } +const bodyClassParser = new TokenListParser(['pages' /*, 'theme-grid' */]); + +export type BodyClassList = ParsedTokenList; + export class BodyDefinition extends DependencyContext { static getChildElementDefinitions( form: XFormDefinition, @@ -156,6 +166,25 @@ export class BodyDefinition extends DependencyContext { } readonly element: Element; + + /** + * @todo this class is already an oddity in that it's **like** an element + * definition, but it isn't one itself. Adding this property here emphasizes + * that awkwardness. It also extends the applicable scope where instances of + * this class are accessed. While it's still ephemeral, it's anticipated that + * this extension might cause some disomfort. If so, the most plausible + * alternative is an additional refactor to: + * + * 1. Introduce a `BodyElementDefinition` sublass for ``. + * 2. Disambiguate the respective names of those, in some reasonable way. + * 3. Add a layer of indirection between this class and that new body element + * definition's class. + * 4. At that point, we may as well prioritize the little bit of grunt work to + * pass the `BodyDefinition` instance by reference rather than assigning it + * to anything. + */ + readonly classes: BodyClassList; + readonly elements: readonly AnyBodyElementDefinition[]; protected readonly elementsByReference: BodyElementMap; @@ -171,6 +200,7 @@ export class BodyDefinition extends DependencyContext { this.reference = form.rootReference; this.element = element; + this.classes = bodyClassParser.parseFrom(element, 'class'); this.elements = BodyDefinition.getChildElementDefinitions(form, this, element); this.elementsByReference = new BodyElementMap(this.elements); } diff --git a/packages/xforms-engine/src/body/RepeatElementDefinition.ts b/packages/xforms-engine/src/body/RepeatElementDefinition.ts index 5e4bb144..67c0e986 100644 --- a/packages/xforms-engine/src/body/RepeatElementDefinition.ts +++ b/packages/xforms-engine/src/body/RepeatElementDefinition.ts @@ -3,6 +3,8 @@ import type { XFormDefinition } from '../XFormDefinition.ts'; import type { BodyElementDefinitionArray, BodyElementParentContext } from './BodyDefinition.ts'; import { BodyDefinition } from './BodyDefinition.ts'; import { BodyElementDefinition } from './BodyElementDefinition.ts'; +import type { StructureElementAppearanceDefinition } from './appearance/structureElementAppearanceParser.ts'; +import { structureElementAppearanceParser } from './appearance/structureElementAppearanceParser.ts'; import { LabelDefinition } from './text/LabelDefinition.ts'; export class RepeatElementDefinition extends BodyElementDefinition<'repeat'> { @@ -13,6 +15,7 @@ export class RepeatElementDefinition extends BodyElementDefinition<'repeat'> { override readonly category = 'structure'; readonly type = 'repeat'; override readonly reference: string; + readonly appearances: StructureElementAppearanceDefinition; override readonly label: LabelDefinition | null; // TODO: this will fall into the growing category of non-`BindExpression` @@ -35,6 +38,7 @@ export class RepeatElementDefinition extends BodyElementDefinition<'repeat'> { } this.reference = reference; + this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance'); this.countExpression = element.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'count'); const childElements = Array.from(element.children).filter((childElement) => { diff --git a/packages/xforms-engine/src/body/appearance/inputAppearanceParser.ts b/packages/xforms-engine/src/body/appearance/inputAppearanceParser.ts new file mode 100644 index 00000000..7e5abb5a --- /dev/null +++ b/packages/xforms-engine/src/body/appearance/inputAppearanceParser.ts @@ -0,0 +1,39 @@ +import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts'; + +export const inputAppearanceParser = new TokenListParser([ + 'multiline', + 'numbers', + 'url', + 'thousand-sep', + + // date (TODO: data types) + 'no-calendar', + 'month-year', + 'year', + // date > calendars + 'ethiopian', + 'coptic', + 'islamic', + 'bikram-sambat', + 'myanmar', + 'persian', + + // geo (TODO: data types) + 'placement-map', + 'maps', + + // image/media (TODO: move to eventual ``?) + 'hidden-answer', + 'annotate', + 'draw', + 'signature', + 'new-front', + 'new', + 'front', + + // *? + 'printer', // Note: actual usage uses `printer:...` (like `ex:...`). + 'masked', +]); + +export type InputAppearanceDefinition = ParsedTokenList; diff --git a/packages/xforms-engine/src/body/appearance/selectAppearanceParser.ts b/packages/xforms-engine/src/body/appearance/selectAppearanceParser.ts new file mode 100644 index 00000000..0cf8f2b0 --- /dev/null +++ b/packages/xforms-engine/src/body/appearance/selectAppearanceParser.ts @@ -0,0 +1,38 @@ +import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts'; + +export const selectAppearanceParser = new TokenListParser( + [ + // From XLSForm Docs: + 'compact', + 'horizontal', + 'horizontal-compact', + 'label', + 'list-nolabel', + 'minimal', + + // From Collect `Appearances.kt`: + 'columns', + 'columns-1', + 'columns-2', + 'columns-3', + 'columns-4', + 'columns-5', + // Note: Collect supports arbitrary columns-n. Technically we do too (we parse + // out any appearance, not just those we know about). But we'll only include + // types/defaults up to 5. + 'columns-pack', + 'autocomplete', + + // TODO: these are `` only + 'likert', + 'quick', + 'quickcompact', + 'map', + // "quick map" + ], + { + aliases: [{ fromAlias: 'search', toCanonical: 'autocomplete' }], + } +); + +export type SelectAppearanceDefinition = ParsedTokenList; diff --git a/packages/xforms-engine/src/body/appearance/structureElementAppearanceParser.ts b/packages/xforms-engine/src/body/appearance/structureElementAppearanceParser.ts new file mode 100644 index 00000000..a1a65f16 --- /dev/null +++ b/packages/xforms-engine/src/body/appearance/structureElementAppearanceParser.ts @@ -0,0 +1,7 @@ +import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts'; + +export const structureElementAppearanceParser = new TokenListParser(['field-list', 'table-list']); + +export type StructureElementAppearanceDefinition = ParsedTokenList< + typeof structureElementAppearanceParser +>; diff --git a/packages/xforms-engine/src/body/control/ControlDefinition.ts b/packages/xforms-engine/src/body/control/ControlDefinition.ts index 8470c531..bbe167bf 100644 --- a/packages/xforms-engine/src/body/control/ControlDefinition.ts +++ b/packages/xforms-engine/src/body/control/ControlDefinition.ts @@ -1,4 +1,5 @@ import type { XFormDefinition } from '../../XFormDefinition.ts'; +import type { ParsedTokenList } from '../../lib/TokenListParser.ts'; import type { BodyElementParentContext } from '../BodyDefinition.ts'; import { BodyElementDefinition } from '../BodyElementDefinition.ts'; import { HintDefinition } from '../text/HintDefinition.ts'; @@ -23,6 +24,9 @@ export abstract class ControlDefinition< override readonly label: LabelDefinition | null; override readonly hint: HintDefinition | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + abstract readonly appearances: ParsedTokenList; + constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) { super(form, parent, element); diff --git a/packages/xforms-engine/src/body/control/InputDefinition.ts b/packages/xforms-engine/src/body/control/InputDefinition.ts index 8c532bf7..8010c2b5 100644 --- a/packages/xforms-engine/src/body/control/InputDefinition.ts +++ b/packages/xforms-engine/src/body/control/InputDefinition.ts @@ -1,3 +1,9 @@ +import type { XFormDefinition } from '../../XFormDefinition.ts'; +import type { BodyElementParentContext } from '../BodyDefinition.ts'; +import { + inputAppearanceParser, + type InputAppearanceDefinition, +} from '../appearance/inputAppearanceParser.ts'; import { ControlDefinition } from './ControlDefinition.ts'; export class InputDefinition extends ControlDefinition<'input'> { @@ -6,4 +12,11 @@ export class InputDefinition extends ControlDefinition<'input'> { } readonly type = 'input'; + readonly appearances: InputAppearanceDefinition; + + constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) { + super(form, parent, element); + + this.appearances = inputAppearanceParser.parseFrom(element, 'appearance'); + } } diff --git a/packages/xforms-engine/src/body/control/select/SelectDefinition.ts b/packages/xforms-engine/src/body/control/select/SelectDefinition.ts index 5e8a696b..7b31a8c3 100644 --- a/packages/xforms-engine/src/body/control/select/SelectDefinition.ts +++ b/packages/xforms-engine/src/body/control/select/SelectDefinition.ts @@ -3,14 +3,21 @@ import type { LocalNamedElement } from '@getodk/common/types/dom.ts'; import type { XFormDefinition } from '../../../XFormDefinition.ts'; import { getItemElements, getItemsetElement } from '../../../lib/dom/query.ts'; import type { AnyBodyElementDefinition, BodyElementParentContext } from '../../BodyDefinition.ts'; +import type { SelectAppearanceDefinition } from '../../appearance/selectAppearanceParser.ts'; +import { selectAppearanceParser } from '../../appearance/selectAppearanceParser.ts'; import { ControlDefinition } from '../ControlDefinition.ts'; import { ItemDefinition } from './ItemDefinition.ts'; import { ItemsetDefinition } from './ItemsetDefinition.ts'; -// TODO: `` is *almost* reasonable to support here too. The main -// hesitation is that its single, implicit "item" does not have a distinct -//