@getodk/xforms-engine 0.2.0 → 0.4.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.
- package/dist/client/BaseNode.d.ts +69 -4
- package/dist/client/EngineConfig.d.ts +0 -1
- package/dist/client/GroupNode.d.ts +4 -3
- package/dist/client/ModelValueNode.d.ts +36 -0
- package/dist/client/NodeAppearances.d.ts +1 -2
- package/dist/client/NoteNode.d.ts +52 -0
- package/dist/client/RootNode.d.ts +4 -3
- package/dist/client/SelectNode.d.ts +6 -5
- package/dist/client/StringNode.d.ts +6 -5
- package/dist/client/SubtreeNode.d.ts +3 -2
- package/dist/client/TextRange.d.ts +85 -3
- package/dist/client/TriggerNode.d.ts +25 -0
- package/dist/client/constants.d.ts +8 -0
- package/dist/client/hierarchy.d.ts +19 -10
- package/dist/client/index.d.ts +0 -1
- package/dist/client/node-types.d.ts +3 -1
- package/dist/client/{RepeatRangeNode.d.ts → repeat/BaseRepeatRangeNode.d.ts} +18 -18
- package/dist/client/{RepeatInstanceNode.d.ts → repeat/RepeatInstanceNode.d.ts} +9 -9
- package/dist/client/repeat/RepeatRangeControlledNode.d.ts +18 -0
- package/dist/client/repeat/RepeatRangeUncontrolledNode.d.ts +19 -0
- package/dist/client/unsupported/RangeNode.d.ts +9 -0
- package/dist/client/unsupported/RankNode.d.ts +9 -0
- package/dist/client/unsupported/UnsupportedControlNode.d.ts +32 -0
- package/dist/client/unsupported/UploadNode.d.ts +9 -0
- package/dist/client/validation.d.ts +162 -0
- package/dist/index.d.ts +14 -6
- package/dist/index.js +39696 -36151
- package/dist/index.js.map +1 -1
- package/dist/instance/Group.d.ts +3 -2
- package/dist/instance/ModelValue.d.ts +39 -0
- package/dist/instance/Note.d.ts +41 -0
- package/dist/instance/Root.d.ts +5 -4
- package/dist/instance/SelectField.d.ts +11 -6
- package/dist/instance/StringField.d.ts +12 -7
- package/dist/instance/Subtree.d.ts +2 -1
- package/dist/instance/TriggerControl.d.ts +40 -0
- package/dist/instance/abstract/DescendantNode.d.ts +8 -10
- package/dist/instance/abstract/InstanceNode.d.ts +3 -2
- package/dist/instance/abstract/UnsupportedControl.d.ts +46 -0
- package/dist/instance/children.d.ts +0 -1
- package/dist/instance/hierarchy.d.ts +15 -6
- package/dist/instance/index.d.ts +0 -1
- package/dist/instance/internal-api/EvaluationContext.d.ts +0 -1
- package/dist/instance/internal-api/InstanceConfig.d.ts +0 -1
- package/dist/instance/internal-api/SubscribableDependency.d.ts +0 -1
- package/dist/instance/internal-api/TranslationContext.d.ts +0 -1
- package/dist/instance/internal-api/ValidationContext.d.ts +28 -0
- package/dist/instance/internal-api/ValueContext.d.ts +3 -4
- package/dist/instance/{RepeatRange.d.ts → repeat/BaseRepeatRange.d.ts} +46 -46
- package/dist/instance/{RepeatInstance.d.ts → repeat/RepeatInstance.d.ts} +13 -13
- package/dist/instance/repeat/RepeatRangeControlled.d.ts +15 -0
- package/dist/instance/repeat/RepeatRangeUncontrolled.d.ts +34 -0
- package/dist/instance/resource.d.ts +0 -1
- package/dist/instance/text/TextChunk.d.ts +0 -1
- package/dist/instance/text/TextRange.d.ts +4 -5
- package/dist/instance/unsupported/RangeControl.d.ts +4 -0
- package/dist/instance/unsupported/RankControl.d.ts +4 -0
- package/dist/instance/unsupported/UploadControl.d.ts +4 -0
- package/dist/lib/TokenListParser.d.ts +3 -3
- package/dist/lib/dom/query.d.ts +1 -2
- package/dist/lib/reactivity/createChildrenState.d.ts +0 -1
- package/dist/lib/reactivity/createComputedExpression.d.ts +7 -3
- package/dist/lib/reactivity/createNoteReadonlyThunk.d.ts +4 -0
- package/dist/lib/reactivity/createSelectItems.d.ts +0 -1
- package/dist/lib/reactivity/createValueState.d.ts +0 -1
- package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +0 -1
- package/dist/lib/reactivity/node-state/createClientState.d.ts +0 -1
- package/dist/lib/reactivity/node-state/createCurrentState.d.ts +0 -1
- package/dist/lib/reactivity/node-state/createEngineState.d.ts +0 -1
- package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +1 -2
- package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +0 -1
- package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +1 -2
- package/dist/lib/reactivity/node-state/representations.d.ts +0 -1
- package/dist/lib/reactivity/scope.d.ts +0 -1
- package/dist/lib/reactivity/text/createFieldHint.d.ts +3 -4
- package/dist/lib/reactivity/text/createNodeLabel.d.ts +3 -4
- package/dist/lib/reactivity/text/createNoteText.d.ts +24 -0
- package/dist/lib/reactivity/text/createTextRange.d.ts +5 -8
- package/dist/lib/reactivity/types.d.ts +0 -1
- package/dist/lib/reactivity/validation/createAggregatedViolations.d.ts +8 -0
- package/dist/lib/reactivity/validation/createValidation.d.ts +17 -0
- package/dist/{XFormDOM.d.ts → parse/XFormDOM.d.ts} +1 -2
- package/dist/{XFormDataType.d.ts → parse/XFormDataType.d.ts} +2 -4
- package/dist/{XFormDefinition.d.ts → parse/XFormDefinition.d.ts} +2 -3
- package/dist/{body → parse/body}/BodyDefinition.d.ts +15 -10
- package/dist/{body → parse/body}/BodyElementDefinition.d.ts +8 -7
- package/dist/{body → parse/body}/RepeatElementDefinition.d.ts +4 -5
- package/dist/{body → parse/body}/UnsupportedBodyElementDefinition.d.ts +0 -1
- package/dist/{body → parse/body}/appearance/inputAppearanceParser.d.ts +1 -2
- package/dist/{body → parse/body}/appearance/selectAppearanceParser.d.ts +1 -2
- package/dist/{body → parse/body}/appearance/structureElementAppearanceParser.d.ts +1 -2
- package/dist/parse/body/appearance/unknownAppearanceParser.d.ts +3 -0
- package/dist/{body → parse/body}/control/ControlDefinition.d.ts +3 -4
- package/dist/{body → parse/body}/control/InputDefinition.d.ts +1 -2
- package/dist/parse/body/control/RangeControlDefinition.d.ts +11 -0
- package/dist/parse/body/control/RankControlDefinition.d.ts +11 -0
- package/dist/parse/body/control/TriggerControlDefinition.d.ts +11 -0
- package/dist/parse/body/control/UploadControlDefinition.d.ts +11 -0
- package/dist/{body → parse/body}/control/select/ItemDefinition.d.ts +3 -4
- package/dist/{body → parse/body}/control/select/ItemsetDefinition.d.ts +7 -7
- package/dist/{body → parse/body}/control/select/ItemsetNodesetContext.d.ts +2 -3
- package/dist/{body → parse/body}/control/select/SelectDefinition.d.ts +3 -12
- package/dist/{body → parse/body}/group/BaseGroupDefinition.d.ts +3 -5
- package/dist/{body → parse/body}/group/LogicalGroupDefinition.d.ts +0 -1
- package/dist/{body → parse/body}/group/PresentationGroupDefinition.d.ts +1 -2
- package/dist/{body → parse/body}/group/StructuralGroupDefinition.d.ts +0 -1
- package/dist/{model/BindComputation.d.ts → parse/expression/BindComputationExpression.d.ts} +5 -6
- package/dist/{body/control/select → parse/expression}/ItemsetNodesetExpression.d.ts +2 -3
- package/dist/{body/control/select → parse/expression}/ItemsetValueExpression.d.ts +2 -3
- package/dist/parse/expression/RepeatCountControlExpression.d.ts +18 -0
- package/dist/parse/expression/TextLiteralExpression.d.ts +9 -0
- package/dist/parse/expression/TextOutputExpression.d.ts +7 -0
- package/dist/parse/expression/TextReferenceExpression.d.ts +7 -0
- package/dist/parse/expression/TextTranslationExpression.d.ts +8 -0
- package/dist/{expression → parse/expression/abstract}/DependencyContext.d.ts +0 -1
- package/dist/{expression → parse/expression/abstract}/DependentExpression.d.ts +13 -10
- package/dist/parse/expression/abstract/TextChunkExpression.d.ts +17 -0
- package/dist/parse/model/BindDefinition.d.ts +39 -0
- package/dist/{model → parse/model}/BindElement.d.ts +1 -0
- package/dist/{model → parse/model}/DescendentNodeDefinition.d.ts +0 -1
- package/dist/{model/ValueNodeDefinition.d.ts → parse/model/LeafNodeDefinition.d.ts} +3 -4
- package/dist/{model → parse/model}/ModelBindMap.d.ts +0 -1
- package/dist/{model → parse/model}/ModelDefinition.d.ts +1 -2
- package/dist/{model → parse/model}/NodeDefinition.d.ts +8 -9
- package/dist/parse/model/NoteNodeDefinition.d.ts +30 -0
- package/dist/{model → parse/model}/RepeatInstanceDefinition.d.ts +3 -4
- package/dist/{model → parse/model}/RepeatRangeDefinition.d.ts +14 -5
- package/dist/{model → parse/model}/RepeatTemplateDefinition.d.ts +2 -3
- package/dist/{model → parse/model}/RootDefinition.d.ts +2 -3
- package/dist/{model → parse/model}/SubtreeDefinition.d.ts +1 -2
- package/dist/parse/text/HintDefinition.d.ts +8 -0
- package/dist/parse/text/ItemLabelDefinition.d.ts +8 -0
- package/dist/parse/text/ItemsetLabelDefinition.d.ts +12 -0
- package/dist/parse/text/LabelDefinition.d.ts +14 -0
- package/dist/parse/text/MessageDefinition.d.ts +14 -0
- package/dist/parse/text/abstract/TextElementDefinition.d.ts +22 -0
- package/dist/parse/text/abstract/TextRangeDefinition.d.ts +34 -0
- package/dist/parse/xpath/dependency-analysis.d.ts +40 -0
- package/dist/parse/xpath/path-resolution.d.ts +69 -0
- package/dist/parse/xpath/predicate-analysis.d.ts +29 -0
- package/dist/parse/xpath/reference-parsing.d.ts +17 -0
- package/dist/parse/xpath/semantic-analysis.d.ts +97 -0
- package/dist/parse/xpath/syntax-traversal.d.ts +68 -0
- package/dist/solid.js +8745 -5186
- package/dist/solid.js.map +1 -1
- package/package.json +15 -16
- package/src/client/BaseNode.ts +74 -8
- package/src/client/GroupNode.ts +4 -2
- package/src/client/ModelValueNode.ts +40 -0
- package/src/client/NodeAppearances.ts +1 -1
- package/src/client/NoteNode.ts +74 -0
- package/src/client/README.md +1 -0
- package/src/client/RootNode.ts +4 -2
- package/src/client/SelectNode.ts +6 -4
- package/src/client/StringNode.ts +6 -4
- package/src/client/SubtreeNode.ts +3 -1
- package/src/client/TextRange.ts +98 -2
- package/src/client/TriggerNode.ts +29 -0
- package/src/client/constants.ts +10 -0
- package/src/client/hierarchy.ts +43 -15
- package/src/client/node-types.ts +17 -2
- package/src/client/{RepeatRangeNode.ts → repeat/BaseRepeatRangeNode.ts} +18 -19
- package/src/client/{RepeatInstanceNode.ts → repeat/RepeatInstanceNode.ts} +10 -8
- package/src/client/repeat/RepeatRangeControlledNode.ts +20 -0
- package/src/client/repeat/RepeatRangeUncontrolledNode.ts +21 -0
- package/src/client/unsupported/RangeNode.ts +14 -0
- package/src/client/unsupported/RankNode.ts +14 -0
- package/src/client/unsupported/UnsupportedControlNode.ts +40 -0
- package/src/client/unsupported/UploadNode.ts +14 -0
- package/src/client/validation.ts +199 -0
- package/src/index.ts +21 -9
- package/src/instance/Group.ts +10 -4
- package/src/instance/ModelValue.ts +104 -0
- package/src/instance/Note.ts +142 -0
- package/src/instance/Root.ts +16 -6
- package/src/instance/SelectField.ts +29 -7
- package/src/instance/StringField.ts +36 -10
- package/src/instance/Subtree.ts +9 -3
- package/src/instance/TriggerControl.ts +134 -0
- package/src/instance/abstract/DescendantNode.ts +9 -10
- package/src/instance/abstract/InstanceNode.ts +29 -7
- package/src/instance/abstract/UnsupportedControl.ts +151 -0
- package/src/instance/children.ts +113 -16
- package/src/instance/hierarchy.ts +42 -5
- package/src/instance/index.ts +1 -1
- package/src/instance/internal-api/EvaluationContext.ts +1 -1
- package/src/instance/internal-api/ValidationContext.ts +32 -0
- package/src/instance/internal-api/ValueContext.ts +3 -3
- package/src/instance/{RepeatRange.ts → repeat/BaseRepeatRange.ts} +114 -99
- package/src/instance/{RepeatInstance.ts → repeat/RepeatInstance.ts} +27 -22
- package/src/instance/repeat/RepeatRangeControlled.ts +82 -0
- package/src/instance/repeat/RepeatRangeUncontrolled.ts +67 -0
- package/src/instance/text/TextRange.ts +10 -4
- package/src/instance/unsupported/RangeControl.ts +5 -0
- package/src/instance/unsupported/RankControl.ts +5 -0
- package/src/instance/unsupported/UploadControl.ts +5 -0
- package/src/lib/TokenListParser.ts +11 -7
- package/src/lib/dom/query.ts +1 -1
- package/src/lib/reactivity/createComputedExpression.ts +25 -27
- package/src/lib/reactivity/createNoteReadonlyThunk.ts +33 -0
- package/src/lib/reactivity/createSelectItems.ts +23 -16
- package/src/lib/reactivity/createValueState.ts +3 -3
- package/src/lib/reactivity/node-state/createSharedNodeState.ts +1 -1
- package/src/lib/reactivity/node-state/createSpecifiedState.ts +1 -1
- package/src/lib/reactivity/text/createFieldHint.ts +9 -7
- package/src/lib/reactivity/text/createNodeLabel.ts +8 -6
- package/src/lib/reactivity/text/createNoteText.ts +72 -0
- package/src/lib/reactivity/text/createTextRange.ts +17 -90
- package/src/lib/reactivity/validation/createAggregatedViolations.ts +75 -0
- package/src/lib/reactivity/validation/createValidation.ts +196 -0
- package/src/{XFormDataType.ts → parse/XFormDataType.ts} +1 -3
- package/src/{XFormDefinition.ts → parse/XFormDefinition.ts} +2 -2
- package/src/{body → parse/body}/BodyDefinition.ts +44 -27
- package/src/{body → parse/body}/BodyElementDefinition.ts +13 -6
- package/src/{body → parse/body}/RepeatElementDefinition.ts +10 -20
- package/src/{body → parse/body}/appearance/inputAppearanceParser.ts +1 -1
- package/src/{body → parse/body}/appearance/selectAppearanceParser.ts +1 -1
- package/src/{body → parse/body}/appearance/structureElementAppearanceParser.ts +1 -1
- package/src/parse/body/appearance/unknownAppearanceParser.ts +5 -0
- package/src/{body → parse/body}/control/ControlDefinition.ts +5 -4
- package/src/parse/body/control/RangeControlDefinition.ts +26 -0
- package/src/parse/body/control/RankControlDefinition.ts +27 -0
- package/src/parse/body/control/TriggerControlDefinition.ts +26 -0
- package/src/parse/body/control/UploadControlDefinition.ts +26 -0
- package/src/{body → parse/body}/control/select/ItemDefinition.ts +4 -4
- package/src/parse/body/control/select/ItemsetDefinition.ts +53 -0
- package/src/{body → parse/body}/control/select/ItemsetNodesetContext.ts +2 -2
- package/src/{body → parse/body}/control/select/SelectDefinition.ts +3 -11
- package/src/{body → parse/body}/group/BaseGroupDefinition.ts +11 -21
- package/src/{body → parse/body}/group/PresentationGroupDefinition.ts +1 -1
- package/src/{model/BindComputation.ts → parse/expression/BindComputationExpression.ts} +8 -12
- package/src/parse/expression/ItemsetNodesetExpression.ts +8 -0
- package/src/{body/control/select → parse/expression}/ItemsetValueExpression.ts +2 -2
- package/src/parse/expression/RepeatCountControlExpression.ts +44 -0
- package/src/parse/expression/TextLiteralExpression.ts +19 -0
- package/src/parse/expression/TextOutputExpression.ts +25 -0
- package/src/parse/expression/TextReferenceExpression.ts +14 -0
- package/src/parse/expression/TextTranslationExpression.ts +38 -0
- package/src/{expression → parse/expression/abstract}/DependentExpression.ts +46 -28
- package/src/parse/expression/abstract/TextChunkExpression.ts +38 -0
- package/src/{model → parse/model}/BindDefinition.ts +30 -27
- package/src/{model → parse/model}/BindElement.ts +1 -0
- package/src/{model/ValueNodeDefinition.ts → parse/model/LeafNodeDefinition.ts} +4 -4
- package/src/{model → parse/model}/ModelBindMap.ts +4 -0
- package/src/{model → parse/model}/NodeDefinition.ts +12 -12
- package/src/parse/model/NoteNodeDefinition.ts +70 -0
- package/src/{model → parse/model}/RepeatInstanceDefinition.ts +2 -2
- package/src/parse/model/RepeatRangeDefinition.ts +94 -0
- package/src/{model → parse/model}/RootDefinition.ts +8 -4
- package/src/parse/text/HintDefinition.ts +25 -0
- package/src/parse/text/ItemLabelDefinition.ts +25 -0
- package/src/parse/text/ItemsetLabelDefinition.ts +44 -0
- package/src/parse/text/LabelDefinition.ts +61 -0
- package/src/parse/text/MessageDefinition.ts +49 -0
- package/src/parse/text/abstract/TextElementDefinition.ts +71 -0
- package/src/parse/text/abstract/TextRangeDefinition.ts +70 -0
- package/src/parse/xpath/dependency-analysis.ts +105 -0
- package/src/parse/xpath/path-resolution.ts +475 -0
- package/src/parse/xpath/predicate-analysis.ts +61 -0
- package/src/parse/xpath/reference-parsing.ts +90 -0
- package/src/parse/xpath/semantic-analysis.ts +466 -0
- package/src/parse/xpath/syntax-traversal.ts +129 -0
- package/dist/body/text/HintDefinition.d.ts +0 -11
- package/dist/body/text/LabelDefinition.d.ts +0 -22
- package/dist/body/text/TextElementDefinition.d.ts +0 -33
- package/dist/body/text/TextElementOutputPart.d.ts +0 -13
- package/dist/body/text/TextElementPart.d.ts +0 -13
- package/dist/body/text/TextElementReferencePart.d.ts +0 -7
- package/dist/body/text/TextElementStaticPart.d.ts +0 -7
- package/dist/lib/xpath/analysis.d.ts +0 -23
- package/dist/model/BindDefinition.d.ts +0 -32
- package/src/body/control/select/ItemsetDefinition.ts +0 -36
- package/src/body/control/select/ItemsetNodesetExpression.ts +0 -8
- package/src/body/text/HintDefinition.ts +0 -26
- package/src/body/text/LabelDefinition.ts +0 -68
- package/src/body/text/TextElementDefinition.ts +0 -97
- package/src/body/text/TextElementOutputPart.ts +0 -27
- package/src/body/text/TextElementPart.ts +0 -31
- package/src/body/text/TextElementReferencePart.ts +0 -21
- package/src/body/text/TextElementStaticPart.ts +0 -26
- package/src/lib/xpath/analysis.ts +0 -241
- package/src/model/RepeatRangeDefinition.ts +0 -53
- package/src/{XFormDOM.ts → parse/XFormDOM.ts} +0 -0
- package/src/{body → parse/body}/UnsupportedBodyElementDefinition.ts +0 -0
- package/src/{body → parse/body}/control/InputDefinition.ts +1 -1
- /package/src/{body → parse/body}/group/LogicalGroupDefinition.ts +0 -0
- /package/src/{body → parse/body}/group/StructuralGroupDefinition.ts +0 -0
- /package/src/{expression → parse/expression/abstract}/DependencyContext.ts +0 -0
- /package/src/{model → parse/model}/DescendentNodeDefinition.ts +0 -0
- /package/src/{model → parse/model}/ModelDefinition.ts +0 -0
- /package/src/{model → parse/model}/RepeatTemplateDefinition.ts +0 -0
- /package/src/{model → parse/model}/SubtreeDefinition.ts +0 -0
|
@@ -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
|
+
};
|
|
@@ -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
|
-
}
|