@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,466 @@
1
+ import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
2
+ import type { CollectionValues } from '@getodk/common/types/collections/CollectionValues.ts';
3
+ import { expressionParser } from '@getodk/xpath/expressionParser.js';
4
+ import type {
5
+ AbsoluteLocationPathNode,
6
+ AnySyntaxNode,
7
+ ArgumentNode,
8
+ FilterExprNode,
9
+ FilterPathExprNode,
10
+ FunctionCallNode,
11
+ FunctionNameNode,
12
+ NumberNode,
13
+ RelativeLocationPathNode,
14
+ StringLiteralNode,
15
+ } from '@getodk/xpath/static/grammar/SyntaxNode.js';
16
+ import type { AnySyntaxType } from '@getodk/xpath/static/grammar/type-names.js';
17
+ import {
18
+ collectTypedNodes,
19
+ findTypedPrincipalExpressionNode,
20
+ isCompleteSubExpression,
21
+ } from './syntax-traversal.ts';
22
+
23
+ // prettier-ignore
24
+ type LocalNameLiteral<LocalName extends string> =
25
+ | LocalName
26
+ | `${string}:${LocalName}`;
27
+
28
+ interface LocalNamedFunctionNameNode<LocalName extends string> extends FunctionNameNode {
29
+ readonly text: LocalNameLiteral<LocalName>;
30
+ }
31
+
32
+ // prettier-ignore
33
+ type LocalNamedFunctionCallLiteral<
34
+ LocalName extends string
35
+ > = `${LocalNameLiteral<LocalName>}(${string})`;
36
+
37
+ interface LocalNamedFunctionCallNode<LocalName extends string> extends FunctionCallNode {
38
+ readonly children: readonly [
39
+ name: LocalNamedFunctionNameNode<LocalName>,
40
+ ...arguments: ArgumentNode[],
41
+ ];
42
+ readonly text: LocalNamedFunctionCallLiteral<LocalName>;
43
+ }
44
+
45
+ const isCallToLocalNamedFunction = <LocalName extends string>(
46
+ syntaxNode: FunctionCallNode,
47
+ localName: LocalName
48
+ ): syntaxNode is LocalNamedFunctionCallNode<LocalName> => {
49
+ const [functionNameNode] = syntaxNode.children;
50
+ const [localNameNode] = collectTypedNodes(['local_part', 'unprefixed_name'], functionNameNode);
51
+
52
+ return localNameNode?.text === localName;
53
+ };
54
+
55
+ const ANY_ARGUMENT_TYPE = Symbol('ANY_ARGUMENT_TYPE');
56
+ type AnyArgumentType = typeof ANY_ARGUMENT_TYPE;
57
+
58
+ type ArgumentFilter = AnyArgumentType | readonly [AnySyntaxType, ...AnySyntaxType[]];
59
+
60
+ const hasCallSignature = (
61
+ syntaxNode: FunctionCallNode,
62
+ expected: readonly ArgumentFilter[]
63
+ ): boolean => {
64
+ const [, ...argumentNodes] = syntaxNode.children;
65
+
66
+ if (argumentNodes.length === 0 && expected.length === 0) {
67
+ return true;
68
+ }
69
+
70
+ if (argumentNodes.length > expected.length) {
71
+ return false;
72
+ }
73
+
74
+ return expected.every((filter, i) => {
75
+ const argumentNode = argumentNodes[i];
76
+
77
+ if (argumentNode == null) {
78
+ return false;
79
+ }
80
+
81
+ if (filter === ANY_ARGUMENT_TYPE) {
82
+ return true;
83
+ }
84
+
85
+ const [firstMatch] = collectTypedNodes(filter, argumentNode);
86
+
87
+ return firstMatch != null && isCompleteSubExpression(argumentNode, firstMatch);
88
+ });
89
+ };
90
+
91
+ const isTranslationFunctionCall = (syntaxNode: FunctionCallNode): boolean => {
92
+ return (
93
+ isCallToLocalNamedFunction(syntaxNode, 'itext') &&
94
+ hasCallSignature(syntaxNode, [
95
+ // We don't need to check the argument type here, just its presence. We'd
96
+ // originally checked for arguments we presume could produce a string.
97
+ // Giving it a little more thought, it was clear that applies to any
98
+ // conceivable argument, because anything can be cast to a string in XPath
99
+ // semantics.
100
+ ANY_ARGUMENT_TYPE,
101
+ ])
102
+ );
103
+ };
104
+
105
+ export type TranslationExpression = LocalNamedFunctionCallLiteral<'itext'>;
106
+
107
+ /**
108
+ * Determines if an arbitrary XPath expression is (in whole) a translation
109
+ * expression (i.e. a call to `jr:itext`).
110
+ *
111
+ * @todo We may also want a companion function: `hasTranslationExpression`,
112
+ * which could be used for `<label ref>`/`<hint ref>` or anywhere else that an
113
+ * arbitrary expression may call `jr:itext`.
114
+ */
115
+ export const isTranslationExpression = (
116
+ expression: string
117
+ ): expression is TranslationExpression => {
118
+ const { rootNode } = expressionParser.parse(expression);
119
+ const functionCallNode = findTypedPrincipalExpressionNode(['function_call'], rootNode);
120
+
121
+ if (functionCallNode == null) {
122
+ return false;
123
+ }
124
+
125
+ return isTranslationFunctionCall(functionCallNode);
126
+ };
127
+
128
+ const isCurrentFunctionCall = (syntaxNode: FunctionCallNode): boolean => {
129
+ return isCallToLocalNamedFunction(syntaxNode, 'current') && hasCallSignature(syntaxNode, []);
130
+ };
131
+
132
+ /**
133
+ * Predicate to determine if a FilterPathExpr (as currently produced by
134
+ * `tree-sitter-xpath`) is one of:
135
+ *
136
+ * - `current()`
137
+ * - `current()/...` (where `...` represents additional steps)
138
+ * - `current()//...` (^)
139
+ *
140
+ * @todo XPath grammar technically also allows for FilterExpr[Predicate],
141
+ * and our `tree-sitter-xpath` grammar/parser also allow for this. But
142
+ * `@getodk/xpath` types do not currently acknowledge this possibility.
143
+ */
144
+ export const isCurrentPath = (syntaxNode: FilterPathExprNode): boolean => {
145
+ const [filterExprNode] = syntaxNode.children;
146
+ const [anyExprNode] = filterExprNode.children;
147
+
148
+ return anyExprNode.type === 'function_call' && isCurrentFunctionCall(anyExprNode);
149
+ };
150
+
151
+ const isInstanceFunctionCall = (syntaxNode: FunctionCallNode): boolean => {
152
+ return (
153
+ isCallToLocalNamedFunction(syntaxNode, 'instance') &&
154
+ hasCallSignature(syntaxNode, [
155
+ // Specified as `instance("id")`, but do we really care about the argument's
156
+ // type here?
157
+ ['argument'],
158
+ ])
159
+ );
160
+ };
161
+
162
+ /**
163
+ * Predicate to determine if a FilterPathExpr (as currently produced by
164
+ * `tree-sitter-xpath`) is one of:
165
+ *
166
+ * - `instance("id")`
167
+ * - `instance("id")/...` (where `...` represents additional steps)
168
+ * - `instance("id")//...` (^)
169
+ *
170
+ * @todo XPath grammar technically also allows for FilterExpr[Predicate],
171
+ * and our `tree-sitter-xpath` grammar/parser also allow for this. But
172
+ * `@getodk/xpath` types do not currently acknowledge this possibility.
173
+ */
174
+ const isInstancePath = (syntaxNode: FilterPathExprNode): boolean => {
175
+ const [filterExprNode] = syntaxNode.children;
176
+ const [anyExprNode] = filterExprNode.children;
177
+
178
+ return anyExprNode.type === 'function_call' && isInstanceFunctionCall(anyExprNode);
179
+ };
180
+
181
+ /**
182
+ * Determines whether a given [sub-]expression:
183
+ *
184
+ * - Begins with FilterExpr syntax
185
+ * - Has any syntactic indication that the expression will produce a node-set
186
+ *
187
+ * This is a last/best effort means to identify aspects of XPath syntax which
188
+ * should be treated as a node-set expression, but isn't currently handled by
189
+ * more explicit checks like {@link isCurrentPath} and {@link isInstancePath}.
190
+ */
191
+ const isArbitraryFilterPath = (syntaxNode: FilterPathExprNode): boolean => {
192
+ const [filterExprNode, ...steps] = syntaxNode.children;
193
+ const [anyExprNode, ...predicates] = filterExprNode.children;
194
+
195
+ /**
196
+ * @todo This is an oversight in the **types** for {@link FilterExprNode}! The
197
+ * `@getodk/tree-sitter-xpath` parser (correctly) parses predicates following
198
+ * a FilterExpr. This case was missed when defining the static types in
199
+ * `@getodk/xpath`.
200
+ *
201
+ * Addressing this oversight would also imply addressing the oversight in the
202
+ * `xpath` runtime.
203
+ *
204
+ * To reviewer: I caught this issue in previous iterations on this set of
205
+ * changes. I have tests and a fix stashed, and I'd be happy to bring it into
206
+ * scope if preferred.
207
+ *
208
+ * When fixed, this `satisfies` check will fail. In either case, the below
209
+ * check will work for our semantic analysis and path resolution purproses.
210
+ */
211
+ predicates satisfies readonly [];
212
+
213
+ return anyExprNode.type === 'function_call' && (steps.length > 0 || predicates.length > 0);
214
+ };
215
+
216
+ declare const FILTER_PATH_NODE: unique symbol;
217
+
218
+ /**
219
+ * Used to narrow types where a SyntaxNode with type 'filter_path_expr' is not
220
+ * **known to produce** a node-set result.
221
+ *
222
+ * This addresses some awkwardness in the XPath grammar (and our implementation
223
+ * parsing it) where FilterExpr _may be_ a FunctionCall, and one of the
224
+ * following _may also be true_:
225
+ *
226
+ * - The function call is known by name to produce a node-set result, **OR**
227
+ *
228
+ * - The function call is followed by one or more Steps (or the Step-like '//'
229
+ * shorthand), which must produce a node-set **OR**
230
+ *
231
+ * - The function call is followed by one or more Predicates, which must produce
232
+ * a node-set
233
+ *
234
+ * Any other FilterExpr (and thus our containing synthetic 'filter_path_expr'
235
+ * SyntaxNode) is treated as a non-path [sub-]expression, excluding it from
236
+ * analysis as such (and any downstream logic such as nodeset resolution).
237
+ */
238
+ export interface FilterPathNode extends FilterPathExprNode {
239
+ readonly [FILTER_PATH_NODE]: true;
240
+ }
241
+
242
+ /**
243
+ * Determines whether a given expression beginning with a FilterExpr is known to
244
+ * produce a node-set result. Used in downstream dependency analysis, as well as
245
+ * path resolution.
246
+ */
247
+ export const isNodeSetFilterPathExpression = (
248
+ syntaxNode: FilterPathExprNode
249
+ ): syntaxNode is FilterPathNode => {
250
+ return (
251
+ isCurrentPath(syntaxNode) || isInstancePath(syntaxNode) || isArbitraryFilterPath(syntaxNode)
252
+ );
253
+ };
254
+
255
+ export type PathExpressionNode =
256
+ | AbsoluteLocationPathNode
257
+ | FilterPathNode
258
+ | RelativeLocationPathNode;
259
+
260
+ const isPathExpression = (syntaxNode: AnySyntaxNode | null): syntaxNode is PathExpressionNode => {
261
+ if (syntaxNode == null) {
262
+ return false;
263
+ }
264
+
265
+ const { type } = syntaxNode;
266
+
267
+ return (
268
+ type === 'absolute_location_path' ||
269
+ type === 'relative_location_path' ||
270
+ (type === 'filter_path_expr' && isNodeSetFilterPathExpression(syntaxNode))
271
+ );
272
+ };
273
+
274
+ type PathExpressionType = PathExpressionNode['type'];
275
+
276
+ const pathExpressionTypes = [
277
+ 'absolute_location_path',
278
+ 'filter_path_expr',
279
+ 'relative_location_path',
280
+ ] satisfies [PathExpressionType, PathExpressionType, PathExpressionType];
281
+
282
+ /**
283
+ * Locates sub-expression {@link PathExpressionNode}s within a parsed XPath
284
+ * expression (or any arbitrary sub-expression thereof).
285
+ */
286
+ export const findLocationPathSubExpressionNodes = (
287
+ syntaxNode: AnySyntaxNode
288
+ ): readonly PathExpressionNode[] => {
289
+ const baseResults = collectTypedNodes(pathExpressionTypes, syntaxNode);
290
+
291
+ return baseResults.flatMap((node) => {
292
+ // Note: `collectTypedNodes`, as called, is shallowly recursive. Our intent
293
+ // is to operate on complete path expressions, relying on downstream logic
294
+ // to determine if and how deeper recursion is appropriate.
295
+ //
296
+ // In this case, we treat paths beginning with a FilterExpr -> FunctionCall
297
+ // as a special case, where we also manually walk the FunctionCall's
298
+ // Arguments. The shallow search performed by `collectTypedNodes` is
299
+ // important here, ensuring we can target further recursion into this syntax
300
+ // case without applying the same logic to other parts of an identified path
301
+ // sub-expression (i.e. allowing specialized contextualization and analysis
302
+ // of Predicates downstream).
303
+ if (node.type === 'filter_path_expr' && isNodeSetFilterPathExpression(node)) {
304
+ const [filterExprNode] = node.children;
305
+ const [functionCallNode] = filterExprNode.children;
306
+
307
+ // This only satisfies the type checker. We could complicate
308
+ // `FilterPathNode` to eliminate it, but seems reasonable for now.
309
+ if (functionCallNode.type !== 'function_call') {
310
+ return node;
311
+ }
312
+
313
+ const [, ...argumentNodes] = functionCallNode.children;
314
+
315
+ argumentNodes satisfies readonly ArgumentNode[];
316
+
317
+ const argumentResults = argumentNodes.flatMap((argumentNode) => {
318
+ return findLocationPathSubExpressionNodes(argumentNode);
319
+ });
320
+
321
+ return [node, ...argumentResults];
322
+ }
323
+
324
+ if (isPathExpression(node)) {
325
+ return node;
326
+ }
327
+
328
+ return node.children.flatMap(findLocationPathSubExpressionNodes);
329
+ });
330
+ };
331
+
332
+ /**
333
+ * Gets the parsed representation of an XPath path expression, iff the complete
334
+ * expression is any {@link PathExpressionNode} syntax type.
335
+ */
336
+ export const getPathExpressionNode = (expression: string): PathExpressionNode | null => {
337
+ const { rootNode } = expressionParser.parse(expression);
338
+ const result = findTypedPrincipalExpressionNode(pathExpressionTypes, rootNode);
339
+
340
+ if (isPathExpression(result)) {
341
+ return result;
342
+ }
343
+
344
+ return null;
345
+ };
346
+
347
+ const constantFunctionCallNames = ['false', 'true'] as const;
348
+
349
+ type ConstantFunctionCallName = CollectionValues<typeof constantFunctionCallNames>;
350
+
351
+ type CosntantFunctionCallNode = LocalNamedFunctionCallNode<ConstantFunctionCallName>;
352
+
353
+ const isConstantFunctionCall = (
354
+ syntaxNode: FunctionCallNode
355
+ ): syntaxNode is CosntantFunctionCallNode => {
356
+ return (
357
+ constantFunctionCallNames.some((functionName) => {
358
+ return isCallToLocalNamedFunction(syntaxNode, functionName);
359
+ }) && hasCallSignature(syntaxNode, [])
360
+ );
361
+ };
362
+
363
+ // prettier-ignore
364
+ type ConstantExpressionSyntaxNode =
365
+ | CosntantFunctionCallNode
366
+ | NumberNode
367
+ | StringLiteralNode;
368
+
369
+ const findConstantExpressionNode = (expression: string): ConstantExpressionSyntaxNode | null => {
370
+ const { rootNode } = expressionParser.parse(expression);
371
+ const syntaxNode = findTypedPrincipalExpressionNode(
372
+ ['function_call', 'number', 'string_literal'],
373
+ rootNode
374
+ );
375
+
376
+ if (syntaxNode == null) {
377
+ return null;
378
+ }
379
+
380
+ switch (syntaxNode.type) {
381
+ case 'function_call':
382
+ if (isConstantFunctionCall(syntaxNode)) {
383
+ return syntaxNode;
384
+ }
385
+
386
+ return null;
387
+
388
+ case 'number':
389
+ case 'string_literal':
390
+ return syntaxNode;
391
+
392
+ default:
393
+ throw new UnreachableError(syntaxNode);
394
+ }
395
+ };
396
+
397
+ type BrandedExpression<Expression extends string, Brand extends symbol> = Expression & {
398
+ readonly [K in Brand]: true;
399
+ };
400
+
401
+ const CONSTANT_EXPRESSION = Symbol('CONSTANT_EXPRESSION');
402
+ type CONSTANT_EXPRESSION = typeof CONSTANT_EXPRESSION;
403
+
404
+ /**
405
+ * Represents an expression which produces a constant result:
406
+ *
407
+ * - Makes no reference to explicit dependencies
408
+ * - Does not depend on any known, implicit state
409
+ * - Evaluation does not depend in any way on context
410
+ * - Evaluation can be treated as referentially transparent
411
+ */
412
+ // prettier-ignore
413
+ export type ConstantExpression = BrandedExpression<
414
+ string,
415
+ CONSTANT_EXPRESSION
416
+ >;
417
+
418
+ /**
419
+ * @see {@link ConstantExpression}
420
+ */
421
+ export const isConstantExpression = (expression: string): expression is ConstantExpression => {
422
+ return findConstantExpressionNode(expression) != null;
423
+ };
424
+
425
+ const CONSTANT_TRUTHY_EXPRESSION = Symbol('CONSTANT_TRUTHY_EXPRESSION');
426
+ type CONSTANT_TRUTHY_EXPRESSION = typeof CONSTANT_TRUTHY_EXPRESSION;
427
+
428
+ /**
429
+ * Represents an expression which is {@link ConstantExpression | constant},
430
+ * and which will always produce `true` when evaluated as a boolean.
431
+ */
432
+ // prettier-ignore
433
+ export type ConstantTruthyExpression = BrandedExpression<
434
+ ConstantExpression,
435
+ CONSTANT_TRUTHY_EXPRESSION
436
+ >;
437
+
438
+ /**
439
+ * @see {@link ConstantTruthyExpression}
440
+ */
441
+ export const isConstantTruthyExpression = (
442
+ expression: string
443
+ ): expression is ConstantTruthyExpression => {
444
+ const syntaxNode = findConstantExpressionNode(expression);
445
+
446
+ if (syntaxNode == null) {
447
+ return false;
448
+ }
449
+
450
+ switch (syntaxNode.type) {
451
+ // Expression is a number, number value is truthy
452
+ case 'number':
453
+ return Boolean(Number(syntaxNode.text));
454
+
455
+ // Expression is a string literal, string value is non-empty
456
+ case 'string_literal':
457
+ return syntaxNode.text.length > 2;
458
+
459
+ // Expression is a `true()` call
460
+ case 'function_call':
461
+ return isCallToLocalNamedFunction(syntaxNode, 'true');
462
+
463
+ default:
464
+ throw new UnreachableError(syntaxNode);
465
+ }
466
+ };
@@ -0,0 +1,129 @@
1
+ import type { Identity } from '@getodk/common/types/helpers.js';
2
+ import type {
3
+ AnySyntaxNode,
4
+ SyntaxNode,
5
+ XPathNode,
6
+ } from '@getodk/xpath/static/grammar/SyntaxNode.js';
7
+ import type { AnySyntaxType } from '@getodk/xpath/static/grammar/type-names.js';
8
+
9
+ export type TypedSyntaxNode<Type extends AnySyntaxType> = Extract<
10
+ AnySyntaxNode,
11
+ { readonly type: Type }
12
+ >;
13
+
14
+ type CollectedNodes<Type extends AnySyntaxType> = Identity<ReadonlyArray<TypedSyntaxNode<Type>>>;
15
+
16
+ const isTypedNodeMatch = <const Type extends AnySyntaxType>(
17
+ types: readonly [Type, ...Type[]],
18
+ syntaxNode: AnySyntaxNode
19
+ ): syntaxNode is TypedSyntaxNode<Type> => {
20
+ return types.includes(syntaxNode.type as Type);
21
+ };
22
+
23
+ interface CollectNodesOptions {
24
+ readonly recurseMatchedNodes?: boolean;
25
+ }
26
+
27
+ const collectTypedChildren = <const Type extends AnySyntaxType>(
28
+ types: readonly [Type, ...Type[]],
29
+ currentNode: AnySyntaxNode,
30
+ options: CollectNodesOptions = {}
31
+ ): CollectedNodes<Type> => {
32
+ return currentNode.children.flatMap((child) => {
33
+ return collectTypedNodes(types, child, options);
34
+ });
35
+ };
36
+
37
+ /**
38
+ * Collects XPath {@link SyntaxNode}s matching any of the specified
39
+ * {@link types}.
40
+ *
41
+ * By default, search is **shallowly recursive** (i.e. the descendants of
42
+ * matches are not searched).
43
+ *
44
+ * {@link CollectNodesOptions.recurseMatchedNodes | `options.recurseMatchedNodes: true`}
45
+ * can be specified to recursively search descendants of matches.
46
+ *
47
+ * This may be useful for analysis of XPath sub-expressions, for instance
48
+ * identifying all LocationPaths referenced by a broader expression.
49
+ */
50
+ export const collectTypedNodes = <const Type extends AnySyntaxType>(
51
+ types: readonly [Type, ...Type[]],
52
+ currentNode: AnySyntaxNode,
53
+ options: CollectNodesOptions = {}
54
+ ): CollectedNodes<Type> => {
55
+ if (isTypedNodeMatch(types, currentNode)) {
56
+ if (options.recurseMatchedNodes) {
57
+ return [currentNode, ...collectTypedChildren(types, currentNode, options)];
58
+ }
59
+
60
+ return [currentNode];
61
+ }
62
+
63
+ return collectTypedChildren(types, currentNode, options);
64
+ };
65
+
66
+ /**
67
+ * Predicate to determine whether {@link descendantNode} represents the complete
68
+ * syntax of {@link subExpressionNode}. This can be useful for targeting
69
+ * specific aspects of syntax which tend to be wrapped (sometimes several layers
70
+ * deep) in more general syntax types.
71
+ *
72
+ * @example
73
+ *
74
+ * ```ts
75
+ * const pathExpression = '/foo[@bar = 1]';
76
+ * const numericExpression = '2';
77
+ *
78
+ * const pathRootNode = expressionParser.parse(pathExpression).rootNode;
79
+ * // ^?: XPathNode
80
+ * const [pathNumberNode] = collectTypedNodes(['number'], pathRootNode);
81
+ * // ^?: NumberNode
82
+ *
83
+ * isCompleteSubExpression(pathRootNode, pathNumberNode); // false
84
+ *
85
+ * const numericRootNode = expressionParser.parse(numericExpression).rootNode;
86
+ * // ^?: XPathNode
87
+ * const [numericNumberNode] = collectTypedNodes(['number'], numericRootNode);
88
+ * // ^?: NumberNode
89
+ *
90
+ * isCompleteSubExpression(numericRootNode, numericNumberNode); // true
91
+ * ```
92
+ */
93
+ export const isCompleteSubExpression = (
94
+ subExpressionNode: AnySyntaxNode,
95
+ descendantNode: AnySyntaxNode
96
+ ): boolean => {
97
+ return descendantNode.text.trim() === subExpressionNode.text.trim();
98
+ };
99
+
100
+ /**
101
+ * Finds a syntax node which:
102
+ *
103
+ * - Matches one of the specified {@link types}, and
104
+ * - Represents the complete expression
105
+ *
106
+ * This may be useful for performing semantic analysis on expressions, for
107
+ * instance identifying when an expression **is a FunctionCall**, and producing
108
+ * the {@link SyntaxNode} representing that FunctionCall.
109
+ *
110
+ * In contrast, this would produce `null` for an expression **containing a
111
+ * FunctionCall** in some sub-expression position (e.g. the call to `position`
112
+ * in `foo[position() = 2]`).
113
+ */
114
+ export const findTypedPrincipalExpressionNode = <const Type extends AnySyntaxType>(
115
+ types: readonly [Type, ...Type[]],
116
+ xpathNode: XPathNode
117
+ ): TypedSyntaxNode<Type> | null => {
118
+ const [first, ...rest] = collectTypedNodes(types, xpathNode);
119
+
120
+ if (first == null || rest.length > 0) {
121
+ return null;
122
+ }
123
+
124
+ if (isCompleteSubExpression(xpathNode, first)) {
125
+ return first;
126
+ }
127
+
128
+ return null;
129
+ };
@@ -1,11 +0,0 @@
1
- import { XFormDefinition } from '../../XFormDefinition.ts';
2
- import { AnyControlDefinition } from '../control/ControlDefinition.ts';
3
- import { TextElement, TextElementDefinition } from './TextElementDefinition.ts';
4
-
5
- export interface HintElement extends TextElement {
6
- readonly localName: 'hint';
7
- }
8
- export declare class HintDefinition extends TextElementDefinition<'hint'> {
9
- static forElement(form: XFormDefinition, definition: AnyControlDefinition): HintDefinition | null;
10
- readonly type = "hint";
11
- }
@@ -1,22 +0,0 @@
1
- import { XFormDefinition } from '../../XFormDefinition.ts';
2
- import { AnyControlDefinition } from '../control/ControlDefinition.ts';
3
- import { ItemDefinition } from '../control/select/ItemDefinition.ts';
4
- import { ItemsetDefinition } from '../control/select/ItemsetDefinition.ts';
5
- import { BaseGroupDefinition } from '../group/BaseGroupDefinition.ts';
6
- import { RepeatElementDefinition } from '../RepeatElementDefinition.ts';
7
- import { TextElement, TextElementOwner, TextElementDefinition } from './TextElementDefinition.ts';
8
-
9
- export interface LabelElement extends TextElement {
10
- readonly localName: 'label';
11
- }
12
- type StaticLabelContext = Exclude<TextElementOwner, ItemsetDefinition>;
13
- export declare class LabelDefinition extends TextElementDefinition<'label'> {
14
- protected static staticDefinition(form: XFormDefinition, definition: StaticLabelContext): LabelDefinition | null;
15
- static forControl(form: XFormDefinition, control: AnyControlDefinition): LabelDefinition | null;
16
- static forRepeatGroup(form: XFormDefinition, repeat: RepeatElementDefinition): LabelDefinition | null;
17
- static forGroup(form: XFormDefinition, group: BaseGroupDefinition<any>): LabelDefinition | null;
18
- static forItem(form: XFormDefinition, item: ItemDefinition): LabelDefinition | null;
19
- static forItemset(form: XFormDefinition, itemset: ItemsetDefinition): LabelDefinition | null;
20
- readonly type = "label";
21
- }
22
- export {};
@@ -1,33 +0,0 @@
1
- import { XFormDefinition } from '../../XFormDefinition.ts';
2
- import { AnyDependentExpression } from '../../expression/DependentExpression.ts';
3
- import { AnyGroupElementDefinition } from '../BodyDefinition.ts';
4
- import { BodyElementDefinition } from '../BodyElementDefinition.ts';
5
- import { RepeatElementDefinition } from '../RepeatElementDefinition.ts';
6
- import { AnyControlDefinition } from '../control/ControlDefinition.ts';
7
- import { ItemDefinition } from '../control/select/ItemDefinition.ts';
8
- import { ItemsetDefinition } from '../control/select/ItemsetDefinition.ts';
9
- import { TextElementOutputPart } from './TextElementOutputPart.ts';
10
- import { TextElementReferencePart } from './TextElementReferencePart.ts';
11
- import { TextElementStaticPart } from './TextElementStaticPart.ts';
12
-
13
- export type TextElementType = 'hint' | 'label';
14
- export interface TextElement extends Element {
15
- readonly localName: TextElementType;
16
- }
17
- export type TextElementOwner = AnyControlDefinition | AnyGroupElementDefinition | ItemDefinition | ItemsetDefinition | RepeatElementDefinition;
18
- export type TextElementChild = TextElementOutputPart | TextElementStaticPart;
19
- export declare abstract class TextElementDefinition<Type extends TextElementType> extends BodyElementDefinition<Type> {
20
- readonly owner: TextElementOwner;
21
- readonly category = "support";
22
- abstract readonly type: Type;
23
- readonly reference: string | null;
24
- readonly parentReference: string | null;
25
- readonly referenceExpression: TextElementReferencePart | null;
26
- readonly children: readonly TextElementChild[];
27
- get isTranslated(): boolean;
28
- set isTranslated(value: true);
29
- protected constructor(form: XFormDefinition, owner: TextElementOwner, element: TextElement);
30
- registerDependentExpression(expression: AnyDependentExpression): void;
31
- toJSON(): object;
32
- }
33
- export type AnyTextElementDefinition = TextElementDefinition<TextElementType>;
@@ -1,13 +0,0 @@
1
- import { AnyTextElementDefinition } from './TextElementDefinition.ts';
2
- import { TextElementPart } from './TextElementPart.ts';
3
-
4
- interface OutputElement extends Element {
5
- readonly localName: 'output';
6
- getAttribute(name: 'value'): string;
7
- getAttribute(name: string): string | null;
8
- }
9
- export declare class TextElementOutputPart extends TextElementPart<'output'> {
10
- static from(context: AnyTextElementDefinition, element: Element): TextElementOutputPart | null;
11
- protected constructor(context: AnyTextElementDefinition, element: OutputElement);
12
- }
13
- export {};
@@ -1,13 +0,0 @@
1
- import { DependentExpression } from '../../expression/DependentExpression.ts';
2
- import { AnyTextElementDefinition } from './TextElementDefinition.ts';
3
- import { TextElementOutputPart } from './TextElementOutputPart.ts';
4
- import { TextElementReferencePart } from './TextElementReferencePart.ts';
5
- import { TextElementStaticPart } from './TextElementStaticPart.ts';
6
-
7
- export type TextElementPartType = 'output' | 'reference' | 'static';
8
- export declare abstract class TextElementPart<Type extends TextElementPartType> extends DependentExpression<'string'> {
9
- readonly type: Type;
10
- readonly stringValue?: string;
11
- constructor(type: Type, context: AnyTextElementDefinition, expression: string);
12
- }
13
- export type AnyTextElementPart = TextElementOutputPart | TextElementReferencePart | TextElementStaticPart;