@getodk/xforms-engine 0.1.1 → 0.3.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 (228) hide show
  1. package/dist/body/BodyDefinition.d.ts +24 -7
  2. package/dist/body/BodyElementDefinition.d.ts +4 -3
  3. package/dist/body/RepeatElementDefinition.d.ts +19 -0
  4. package/dist/body/appearance/inputAppearanceParser.d.ts +4 -0
  5. package/dist/body/appearance/selectAppearanceParser.d.ts +4 -0
  6. package/dist/body/appearance/structureElementAppearanceParser.d.ts +4 -0
  7. package/dist/body/control/ControlDefinition.d.ts +4 -2
  8. package/dist/body/control/InputDefinition.d.ts +5 -0
  9. package/dist/body/control/select/ItemDefinition.d.ts +2 -2
  10. package/dist/body/control/select/ItemsetDefinition.d.ts +5 -4
  11. package/dist/body/control/select/SelectDefinition.d.ts +11 -1
  12. package/dist/body/group/BaseGroupDefinition.d.ts +4 -9
  13. package/dist/body/group/PresentationGroupDefinition.d.ts +1 -1
  14. package/dist/client/BaseNode.d.ts +74 -3
  15. package/dist/client/GroupNode.d.ts +7 -2
  16. package/dist/client/ModelValueNode.d.ts +37 -0
  17. package/dist/client/NodeAppearances.d.ts +15 -0
  18. package/dist/client/NoteNode.d.ts +53 -0
  19. package/dist/client/RootNode.d.ts +21 -0
  20. package/dist/client/SelectNode.d.ts +8 -3
  21. package/dist/client/StringNode.d.ts +8 -3
  22. package/dist/client/SubtreeNode.d.ts +3 -0
  23. package/dist/client/TextRange.d.ts +85 -2
  24. package/dist/client/constants.d.ts +9 -0
  25. package/dist/client/hierarchy.d.ts +14 -9
  26. package/dist/client/node-types.d.ts +2 -1
  27. package/dist/client/{RepeatRangeNode.d.ts → repeat/BaseRepeatRangeNode.d.ts} +19 -15
  28. package/dist/client/{RepeatInstanceNode.d.ts → repeat/RepeatInstanceNode.d.ts} +11 -7
  29. package/dist/client/repeat/RepeatRangeControlledNode.d.ts +19 -0
  30. package/dist/client/repeat/RepeatRangeUncontrolledNode.d.ts +20 -0
  31. package/dist/client/validation.d.ts +163 -0
  32. package/dist/expression/DependentExpression.d.ts +12 -8
  33. package/dist/index.d.ts +9 -4
  34. package/dist/index.js +3173 -960
  35. package/dist/index.js.map +1 -1
  36. package/dist/instance/Group.d.ts +6 -4
  37. package/dist/instance/ModelValue.d.ts +40 -0
  38. package/dist/instance/Note.d.ts +42 -0
  39. package/dist/instance/Root.d.ts +10 -23
  40. package/dist/instance/SelectField.d.ts +12 -6
  41. package/dist/instance/StringField.d.ts +13 -7
  42. package/dist/instance/Subtree.d.ts +3 -1
  43. package/dist/instance/abstract/DescendantNode.d.ts +16 -9
  44. package/dist/instance/abstract/InstanceNode.d.ts +28 -29
  45. package/dist/instance/hierarchy.d.ts +10 -5
  46. package/dist/instance/internal-api/EvaluationContext.d.ts +5 -4
  47. package/dist/instance/internal-api/ValidationContext.d.ts +21 -0
  48. package/dist/instance/internal-api/ValueContext.d.ts +2 -2
  49. package/dist/instance/repeat/BaseRepeatRange.d.ts +160 -0
  50. package/dist/instance/{RepeatInstance.d.ts → repeat/RepeatInstance.d.ts} +38 -13
  51. package/dist/instance/repeat/RepeatRangeControlled.d.ts +16 -0
  52. package/dist/instance/repeat/RepeatRangeUncontrolled.d.ts +35 -0
  53. package/dist/instance/text/TextRange.d.ts +4 -4
  54. package/dist/lib/TokenListParser.d.ts +84 -0
  55. package/dist/lib/dom/query.d.ts +5 -0
  56. package/dist/lib/reactivity/createComputedExpression.d.ts +6 -1
  57. package/dist/lib/reactivity/createNoteReadonlyThunk.d.ts +5 -0
  58. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +2 -1
  59. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +1 -1
  60. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +1 -1
  61. package/dist/lib/reactivity/text/createFieldHint.d.ts +3 -3
  62. package/dist/lib/reactivity/text/createNodeLabel.d.ts +2 -2
  63. package/dist/lib/reactivity/text/createNoteText.d.ts +25 -0
  64. package/dist/lib/reactivity/text/createTextRange.d.ts +5 -7
  65. package/dist/lib/reactivity/validation/createAggregatedViolations.d.ts +9 -0
  66. package/dist/lib/reactivity/validation/createValidation.d.ts +18 -0
  67. package/dist/model/BindDefinition.d.ts +4 -2
  68. package/dist/model/BindElement.d.ts +1 -0
  69. package/dist/model/DescendentNodeDefinition.d.ts +1 -2
  70. package/dist/model/{ValueNodeDefinition.d.ts → LeafNodeDefinition.d.ts} +3 -4
  71. package/dist/model/NodeDefinition.d.ts +16 -16
  72. package/dist/model/RepeatInstanceDefinition.d.ts +5 -6
  73. package/dist/model/RepeatRangeDefinition.d.ts +30 -0
  74. package/dist/model/RepeatTemplateDefinition.d.ts +6 -7
  75. package/dist/model/RootDefinition.d.ts +3 -1
  76. package/dist/model/SubtreeDefinition.d.ts +2 -2
  77. package/dist/parse/NoteNodeDefinition.d.ts +31 -0
  78. package/dist/parse/expression/RepeatCountControlExpression.d.ts +19 -0
  79. package/dist/parse/text/HintDefinition.d.ts +9 -0
  80. package/dist/parse/text/ItemLabelDefinition.d.ts +9 -0
  81. package/dist/parse/text/ItemsetLabelDefinition.d.ts +13 -0
  82. package/dist/parse/text/LabelDefinition.d.ts +15 -0
  83. package/dist/parse/text/MessageDefinition.d.ts +15 -0
  84. package/dist/parse/text/OutputChunkDefinition.d.ts +8 -0
  85. package/dist/parse/text/ReferenceChunkDefinition.d.ts +8 -0
  86. package/dist/parse/text/StaticTextChunkDefinition.d.ts +10 -0
  87. package/dist/parse/text/TranslationChunkDefinition.d.ts +9 -0
  88. package/dist/parse/text/abstract/TextChunkDefinition.d.ts +18 -0
  89. package/dist/parse/text/abstract/TextElementDefinition.d.ts +23 -0
  90. package/dist/parse/text/abstract/TextRangeDefinition.d.ts +35 -0
  91. package/dist/parse/xpath/dependency-analysis.d.ts +40 -0
  92. package/dist/parse/xpath/path-resolution.d.ts +70 -0
  93. package/dist/parse/xpath/predicate-analysis.d.ts +30 -0
  94. package/dist/parse/xpath/reference-parsing.d.ts +18 -0
  95. package/dist/parse/xpath/semantic-analysis.d.ts +98 -0
  96. package/dist/parse/xpath/syntax-traversal.d.ts +69 -0
  97. package/dist/solid.js +3174 -961
  98. package/dist/solid.js.map +1 -1
  99. package/package.json +14 -15
  100. package/src/XFormDOM.ts +81 -8
  101. package/src/body/BodyDefinition.ts +38 -23
  102. package/src/body/BodyElementDefinition.ts +4 -3
  103. package/src/body/RepeatElementDefinition.ts +58 -0
  104. package/src/body/appearance/inputAppearanceParser.ts +39 -0
  105. package/src/body/appearance/selectAppearanceParser.ts +38 -0
  106. package/src/body/appearance/structureElementAppearanceParser.ts +7 -0
  107. package/src/body/control/ControlDefinition.ts +8 -3
  108. package/src/body/control/InputDefinition.ts +13 -0
  109. package/src/body/control/select/ItemDefinition.ts +3 -3
  110. package/src/body/control/select/ItemsetDefinition.ts +29 -12
  111. package/src/body/control/select/ItemsetNodesetExpression.ts +1 -1
  112. package/src/body/control/select/SelectDefinition.ts +14 -5
  113. package/src/body/group/BaseGroupDefinition.ts +14 -51
  114. package/src/body/group/PresentationGroupDefinition.ts +1 -1
  115. package/src/client/BaseNode.ts +82 -8
  116. package/src/client/GroupNode.ts +8 -2
  117. package/src/client/ModelValueNode.ts +40 -0
  118. package/src/client/NodeAppearances.ts +22 -0
  119. package/src/client/NoteNode.ts +74 -0
  120. package/src/client/README.md +1 -0
  121. package/src/client/RootNode.ts +24 -0
  122. package/src/client/SelectNode.ts +9 -3
  123. package/src/client/StringNode.ts +9 -3
  124. package/src/client/SubtreeNode.ts +3 -0
  125. package/src/client/TextRange.ts +99 -2
  126. package/src/client/constants.ts +10 -0
  127. package/src/client/hierarchy.ts +30 -14
  128. package/src/client/node-types.ts +8 -1
  129. package/src/client/{RepeatRangeNode.ts → repeat/BaseRepeatRangeNode.ts} +20 -17
  130. package/src/client/{RepeatInstanceNode.ts → repeat/RepeatInstanceNode.ts} +13 -7
  131. package/src/client/repeat/RepeatRangeControlledNode.ts +20 -0
  132. package/src/client/repeat/RepeatRangeUncontrolledNode.ts +21 -0
  133. package/src/client/validation.ts +199 -0
  134. package/src/expression/DependentExpression.ts +45 -27
  135. package/src/index.ts +15 -8
  136. package/src/instance/Group.ts +24 -13
  137. package/src/instance/ModelValue.ts +104 -0
  138. package/src/instance/Note.ts +142 -0
  139. package/src/instance/Root.ts +29 -67
  140. package/src/instance/SelectField.ts +35 -13
  141. package/src/instance/StringField.ts +40 -13
  142. package/src/instance/Subtree.ts +19 -10
  143. package/src/instance/abstract/DescendantNode.ts +50 -49
  144. package/src/instance/abstract/InstanceNode.ts +89 -92
  145. package/src/instance/children.ts +47 -10
  146. package/src/instance/hierarchy.ts +21 -2
  147. package/src/instance/index.ts +1 -1
  148. package/src/instance/internal-api/EvaluationContext.ts +5 -6
  149. package/src/instance/internal-api/ValidationContext.ts +23 -0
  150. package/src/instance/internal-api/ValueContext.ts +2 -2
  151. package/src/instance/repeat/BaseRepeatRange.ts +347 -0
  152. package/src/instance/{RepeatInstance.ts → repeat/RepeatInstance.ts} +85 -36
  153. package/src/instance/repeat/RepeatRangeControlled.ts +82 -0
  154. package/src/instance/repeat/RepeatRangeUncontrolled.ts +67 -0
  155. package/src/instance/text/TextRange.ts +10 -4
  156. package/src/lib/TokenListParser.ts +156 -0
  157. package/src/lib/dom/query.ts +13 -0
  158. package/src/lib/reactivity/createChildrenState.ts +51 -6
  159. package/src/lib/reactivity/createComputedExpression.ts +23 -25
  160. package/src/lib/reactivity/createNoteReadonlyThunk.ts +33 -0
  161. package/src/lib/reactivity/createSelectItems.ts +25 -20
  162. package/src/lib/reactivity/createValueState.ts +6 -6
  163. package/src/lib/reactivity/materializeCurrentStateChildren.ts +3 -1
  164. package/src/lib/reactivity/node-state/createSharedNodeState.ts +1 -1
  165. package/src/lib/reactivity/text/createFieldHint.ts +9 -7
  166. package/src/lib/reactivity/text/createNodeLabel.ts +7 -5
  167. package/src/lib/reactivity/text/createNoteText.ts +72 -0
  168. package/src/lib/reactivity/text/createTextRange.ts +17 -90
  169. package/src/lib/reactivity/validation/createAggregatedViolations.ts +70 -0
  170. package/src/lib/reactivity/validation/createValidation.ts +196 -0
  171. package/src/model/BindComputation.ts +0 -4
  172. package/src/model/BindDefinition.ts +8 -6
  173. package/src/model/BindElement.ts +1 -0
  174. package/src/model/DescendentNodeDefinition.ts +1 -2
  175. package/src/model/{ValueNodeDefinition.ts → LeafNodeDefinition.ts} +5 -6
  176. package/src/model/ModelBindMap.ts +4 -0
  177. package/src/model/ModelDefinition.ts +1 -1
  178. package/src/model/NodeDefinition.ts +21 -21
  179. package/src/model/RepeatInstanceDefinition.ts +8 -13
  180. package/src/model/RepeatRangeDefinition.ts +94 -0
  181. package/src/model/RepeatTemplateDefinition.ts +10 -15
  182. package/src/model/RootDefinition.ts +12 -14
  183. package/src/model/SubtreeDefinition.ts +3 -3
  184. package/src/parse/NoteNodeDefinition.ts +70 -0
  185. package/src/parse/TODO.md +3 -0
  186. package/src/parse/expression/RepeatCountControlExpression.ts +44 -0
  187. package/src/parse/text/HintDefinition.ts +25 -0
  188. package/src/parse/text/ItemLabelDefinition.ts +25 -0
  189. package/src/parse/text/ItemsetLabelDefinition.ts +44 -0
  190. package/src/parse/text/LabelDefinition.ts +61 -0
  191. package/src/parse/text/MessageDefinition.ts +49 -0
  192. package/src/parse/text/OutputChunkDefinition.ts +25 -0
  193. package/src/parse/text/ReferenceChunkDefinition.ts +14 -0
  194. package/src/parse/text/StaticTextChunkDefinition.ts +19 -0
  195. package/src/parse/text/TranslationChunkDefinition.ts +38 -0
  196. package/src/parse/text/abstract/TextChunkDefinition.ts +38 -0
  197. package/src/parse/text/abstract/TextElementDefinition.ts +71 -0
  198. package/src/parse/text/abstract/TextRangeDefinition.ts +70 -0
  199. package/src/parse/xpath/dependency-analysis.ts +105 -0
  200. package/src/parse/xpath/path-resolution.ts +475 -0
  201. package/src/parse/xpath/predicate-analysis.ts +61 -0
  202. package/src/parse/xpath/reference-parsing.ts +90 -0
  203. package/src/parse/xpath/semantic-analysis.ts +466 -0
  204. package/src/parse/xpath/syntax-traversal.ts +129 -0
  205. package/dist/body/RepeatDefinition.d.ts +0 -16
  206. package/dist/body/group/RepeatGroupDefinition.d.ts +0 -13
  207. package/dist/body/text/HintDefinition.d.ts +0 -11
  208. package/dist/body/text/LabelDefinition.d.ts +0 -20
  209. package/dist/body/text/TextElementDefinition.d.ts +0 -33
  210. package/dist/body/text/TextElementOutputPart.d.ts +0 -13
  211. package/dist/body/text/TextElementPart.d.ts +0 -13
  212. package/dist/body/text/TextElementReferencePart.d.ts +0 -7
  213. package/dist/body/text/TextElementStaticPart.d.ts +0 -7
  214. package/dist/instance/RepeatRange.d.ts +0 -80
  215. package/dist/lib/xpath/analysis.d.ts +0 -23
  216. package/dist/model/RepeatSequenceDefinition.d.ts +0 -20
  217. package/src/body/RepeatDefinition.ts +0 -54
  218. package/src/body/group/RepeatGroupDefinition.ts +0 -91
  219. package/src/body/text/HintDefinition.ts +0 -26
  220. package/src/body/text/LabelDefinition.ts +0 -54
  221. package/src/body/text/TextElementDefinition.ts +0 -97
  222. package/src/body/text/TextElementOutputPart.ts +0 -27
  223. package/src/body/text/TextElementPart.ts +0 -31
  224. package/src/body/text/TextElementReferencePart.ts +0 -21
  225. package/src/body/text/TextElementStaticPart.ts +0 -26
  226. package/src/instance/RepeatRange.ts +0 -214
  227. package/src/lib/xpath/analysis.ts +0 -241
  228. package/src/model/RepeatSequenceDefinition.ts +0 -53
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getodk/xforms-engine",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "XForms engine for ODK Web Forms",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  "README.md"
30
30
  ],
31
31
  "engines": {
32
- "node": "^18.20.3 || ^20.13.1 || ^22.2.0",
33
- "yarn": "1.22.19"
32
+ "node": "^18.20.4 || ^20.15.1 || ^22.4.1",
33
+ "yarn": "1.22.22"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "npm-run-all -nl build:*",
@@ -54,26 +54,25 @@
54
54
  "test:types": "tsc --project ./tsconfig.json --emitDeclarationOnly false --noEmit"
55
55
  },
56
56
  "dependencies": {
57
- "solid-js": "^1.8.17"
57
+ "solid-js": "^1.8.18"
58
58
  },
59
59
  "devDependencies": {
60
- "@babel/core": "^7.24.6",
61
- "@getodk/tree-sitter-xpath": "0.1.1",
62
- "@getodk/xpath": "0.1.1",
63
- "@playwright/test": "^1.44.1",
64
- "@vitest/browser": "^1.6.0",
60
+ "@babel/core": "^7.24.8",
61
+ "@getodk/tree-sitter-xpath": "0.1.2",
62
+ "@getodk/xpath": "0.2.0",
63
+ "@playwright/test": "^1.45.1",
64
+ "@vitest/browser": "^2.0.2",
65
65
  "babel-plugin-transform-jsbi-to-bigint": "^1.4.0",
66
66
  "http-server": "^14.1.1",
67
- "jsdom": "^24.0.0",
68
- "typedoc": "^0.25.13",
69
- "unplugin-fonts": "^1.1.1",
70
- "vite": "^5.2.11",
67
+ "jsdom": "^24.1.0",
68
+ "typedoc": "^0.26.4",
69
+ "vite": "^5.3.3",
71
70
  "vite-plugin-dts": "^3.9.1",
72
71
  "vite-plugin-no-bundle": "^4.0.0",
73
- "vitest": "^1.6.0"
72
+ "vitest": "^2.0.2"
74
73
  },
75
74
  "peerDependencies": {
76
- "solid-js": "^1.8.17"
75
+ "solid-js": "^1.8.18"
77
76
  },
78
77
  "peerDependenciesMeta": {
79
78
  "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
 
@@ -1,8 +1,9 @@
1
1
  import type { XFormDefinition } from '../XFormDefinition.ts';
2
2
  import { DependencyContext } from '../expression/DependencyContext.ts';
3
+ import type { HintDefinition } from '../parse/text/HintDefinition.ts';
4
+ import type { ItemLabelDefinition } from '../parse/text/ItemLabelDefinition.ts';
5
+ import type { LabelDefinition } from '../parse/text/LabelDefinition.ts';
3
6
  import type { BodyElementParentContext } from './BodyDefinition.ts';
4
- import type { HintDefinition } from './text/HintDefinition.ts';
5
- import type { LabelDefinition } from './text/LabelDefinition.ts';
6
7
 
7
8
  /**
8
9
  * These category names roughly correspond to each of the ODK XForms spec's
@@ -22,7 +23,7 @@ export abstract class BodyElementDefinition<Type extends string> extends Depende
22
23
  abstract readonly category: BodyElementCategory;
23
24
  abstract readonly type: Type;
24
25
  readonly hint: HintDefinition | null = null;
25
- readonly label: LabelDefinition | null = null;
26
+ readonly label: ItemLabelDefinition | LabelDefinition | null = null;
26
27
 
27
28
  readonly reference: string | null = null;
28
29
  readonly parentReference: string | null;
@@ -0,0 +1,58 @@
1
+ import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
+ import type { XFormDefinition } from '../XFormDefinition.ts';
3
+ import { LabelDefinition } from '../parse/text/LabelDefinition.ts';
4
+ import { parseNodesetReference } from '../parse/xpath/reference-parsing.ts';
5
+ import type { BodyElementDefinitionArray, BodyElementParentContext } from './BodyDefinition.ts';
6
+ import { BodyDefinition } from './BodyDefinition.ts';
7
+ import { BodyElementDefinition } from './BodyElementDefinition.ts';
8
+ import type { StructureElementAppearanceDefinition } from './appearance/structureElementAppearanceParser.ts';
9
+ import { structureElementAppearanceParser } from './appearance/structureElementAppearanceParser.ts';
10
+
11
+ export class RepeatElementDefinition extends BodyElementDefinition<'repeat'> {
12
+ static override isCompatible(localName: string): boolean {
13
+ return localName === 'repeat';
14
+ }
15
+
16
+ override readonly category = 'structure';
17
+ readonly type = 'repeat';
18
+ override readonly reference: string;
19
+ readonly appearances: StructureElementAppearanceDefinition;
20
+ override readonly label: LabelDefinition | null;
21
+
22
+ readonly countExpression: string | null;
23
+ readonly noAddRemoveExpression: string | null;
24
+
25
+ readonly children: BodyElementDefinitionArray;
26
+
27
+ constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
28
+ super(form, parent, element);
29
+
30
+ this.label = LabelDefinition.forRepeatGroup(form, this);
31
+
32
+ const reference = parseNodesetReference(parent, element, 'nodeset');
33
+
34
+ if (reference == null) {
35
+ throw new Error('Invalid repeat: missing `nodeset` reference');
36
+ }
37
+
38
+ this.reference = reference;
39
+ this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
40
+ this.countExpression = element.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'count');
41
+ this.noAddRemoveExpression = element.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'noAddRemove');
42
+
43
+ const childElements = Array.from(element.children).filter((childElement) => {
44
+ const { localName } = childElement;
45
+
46
+ return localName !== 'label' && localName !== 'group-label';
47
+ });
48
+ const children = BodyDefinition.getChildElementDefinitions(form, this, element, childElements);
49
+
50
+ this.children = children;
51
+ }
52
+
53
+ override toJSON() {
54
+ const { form, parent, ...rest } = this;
55
+
56
+ return rest;
57
+ }
58
+ }
@@ -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,8 +1,10 @@
1
1
  import type { XFormDefinition } from '../../XFormDefinition.ts';
2
+ import type { ParsedTokenList } from '../../lib/TokenListParser.ts';
3
+ import { HintDefinition } from '../../parse/text/HintDefinition.ts';
4
+ import { LabelDefinition } from '../../parse/text/LabelDefinition.ts';
5
+ import { parseNodesetReference } from '../../parse/xpath/reference-parsing.ts';
2
6
  import type { BodyElementParentContext } from '../BodyDefinition.ts';
3
7
  import { BodyElementDefinition } from '../BodyElementDefinition.ts';
4
- import { HintDefinition } from '../text/HintDefinition.ts';
5
- import { LabelDefinition } from '../text/LabelDefinition.ts';
6
8
 
7
9
  // prettier-ignore
8
10
  type ControlType =
@@ -23,10 +25,13 @@ export abstract class ControlDefinition<
23
25
  override readonly label: LabelDefinition | null;
24
26
  override readonly hint: HintDefinition | null;
25
27
 
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ abstract readonly appearances: ParsedTokenList<any>;
30
+
26
31
  constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
27
32
  super(form, parent, element);
28
33
 
29
- const reference = element.getAttribute('ref');
34
+ const reference = parseNodesetReference(parent, element, 'ref');
30
35
 
31
36
  if (reference == null) {
32
37
  throw new Error(`Invalid control: missing ref attribute`);
@@ -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
  }
@@ -1,14 +1,14 @@
1
1
  import { getValueElement, type ItemElement } from '../../../lib/dom/query.ts';
2
+ import { ItemLabelDefinition } from '../../../parse/text/ItemLabelDefinition.ts';
2
3
  import type { XFormDefinition } from '../../../XFormDefinition.ts';
3
4
  import { BodyElementDefinition } from '../../BodyElementDefinition.ts';
4
- import { LabelDefinition } from '../../text/LabelDefinition.ts';
5
5
  import type { AnySelectDefinition } from './SelectDefinition.ts';
6
6
 
7
7
  export class ItemDefinition extends BodyElementDefinition<'item'> {
8
8
  override readonly category = 'support';
9
9
  override readonly type = 'item';
10
10
 
11
- override readonly label: LabelDefinition | null;
11
+ override readonly label: ItemLabelDefinition | null;
12
12
  readonly value: string;
13
13
 
14
14
  constructor(
@@ -25,7 +25,7 @@ export class ItemDefinition extends BodyElementDefinition<'item'> {
25
25
 
26
26
  super(form, parent, element);
27
27
 
28
- this.label = LabelDefinition.forItem(form, this);
28
+ this.label = ItemLabelDefinition.from(form, this);
29
29
  this.value = value;
30
30
  }
31
31
  }
@@ -1,7 +1,8 @@
1
- import { getValueElement, type ItemsetElement } from '../../../lib/dom/query.ts';
2
1
  import type { XFormDefinition } from '../../../XFormDefinition.ts';
2
+ import { getValueElement, type ItemsetElement } from '../../../lib/dom/query.ts';
3
+ import { ItemsetLabelDefinition } from '../../../parse/text/ItemsetLabelDefinition.ts';
4
+ import { parseNodesetReference } from '../../../parse/xpath/reference-parsing.ts';
3
5
  import { BodyElementDefinition } from '../../BodyElementDefinition.ts';
4
- import { LabelDefinition } from '../../text/LabelDefinition.ts';
5
6
  import { ItemsetNodesetExpression } from './ItemsetNodesetExpression.ts';
6
7
  import { ItemsetValueExpression } from './ItemsetValueExpression.ts';
7
8
  import type { AnySelectDefinition } from './SelectDefinition.ts';
@@ -11,26 +12,42 @@ export class ItemsetDefinition extends BodyElementDefinition<'itemset'> {
11
12
  readonly type = 'itemset';
12
13
 
13
14
  override readonly reference: string;
14
- override readonly label: LabelDefinition | null;
15
+ override readonly label: ItemsetLabelDefinition | null;
15
16
 
16
17
  readonly nodes: ItemsetNodesetExpression;
17
18
  readonly value: ItemsetValueExpression;
18
19
 
19
- constructor(form: XFormDefinition, select: AnySelectDefinition, element: ItemsetElement) {
20
+ constructor(
21
+ form: XFormDefinition,
22
+ override readonly parent: AnySelectDefinition,
23
+ element: ItemsetElement
24
+ ) {
25
+ super(form, parent, element);
26
+
27
+ const nodesetExpression = parseNodesetReference(parent, element, 'nodeset');
28
+
29
+ this.nodes = new ItemsetNodesetExpression(this, nodesetExpression);
30
+ this.reference = nodesetExpression;
31
+
20
32
  const valueElement = getValueElement(element);
21
- const valueExpression = valueElement?.getAttribute('ref');
22
33
 
23
- if (valueExpression == null) {
24
- throw new Error(`<itemset> has no <value>`);
34
+ if (valueElement == null) {
35
+ throw new Error('<itemset> has no <value>');
25
36
  }
26
37
 
27
- super(form, select, element);
38
+ const valueExpression = parseNodesetReference(
39
+ {
40
+ reference: null,
41
+ },
42
+ valueElement,
43
+ 'ref'
44
+ );
28
45
 
29
- const nodesetExpression = element.getAttribute('nodeset');
46
+ if (valueExpression == null) {
47
+ throw new Error(`<itemset> has no <value>`);
48
+ }
30
49
 
31
- this.reference = nodesetExpression;
32
- this.nodes = new ItemsetNodesetExpression(this, nodesetExpression);
33
50
  this.value = new ItemsetValueExpression(this, valueExpression);
34
- this.label = LabelDefinition.forItemset(form, this);
51
+ this.label = ItemsetLabelDefinition.from(form, this);
35
52
  }
36
53
  }
@@ -3,6 +3,6 @@ import type { ItemsetDefinition } from './ItemsetDefinition.ts';
3
3
 
4
4
  export class ItemsetNodesetExpression extends DependentExpression<'nodes'> {
5
5
  constructor(itemset: ItemsetDefinition, nodesetExpression: string) {
6
- super(itemset, 'nodes', nodesetExpression);
6
+ super(itemset.parent, 'nodes', nodesetExpression);
7
7
  }
8
8
  }
@@ -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);