@getodk/xforms-engine 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/.vite/manifest.json +1 -0
  2. package/dist/XFormDOM.d.ts +1 -0
  3. package/dist/XFormDataType.d.ts +2 -1
  4. package/dist/XFormDefinition.d.ts +1 -0
  5. package/dist/body/BodyDefinition.d.ts +27 -9
  6. package/dist/body/BodyElementDefinition.d.ts +5 -4
  7. package/dist/body/RepeatElementDefinition.d.ts +19 -0
  8. package/dist/body/UnsupportedBodyElementDefinition.d.ts +3 -2
  9. package/dist/body/appearance/inputAppearanceParser.d.ts +4 -0
  10. package/dist/body/appearance/selectAppearanceParser.d.ts +4 -0
  11. package/dist/body/appearance/structureElementAppearanceParser.d.ts +4 -0
  12. package/dist/body/control/ControlDefinition.d.ts +5 -2
  13. package/dist/body/control/InputDefinition.d.ts +6 -0
  14. package/dist/body/control/select/ItemDefinition.d.ts +4 -3
  15. package/dist/body/control/select/ItemsetDefinition.d.ts +4 -3
  16. package/dist/body/control/select/ItemsetNodesetContext.d.ts +3 -2
  17. package/dist/body/control/select/ItemsetNodesetExpression.d.ts +2 -1
  18. package/dist/body/control/select/ItemsetValueExpression.d.ts +2 -1
  19. package/dist/body/control/select/SelectDefinition.d.ts +16 -5
  20. package/dist/body/group/BaseGroupDefinition.d.ts +6 -10
  21. package/dist/body/group/LogicalGroupDefinition.d.ts +1 -0
  22. package/dist/body/group/PresentationGroupDefinition.d.ts +3 -2
  23. package/dist/body/group/StructuralGroupDefinition.d.ts +1 -0
  24. package/dist/body/text/HintDefinition.d.ts +4 -4
  25. package/dist/body/text/LabelDefinition.d.ts +9 -7
  26. package/dist/body/text/TextElementDefinition.d.ts +9 -8
  27. package/dist/body/text/TextElementOutputPart.d.ts +2 -1
  28. package/dist/body/text/TextElementPart.d.ts +5 -4
  29. package/dist/body/text/TextElementReferencePart.d.ts +2 -1
  30. package/dist/body/text/TextElementStaticPart.d.ts +2 -1
  31. package/dist/client/BaseNode.d.ts +9 -3
  32. package/dist/client/EngineConfig.d.ts +2 -1
  33. package/dist/client/GroupNode.d.ts +10 -6
  34. package/dist/client/NodeAppearances.d.ts +15 -0
  35. package/dist/client/RepeatInstanceNode.d.ts +10 -6
  36. package/dist/client/RepeatRangeNode.d.ts +11 -7
  37. package/dist/client/RootNode.d.ts +24 -4
  38. package/dist/client/SelectNode.d.ts +10 -6
  39. package/dist/client/StringNode.d.ts +9 -5
  40. package/dist/client/SubtreeNode.d.ts +6 -4
  41. package/dist/client/TextRange.d.ts +2 -1
  42. package/dist/client/hierarchy.d.ts +9 -8
  43. package/dist/client/index.d.ts +3 -2
  44. package/dist/expression/DependencyContext.d.ts +2 -1
  45. package/dist/expression/DependentExpression.d.ts +3 -2
  46. package/dist/index.d.ts +2 -1
  47. package/dist/index.js +1882 -1757
  48. package/dist/index.js.map +1 -1
  49. package/dist/instance/Group.d.ts +15 -15
  50. package/dist/instance/RepeatInstance.d.ts +39 -15
  51. package/dist/instance/RepeatRange.d.ts +98 -20
  52. package/dist/instance/Root.d.ts +25 -39
  53. package/dist/instance/SelectField.d.ts +17 -17
  54. package/dist/instance/StringField.d.ts +17 -17
  55. package/dist/instance/Subtree.d.ts +13 -13
  56. package/dist/instance/abstract/DescendantNode.d.ts +26 -18
  57. package/dist/instance/abstract/InstanceNode.d.ts +44 -46
  58. package/dist/instance/children.d.ts +2 -1
  59. package/dist/instance/hierarchy.d.ts +8 -7
  60. package/dist/instance/index.d.ts +4 -3
  61. package/dist/instance/internal-api/EvaluationContext.d.ts +10 -8
  62. package/dist/instance/internal-api/InstanceConfig.d.ts +3 -2
  63. package/dist/instance/internal-api/SubscribableDependency.d.ts +2 -1
  64. package/dist/instance/internal-api/TranslationContext.d.ts +2 -1
  65. package/dist/instance/internal-api/ValueContext.d.ts +6 -5
  66. package/dist/instance/resource.d.ts +3 -2
  67. package/dist/instance/text/TextChunk.d.ts +4 -3
  68. package/dist/instance/text/TextRange.d.ts +2 -1
  69. package/dist/lib/TokenListParser.d.ts +84 -0
  70. package/dist/lib/dom/query.d.ts +8 -2
  71. package/dist/lib/reactivity/createChildrenState.d.ts +4 -3
  72. package/dist/lib/reactivity/createComputedExpression.d.ts +4 -3
  73. package/dist/lib/reactivity/createSelectItems.d.ts +4 -3
  74. package/dist/lib/reactivity/createValueState.d.ts +3 -2
  75. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +6 -4
  76. package/dist/lib/reactivity/node-state/createClientState.d.ts +7 -6
  77. package/dist/lib/reactivity/node-state/createCurrentState.d.ts +5 -4
  78. package/dist/lib/reactivity/node-state/createEngineState.d.ts +4 -3
  79. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +7 -6
  80. package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +2 -1
  81. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +3 -2
  82. package/dist/lib/reactivity/node-state/representations.d.ts +2 -1
  83. package/dist/lib/reactivity/scope.d.ts +2 -1
  84. package/dist/lib/reactivity/text/createFieldHint.d.ts +4 -3
  85. package/dist/lib/reactivity/text/createNodeLabel.d.ts +4 -3
  86. package/dist/lib/reactivity/text/createTextRange.d.ts +6 -5
  87. package/dist/lib/reactivity/types.d.ts +2 -1
  88. package/dist/lib/xpath/analysis.d.ts +2 -1
  89. package/dist/model/BindComputation.d.ts +2 -1
  90. package/dist/model/BindDefinition.d.ts +6 -5
  91. package/dist/model/DescendentNodeDefinition.d.ts +6 -6
  92. package/dist/model/ModelBindMap.d.ts +4 -3
  93. package/dist/model/ModelDefinition.d.ts +2 -1
  94. package/dist/model/NodeDefinition.d.ts +20 -19
  95. package/dist/model/RepeatInstanceDefinition.d.ts +7 -7
  96. package/dist/model/{RepeatSequenceDefinition.d.ts → RepeatRangeDefinition.d.ts} +7 -6
  97. package/dist/model/RepeatTemplateDefinition.d.ts +8 -8
  98. package/dist/model/RootDefinition.d.ts +8 -5
  99. package/dist/model/SubtreeDefinition.d.ts +5 -4
  100. package/dist/model/ValueNodeDefinition.d.ts +5 -5
  101. package/dist/solid.js +1873 -1751
  102. package/dist/solid.js.map +1 -1
  103. package/package.json +14 -18
  104. package/src/XFormDOM.ts +81 -8
  105. package/src/body/BodyDefinition.ts +38 -23
  106. package/src/body/RepeatElementDefinition.ts +70 -0
  107. package/src/body/appearance/inputAppearanceParser.ts +39 -0
  108. package/src/body/appearance/selectAppearanceParser.ts +38 -0
  109. package/src/body/appearance/structureElementAppearanceParser.ts +7 -0
  110. package/src/body/control/ControlDefinition.ts +4 -0
  111. package/src/body/control/InputDefinition.ts +13 -0
  112. package/src/body/control/select/SelectDefinition.ts +14 -5
  113. package/src/body/group/BaseGroupDefinition.ts +11 -49
  114. package/src/body/text/LabelDefinition.ts +15 -1
  115. package/src/body/text/TextElementDefinition.ts +5 -5
  116. package/src/client/BaseNode.ts +9 -1
  117. package/src/client/GroupNode.ts +6 -2
  118. package/src/client/NodeAppearances.ts +22 -0
  119. package/src/client/RepeatInstanceNode.ts +4 -0
  120. package/src/client/RepeatRangeNode.ts +6 -2
  121. package/src/client/RootNode.ts +22 -0
  122. package/src/client/SelectNode.ts +4 -0
  123. package/src/client/StringNode.ts +4 -0
  124. package/src/client/SubtreeNode.ts +1 -0
  125. package/src/instance/Group.ts +14 -9
  126. package/src/instance/RepeatInstance.ts +59 -15
  127. package/src/instance/RepeatRange.ts +133 -15
  128. package/src/instance/Root.ts +20 -64
  129. package/src/instance/SelectField.ts +7 -7
  130. package/src/instance/StringField.ts +8 -7
  131. package/src/instance/Subtree.ts +10 -7
  132. package/src/instance/abstract/DescendantNode.ts +45 -43
  133. package/src/instance/abstract/InstanceNode.ts +69 -86
  134. package/src/instance/children.ts +17 -7
  135. package/src/instance/index.ts +1 -1
  136. package/src/instance/internal-api/EvaluationContext.ts +5 -6
  137. package/src/instance/internal-api/ValueContext.ts +2 -2
  138. package/src/lib/TokenListParser.ts +156 -0
  139. package/src/lib/dom/query.ts +13 -0
  140. package/src/lib/reactivity/createChildrenState.ts +51 -6
  141. package/src/lib/reactivity/createComputedExpression.ts +1 -1
  142. package/src/lib/reactivity/createSelectItems.ts +4 -6
  143. package/src/lib/reactivity/createValueState.ts +6 -6
  144. package/src/lib/reactivity/materializeCurrentStateChildren.ts +3 -1
  145. package/src/model/DescendentNodeDefinition.ts +1 -2
  146. package/src/model/ModelDefinition.ts +1 -1
  147. package/src/model/NodeDefinition.ts +12 -12
  148. package/src/model/RepeatInstanceDefinition.ts +8 -13
  149. package/src/model/{RepeatSequenceDefinition.ts → RepeatRangeDefinition.ts} +6 -6
  150. package/src/model/RepeatTemplateDefinition.ts +10 -15
  151. package/src/model/RootDefinition.ts +6 -12
  152. package/src/model/SubtreeDefinition.ts +3 -3
  153. package/src/model/ValueNodeDefinition.ts +2 -3
  154. package/dist/body/RepeatDefinition.d.ts +0 -15
  155. package/dist/body/group/RepeatGroupDefinition.d.ts +0 -12
  156. package/src/body/RepeatDefinition.ts +0 -54
  157. package/src/body/group/RepeatGroupDefinition.ts +0 -91
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getodk/xforms-engine",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "XForms engine for ODK Web Forms",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "README.md"
30
30
  ],
31
31
  "engines": {
32
- "node": "^18.19.1 || ^20.11.1",
32
+ "node": "^18.20.3 || ^20.13.1 || ^22.2.0",
33
33
  "yarn": "1.22.19"
34
34
  },
35
35
  "scripts": {
@@ -54,30 +54,26 @@
54
54
  "test:types": "tsc --project ./tsconfig.json --emitDeclarationOnly false --noEmit"
55
55
  },
56
56
  "dependencies": {
57
- "solid-js": "^1.8.3"
57
+ "solid-js": "^1.8.17"
58
58
  },
59
59
  "devDependencies": {
60
- "@babel/core": "^7.23.2",
61
- "@getodk/tree-sitter-xpath": "0.1.0",
62
- "@getodk/xpath": "0.1.0",
63
- "@playwright/test": "^1.42.1",
64
- "@suid/vite-plugin": "^0.1.5",
65
- "@vitest/browser": "^1.3.1",
60
+ "@babel/core": "^7.24.6",
61
+ "@getodk/tree-sitter-xpath": "0.1.1",
62
+ "@getodk/xpath": "0.1.2",
63
+ "@playwright/test": "^1.44.1",
64
+ "@vitest/browser": "^1.6.0",
66
65
  "babel-plugin-transform-jsbi-to-bigint": "^1.4.0",
67
66
  "http-server": "^14.1.1",
68
67
  "jsdom": "^24.0.0",
69
- "typedoc": "^0.25.12",
68
+ "typedoc": "^0.25.13",
70
69
  "unplugin-fonts": "^1.1.1",
71
- "vite": "^5.1.5",
72
- "vite-plugin-babel": "^1.2.0",
73
- "vite-plugin-dts": "^3.7.3",
74
- "vite-plugin-no-bundle": "^3.0.0",
75
- "vite-plugin-solid": "^2.10.1",
76
- "vitest": "^1.3.1",
77
- "vitest-github-actions-reporter": "^0.11.1"
70
+ "vite": "^5.2.11",
71
+ "vite-plugin-dts": "^3.9.1",
72
+ "vite-plugin-no-bundle": "^4.0.0",
73
+ "vitest": "^1.6.0"
78
74
  },
79
75
  "peerDependencies": {
80
- "solid-js": "^1.8.3"
76
+ "solid-js": "^1.8.17"
81
77
  },
82
78
  "peerDependenciesMeta": {
83
79
  "solid-js": {
package/src/XFormDOM.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { XFORMS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
1
+ import { XFORMS_NAMESPACE_URI, XMLNS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
2
  import { XFormsXPathEvaluator } from '@getodk/xpath';
3
3
 
4
4
  const domParser = new DOMParser();
@@ -41,7 +41,83 @@ const normalizeBodyRefNodesetAttributes = (body: Element): void => {
41
41
  }
42
42
  };
43
43
 
44
- const normalizeRepeatGroups = (xformDocument: XMLDocument, body: Element): void => {
44
+ const normalizeRepeatGroupAttributes = (group: Element, repeat: Element): void => {
45
+ for (const groupAttribute of group.attributes) {
46
+ const { localName, namespaceURI, nodeName, value } = groupAttribute;
47
+
48
+ if (
49
+ // Don't propagate namespace declarations (which appear as attributes in
50
+ // the browser/XML DOM, either named `xmlns` or with an `xmlns` prefix,
51
+ // always in the XMLNS namespace).
52
+ namespaceURI === XMLNS_NAMESPACE_URI ||
53
+ // Don't propagate `ref`, it has been normalized as `nodeset` on the
54
+ // repeat element.
55
+ localName === 'ref' ||
56
+ // TODO: this accommodates tests of this normalization process, where
57
+ // certain nodes of interest are given an `id` attribute, and looked up
58
+ // for the purpose of asserting what was normalized about them. It's
59
+ // unclear if there's a generally expected behavior around the attribute.
60
+ localName === 'id'
61
+ ) {
62
+ continue;
63
+ }
64
+
65
+ // TODO: The `appearance` attribute is propagated from
66
+ // `<group appearance><repeat>` to `<repeat appearance>`. But we presently
67
+ // bail if both elements define the attribute.
68
+ //
69
+ // The spec is clear that the attribute is only supported on `<group>` and
70
+ // control elements, which would suggest it should not be present on a
71
+ // `<repeat>` element directly. But many form fixtures (in e.g. Enketo)
72
+ // do have `<repeat apperance>`.
73
+ //
74
+ // It may be reasonable to relax this by:
75
+ //
76
+ // - Detecting if they share the same appearances, treated as a no-op.
77
+ //
78
+ // - Assume they're both meant to apply, and concatenate.
79
+ if (
80
+ localName === 'appearance' &&
81
+ namespaceURI === XFORMS_NAMESPACE_URI &&
82
+ repeat.hasAttribute(localName)
83
+ ) {
84
+ const ref = group.getAttribute('ref');
85
+
86
+ throw new Error(
87
+ `Failed to normalize conflicting "appearances" attribute of group/repeat "${ref}"`
88
+ );
89
+ }
90
+
91
+ repeat.setAttributeNS(namespaceURI, nodeName, value);
92
+ }
93
+ };
94
+
95
+ const normalizeRepeatGroupLabel = (group: Element, repeat: Element): void => {
96
+ const groupLabel = Array.from(group.children).find((child) => {
97
+ return child.localName === 'label';
98
+ });
99
+
100
+ if (groupLabel == null) {
101
+ return;
102
+ }
103
+
104
+ const repeatLabel = groupLabel.cloneNode(true) as Element;
105
+
106
+ repeatLabel.setAttribute('form-definition-source', 'repeat-group');
107
+
108
+ repeat.prepend(repeatLabel);
109
+
110
+ groupLabel.remove();
111
+ };
112
+
113
+ const unwrapRepeatGroup = (group: Element, repeat: Element): void => {
114
+ normalizeRepeatGroupAttributes(group, repeat);
115
+ normalizeRepeatGroupLabel(group, repeat);
116
+
117
+ group.replaceWith(repeat);
118
+ };
119
+
120
+ const normalizeRepeatGroups = (body: Element): void => {
45
121
  const repeats = body.querySelectorAll('repeat');
46
122
 
47
123
  for (const repeat of repeats) {
@@ -63,11 +139,8 @@ const normalizeRepeatGroups = (xformDocument: XMLDocument, body: Element): void
63
139
  }
64
140
  }
65
141
 
66
- if (group == null) {
67
- group = xformDocument.createElementNS(XFORMS_NAMESPACE_URI, 'group');
68
- group.setAttribute('ref', repeatNodeset);
69
- repeat.before(group);
70
- group.append(repeat);
142
+ if (group != null) {
143
+ unwrapRepeatGroup(group, repeat);
71
144
  }
72
145
  }
73
146
  };
@@ -113,7 +186,7 @@ const parseNormalizedXForm = (
113
186
  normalizedXML = sourceXML;
114
187
  } else {
115
188
  normalizeBodyRefNodesetAttributes(body);
116
- normalizeRepeatGroups(xformDocument, body);
189
+ normalizeRepeatGroups(body);
117
190
 
118
191
  normalizedXML = html.outerHTML;
119
192
  }
@@ -1,5 +1,8 @@
1
1
  import type { XFormDefinition } from '../XFormDefinition.ts';
2
2
  import { DependencyContext } from '../expression/DependencyContext.ts';
3
+ import type { ParsedTokenList } from '../lib/TokenListParser.ts';
4
+ import { TokenListParser } from '../lib/TokenListParser.ts';
5
+ import { RepeatElementDefinition } from './RepeatElementDefinition.ts';
3
6
  import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts';
4
7
  import { ControlDefinition } from './control/ControlDefinition.ts';
5
8
  import { InputDefinition } from './control/InputDefinition.ts';
@@ -7,7 +10,6 @@ import type { AnySelectDefinition } from './control/select/SelectDefinition.ts';
7
10
  import { SelectDefinition } from './control/select/SelectDefinition.ts';
8
11
  import { LogicalGroupDefinition } from './group/LogicalGroupDefinition.ts';
9
12
  import { PresentationGroupDefinition } from './group/PresentationGroupDefinition.ts';
10
- import { RepeatGroupDefinition } from './group/RepeatGroupDefinition.ts';
11
13
  import { StructuralGroupDefinition } from './group/StructuralGroupDefinition.ts';
12
14
 
13
15
  export interface BodyElementParentContext {
@@ -15,20 +17,24 @@ export interface BodyElementParentContext {
15
17
  readonly element: Element;
16
18
  }
17
19
 
20
+ // prettier-ignore
21
+ export type ControlElementDefinition =
22
+ | AnySelectDefinition
23
+ | InputDefinition;
24
+
18
25
  type SupportedBodyElementDefinition =
19
26
  // eslint-disable-next-line @typescript-eslint/sort-type-constituents
20
- | RepeatGroupDefinition
27
+ | RepeatElementDefinition
21
28
  | LogicalGroupDefinition
22
29
  | PresentationGroupDefinition
23
30
  | StructuralGroupDefinition
24
- | InputDefinition
25
- | AnySelectDefinition;
31
+ | ControlElementDefinition;
26
32
 
27
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
34
  type BodyElementDefinitionConstructor = new (...args: any[]) => SupportedBodyElementDefinition;
29
35
 
30
36
  const BodyElementDefinitionConstructors = [
31
- RepeatGroupDefinition,
37
+ RepeatElementDefinition,
32
38
  LogicalGroupDefinition,
33
39
  PresentationGroupDefinition,
34
40
  StructuralGroupDefinition,
@@ -49,11 +55,6 @@ export type AnyGroupElementDefinition = Extract<
49
55
  { readonly type: `${string}-group` }
50
56
  >;
51
57
 
52
- export type NonRepeatGroupElementDefinition = Exclude<
53
- AnyGroupElementDefinition,
54
- { readonly type: 'repeat-group' }
55
- >;
56
-
57
58
  const isGroupElementDefinition = (
58
59
  element: AnyBodyElementDefinition
59
60
  ): element is AnyGroupElementDefinition => {
@@ -96,13 +97,13 @@ class BodyElementMap extends Map<BodyElementReference, AnyBodyElementDefinition>
96
97
  for (const element of elements) {
97
98
  const { reference } = element;
98
99
 
99
- if (element instanceof RepeatGroupDefinition) {
100
+ if (element instanceof RepeatElementDefinition) {
100
101
  if (reference == null) {
101
- throw new Error('Missing reference for repeat/repeat group');
102
+ throw new Error('Missing reference for repeat');
102
103
  }
103
104
 
104
105
  this.set(reference, element);
105
- this.mapElementsByReference(element.repeatChildren);
106
+ this.mapElementsByReference(element.children);
106
107
  }
107
108
 
108
109
  if (
@@ -140,6 +141,10 @@ class BodyElementMap extends Map<BodyElementReference, AnyBodyElementDefinition>
140
141
  }
141
142
  }
142
143
 
144
+ const bodyClassParser = new TokenListParser(['pages' /*, 'theme-grid' */]);
145
+
146
+ export type BodyClassList = ParsedTokenList<typeof bodyClassParser>;
147
+
143
148
  export class BodyDefinition extends DependencyContext {
144
149
  static getChildElementDefinitions(
145
150
  form: XFormDefinition,
@@ -161,6 +166,25 @@ export class BodyDefinition extends DependencyContext {
161
166
  }
162
167
 
163
168
  readonly element: Element;
169
+
170
+ /**
171
+ * @todo this class is already an oddity in that it's **like** an element
172
+ * definition, but it isn't one itself. Adding this property here emphasizes
173
+ * that awkwardness. It also extends the applicable scope where instances of
174
+ * this class are accessed. While it's still ephemeral, it's anticipated that
175
+ * this extension might cause some disomfort. If so, the most plausible
176
+ * alternative is an additional refactor to:
177
+ *
178
+ * 1. Introduce a `BodyElementDefinition` sublass for `<h:body>`.
179
+ * 2. Disambiguate the respective names of those, in some reasonable way.
180
+ * 3. Add a layer of indirection between this class and that new body element
181
+ * definition's class.
182
+ * 4. At that point, we may as well prioritize the little bit of grunt work to
183
+ * pass the `BodyDefinition` instance by reference rather than assigning it
184
+ * to anything.
185
+ */
186
+ readonly classes: BodyClassList;
187
+
164
188
  readonly elements: readonly AnyBodyElementDefinition[];
165
189
 
166
190
  protected readonly elementsByReference: BodyElementMap;
@@ -176,6 +200,7 @@ export class BodyDefinition extends DependencyContext {
176
200
 
177
201
  this.reference = form.rootReference;
178
202
  this.element = element;
203
+ this.classes = bodyClassParser.parseFrom(element, 'class');
179
204
  this.elements = BodyDefinition.getChildElementDefinitions(form, this, element);
180
205
  this.elementsByReference = new BodyElementMap(this.elements);
181
206
  }
@@ -184,16 +209,6 @@ export class BodyDefinition extends DependencyContext {
184
209
  return this.elementsByReference.get(reference) ?? null;
185
210
  }
186
211
 
187
- getRepeatGroup(reference: string): RepeatGroupDefinition | null {
188
- const element = this.getBodyElement(reference);
189
-
190
- if (element?.type === 'repeat-group') {
191
- return element;
192
- }
193
-
194
- return null;
195
- }
196
-
197
212
  toJSON() {
198
213
  const { form, ...rest } = this;
199
214
 
@@ -0,0 +1,70 @@
1
+ import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
+ import type { XFormDefinition } from '../XFormDefinition.ts';
3
+ import type { BodyElementDefinitionArray, BodyElementParentContext } from './BodyDefinition.ts';
4
+ import { BodyDefinition } from './BodyDefinition.ts';
5
+ import { BodyElementDefinition } from './BodyElementDefinition.ts';
6
+ import type { StructureElementAppearanceDefinition } from './appearance/structureElementAppearanceParser.ts';
7
+ import { structureElementAppearanceParser } from './appearance/structureElementAppearanceParser.ts';
8
+ import { LabelDefinition } from './text/LabelDefinition.ts';
9
+
10
+ export class RepeatElementDefinition extends BodyElementDefinition<'repeat'> {
11
+ static override isCompatible(localName: string): boolean {
12
+ return localName === 'repeat';
13
+ }
14
+
15
+ override readonly category = 'structure';
16
+ readonly type = 'repeat';
17
+ override readonly reference: string;
18
+ readonly appearances: StructureElementAppearanceDefinition;
19
+ override readonly label: LabelDefinition | null;
20
+
21
+ // TODO: this will fall into the growing category of non-`BindExpression`
22
+ // cases which have roughly the same design story.
23
+ readonly countExpression: string | null;
24
+
25
+ readonly isFixedCount: boolean;
26
+
27
+ readonly children: BodyElementDefinitionArray;
28
+
29
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
30
+ super(form, parent, element);
31
+
32
+ this.label = LabelDefinition.forRepeatGroup(form, this);
33
+
34
+ const reference = element.getAttribute('nodeset');
35
+
36
+ if (reference == null) {
37
+ throw new Error('Invalid repeat: missing `nodeset` reference');
38
+ }
39
+
40
+ this.reference = reference;
41
+ this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
42
+ this.countExpression = element.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'count');
43
+
44
+ const childElements = Array.from(element.children).filter((childElement) => {
45
+ const { localName } = childElement;
46
+
47
+ return localName !== 'label' && localName !== 'group-label';
48
+ });
49
+ const children = BodyDefinition.getChildElementDefinitions(form, this, element, childElements);
50
+
51
+ this.children = children;
52
+
53
+ // Spec says this can be either `true()` or `false()`. That said, it
54
+ // could also presumably be `true ( )` or whatever.
55
+ const noAddRemove =
56
+ element
57
+ .getAttributeNS(JAVAROSA_NAMESPACE_URI, 'noAddRemove')
58
+ ?.trim()
59
+ .replaceAll(/\s+/g, '') ?? 'false()';
60
+
61
+ // TODO: **probably** safe to disregard anything else?
62
+ this.isFixedCount = noAddRemove === 'true()';
63
+ }
64
+
65
+ override toJSON() {
66
+ const { form, parent, ...rest } = this;
67
+
68
+ return rest;
69
+ }
70
+ }
@@ -0,0 +1,39 @@
1
+ import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts';
2
+
3
+ export const inputAppearanceParser = new TokenListParser([
4
+ 'multiline',
5
+ 'numbers',
6
+ 'url',
7
+ 'thousand-sep',
8
+
9
+ // date (TODO: data types)
10
+ 'no-calendar',
11
+ 'month-year',
12
+ 'year',
13
+ // date > calendars
14
+ 'ethiopian',
15
+ 'coptic',
16
+ 'islamic',
17
+ 'bikram-sambat',
18
+ 'myanmar',
19
+ 'persian',
20
+
21
+ // geo (TODO: data types)
22
+ 'placement-map',
23
+ 'maps',
24
+
25
+ // image/media (TODO: move to eventual `<upload>`?)
26
+ 'hidden-answer',
27
+ 'annotate',
28
+ 'draw',
29
+ 'signature',
30
+ 'new-front',
31
+ 'new',
32
+ 'front',
33
+
34
+ // *?
35
+ 'printer', // Note: actual usage uses `printer:...` (like `ex:...`).
36
+ 'masked',
37
+ ]);
38
+
39
+ export type InputAppearanceDefinition = ParsedTokenList<typeof inputAppearanceParser>;
@@ -0,0 +1,38 @@
1
+ import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts';
2
+
3
+ export const selectAppearanceParser = new TokenListParser(
4
+ [
5
+ // From XLSForm Docs:
6
+ 'compact',
7
+ 'horizontal',
8
+ 'horizontal-compact',
9
+ 'label',
10
+ 'list-nolabel',
11
+ 'minimal',
12
+
13
+ // From Collect `Appearances.kt`:
14
+ 'columns',
15
+ 'columns-1',
16
+ 'columns-2',
17
+ 'columns-3',
18
+ 'columns-4',
19
+ 'columns-5',
20
+ // Note: Collect supports arbitrary columns-n. Technically we do too (we parse
21
+ // out any appearance, not just those we know about). But we'll only include
22
+ // types/defaults up to 5.
23
+ 'columns-pack',
24
+ 'autocomplete',
25
+
26
+ // TODO: these are `<select1>` only
27
+ 'likert',
28
+ 'quick',
29
+ 'quickcompact',
30
+ 'map',
31
+ // "quick map"
32
+ ],
33
+ {
34
+ aliases: [{ fromAlias: 'search', toCanonical: 'autocomplete' }],
35
+ }
36
+ );
37
+
38
+ export type SelectAppearanceDefinition = ParsedTokenList<typeof selectAppearanceParser>;
@@ -0,0 +1,7 @@
1
+ import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts';
2
+
3
+ export const structureElementAppearanceParser = new TokenListParser(['field-list', 'table-list']);
4
+
5
+ export type StructureElementAppearanceDefinition = ParsedTokenList<
6
+ typeof structureElementAppearanceParser
7
+ >;
@@ -1,4 +1,5 @@
1
1
  import type { XFormDefinition } from '../../XFormDefinition.ts';
2
+ import type { ParsedTokenList } from '../../lib/TokenListParser.ts';
2
3
  import type { BodyElementParentContext } from '../BodyDefinition.ts';
3
4
  import { BodyElementDefinition } from '../BodyElementDefinition.ts';
4
5
  import { HintDefinition } from '../text/HintDefinition.ts';
@@ -23,6 +24,9 @@ export abstract class ControlDefinition<
23
24
  override readonly label: LabelDefinition | null;
24
25
  override readonly hint: HintDefinition | null;
25
26
 
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ abstract readonly appearances: ParsedTokenList<any>;
29
+
26
30
  constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
27
31
  super(form, parent, element);
28
32
 
@@ -1,3 +1,9 @@
1
+ import type { XFormDefinition } from '../../XFormDefinition.ts';
2
+ import type { BodyElementParentContext } from '../BodyDefinition.ts';
3
+ import {
4
+ inputAppearanceParser,
5
+ type InputAppearanceDefinition,
6
+ } from '../appearance/inputAppearanceParser.ts';
1
7
  import { ControlDefinition } from './ControlDefinition.ts';
2
8
 
3
9
  export class InputDefinition extends ControlDefinition<'input'> {
@@ -6,4 +12,11 @@ export class InputDefinition extends ControlDefinition<'input'> {
6
12
  }
7
13
 
8
14
  readonly type = 'input';
15
+ readonly appearances: InputAppearanceDefinition;
16
+
17
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
18
+ super(form, parent, element);
19
+
20
+ this.appearances = inputAppearanceParser.parseFrom(element, 'appearance');
21
+ }
9
22
  }
@@ -3,14 +3,21 @@ import type { LocalNamedElement } from '@getodk/common/types/dom.ts';
3
3
  import type { XFormDefinition } from '../../../XFormDefinition.ts';
4
4
  import { getItemElements, getItemsetElement } from '../../../lib/dom/query.ts';
5
5
  import type { AnyBodyElementDefinition, BodyElementParentContext } from '../../BodyDefinition.ts';
6
+ import type { SelectAppearanceDefinition } from '../../appearance/selectAppearanceParser.ts';
7
+ import { selectAppearanceParser } from '../../appearance/selectAppearanceParser.ts';
6
8
  import { ControlDefinition } from '../ControlDefinition.ts';
7
9
  import { ItemDefinition } from './ItemDefinition.ts';
8
10
  import { ItemsetDefinition } from './ItemsetDefinition.ts';
9
11
 
10
- // TODO: `<trigger>` is *almost* reasonable to support here too. The main
11
- // hesitation is that its single, implicit "item" does not have a distinct
12
- // <label>, and presumably has different UX **and translation** considerations.
13
- const selectLocalNames = new Set(['rank', 'select', 'select1'] as const);
12
+ /**
13
+ * @todo We were previously a bit overzealous about introducing `<rank>` support
14
+ * here. It'll likely still fit, but we should approach it with more intention.
15
+ *
16
+ * @todo `<trigger>` is *almost* reasonable to support here too. The main
17
+ * hesitation is that its single, implicit "item" does not have a distinct
18
+ * <label>, and presumably has different UX **and translation** considerations.
19
+ */
20
+ const selectLocalNames = new Set([/* 'rank', */ 'select', 'select1'] as const);
14
21
 
15
22
  export type SelectType = CollectionValues<typeof selectLocalNames>;
16
23
 
@@ -34,6 +41,7 @@ export class SelectDefinition<Type extends SelectType> extends ControlDefinition
34
41
 
35
42
  override readonly type: Type;
36
43
  override readonly element: SelectElement;
44
+ readonly appearances: SelectAppearanceDefinition;
37
45
 
38
46
  readonly itemset: ItemsetDefinition | null;
39
47
  readonly items: readonly ItemDefinition[];
@@ -45,8 +53,9 @@ export class SelectDefinition<Type extends SelectType> extends ControlDefinition
45
53
 
46
54
  super(form, parent, element);
47
55
 
48
- this.element = element;
49
56
  this.type = element.localName as Type;
57
+ this.element = element;
58
+ this.appearances = selectAppearanceParser.parseFrom(element, 'appearance');
50
59
 
51
60
  const itemsetElement = getItemsetElement(element);
52
61
  const itemElements = getItemElements(element);
@@ -1,13 +1,14 @@
1
1
  import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
2
- import type { XFormDOM } from '../../XFormDOM.ts';
3
2
  import type { XFormDefinition } from '../../XFormDefinition.ts';
4
- import { getLabelElement, getRepeatElement } from '../../lib/dom/query.ts';
3
+ import { getLabelElement } from '../../lib/dom/query.ts';
5
4
  import {
6
5
  BodyDefinition,
7
6
  type BodyElementDefinitionArray,
8
7
  type BodyElementParentContext,
9
8
  } from '../BodyDefinition.ts';
10
9
  import { BodyElementDefinition } from '../BodyElementDefinition.ts';
10
+ import type { StructureElementAppearanceDefinition } from '../appearance/structureElementAppearanceParser.ts';
11
+ import { structureElementAppearanceParser } from '../appearance/structureElementAppearanceParser.ts';
11
12
  import { LabelDefinition } from '../text/LabelDefinition.ts';
12
13
 
13
14
  /**
@@ -23,10 +24,6 @@ import { LabelDefinition } from '../text/LabelDefinition.ts';
23
24
  * - `presentation-group` is a group with a `<label>` child; its usage here
24
25
  * differs from the spec language in that `presentation-group` does **not**
25
26
  * have a `ref`
26
- * - `repeat-group` is not mentioned by the spec; it is an extension of
27
- * `logical-group`, wherein its `ref` is the same as its immediate `<repeat>`
28
- * child's `nodeset` (usage of each attribute is normalized during
29
- * initialization, in {@link XFormDOM})
30
27
  * - `structural-group` is any `<group>` element which does not satisfy any of
31
28
  * the other usage scenarios; this isn't exactly the terminology used, but is
32
29
  * the most closely fitting name for the concept where the other sceanarios
@@ -34,20 +31,17 @@ import { LabelDefinition } from '../text/LabelDefinition.ts';
34
31
  *
35
32
  * A more succinct decision tree:
36
33
  *
37
- * - `<group ref="$ref"><repeat nodeset="$ref">` -> `repeat-group`, else
38
34
  * - `<group ref="$ref">` -> `logical-group`, else
39
35
  * - `<group><label>` -> `presentation-group`, else
40
36
  * - `<group>` -> `structural-group`
41
37
  */
42
- export type GroupType =
43
- | 'logical-group'
44
- | 'presentation-group'
45
- | 'repeat-group'
46
- | 'structural-group';
38
+ export type GroupType = 'logical-group' | 'presentation-group' | 'structural-group';
47
39
 
48
40
  export abstract class BaseGroupDefinition<
49
41
  Type extends GroupType,
50
42
  > extends BodyElementDefinition<Type> {
43
+ // TODO: does this really accomplish anything? It seems highly unlikely it
44
+ // has enough performance benefit to outweigh its memory and lookup costs.
51
45
  private static groupTypes = new UpsertableMap<Element, GroupType | null>();
52
46
 
53
47
  protected static getGroupType(localName: string, element: Element): GroupType | null {
@@ -56,20 +50,8 @@ export abstract class BaseGroupDefinition<
56
50
  return null;
57
51
  }
58
52
 
59
- const ref = element.getAttribute('ref');
60
-
61
- if (ref != null) {
62
- const repeat = getRepeatElement(element);
63
-
64
- if (repeat == null) {
65
- return 'logical-group';
66
- }
67
-
68
- if (repeat.getAttribute('nodeset') === ref) {
69
- return 'repeat-group';
70
- }
71
-
72
- throw new Error('Unexpected <repeat> child of unrelated <group>');
53
+ if (element.hasAttribute('ref')) {
54
+ return 'logical-group';
73
55
  }
74
56
 
75
57
  const label = getLabelElement(element);
@@ -87,6 +69,7 @@ export abstract class BaseGroupDefinition<
87
69
  readonly children: BodyElementDefinitionArray;
88
70
 
89
71
  override readonly reference: string | null;
72
+ readonly appearances: StructureElementAppearanceDefinition;
90
73
  override readonly label: LabelDefinition | null;
91
74
 
92
75
  constructor(
@@ -99,6 +82,7 @@ export abstract class BaseGroupDefinition<
99
82
 
100
83
  this.children = children ?? this.getChildren(element);
101
84
  this.reference = element.getAttribute('ref');
85
+ this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
102
86
  this.label = LabelDefinition.forGroup(form, this);
103
87
  }
104
88
 
@@ -107,31 +91,9 @@ export abstract class BaseGroupDefinition<
107
91
  const children = Array.from(element.children).filter((child) => {
108
92
  const childName = child.localName;
109
93
 
110
- return childName !== 'label' && childName !== 'repeat';
94
+ return childName !== 'label';
111
95
  });
112
96
 
113
97
  return BodyDefinition.getChildElementDefinitions(form, this, element, children);
114
98
  }
115
99
  }
116
-
117
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
- export const repeatGroup = <T extends BaseGroupDefinition<any>>(
119
- groupDefinition: T
120
- ): Extract<T, BaseGroupDefinition<'repeat-group'>> | null => {
121
- if (groupDefinition.type === 'repeat-group') {
122
- return groupDefinition as Extract<T, BaseGroupDefinition<'repeat-group'>>;
123
- }
124
-
125
- return null;
126
- };
127
-
128
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
- export const nonRepeatGroup = <T extends BaseGroupDefinition<any>>(
130
- groupDefinition: T
131
- ): Exclude<T, BaseGroupDefinition<'repeat-group'>> | null => {
132
- if (groupDefinition.type === 'repeat-group') {
133
- return null;
134
- }
135
-
136
- return groupDefinition as Exclude<T, BaseGroupDefinition<'repeat-group'>>;
137
- };