From b0ec3d4375985db79f50a942e1dfb8693bd3060e Mon Sep 17 00:00:00 2001 From: eyelidlessness Date: Mon, 27 May 2024 11:35:51 -0700 Subject: [PATCH] Initial support for appearances (and body classes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated testing is pending. Some manual validation has been done to verify that this likely works as intended. The intent is to make this first pass available for client iteration as quickly as possible. While it’s possible to include unit tests for `TokenListParser`, it seems more likely we’ll want to add integration tests in the `scenario` package. Given there’s a ton of unaddressed feedback in #110, it seems most prudent to get this into draft first, and bring in integration tests once that lands. --- .../types/string/PartiallyKnownString.ts | 27 +++ .../xforms-engine/src/body/BodyDefinition.ts | 34 +++- .../src/body/RepeatElementDefinition.ts | 4 + .../body/appearance/inputAppearanceParser.ts | 39 +++++ .../body/appearance/selectAppearanceParser.ts | 38 +++++ .../structureElementAppearanceParser.ts | 7 + .../src/body/control/ControlDefinition.ts | 4 + .../src/body/control/InputDefinition.ts | 13 ++ .../body/control/select/SelectDefinition.ts | 19 ++- .../src/body/group/BaseGroupDefinition.ts | 4 + .../src/body/text/TextElementDefinition.ts | 6 +- packages/xforms-engine/src/client/BaseNode.ts | 10 +- .../xforms-engine/src/client/GroupNode.ts | 4 + .../src/client/NodeAppearances.ts | 22 +++ .../src/client/RepeatInstanceNode.ts | 4 + .../src/client/RepeatRangeNode.ts | 4 + packages/xforms-engine/src/client/RootNode.ts | 22 +++ .../xforms-engine/src/client/SelectNode.ts | 4 + .../xforms-engine/src/client/StringNode.ts | 4 + .../xforms-engine/src/client/SubtreeNode.ts | 1 + packages/xforms-engine/src/instance/Group.ts | 8 +- .../src/instance/RepeatInstance.ts | 13 +- .../xforms-engine/src/instance/RepeatRange.ts | 39 ++++- packages/xforms-engine/src/instance/Root.ts | 6 +- .../xforms-engine/src/instance/SelectField.ts | 5 +- .../xforms-engine/src/instance/StringField.ts | 6 +- .../xforms-engine/src/instance/Subtree.ts | 2 +- .../src/instance/abstract/InstanceNode.ts | 3 + .../xforms-engine/src/instance/children.ts | 22 ++- .../xforms-engine/src/lib/TokenListParser.ts | 156 ++++++++++++++++++ .../src/model/ModelDefinition.ts | 2 +- .../xforms-engine/src/model/RootDefinition.ts | 4 +- .../src/model/ValueNodeDefinition.ts | 5 +- 33 files changed, 507 insertions(+), 34 deletions(-) create mode 100644 packages/common/types/string/PartiallyKnownString.ts create mode 100644 packages/xforms-engine/src/body/appearance/inputAppearanceParser.ts create mode 100644 packages/xforms-engine/src/body/appearance/selectAppearanceParser.ts create mode 100644 packages/xforms-engine/src/body/appearance/structureElementAppearanceParser.ts create mode 100644 packages/xforms-engine/src/client/NodeAppearances.ts create mode 100644 packages/xforms-engine/src/lib/TokenListParser.ts 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 -//