@getodk/xforms-engine 0.1.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 (208) hide show
  1. package/README.md +44 -0
  2. package/dist/.vite/manifest.json +7 -0
  3. package/dist/XFormDOM.d.ts +31 -0
  4. package/dist/XFormDataType.d.ts +26 -0
  5. package/dist/XFormDefinition.d.ts +14 -0
  6. package/dist/body/BodyDefinition.d.ts +52 -0
  7. package/dist/body/BodyElementDefinition.d.ts +32 -0
  8. package/dist/body/RepeatDefinition.d.ts +15 -0
  9. package/dist/body/UnsupportedBodyElementDefinition.d.ts +10 -0
  10. package/dist/body/control/ControlDefinition.d.ts +16 -0
  11. package/dist/body/control/InputDefinition.d.ts +5 -0
  12. package/dist/body/control/select/ItemDefinition.d.ts +13 -0
  13. package/dist/body/control/select/ItemsetDefinition.d.ts +16 -0
  14. package/dist/body/control/select/ItemsetNodesetContext.d.ts +11 -0
  15. package/dist/body/control/select/ItemsetNodesetExpression.d.ts +5 -0
  16. package/dist/body/control/select/ItemsetValueExpression.d.ts +6 -0
  17. package/dist/body/control/select/SelectDefinition.d.ts +23 -0
  18. package/dist/body/group/BaseGroupDefinition.d.ts +46 -0
  19. package/dist/body/group/LogicalGroupDefinition.d.ts +6 -0
  20. package/dist/body/group/PresentationGroupDefinition.d.ts +11 -0
  21. package/dist/body/group/RepeatGroupDefinition.d.ts +12 -0
  22. package/dist/body/group/StructuralGroupDefinition.d.ts +6 -0
  23. package/dist/body/text/HintDefinition.d.ts +11 -0
  24. package/dist/body/text/LabelDefinition.d.ts +20 -0
  25. package/dist/body/text/TextElementDefinition.d.ts +32 -0
  26. package/dist/body/text/TextElementOutputPart.d.ts +12 -0
  27. package/dist/body/text/TextElementPart.d.ts +12 -0
  28. package/dist/body/text/TextElementReferencePart.d.ts +6 -0
  29. package/dist/body/text/TextElementStaticPart.d.ts +6 -0
  30. package/dist/client/BaseNode.d.ts +138 -0
  31. package/dist/client/EngineConfig.d.ts +78 -0
  32. package/dist/client/FormLanguage.d.ts +63 -0
  33. package/dist/client/GroupNode.d.ts +24 -0
  34. package/dist/client/OpaqueReactiveObjectFactory.d.ts +70 -0
  35. package/dist/client/RepeatInstanceNode.d.ts +28 -0
  36. package/dist/client/RepeatRangeNode.d.ts +94 -0
  37. package/dist/client/RootNode.d.ts +31 -0
  38. package/dist/client/SelectNode.d.ts +60 -0
  39. package/dist/client/StringNode.d.ts +41 -0
  40. package/dist/client/SubtreeNode.d.ts +52 -0
  41. package/dist/client/TextRange.d.ts +55 -0
  42. package/dist/client/hierarchy.d.ts +48 -0
  43. package/dist/client/index.d.ts +11 -0
  44. package/dist/client/node-types.d.ts +1 -0
  45. package/dist/expression/DependencyContext.d.ts +12 -0
  46. package/dist/expression/DependentExpression.d.ts +43 -0
  47. package/dist/index.d.ts +16 -0
  48. package/dist/index.js +37622 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/instance/Group.d.ts +31 -0
  51. package/dist/instance/RepeatInstance.d.ts +60 -0
  52. package/dist/instance/RepeatRange.d.ts +81 -0
  53. package/dist/instance/Root.d.ts +70 -0
  54. package/dist/instance/SelectField.d.ts +45 -0
  55. package/dist/instance/StringField.d.ts +39 -0
  56. package/dist/instance/Subtree.d.ts +30 -0
  57. package/dist/instance/abstract/DescendantNode.d.ts +76 -0
  58. package/dist/instance/abstract/InstanceNode.d.ts +107 -0
  59. package/dist/instance/children.d.ts +2 -0
  60. package/dist/instance/hierarchy.d.ts +12 -0
  61. package/dist/instance/identity.d.ts +7 -0
  62. package/dist/instance/index.d.ts +8 -0
  63. package/dist/instance/internal-api/EvaluationContext.d.ts +34 -0
  64. package/dist/instance/internal-api/InstanceConfig.d.ts +8 -0
  65. package/dist/instance/internal-api/SubscribableDependency.d.ts +59 -0
  66. package/dist/instance/internal-api/TranslationContext.d.ts +4 -0
  67. package/dist/instance/internal-api/ValueContext.d.ts +22 -0
  68. package/dist/instance/resource.d.ts +10 -0
  69. package/dist/instance/text/FormattedTextStub.d.ts +1 -0
  70. package/dist/instance/text/TextChunk.d.ts +11 -0
  71. package/dist/instance/text/TextRange.d.ts +10 -0
  72. package/dist/lib/dom/query.d.ts +20 -0
  73. package/dist/lib/reactivity/createChildrenState.d.ts +36 -0
  74. package/dist/lib/reactivity/createComputedExpression.d.ts +12 -0
  75. package/dist/lib/reactivity/createSelectItems.d.ts +16 -0
  76. package/dist/lib/reactivity/createValueState.d.ts +44 -0
  77. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +18 -0
  78. package/dist/lib/reactivity/node-state/createClientState.d.ts +9 -0
  79. package/dist/lib/reactivity/node-state/createCurrentState.d.ts +6 -0
  80. package/dist/lib/reactivity/node-state/createEngineState.d.ts +5 -0
  81. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +22 -0
  82. package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +6 -0
  83. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +139 -0
  84. package/dist/lib/reactivity/node-state/representations.d.ts +25 -0
  85. package/dist/lib/reactivity/scope.d.ts +23 -0
  86. package/dist/lib/reactivity/text/createFieldHint.d.ts +5 -0
  87. package/dist/lib/reactivity/text/createNodeLabel.d.ts +5 -0
  88. package/dist/lib/reactivity/text/createTextRange.d.ts +19 -0
  89. package/dist/lib/reactivity/types.d.ts +21 -0
  90. package/dist/lib/unique-id.d.ts +27 -0
  91. package/dist/lib/xpath/analysis.d.ts +22 -0
  92. package/dist/model/BindComputation.d.ts +30 -0
  93. package/dist/model/BindDefinition.d.ts +31 -0
  94. package/dist/model/BindElement.d.ts +6 -0
  95. package/dist/model/DescendentNodeDefinition.d.ts +25 -0
  96. package/dist/model/ModelBindMap.d.ts +15 -0
  97. package/dist/model/ModelDefinition.d.ts +10 -0
  98. package/dist/model/NodeDefinition.d.ts +74 -0
  99. package/dist/model/RepeatInstanceDefinition.d.ts +15 -0
  100. package/dist/model/RepeatSequenceDefinition.d.ts +19 -0
  101. package/dist/model/RepeatTemplateDefinition.d.ts +29 -0
  102. package/dist/model/RootDefinition.d.ts +24 -0
  103. package/dist/model/SubtreeDefinition.d.ts +14 -0
  104. package/dist/model/ValueNodeDefinition.d.ts +15 -0
  105. package/dist/solid.js +37273 -0
  106. package/dist/solid.js.map +1 -0
  107. package/package.json +87 -0
  108. package/src/XFormDOM.ts +224 -0
  109. package/src/XFormDataType.ts +64 -0
  110. package/src/XFormDefinition.ts +40 -0
  111. package/src/body/BodyDefinition.ts +202 -0
  112. package/src/body/BodyElementDefinition.ts +62 -0
  113. package/src/body/RepeatDefinition.ts +54 -0
  114. package/src/body/UnsupportedBodyElementDefinition.ts +17 -0
  115. package/src/body/control/ControlDefinition.ts +42 -0
  116. package/src/body/control/InputDefinition.ts +9 -0
  117. package/src/body/control/select/ItemDefinition.ts +31 -0
  118. package/src/body/control/select/ItemsetDefinition.ts +36 -0
  119. package/src/body/control/select/ItemsetNodesetContext.ts +26 -0
  120. package/src/body/control/select/ItemsetNodesetExpression.ts +8 -0
  121. package/src/body/control/select/ItemsetValueExpression.ts +11 -0
  122. package/src/body/control/select/SelectDefinition.ts +74 -0
  123. package/src/body/group/BaseGroupDefinition.ts +137 -0
  124. package/src/body/group/LogicalGroupDefinition.ts +11 -0
  125. package/src/body/group/PresentationGroupDefinition.ts +28 -0
  126. package/src/body/group/RepeatGroupDefinition.ts +91 -0
  127. package/src/body/group/StructuralGroupDefinition.ts +11 -0
  128. package/src/body/text/HintDefinition.ts +26 -0
  129. package/src/body/text/LabelDefinition.ts +54 -0
  130. package/src/body/text/TextElementDefinition.ts +97 -0
  131. package/src/body/text/TextElementOutputPart.ts +27 -0
  132. package/src/body/text/TextElementPart.ts +31 -0
  133. package/src/body/text/TextElementReferencePart.ts +21 -0
  134. package/src/body/text/TextElementStaticPart.ts +26 -0
  135. package/src/client/BaseNode.ts +180 -0
  136. package/src/client/EngineConfig.ts +83 -0
  137. package/src/client/FormLanguage.ts +77 -0
  138. package/src/client/GroupNode.ts +33 -0
  139. package/src/client/OpaqueReactiveObjectFactory.ts +100 -0
  140. package/src/client/README.md +39 -0
  141. package/src/client/RepeatInstanceNode.ts +41 -0
  142. package/src/client/RepeatRangeNode.ts +100 -0
  143. package/src/client/RootNode.ts +36 -0
  144. package/src/client/SelectNode.ts +69 -0
  145. package/src/client/StringNode.ts +46 -0
  146. package/src/client/SubtreeNode.ts +57 -0
  147. package/src/client/TextRange.ts +63 -0
  148. package/src/client/hierarchy.ts +63 -0
  149. package/src/client/index.ts +29 -0
  150. package/src/client/node-types.ts +10 -0
  151. package/src/expression/DependencyContext.ts +53 -0
  152. package/src/expression/DependentExpression.ts +102 -0
  153. package/src/index.ts +35 -0
  154. package/src/instance/Group.ts +82 -0
  155. package/src/instance/RepeatInstance.ts +164 -0
  156. package/src/instance/RepeatRange.ts +214 -0
  157. package/src/instance/Root.ts +264 -0
  158. package/src/instance/SelectField.ts +204 -0
  159. package/src/instance/StringField.ts +93 -0
  160. package/src/instance/Subtree.ts +79 -0
  161. package/src/instance/abstract/DescendantNode.ts +182 -0
  162. package/src/instance/abstract/InstanceNode.ts +257 -0
  163. package/src/instance/children.ts +52 -0
  164. package/src/instance/hierarchy.ts +54 -0
  165. package/src/instance/identity.ts +11 -0
  166. package/src/instance/index.ts +37 -0
  167. package/src/instance/internal-api/EvaluationContext.ts +41 -0
  168. package/src/instance/internal-api/InstanceConfig.ts +9 -0
  169. package/src/instance/internal-api/SubscribableDependency.ts +61 -0
  170. package/src/instance/internal-api/TranslationContext.ts +5 -0
  171. package/src/instance/internal-api/ValueContext.ts +27 -0
  172. package/src/instance/resource.ts +75 -0
  173. package/src/instance/text/FormattedTextStub.ts +8 -0
  174. package/src/instance/text/TextChunk.ts +20 -0
  175. package/src/instance/text/TextRange.ts +23 -0
  176. package/src/lib/dom/query.ts +49 -0
  177. package/src/lib/reactivity/createChildrenState.ts +60 -0
  178. package/src/lib/reactivity/createComputedExpression.ts +114 -0
  179. package/src/lib/reactivity/createSelectItems.ts +163 -0
  180. package/src/lib/reactivity/createValueState.ts +258 -0
  181. package/src/lib/reactivity/materializeCurrentStateChildren.ts +121 -0
  182. package/src/lib/reactivity/node-state/createClientState.ts +51 -0
  183. package/src/lib/reactivity/node-state/createCurrentState.ts +27 -0
  184. package/src/lib/reactivity/node-state/createEngineState.ts +18 -0
  185. package/src/lib/reactivity/node-state/createSharedNodeState.ts +79 -0
  186. package/src/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.ts +85 -0
  187. package/src/lib/reactivity/node-state/createSpecifiedState.ts +229 -0
  188. package/src/lib/reactivity/node-state/representations.ts +64 -0
  189. package/src/lib/reactivity/scope.ts +106 -0
  190. package/src/lib/reactivity/text/createFieldHint.ts +16 -0
  191. package/src/lib/reactivity/text/createNodeLabel.ts +16 -0
  192. package/src/lib/reactivity/text/createTextRange.ts +155 -0
  193. package/src/lib/reactivity/types.ts +27 -0
  194. package/src/lib/unique-id.ts +34 -0
  195. package/src/lib/xpath/analysis.ts +241 -0
  196. package/src/model/BindComputation.ts +88 -0
  197. package/src/model/BindDefinition.ts +104 -0
  198. package/src/model/BindElement.ts +8 -0
  199. package/src/model/DescendentNodeDefinition.ts +56 -0
  200. package/src/model/ModelBindMap.ts +71 -0
  201. package/src/model/ModelDefinition.ts +19 -0
  202. package/src/model/NodeDefinition.ts +146 -0
  203. package/src/model/RepeatInstanceDefinition.ts +39 -0
  204. package/src/model/RepeatSequenceDefinition.ts +53 -0
  205. package/src/model/RepeatTemplateDefinition.ts +150 -0
  206. package/src/model/RootDefinition.ts +121 -0
  207. package/src/model/SubtreeDefinition.ts +50 -0
  208. package/src/model/ValueNodeDefinition.ts +39 -0
@@ -0,0 +1,54 @@
1
+ import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
+ import type { XFormDefinition } from '../XFormDefinition.ts';
3
+ import type { BodyElementDefinitionArray } from './BodyDefinition.ts';
4
+ import { BodyElementDefinition } from './BodyElementDefinition.ts';
5
+ import type { RepeatGroupDefinition } from './group/RepeatGroupDefinition.ts';
6
+
7
+ export class RepeatDefinition extends BodyElementDefinition<'repeat'> {
8
+ override readonly category = 'structure';
9
+ readonly type = 'repeat';
10
+ override readonly reference: string;
11
+
12
+ // TODO: this will fall into the growing category of non-`BindExpression`
13
+ // cases which have roughly the same design story.
14
+ readonly countExpression: string | null;
15
+
16
+ readonly isFixedCount: boolean;
17
+
18
+ readonly children: BodyElementDefinitionArray;
19
+
20
+ constructor(
21
+ form: XFormDefinition,
22
+ readonly groupDefinition: RepeatGroupDefinition,
23
+ element: Element
24
+ ) {
25
+ super(form, groupDefinition, element);
26
+
27
+ const reference = element.getAttribute('nodeset');
28
+
29
+ if (reference == null) {
30
+ throw new Error('Invalid repeat: missing `nodeset` reference');
31
+ }
32
+
33
+ this.reference = reference;
34
+ this.countExpression = element.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'count');
35
+ this.children = groupDefinition.getChildren(element);
36
+
37
+ // Spec says this can be either `true()` or `false()`. That said, it
38
+ // could also presumably be `true ( )` or whatever.
39
+ const noAddRemove =
40
+ element
41
+ .getAttributeNS(JAVAROSA_NAMESPACE_URI, 'noAddRemove')
42
+ ?.trim()
43
+ .replaceAll(/\s+/g, '') ?? 'false()';
44
+
45
+ // TODO: **probably** safe to disregard anything else?
46
+ this.isFixedCount = noAddRemove === 'true()';
47
+ }
48
+
49
+ override toJSON() {
50
+ const { form, groupDefinition, parent, ...rest } = this;
51
+
52
+ return rest;
53
+ }
54
+ }
@@ -0,0 +1,17 @@
1
+ import type { XFormDefinition } from '../XFormDefinition.ts';
2
+ import type { BodyElementParentContext } from './BodyDefinition.ts';
3
+ import { BodyElementDefinition } from './BodyElementDefinition.ts';
4
+
5
+ export class UnsupportedBodyElementDefinition extends BodyElementDefinition<'UNSUPPORTED'> {
6
+ static override isCompatible(): boolean {
7
+ return true;
8
+ }
9
+
10
+ override readonly category = 'UNSUPPORTED';
11
+ readonly type = 'UNSUPPORTED';
12
+ override readonly reference: null = null;
13
+
14
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
15
+ super(form, parent, element);
16
+ }
17
+ }
@@ -0,0 +1,42 @@
1
+ import type { XFormDefinition } from '../../XFormDefinition.ts';
2
+ import type { BodyElementParentContext } from '../BodyDefinition.ts';
3
+ import { BodyElementDefinition } from '../BodyElementDefinition.ts';
4
+ import { HintDefinition } from '../text/HintDefinition.ts';
5
+ import { LabelDefinition } from '../text/LabelDefinition.ts';
6
+
7
+ // prettier-ignore
8
+ type ControlType =
9
+ | 'input'
10
+ | 'range'
11
+ | 'rank'
12
+ | 'select'
13
+ | 'select1'
14
+ | 'trigger'
15
+ | 'upload';
16
+
17
+ export abstract class ControlDefinition<
18
+ Type extends ControlType,
19
+ > extends BodyElementDefinition<Type> {
20
+ override readonly category = 'control';
21
+ abstract override readonly type: Type;
22
+ override readonly reference: string;
23
+ override readonly label: LabelDefinition | null;
24
+ override readonly hint: HintDefinition | null;
25
+
26
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
27
+ super(form, parent, element);
28
+
29
+ const reference = element.getAttribute('ref');
30
+
31
+ if (reference == null) {
32
+ throw new Error(`Invalid control: missing ref attribute`);
33
+ }
34
+
35
+ this.reference = reference;
36
+ this.label = LabelDefinition.forControl(form, this);
37
+ this.hint = HintDefinition.forElement(form, this);
38
+ }
39
+ }
40
+
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ export type AnyControlDefinition = ControlDefinition<any>;
@@ -0,0 +1,9 @@
1
+ import { ControlDefinition } from './ControlDefinition.ts';
2
+
3
+ export class InputDefinition extends ControlDefinition<'input'> {
4
+ static override isCompatible(localName: string): boolean {
5
+ return localName === 'input';
6
+ }
7
+
8
+ readonly type = 'input';
9
+ }
@@ -0,0 +1,31 @@
1
+ import { getValueElement, type ItemElement } from '../../../lib/dom/query.ts';
2
+ import type { XFormDefinition } from '../../../XFormDefinition.ts';
3
+ import { BodyElementDefinition } from '../../BodyElementDefinition.ts';
4
+ import { LabelDefinition } from '../../text/LabelDefinition.ts';
5
+ import type { AnySelectDefinition } from './SelectDefinition.ts';
6
+
7
+ export class ItemDefinition extends BodyElementDefinition<'item'> {
8
+ override readonly category = 'support';
9
+ override readonly type = 'item';
10
+
11
+ override readonly label: LabelDefinition | null;
12
+ readonly value: string;
13
+
14
+ constructor(
15
+ form: XFormDefinition,
16
+ override readonly parent: AnySelectDefinition,
17
+ element: ItemElement
18
+ ) {
19
+ const valueElement = getValueElement(element);
20
+ const value = valueElement?.textContent;
21
+
22
+ if (value == null) {
23
+ throw new Error('<item> has no <value>');
24
+ }
25
+
26
+ super(form, parent, element);
27
+
28
+ this.label = LabelDefinition.forItem(form, this);
29
+ this.value = value;
30
+ }
31
+ }
@@ -0,0 +1,36 @@
1
+ import { getValueElement, type ItemsetElement } from '../../../lib/dom/query.ts';
2
+ import type { XFormDefinition } from '../../../XFormDefinition.ts';
3
+ import { BodyElementDefinition } from '../../BodyElementDefinition.ts';
4
+ import { LabelDefinition } from '../../text/LabelDefinition.ts';
5
+ import { ItemsetNodesetExpression } from './ItemsetNodesetExpression.ts';
6
+ import { ItemsetValueExpression } from './ItemsetValueExpression.ts';
7
+ import type { AnySelectDefinition } from './SelectDefinition.ts';
8
+
9
+ export class ItemsetDefinition extends BodyElementDefinition<'itemset'> {
10
+ override readonly category = 'support';
11
+ readonly type = 'itemset';
12
+
13
+ override readonly reference: string;
14
+ override readonly label: LabelDefinition | null;
15
+
16
+ readonly nodes: ItemsetNodesetExpression;
17
+ readonly value: ItemsetValueExpression;
18
+
19
+ constructor(form: XFormDefinition, select: AnySelectDefinition, element: ItemsetElement) {
20
+ const valueElement = getValueElement(element);
21
+ const valueExpression = valueElement?.getAttribute('ref');
22
+
23
+ if (valueExpression == null) {
24
+ throw new Error(`<itemset> has no <value>`);
25
+ }
26
+
27
+ super(form, select, element);
28
+
29
+ const nodesetExpression = element.getAttribute('nodeset');
30
+
31
+ this.reference = nodesetExpression;
32
+ this.nodes = new ItemsetNodesetExpression(this, nodesetExpression);
33
+ this.value = new ItemsetValueExpression(this, valueExpression);
34
+ this.label = LabelDefinition.forItemset(form, this);
35
+ }
36
+ }
@@ -0,0 +1,26 @@
1
+ import { DependencyContext } from '../../../expression/DependencyContext.ts';
2
+ import type { AnyDependentExpression } from '../../../expression/DependentExpression.ts';
3
+ import type { ItemsetDefinition } from './ItemsetDefinition.ts';
4
+
5
+ export class ItemsetNodesetContext extends DependencyContext {
6
+ override readonly parentReference = null;
7
+ override readonly reference: string;
8
+
9
+ constructor(
10
+ protected readonly itemset: ItemsetDefinition,
11
+ nodesetExpression: string
12
+ ) {
13
+ super();
14
+
15
+ this.reference = nodesetExpression;
16
+ }
17
+
18
+ override set isTranslated(value: true) {
19
+ super.isTranslated = value;
20
+ this.itemset.isTranslated = value;
21
+ }
22
+
23
+ override registerDependentExpression(expression: AnyDependentExpression): void {
24
+ this.itemset.registerDependentExpression(expression);
25
+ }
26
+ }
@@ -0,0 +1,8 @@
1
+ import { DependentExpression } from '../../../expression/DependentExpression.ts';
2
+ import type { ItemsetDefinition } from './ItemsetDefinition.ts';
3
+
4
+ export class ItemsetNodesetExpression extends DependentExpression<'nodes'> {
5
+ constructor(itemset: ItemsetDefinition, nodesetExpression: string) {
6
+ super(itemset, 'nodes', nodesetExpression);
7
+ }
8
+ }
@@ -0,0 +1,11 @@
1
+ import { DependentExpression } from '../../../expression/DependentExpression.ts';
2
+ import type { ItemsetDefinition } from './ItemsetDefinition.ts';
3
+
4
+ export class ItemsetValueExpression extends DependentExpression<'string'> {
5
+ constructor(
6
+ readonly itemset: ItemsetDefinition,
7
+ expression: string
8
+ ) {
9
+ super(itemset, 'string', expression);
10
+ }
11
+ }
@@ -0,0 +1,74 @@
1
+ import type { CollectionValues } from '@getodk/common/types/collections/CollectionValues.ts';
2
+ import type { LocalNamedElement } from '@getodk/common/types/dom.ts';
3
+ import type { XFormDefinition } from '../../../XFormDefinition.ts';
4
+ import { getItemElements, getItemsetElement } from '../../../lib/dom/query.ts';
5
+ import type { AnyBodyElementDefinition, BodyElementParentContext } from '../../BodyDefinition.ts';
6
+ import { ControlDefinition } from '../ControlDefinition.ts';
7
+ import { ItemDefinition } from './ItemDefinition.ts';
8
+ import { ItemsetDefinition } from './ItemsetDefinition.ts';
9
+
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);
14
+
15
+ export type SelectType = CollectionValues<typeof selectLocalNames>;
16
+
17
+ export interface SelectElement extends LocalNamedElement<SelectType> {}
18
+
19
+ const isSelectElement = (
20
+ element: Element,
21
+ localName: string = element.localName
22
+ ): element is SelectElement => {
23
+ return selectLocalNames.has(localName as SelectType);
24
+ };
25
+
26
+ export class SelectDefinition<Type extends SelectType> extends ControlDefinition<Type> {
27
+ static override isCompatible(localName: string, element: Element): boolean {
28
+ return isSelectElement(element, localName);
29
+ }
30
+
31
+ static isSelect(element: AnyBodyElementDefinition): element is AnySelectDefinition {
32
+ return selectLocalNames.has(element.type as SelectType);
33
+ }
34
+
35
+ override readonly type: Type;
36
+ override readonly element: SelectElement;
37
+
38
+ readonly itemset: ItemsetDefinition | null;
39
+ readonly items: readonly ItemDefinition[];
40
+
41
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
42
+ if (!isSelectElement(element)) {
43
+ throw new Error(`Invalid select element: <${element.nodeName}>`);
44
+ }
45
+
46
+ super(form, parent, element);
47
+
48
+ this.element = element;
49
+ this.type = element.localName as Type;
50
+
51
+ const itemsetElement = getItemsetElement(element);
52
+ const itemElements = getItemElements(element);
53
+
54
+ if (itemsetElement == null) {
55
+ this.itemset = null;
56
+ this.items = itemElements.map((itemElement) => {
57
+ return new ItemDefinition(form, this, itemElement);
58
+ });
59
+ } else {
60
+ if (itemElements.length > 0) {
61
+ throw new Error(`<${element.nodeName}> has both <itemset> and <item> children`);
62
+ }
63
+
64
+ this.items = [];
65
+ this.itemset = new ItemsetDefinition(form, this, itemsetElement);
66
+ }
67
+ }
68
+
69
+ override toJSON() {
70
+ return {};
71
+ }
72
+ }
73
+
74
+ export type AnySelectDefinition = SelectDefinition<SelectType>;
@@ -0,0 +1,137 @@
1
+ import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
2
+ import type { XFormDOM } from '../../XFormDOM.ts';
3
+ import type { XFormDefinition } from '../../XFormDefinition.ts';
4
+ import { getLabelElement, getRepeatElement } from '../../lib/dom/query.ts';
5
+ import {
6
+ BodyDefinition,
7
+ type BodyElementDefinitionArray,
8
+ type BodyElementParentContext,
9
+ } from '../BodyDefinition.ts';
10
+ import { BodyElementDefinition } from '../BodyElementDefinition.ts';
11
+ import { LabelDefinition } from '../text/LabelDefinition.ts';
12
+
13
+ /**
14
+ * These type names are derived from **and expand upon** the language used in
15
+ * the ODK XForms spec to describe various group usage. Whereas the spec
16
+ * language allows for a group to be described as more than one case, the intent
17
+ * here is to establish exclusive naming which may or may not compound. As such:
18
+ *
19
+ * - `logical-group`, per spec language, is a group with a `ref`; its usage here
20
+ * differs from the spec language in that it _may_ have a `<label>` child but
21
+ * is not also treated as a `presentation-group` (which is only used for
22
+ * groups which do not have a `ref`)
23
+ * - `presentation-group` is a group with a `<label>` child; its usage here
24
+ * differs from the spec language in that `presentation-group` does **not**
25
+ * 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
+ * - `structural-group` is any `<group>` element which does not satisfy any of
31
+ * the other usage scenarios; this isn't exactly the terminology used, but is
32
+ * the most closely fitting name for the concept where the other sceanarios
33
+ * do not apply
34
+ *
35
+ * A more succinct decision tree:
36
+ *
37
+ * - `<group ref="$ref"><repeat nodeset="$ref">` -> `repeat-group`, else
38
+ * - `<group ref="$ref">` -> `logical-group`, else
39
+ * - `<group><label>` -> `presentation-group`, else
40
+ * - `<group>` -> `structural-group`
41
+ */
42
+ export type GroupType =
43
+ | 'logical-group'
44
+ | 'presentation-group'
45
+ | 'repeat-group'
46
+ | 'structural-group';
47
+
48
+ export abstract class BaseGroupDefinition<
49
+ Type extends GroupType,
50
+ > extends BodyElementDefinition<Type> {
51
+ private static groupTypes = new UpsertableMap<Element, GroupType | null>();
52
+
53
+ protected static getGroupType(localName: string, element: Element): GroupType | null {
54
+ return this.groupTypes.upsert(element, () => {
55
+ if (localName !== 'group') {
56
+ return null;
57
+ }
58
+
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>');
73
+ }
74
+
75
+ const label = getLabelElement(element);
76
+
77
+ if (label == null) {
78
+ return 'structural-group';
79
+ }
80
+
81
+ return 'presentation-group';
82
+ });
83
+ }
84
+
85
+ override readonly category = 'structure';
86
+
87
+ readonly children: BodyElementDefinitionArray;
88
+
89
+ override readonly reference: string | null;
90
+ override readonly label: LabelDefinition | null;
91
+
92
+ constructor(
93
+ form: XFormDefinition,
94
+ parent: BodyElementParentContext,
95
+ element: Element,
96
+ children?: BodyElementDefinitionArray
97
+ ) {
98
+ super(form, parent, element);
99
+
100
+ this.children = children ?? this.getChildren(element);
101
+ this.reference = element.getAttribute('ref');
102
+ this.label = LabelDefinition.forGroup(form, this);
103
+ }
104
+
105
+ getChildren(element: Element): BodyElementDefinitionArray {
106
+ const { form } = this;
107
+ const children = Array.from(element.children).filter((child) => {
108
+ const childName = child.localName;
109
+
110
+ return childName !== 'label' && childName !== 'repeat';
111
+ });
112
+
113
+ return BodyDefinition.getChildElementDefinitions(form, this, element, children);
114
+ }
115
+ }
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
+ };
@@ -0,0 +1,11 @@
1
+ import { BaseGroupDefinition } from './BaseGroupDefinition.ts';
2
+
3
+ export class LogicalGroupDefinition extends BaseGroupDefinition<'logical-group'> {
4
+ static override isCompatible(localName: string, element: Element): boolean {
5
+ return this.getGroupType(localName, element) === 'logical-group';
6
+ }
7
+
8
+ readonly type = 'logical-group';
9
+ }
10
+
11
+ export type LogicalGroupDefinitionClass = typeof LogicalGroupDefinition;
@@ -0,0 +1,28 @@
1
+ import type { XFormDefinition } from '../../XFormDefinition.ts';
2
+ import type { BodyElementParentContext } from '../BodyDefinition.ts';
3
+ import { LabelDefinition } from '../text/LabelDefinition.ts';
4
+ import { BaseGroupDefinition } from './BaseGroupDefinition.ts';
5
+
6
+ export class PresentationGroupDefinition extends BaseGroupDefinition<'presentation-group'> {
7
+ static override isCompatible(localName: string, element: Element): boolean {
8
+ return this.getGroupType(localName, element) === 'presentation-group';
9
+ }
10
+
11
+ readonly type = 'presentation-group';
12
+
13
+ override readonly label: LabelDefinition;
14
+
15
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
16
+ super(form, parent, element);
17
+
18
+ const label = LabelDefinition.forGroup(form, this);
19
+
20
+ if (label == null) {
21
+ throw new Error('Invalid presentation-group: missing label');
22
+ }
23
+
24
+ this.label = label;
25
+ }
26
+ }
27
+
28
+ export type PresentationGroupDefinitionClass = typeof PresentationGroupDefinition;
@@ -0,0 +1,91 @@
1
+ import { getRepeatElement } from '../../lib/dom/query.ts';
2
+ import type { XFormDefinition } from '../../XFormDefinition.ts';
3
+ import type { BodyElementDefinitionArray, BodyElementParentContext } from '../BodyDefinition.ts';
4
+ import { RepeatDefinition } from '../RepeatDefinition.ts';
5
+ import { BaseGroupDefinition } from './BaseGroupDefinition.ts';
6
+
7
+ /**
8
+ * TODO: The `RepeatGroupDefinition`/`RepeatDefinition` types are currently
9
+ * expected to always pair, mirroring the corresponding XForm structure, e.g.
10
+ *
11
+ * ```xml
12
+ * <group ref="/root/rep"> <!-- RepeatGroupDefinition -->
13
+ * <repeat nodeset="/root/rep" /> <!-- RepeatDefinition -->
14
+ * </group>
15
+ * ```
16
+ *
17
+ * This structure has already been a source of one bug (where a recursive walk
18
+ * through a `BodyDefinition`'s descendants failed to reach the children of
19
+ * `RepeatDefinition`s). It seems likely this will continue to be a footgun.
20
+ * After some discussion with @lognaturel, I concluded that the pairing isn't
21
+ * strictly necessary, as this should be considered invalid:
22
+ *
23
+ * ```xml
24
+ * <group ref="/root/rep">
25
+ * <repeat nodeset="/root/rep" />
26
+ * <input ref="/root/rep/not-a-repeat-child" />
27
+ * </group>
28
+ * ```
29
+ *
30
+ * It **may** make sense to collapse these types in the future, but I've held
31
+ * off, following another discussion with @lognaturel. There's some potential
32
+ * for group/repeat labeling ambiguity.
33
+ *
34
+ * - The current design accommodates sharing a repeat-containing group's label
35
+ * across multiple repeat instances, but
36
+ * - The
37
+ * {@link https://github.com/getodk/xforms-spec/blob/791753a09fabd3d64d8cb95776dc0cef71fa4446/_sections/60-repeats.md?plain=1#L46 | ODK XForms spec suggests}
38
+ * a repeat's first group might be a better candidate
39
+ *
40
+ * Example of such a structure:
41
+ *
42
+ * ```xml
43
+ * <group ref="/root/foo">
44
+ * <label>Foo (outer label)</label>
45
+ * <repeat nodeset="/root/foo">
46
+ * <group>
47
+ * <label>Foo (inner label)</label>
48
+ * <!-- ... -->
49
+ * </group>
50
+ * </repeat>
51
+ * </group>
52
+ * ```
53
+ */
54
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
55
+ // @ts-ignore - All this so I could attach a JSDoc comment to something other
56
+ // than the actual class...
57
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-unused-vars
58
+ interface IgnoreMeIAmJustHereForTheJSDoc {}
59
+
60
+ export class RepeatGroupDefinition extends BaseGroupDefinition<'repeat-group'> {
61
+ static override isCompatible(localName: string, element: Element): boolean {
62
+ return this.getGroupType(localName, element) === 'repeat-group';
63
+ }
64
+
65
+ readonly type = 'repeat-group';
66
+
67
+ readonly repeat: RepeatDefinition;
68
+
69
+ get repeatChildren(): BodyElementDefinitionArray {
70
+ return this.repeat.children;
71
+ }
72
+
73
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
74
+ // TODO: this has already been queried at least twice before reaching this
75
+ // point!
76
+ const repeat = getRepeatElement(element);
77
+
78
+ // TODO: and as such, this should not happen
79
+ if (repeat == null) {
80
+ throw new Error('Invalid repeat-group');
81
+ }
82
+
83
+ super(form, parent, element);
84
+
85
+ const repeatDefinition = new RepeatDefinition(form, this, repeat);
86
+
87
+ this.repeat = repeatDefinition;
88
+ }
89
+ }
90
+
91
+ export type RepeatGroupDefinitionClass = typeof RepeatGroupDefinition;
@@ -0,0 +1,11 @@
1
+ import { BaseGroupDefinition } from './BaseGroupDefinition.ts';
2
+
3
+ export class StructuralGroupDefinition extends BaseGroupDefinition<'structural-group'> {
4
+ static override isCompatible(localName: string, element: Element): boolean {
5
+ return this.getGroupType(localName, element) === 'structural-group';
6
+ }
7
+
8
+ readonly type = 'structural-group';
9
+ }
10
+
11
+ export type StructuralGroupDefinitionClass = typeof StructuralGroupDefinition;
@@ -0,0 +1,26 @@
1
+ import { getHintElement } from '../../lib/dom/query.ts';
2
+ import type { XFormDefinition } from '../../XFormDefinition.ts';
3
+ import type { AnyControlDefinition } from '../control/ControlDefinition.ts';
4
+ import type { TextElement } from './TextElementDefinition.ts';
5
+ import { TextElementDefinition } from './TextElementDefinition.ts';
6
+
7
+ export interface HintElement extends TextElement {
8
+ readonly localName: 'hint';
9
+ }
10
+
11
+ export class HintDefinition extends TextElementDefinition<'hint'> {
12
+ static forElement(
13
+ form: XFormDefinition,
14
+ definition: AnyControlDefinition
15
+ ): HintDefinition | null {
16
+ const hintElement = getHintElement(definition.element);
17
+
18
+ if (hintElement == null) {
19
+ return null;
20
+ }
21
+
22
+ return new this(form, definition, hintElement);
23
+ }
24
+
25
+ readonly type = 'hint';
26
+ }