@getodk/xforms-engine 0.2.0 → 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 (184) hide show
  1. package/dist/body/BodyElementDefinition.d.ts +4 -3
  2. package/dist/body/RepeatElementDefinition.d.ts +2 -2
  3. package/dist/body/control/ControlDefinition.d.ts +2 -2
  4. package/dist/body/control/select/ItemDefinition.d.ts +2 -2
  5. package/dist/body/control/select/ItemsetDefinition.d.ts +5 -4
  6. package/dist/body/group/BaseGroupDefinition.d.ts +1 -1
  7. package/dist/body/group/PresentationGroupDefinition.d.ts +1 -1
  8. package/dist/client/BaseNode.d.ts +68 -2
  9. package/dist/client/GroupNode.d.ts +2 -0
  10. package/dist/client/ModelValueNode.d.ts +37 -0
  11. package/dist/client/NoteNode.d.ts +53 -0
  12. package/dist/client/RootNode.d.ts +2 -0
  13. package/dist/client/SelectNode.d.ts +5 -3
  14. package/dist/client/StringNode.d.ts +5 -3
  15. package/dist/client/SubtreeNode.d.ts +2 -0
  16. package/dist/client/TextRange.d.ts +85 -2
  17. package/dist/client/constants.d.ts +9 -0
  18. package/dist/client/hierarchy.d.ts +14 -9
  19. package/dist/client/node-types.d.ts +2 -1
  20. package/dist/client/{RepeatRangeNode.d.ts → repeat/BaseRepeatRangeNode.d.ts} +18 -17
  21. package/dist/client/{RepeatInstanceNode.d.ts → repeat/RepeatInstanceNode.d.ts} +9 -8
  22. package/dist/client/repeat/RepeatRangeControlledNode.d.ts +19 -0
  23. package/dist/client/repeat/RepeatRangeUncontrolledNode.d.ts +20 -0
  24. package/dist/client/validation.d.ts +163 -0
  25. package/dist/expression/DependentExpression.d.ts +12 -8
  26. package/dist/index.d.ts +9 -4
  27. package/dist/index.js +2635 -678
  28. package/dist/index.js.map +1 -1
  29. package/dist/instance/Group.d.ts +3 -1
  30. package/dist/instance/ModelValue.d.ts +40 -0
  31. package/dist/instance/Note.d.ts +42 -0
  32. package/dist/instance/Root.d.ts +2 -0
  33. package/dist/instance/SelectField.d.ts +10 -4
  34. package/dist/instance/StringField.d.ts +11 -5
  35. package/dist/instance/Subtree.d.ts +2 -0
  36. package/dist/instance/abstract/DescendantNode.d.ts +5 -6
  37. package/dist/instance/abstract/InstanceNode.d.ts +2 -0
  38. package/dist/instance/hierarchy.d.ts +10 -5
  39. package/dist/instance/internal-api/ValidationContext.d.ts +21 -0
  40. package/dist/instance/{RepeatRange.d.ts → repeat/BaseRepeatRange.d.ts} +46 -45
  41. package/dist/instance/{RepeatInstance.d.ts → repeat/RepeatInstance.d.ts} +13 -12
  42. package/dist/instance/repeat/RepeatRangeControlled.d.ts +16 -0
  43. package/dist/instance/repeat/RepeatRangeUncontrolled.d.ts +35 -0
  44. package/dist/instance/text/TextRange.d.ts +4 -4
  45. package/dist/lib/reactivity/createComputedExpression.d.ts +6 -1
  46. package/dist/lib/reactivity/createNoteReadonlyThunk.d.ts +5 -0
  47. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +1 -1
  48. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +1 -1
  49. package/dist/lib/reactivity/text/createFieldHint.d.ts +3 -3
  50. package/dist/lib/reactivity/text/createNodeLabel.d.ts +2 -2
  51. package/dist/lib/reactivity/text/createNoteText.d.ts +25 -0
  52. package/dist/lib/reactivity/text/createTextRange.d.ts +5 -7
  53. package/dist/lib/reactivity/validation/createAggregatedViolations.d.ts +9 -0
  54. package/dist/lib/reactivity/validation/createValidation.d.ts +18 -0
  55. package/dist/model/BindDefinition.d.ts +4 -2
  56. package/dist/model/BindElement.d.ts +1 -0
  57. package/dist/model/{ValueNodeDefinition.d.ts → LeafNodeDefinition.d.ts} +2 -2
  58. package/dist/model/NodeDefinition.d.ts +8 -8
  59. package/dist/model/RepeatInstanceDefinition.d.ts +2 -2
  60. package/dist/model/RepeatRangeDefinition.d.ts +14 -4
  61. package/dist/parse/NoteNodeDefinition.d.ts +31 -0
  62. package/dist/parse/expression/RepeatCountControlExpression.d.ts +19 -0
  63. package/dist/parse/text/HintDefinition.d.ts +9 -0
  64. package/dist/parse/text/ItemLabelDefinition.d.ts +9 -0
  65. package/dist/parse/text/ItemsetLabelDefinition.d.ts +13 -0
  66. package/dist/parse/text/LabelDefinition.d.ts +15 -0
  67. package/dist/parse/text/MessageDefinition.d.ts +15 -0
  68. package/dist/parse/text/OutputChunkDefinition.d.ts +8 -0
  69. package/dist/parse/text/ReferenceChunkDefinition.d.ts +8 -0
  70. package/dist/parse/text/StaticTextChunkDefinition.d.ts +10 -0
  71. package/dist/parse/text/TranslationChunkDefinition.d.ts +9 -0
  72. package/dist/parse/text/abstract/TextChunkDefinition.d.ts +18 -0
  73. package/dist/parse/text/abstract/TextElementDefinition.d.ts +23 -0
  74. package/dist/parse/text/abstract/TextRangeDefinition.d.ts +35 -0
  75. package/dist/parse/xpath/dependency-analysis.d.ts +40 -0
  76. package/dist/parse/xpath/path-resolution.d.ts +70 -0
  77. package/dist/parse/xpath/predicate-analysis.d.ts +30 -0
  78. package/dist/parse/xpath/reference-parsing.d.ts +18 -0
  79. package/dist/parse/xpath/semantic-analysis.d.ts +98 -0
  80. package/dist/parse/xpath/syntax-traversal.d.ts +69 -0
  81. package/dist/solid.js +2636 -679
  82. package/dist/solid.js.map +1 -1
  83. package/package.json +14 -15
  84. package/src/body/BodyElementDefinition.ts +4 -3
  85. package/src/body/RepeatElementDefinition.ts +5 -17
  86. package/src/body/control/ControlDefinition.ts +4 -3
  87. package/src/body/control/select/ItemDefinition.ts +3 -3
  88. package/src/body/control/select/ItemsetDefinition.ts +29 -12
  89. package/src/body/control/select/ItemsetNodesetExpression.ts +1 -1
  90. package/src/body/group/BaseGroupDefinition.ts +3 -2
  91. package/src/body/group/PresentationGroupDefinition.ts +1 -1
  92. package/src/client/BaseNode.ts +73 -7
  93. package/src/client/GroupNode.ts +2 -0
  94. package/src/client/ModelValueNode.ts +40 -0
  95. package/src/client/NoteNode.ts +74 -0
  96. package/src/client/README.md +1 -0
  97. package/src/client/RootNode.ts +2 -0
  98. package/src/client/SelectNode.ts +5 -3
  99. package/src/client/StringNode.ts +5 -3
  100. package/src/client/SubtreeNode.ts +2 -0
  101. package/src/client/TextRange.ts +99 -2
  102. package/src/client/constants.ts +10 -0
  103. package/src/client/hierarchy.ts +30 -14
  104. package/src/client/node-types.ts +8 -1
  105. package/src/client/{RepeatRangeNode.ts → repeat/BaseRepeatRangeNode.ts} +18 -19
  106. package/src/client/{RepeatInstanceNode.ts → repeat/RepeatInstanceNode.ts} +10 -8
  107. package/src/client/repeat/RepeatRangeControlledNode.ts +20 -0
  108. package/src/client/repeat/RepeatRangeUncontrolledNode.ts +21 -0
  109. package/src/client/validation.ts +199 -0
  110. package/src/expression/DependentExpression.ts +45 -27
  111. package/src/index.ts +15 -8
  112. package/src/instance/Group.ts +10 -4
  113. package/src/instance/ModelValue.ts +104 -0
  114. package/src/instance/Note.ts +142 -0
  115. package/src/instance/Root.ts +9 -3
  116. package/src/instance/SelectField.ts +28 -6
  117. package/src/instance/StringField.ts +35 -9
  118. package/src/instance/Subtree.ts +9 -3
  119. package/src/instance/abstract/DescendantNode.ts +6 -7
  120. package/src/instance/abstract/InstanceNode.ts +20 -6
  121. package/src/instance/children.ts +42 -15
  122. package/src/instance/hierarchy.ts +21 -2
  123. package/src/instance/internal-api/ValidationContext.ts +23 -0
  124. package/src/instance/{RepeatRange.ts → repeat/BaseRepeatRange.ts} +114 -99
  125. package/src/instance/{RepeatInstance.ts → repeat/RepeatInstance.ts} +27 -22
  126. package/src/instance/repeat/RepeatRangeControlled.ts +82 -0
  127. package/src/instance/repeat/RepeatRangeUncontrolled.ts +67 -0
  128. package/src/instance/text/TextRange.ts +10 -4
  129. package/src/lib/reactivity/createComputedExpression.ts +22 -24
  130. package/src/lib/reactivity/createNoteReadonlyThunk.ts +33 -0
  131. package/src/lib/reactivity/createSelectItems.ts +21 -14
  132. package/src/lib/reactivity/node-state/createSharedNodeState.ts +1 -1
  133. package/src/lib/reactivity/text/createFieldHint.ts +9 -7
  134. package/src/lib/reactivity/text/createNodeLabel.ts +7 -5
  135. package/src/lib/reactivity/text/createNoteText.ts +72 -0
  136. package/src/lib/reactivity/text/createTextRange.ts +17 -90
  137. package/src/lib/reactivity/validation/createAggregatedViolations.ts +70 -0
  138. package/src/lib/reactivity/validation/createValidation.ts +196 -0
  139. package/src/model/BindComputation.ts +0 -4
  140. package/src/model/BindDefinition.ts +8 -6
  141. package/src/model/BindElement.ts +1 -0
  142. package/src/model/{ValueNodeDefinition.ts → LeafNodeDefinition.ts} +4 -4
  143. package/src/model/ModelBindMap.ts +4 -0
  144. package/src/model/NodeDefinition.ts +12 -12
  145. package/src/model/RepeatInstanceDefinition.ts +2 -2
  146. package/src/model/RepeatRangeDefinition.ts +49 -8
  147. package/src/model/RootDefinition.ts +7 -3
  148. package/src/parse/NoteNodeDefinition.ts +70 -0
  149. package/src/parse/TODO.md +3 -0
  150. package/src/parse/expression/RepeatCountControlExpression.ts +44 -0
  151. package/src/parse/text/HintDefinition.ts +25 -0
  152. package/src/parse/text/ItemLabelDefinition.ts +25 -0
  153. package/src/parse/text/ItemsetLabelDefinition.ts +44 -0
  154. package/src/parse/text/LabelDefinition.ts +61 -0
  155. package/src/parse/text/MessageDefinition.ts +49 -0
  156. package/src/parse/text/OutputChunkDefinition.ts +25 -0
  157. package/src/parse/text/ReferenceChunkDefinition.ts +14 -0
  158. package/src/parse/text/StaticTextChunkDefinition.ts +19 -0
  159. package/src/parse/text/TranslationChunkDefinition.ts +38 -0
  160. package/src/parse/text/abstract/TextChunkDefinition.ts +38 -0
  161. package/src/parse/text/abstract/TextElementDefinition.ts +71 -0
  162. package/src/parse/text/abstract/TextRangeDefinition.ts +70 -0
  163. package/src/parse/xpath/dependency-analysis.ts +105 -0
  164. package/src/parse/xpath/path-resolution.ts +475 -0
  165. package/src/parse/xpath/predicate-analysis.ts +61 -0
  166. package/src/parse/xpath/reference-parsing.ts +90 -0
  167. package/src/parse/xpath/semantic-analysis.ts +466 -0
  168. package/src/parse/xpath/syntax-traversal.ts +129 -0
  169. package/dist/body/text/HintDefinition.d.ts +0 -11
  170. package/dist/body/text/LabelDefinition.d.ts +0 -22
  171. package/dist/body/text/TextElementDefinition.d.ts +0 -33
  172. package/dist/body/text/TextElementOutputPart.d.ts +0 -13
  173. package/dist/body/text/TextElementPart.d.ts +0 -13
  174. package/dist/body/text/TextElementReferencePart.d.ts +0 -7
  175. package/dist/body/text/TextElementStaticPart.d.ts +0 -7
  176. package/dist/lib/xpath/analysis.d.ts +0 -23
  177. package/src/body/text/HintDefinition.ts +0 -26
  178. package/src/body/text/LabelDefinition.ts +0 -68
  179. package/src/body/text/TextElementDefinition.ts +0 -97
  180. package/src/body/text/TextElementOutputPart.ts +0 -27
  181. package/src/body/text/TextElementPart.ts +0 -31
  182. package/src/body/text/TextElementReferencePart.ts +0 -21
  183. package/src/body/text/TextElementStaticPart.ts +0 -26
  184. package/src/lib/xpath/analysis.ts +0 -241
@@ -0,0 +1,475 @@
1
+ import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
2
+ import type {
3
+ AbsoluteLocationPathNode,
4
+ AbsoluteRootLocationPathNode,
5
+ FilterExprNode,
6
+ PredicateNode,
7
+ RelativeStepSyntaxLiteralNode,
8
+ StepNode,
9
+ } from '@getodk/xpath/static/grammar/SyntaxNode.js';
10
+ import type { PathExpressionNode } from './semantic-analysis.ts';
11
+ import { isCurrentPath } from './semantic-analysis.ts';
12
+
13
+ type AbsolutePathHead =
14
+ /** / - as first character in LocationPath */
15
+ | AbsoluteRootLocationPathNode
16
+
17
+ /** // - as first characters in LocationPath */
18
+ | RelativeStepSyntaxLiteralNode;
19
+
20
+ /**
21
+ * fn(...args) - as first (and potentially only) part of a path expression,
22
+ * where the function is either known to produce a node-set result, or where
23
+ * other aspects of the exression's syntax are inherently node-set producing.
24
+ */
25
+ type FilterPathExprHead = FilterExprNode;
26
+
27
+ type StepLikeNode =
28
+ /** // - shorthand for `/descendant-or-self::node()/` */
29
+ | RelativeStepSyntaxLiteralNode
30
+
31
+ /** Any _actual_ Step in a LocationPath */
32
+ | StepNode;
33
+
34
+ type PathNodeListHead = AbsolutePathHead | FilterPathExprHead | StepLikeNode;
35
+
36
+ /**
37
+ * A path node list is a semi-flattened representation of...
38
+ *
39
+ * - Any XPath LocationPath expression:
40
+ * - AbsoluteLocationPath
41
+ * - RelativeLocationPath
42
+ *
43
+ * - Any expression beginning with a FilterExpr which is known to produce a
44
+ * node-set result
45
+ *
46
+ * The flattening of these syntax representations is used to perform various
47
+ * aspects of path resolution logic, accounting for complexities of XPath syntax
48
+ * and semantics in a roughly linear/list processing manner.
49
+ */
50
+ // prettier-ignore
51
+ export type PathNodeList<
52
+ Head extends PathNodeListHead = PathNodeListHead
53
+ > = readonly [
54
+ head: Head,
55
+ ...tail: StepLikeNode[]
56
+ ];
57
+
58
+ /**
59
+ * Produces a semi-flattened representation of an AbsoluteLocationPath.
60
+ *
61
+ * @see {@link PathNodeList}
62
+ */
63
+ const absolutePathNodeList = (
64
+ pathNode: AbsoluteLocationPathNode
65
+ ): PathNodeList<AbsolutePathHead> => {
66
+ const [head, ...tail] = pathNode.children;
67
+
68
+ switch (head.type) {
69
+ case 'abbreviated_absolute_location_path': {
70
+ return head.children;
71
+ }
72
+
73
+ case 'absolute_root_location_path':
74
+ return [head, ...tail];
75
+
76
+ default:
77
+ throw new UnreachableError(head);
78
+ }
79
+ };
80
+
81
+ const pathNodeList = (pathNode: PathExpressionNode): PathNodeList => {
82
+ switch (pathNode.type) {
83
+ case 'absolute_location_path':
84
+ return absolutePathNodeList(pathNode);
85
+
86
+ case 'filter_path_expr':
87
+ return pathNode.children satisfies PathNodeList<FilterExprNode>;
88
+
89
+ case 'relative_location_path':
90
+ return pathNode.children satisfies PathNodeList<StepLikeNode>;
91
+
92
+ default:
93
+ throw new UnreachableError(pathNode);
94
+ }
95
+ };
96
+
97
+ const optionalPathNodeList = (pathNode: PathExpressionNode | null): PathNodeList | null => {
98
+ if (pathNode == null) {
99
+ return null;
100
+ }
101
+
102
+ return pathNodeList(pathNode);
103
+ };
104
+
105
+ type UnresolvedContextualizedPathListNode = FilterExprNode | StepLikeNode;
106
+
107
+ /**
108
+ * Like {@link PathNodeList}, provides a semi-flattened representation of path
109
+ * syntax. Distinct from that type, an unresolved list represents an
110
+ * intermediate concatenation of:
111
+ *
112
+ * - A **contextual** {@link PathNodeList} against which the to-be-resolved path
113
+ * will eventually be resolved.
114
+ *
115
+ * - The same structural representation of the to-be-resolved path.
116
+ */
117
+ type UnresolvedPathNodeList = readonly [
118
+ head: PathNodeListHead,
119
+ ...tail: UnresolvedContextualizedPathListNode[],
120
+ ];
121
+
122
+ type ResolvableTraversalType = 'parent' | 'self';
123
+
124
+ const abbreviatedStepTextToResolvableStepType = {
125
+ '..': 'parent',
126
+ '.': 'self',
127
+ } as const satisfies Record<string, ResolvableTraversalType>;
128
+
129
+ const getResolvableTraversalType = (syntaxNode: StepNode): ResolvableTraversalType | null => {
130
+ const [stepTest, ...predicates] = syntaxNode.children;
131
+
132
+ // Ensure that we preserve self/parent traversal Steps with predicates
133
+ if (predicates.length > 0) {
134
+ return null;
135
+ }
136
+
137
+ if (stepTest.type === 'abbreviated_step') {
138
+ return abbreviatedStepTextToResolvableStepType[stepTest.text];
139
+ }
140
+
141
+ if (stepTest.type !== 'axis_test' || predicates.length > 0) {
142
+ return null;
143
+ }
144
+
145
+ const [axisNameNode, axisTest] = stepTest.children;
146
+ const axisName = axisNameNode.text;
147
+
148
+ if (axisName !== 'parent' && axisName !== 'self') {
149
+ return null;
150
+ }
151
+
152
+ const axisTestType = axisTest.type;
153
+
154
+ if (
155
+ // `self::*`, `parent::*`
156
+ axisTestType === 'unprefixed_wildcard_name_test' ||
157
+ // `self::node()`, `parent::node()`
158
+ (axisTestType === 'node_type_test' && axisTest.text.startsWith('node'))
159
+ ) {
160
+ return axisName;
161
+ }
162
+
163
+ return null;
164
+ };
165
+
166
+ /**
167
+ * Resolves the component parts of an XPath LocationPath/path-like expression
168
+ * (as represented by an {@link UnresolvedPathNodeList}
169
+ */
170
+ const resolvePathNodeList = (nodes: UnresolvedPathNodeList): PathNodeList => {
171
+ const [head, ...tail] = nodes;
172
+ const lastTailIndex = tail.length - 1;
173
+
174
+ // Local representation, mutable during resolution
175
+ type PathNodeResolution = [...PathNodeList];
176
+
177
+ return tail.reduce<PathNodeResolution>(
178
+ (acc, node, index) => {
179
+ // Because we've filtered non-`current()` cases before we reach this point,
180
+ // we can safely assume this is a `current()` call. For resolving **nodeset
181
+ // references** specifically, we treat this as equivalent to `.`.
182
+ //
183
+ // TODO: can we make this check/result more self-documenting, and reduce
184
+ // coupling with the `current()` check? Probably! We could move the check
185
+ // here, returning `acc` for `current()`, and returning `[node]` (i.e.
186
+ // establishing a new absolute-like context) otherwise. It seems like we
187
+ // could even do a similar check for the actual AbsoluteLocationPath case,
188
+ // and probably eliminate a bunch of the special cases in the main
189
+ // `resolvePathExpression` body.
190
+ if (node.type === 'filter_expr') {
191
+ return acc;
192
+ }
193
+
194
+ const [currentHead, ...currentTail] = acc;
195
+
196
+ // Special case `//` shorthand (1):
197
+ if (
198
+ // - Current accumulated path is a single head node.
199
+ currentTail.length === 0 &&
200
+ // - Head is `//` shorthand.
201
+ currentHead.type === '//' &&
202
+ // - Current node is last in source expression.
203
+ index === lastTailIndex
204
+ ) {
205
+ // Even if current node could otherwise be resolved, we must append
206
+ // because `//` without a following Step-like node does not produce a
207
+ // valid XPath expression.
208
+ acc.push(node);
209
+
210
+ return acc;
211
+ }
212
+
213
+ // Special case `//` shorthand (2):
214
+ //
215
+ // Current node is `//`, and will be guaranteed (by `tree-sitter-xpath`
216
+ // grammar) to be followed by a valid Step-like node. Append and continue.
217
+ if (node.type === '//') {
218
+ acc.push(node);
219
+
220
+ return acc;
221
+ }
222
+
223
+ // Any further cases are Step syntax
224
+ node satisfies StepNode;
225
+
226
+ const traversalType = getResolvableTraversalType(node);
227
+
228
+ if (traversalType == null) {
229
+ acc.push(node);
230
+
231
+ return acc;
232
+ }
233
+
234
+ if (traversalType === 'self') {
235
+ return acc;
236
+ }
237
+
238
+ // All further cases are parent traversal, which we will resolve where
239
+ // possible. Each variation is detailed below.
240
+ traversalType satisfies 'parent';
241
+
242
+ // For our resolution purposes, the following expressions are
243
+ // functionally equivalent:
244
+ //
245
+ // - `$head/$currentTail/$Step-like/..`
246
+ // - `$head/$currentTail`
247
+ //
248
+ // As such, when we encounter `..` which is currently preceded by
249
+ // any non-head, Step-like node, we can remove that trailing node
250
+ // rather than appending `..`.
251
+ //
252
+ // Note: `$head/..` (without any intervening Step-like nodes) is
253
+ // more complicated. It is handled below, explaining each case in
254
+ // greater detail.
255
+ if (currentTail.length > 0) {
256
+ return [currentHead, ...currentTail.slice(0, -1)];
257
+ }
258
+
259
+ // Resolving `$head/..`, with no intervening Step-like nodes...
260
+ switch (currentHead.type) {
261
+ // Head of expression is `/`. `/..` cannot resolve to any node-set,
262
+ // but it **is a valid LocationPath expression**. We concatenate
263
+ // here to produce that expression, as a concession that this is
264
+ // the only valid outcome.
265
+ case 'absolute_root_location_path':
266
+ return [currentHead, node];
267
+
268
+ // Head of expression is a FilterExpr. Attempting to resolve parent step
269
+ // would lose that original context. Concatenating instead is a
270
+ // concession to this fact: we cannot fully resolve the nodeset/path
271
+ // path in this context.
272
+ //
273
+ // If the head FilterExpr is a `current()` call, it is possible that the
274
+ // path may be further resolved against another context (likely one from
275
+ // an outer element in the form definition, or any other case which
276
+ // would establish further context and be reached by recursing up the
277
+ // definition hierarchy). For now, we must accept that the resulting
278
+ // LocationPath will not be fully resolved.
279
+ //
280
+ // If the FilterExpr is an `instance("id")` call, this will effectively
281
+ // terminate resolution of the step:
282
+ // `instance("id")/../$remainingTail` **is a valid expression**;
283
+ // however, downstream evaluation of that expression is expected to
284
+ // produce an empty node-set (as it traverses into a part of the
285
+ // document outside of the subtree[s] handled by the `@getodk/xpath`
286
+ // evaluator).
287
+ case 'filter_expr':
288
+ return [currentHead, node];
289
+
290
+ // Head of expression is relative. Resolution is as follows:
291
+ case 'step': {
292
+ // - If head is a self-reference, we replace that head with this step.
293
+ // This is safe (and correct) because `./../$remainingTail` is
294
+ // functionally equivalent to `../$remainingTail`.
295
+ if (getResolvableTraversalType(currentHead) === 'self') {
296
+ return [node];
297
+ }
298
+
299
+ // - If head is any other Step-like node we concatenate (e.g.
300
+ // where head is `foo`, we will produce `foo/..`), with the same
301
+ // reasoning as the above FilterExpr case. And as with that
302
+ // case, it is possible some downstream processing may allow
303
+ // further resolution.
304
+ return [currentHead, node];
305
+ }
306
+
307
+ // Head of current path is `//`. Above, we already handled the special
308
+ // case where this parent traversal would be the end of the path
309
+ // expression, so here we can safely collapse `//../$remainingTail`
310
+ // to `//$remainingTail`
311
+ case '//': {
312
+ return acc;
313
+ }
314
+
315
+ default:
316
+ throw new UnreachableError(currentHead);
317
+ }
318
+ },
319
+ [head]
320
+ );
321
+ };
322
+
323
+ export const resolvePath = (
324
+ contextNode: PathExpressionNode | null,
325
+ pathNode: PathExpressionNode
326
+ ): PathNodeList => {
327
+ const contextNodes = optionalPathNodeList(contextNode) ?? [];
328
+
329
+ // Path expression is **resolved without context** (or as its own context) in
330
+ // any of the following conditions...
331
+ //
332
+ // - No context is available
333
+ // - Expression path is absolute
334
+ // - Expression begins with any FilterExpr besides a `current()` call
335
+ //
336
+ // We still resolve the Step-like syntax parts **within the path**, e.g.
337
+ // allowing us to collapse `Step/..` pairs.
338
+ if (
339
+ // - We have no context, so there's nothing to resolve the expression path
340
+ // against, regardless of any other factor
341
+ contextNode == null ||
342
+ // - Expression path is absolute, so it is its own context
343
+ pathNode.type === 'absolute_location_path' ||
344
+ // - Expression path has leading FilterExpr, which is **not** a call to
345
+ // `current()`, in which case it is treated as if it were absolute.
346
+ (pathNode.type === 'filter_path_expr' && !isCurrentPath(pathNode))
347
+ ) {
348
+ const pathNodes = pathNodeList(pathNode);
349
+
350
+ return resolvePathNodeList(pathNodes);
351
+ }
352
+
353
+ const contextualizedNodes: UnresolvedPathNodeList = [...contextNodes, ...pathNode.children];
354
+
355
+ return resolvePathNodeList(contextualizedNodes);
356
+ };
357
+
358
+ /**
359
+ * Resolves the parsed path {@link predicatePathNode}, in the context of:
360
+ *
361
+ * - The {@link contextNode} context, representing the original expression's
362
+ * context (if one was available)
363
+ *
364
+ * - The {@link stepContextNodes} context, representing the cumulative portion
365
+ * of the source path where {@link predicatePathNode} was parsed from a
366
+ * Predicate sub-expression
367
+ *
368
+ * Both contexts are necessary for resolution to ensure that:
369
+ *
370
+ * - A `current()` call within the predicate's sub-expression is contextualized
371
+ * to the current `nodeset` reference associated with the original expression
372
+ *
373
+ * - A `.` self-reference within the predicate's sub-expression is
374
+ * contextualized to the Step in which it occurred
375
+ */
376
+ export const resolvePredicateReference = (
377
+ contextNode: PathExpressionNode | null,
378
+ stepContextNodes: PathNodeList,
379
+ predicatePathNode: PathExpressionNode
380
+ ): PathNodeList => {
381
+ const predicatePathNodes = pathNodeList(predicatePathNode);
382
+
383
+ const [head, ...tail] = predicatePathNodes;
384
+
385
+ if (head.type === 'absolute_root_location_path' || head.type === '//') {
386
+ return predicatePathNodes;
387
+ }
388
+
389
+ let contextNodes: PathNodeList;
390
+
391
+ if (
392
+ contextNode != null &&
393
+ predicatePathNode.type === 'filter_path_expr' &&
394
+ isCurrentPath(predicatePathNode)
395
+ ) {
396
+ contextNodes = pathNodeList(contextNode);
397
+ } else {
398
+ contextNodes = stepContextNodes;
399
+ }
400
+
401
+ const contextualizedNodes: UnresolvedPathNodeList = [...contextNodes, head, ...tail];
402
+
403
+ return resolvePathNodeList(contextualizedNodes);
404
+ };
405
+ interface PathSerializationOptions {
406
+ /**
407
+ * @default false
408
+ */
409
+ readonly stripPredicates: boolean;
410
+ }
411
+
412
+ type AnyPathNode = PathNodeList[number];
413
+
414
+ const serializePathNode = (node: AnyPathNode, options: PathSerializationOptions): string => {
415
+ const { type, text } = node;
416
+ if (type === 'step') {
417
+ switch (getResolvableTraversalType(node)) {
418
+ case 'self':
419
+ return '.';
420
+
421
+ case 'parent':
422
+ return '..';
423
+ }
424
+ }
425
+
426
+ if (options.stripPredicates) {
427
+ switch (type) {
428
+ case 'absolute_root_location_path':
429
+ case '//':
430
+ return text;
431
+
432
+ case 'filter_expr':
433
+ case 'step': {
434
+ const [head, ..._predicates] = node.children;
435
+
436
+ _predicates satisfies readonly PredicateNode[];
437
+
438
+ return head.text;
439
+ }
440
+ }
441
+ }
442
+
443
+ return text;
444
+ };
445
+
446
+ /**
447
+ * Serializes a resolved {@link PathNodeList} to its XPath expression
448
+ * representation, optionally stripping predicates.
449
+ */
450
+ export const serializeNodesetReference = (
451
+ nodes: PathNodeList,
452
+ options: PathSerializationOptions
453
+ ): string => {
454
+ const [head, ...tail] = nodes;
455
+ const strings: string[] = [serializePathNode(head, options)];
456
+
457
+ let previousNode = head;
458
+
459
+ for (const node of tail) {
460
+ const previousNodeType = previousNode.type;
461
+
462
+ if (
463
+ previousNodeType !== 'absolute_root_location_path' &&
464
+ previousNodeType !== '//' &&
465
+ node.type !== '//'
466
+ ) {
467
+ strings.push('/');
468
+ }
469
+
470
+ strings.push(serializePathNode(node, options));
471
+ previousNode = node;
472
+ }
473
+
474
+ return strings.join('');
475
+ };
@@ -0,0 +1,61 @@
1
+ import type { PathNodeList } from './path-resolution.ts';
2
+ import type { PathExpressionNode } from './semantic-analysis.ts';
3
+ import { findLocationPathSubExpressionNodes } from './semantic-analysis.ts';
4
+
5
+ /**
6
+ * Represents a pair of:
7
+ *
8
+ * - Path expression syntax referenced within a particular predicate, on any
9
+ * Step within a source LocationPath and/or FilterExpr path. This
10
+ * sub-expression is resolved as a member of the dependencies which may be
11
+ * referenced by any arbitrary form expression.
12
+ *
13
+ * - The cumulative set of path nodes, from the start of the source path
14
+ * expression, to the Step and Predicate where the sub-expression reference
15
+ * was identified. This representation is used as **part** of the context used
16
+ * to resolve the identified Predicate sub-expression for downstream
17
+ * dependency subscriptions. (Each predicate sub-expression is **further
18
+ * contextualized** by the original context in which the source path is
19
+ * defined.)
20
+ */
21
+ export interface PredicateReference {
22
+ readonly predicatePathNode: PathExpressionNode;
23
+ readonly stepContextNodes: PathNodeList;
24
+ }
25
+
26
+ /**
27
+ * Identifies path sub-expressions within any of a path's Predicates, along with
28
+ * the step context in which they are found.
29
+ *
30
+ * @see {@link PredicateReference} for details on the produced structures.
31
+ */
32
+ export const findPredicateReferences = (pathNodes: PathNodeList): readonly PredicateReference[] => {
33
+ const [head, ...tail] = pathNodes;
34
+
35
+ return pathNodes.flatMap((targetNode, i) => {
36
+ const cumulativePath: PathNodeList = [head, ...tail.slice(0, i)];
37
+
38
+ switch (targetNode.type) {
39
+ case 'absolute_root_location_path':
40
+ case '//':
41
+ return [];
42
+ }
43
+
44
+ const [, ...predicates] = targetNode.children;
45
+
46
+ if (predicates.length === 0) {
47
+ return [];
48
+ }
49
+
50
+ return predicates.flatMap((predicate) => {
51
+ const predicateSubExpressions = findLocationPathSubExpressionNodes(predicate);
52
+
53
+ return predicateSubExpressions.map((predicatePathNode) => {
54
+ return {
55
+ predicatePathNode,
56
+ stepContextNodes: cumulativePath,
57
+ };
58
+ });
59
+ });
60
+ });
61
+ };
@@ -0,0 +1,90 @@
1
+ import type { PartiallyKnownString } from '@getodk/common/types/string/PartiallyKnownString.ts';
2
+ import type { resolveDependencyNodesets } from './dependency-analysis.ts';
3
+ import { resolvePath, serializeNodesetReference } from './path-resolution.ts';
4
+ import { getPathExpressionNode } from './semantic-analysis.ts';
5
+
6
+ /**
7
+ * Resolves a (potentially relative) `nodeset` reference, to its context (if one
8
+ * is available). This is a slight variation on the dependency analysis behavior
9
+ * of {@link resolveDependencyNodesets}, different in the following ways:
10
+ *
11
+ * - Output is the resolved input expression, rather than sub-expressions
12
+ * identified within it
13
+ *
14
+ * - Output expression preserves Predicates
15
+ *
16
+ * The purprose of these differences is to generalize resolution of a form
17
+ * definition's direct `nodeset` references, where they may be...
18
+ *
19
+ * - defined relative to some contextual aspect of the form (e.g. `<group
20
+ * ref="/data/some-absolute-grp"><input ref="some-child">`)
21
+ *
22
+ * - a more complex `nodeset` expression (e.g. `<itemset nodeset>`) which may
23
+ * also lack context in the original form definition, but which may itself
24
+ * contain predicates defining specific form behavior
25
+ */
26
+ const resolveParsedNodesetReference = (
27
+ contextReference: string | null,
28
+ reference: string
29
+ ): string => {
30
+ const referenceNode = getPathExpressionNode(reference);
31
+
32
+ if (referenceNode == null) {
33
+ return reference;
34
+ }
35
+
36
+ const contextNode = contextReference == null ? null : getPathExpressionNode(contextReference);
37
+ const resolved = resolvePath(contextNode, referenceNode);
38
+
39
+ return serializeNodesetReference(resolved, {
40
+ stripPredicates: false,
41
+ });
42
+ };
43
+
44
+ interface ReferenceParsingContext {
45
+ readonly reference: string | null;
46
+ readonly parent?: ReferenceParsingContext | null;
47
+ }
48
+
49
+ type ReferenceAttributeName = PartiallyKnownString<'nodeset' | 'ref'>;
50
+
51
+ interface KnownAttributeElement<AttributeName extends string> extends Element {
52
+ getAttribute(name: AttributeName): string;
53
+ getAttribute(name: string): string | null;
54
+ }
55
+
56
+ type ParsedReferenceAttribute<T extends Element, AttributeName extends string> =
57
+ T extends KnownAttributeElement<AttributeName> ? string : string | null;
58
+
59
+ /**
60
+ * Parses a `nodeset` reference from an arbitrary form definition element, and
61
+ * resolves that (potentially relative) reference to the provided context.
62
+ */
63
+ export const parseNodesetReference = <
64
+ const AttributeName extends ReferenceAttributeName,
65
+ T extends Element | KnownAttributeElement<AttributeName>,
66
+ >(
67
+ parentContext: ReferenceParsingContext,
68
+ element: T,
69
+ attributeName: AttributeName
70
+ ): ParsedReferenceAttribute<T, AttributeName> => {
71
+ const referenceExpression = element.getAttribute(attributeName);
72
+
73
+ if (referenceExpression == null) {
74
+ return referenceExpression as ParsedReferenceAttribute<T, AttributeName>;
75
+ }
76
+
77
+ let currentContext: ReferenceParsingContext | null = parentContext;
78
+ let parentReference: string | null = parentContext.reference;
79
+
80
+ while (currentContext != null && parentReference == null) {
81
+ parentReference = currentContext?.parent?.reference ?? null;
82
+ currentContext = currentContext.parent ?? null;
83
+ }
84
+
85
+ if (parentReference == null) {
86
+ return referenceExpression;
87
+ }
88
+
89
+ return resolveParsedNodesetReference(parentReference, referenceExpression);
90
+ };