@getodk/xforms-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +44 -0
  2. package/dist/.vite/manifest.json +7 -0
  3. package/dist/XFormDOM.d.ts +31 -0
  4. package/dist/XFormDataType.d.ts +26 -0
  5. package/dist/XFormDefinition.d.ts +14 -0
  6. package/dist/body/BodyDefinition.d.ts +52 -0
  7. package/dist/body/BodyElementDefinition.d.ts +32 -0
  8. package/dist/body/RepeatDefinition.d.ts +15 -0
  9. package/dist/body/UnsupportedBodyElementDefinition.d.ts +10 -0
  10. package/dist/body/control/ControlDefinition.d.ts +16 -0
  11. package/dist/body/control/InputDefinition.d.ts +5 -0
  12. package/dist/body/control/select/ItemDefinition.d.ts +13 -0
  13. package/dist/body/control/select/ItemsetDefinition.d.ts +16 -0
  14. package/dist/body/control/select/ItemsetNodesetContext.d.ts +11 -0
  15. package/dist/body/control/select/ItemsetNodesetExpression.d.ts +5 -0
  16. package/dist/body/control/select/ItemsetValueExpression.d.ts +6 -0
  17. package/dist/body/control/select/SelectDefinition.d.ts +23 -0
  18. package/dist/body/group/BaseGroupDefinition.d.ts +46 -0
  19. package/dist/body/group/LogicalGroupDefinition.d.ts +6 -0
  20. package/dist/body/group/PresentationGroupDefinition.d.ts +11 -0
  21. package/dist/body/group/RepeatGroupDefinition.d.ts +12 -0
  22. package/dist/body/group/StructuralGroupDefinition.d.ts +6 -0
  23. package/dist/body/text/HintDefinition.d.ts +11 -0
  24. package/dist/body/text/LabelDefinition.d.ts +20 -0
  25. package/dist/body/text/TextElementDefinition.d.ts +32 -0
  26. package/dist/body/text/TextElementOutputPart.d.ts +12 -0
  27. package/dist/body/text/TextElementPart.d.ts +12 -0
  28. package/dist/body/text/TextElementReferencePart.d.ts +6 -0
  29. package/dist/body/text/TextElementStaticPart.d.ts +6 -0
  30. package/dist/client/BaseNode.d.ts +138 -0
  31. package/dist/client/EngineConfig.d.ts +78 -0
  32. package/dist/client/FormLanguage.d.ts +63 -0
  33. package/dist/client/GroupNode.d.ts +24 -0
  34. package/dist/client/OpaqueReactiveObjectFactory.d.ts +70 -0
  35. package/dist/client/RepeatInstanceNode.d.ts +28 -0
  36. package/dist/client/RepeatRangeNode.d.ts +94 -0
  37. package/dist/client/RootNode.d.ts +31 -0
  38. package/dist/client/SelectNode.d.ts +60 -0
  39. package/dist/client/StringNode.d.ts +41 -0
  40. package/dist/client/SubtreeNode.d.ts +52 -0
  41. package/dist/client/TextRange.d.ts +55 -0
  42. package/dist/client/hierarchy.d.ts +48 -0
  43. package/dist/client/index.d.ts +11 -0
  44. package/dist/client/node-types.d.ts +1 -0
  45. package/dist/expression/DependencyContext.d.ts +12 -0
  46. package/dist/expression/DependentExpression.d.ts +43 -0
  47. package/dist/index.d.ts +16 -0
  48. package/dist/index.js +37622 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/instance/Group.d.ts +31 -0
  51. package/dist/instance/RepeatInstance.d.ts +60 -0
  52. package/dist/instance/RepeatRange.d.ts +81 -0
  53. package/dist/instance/Root.d.ts +70 -0
  54. package/dist/instance/SelectField.d.ts +45 -0
  55. package/dist/instance/StringField.d.ts +39 -0
  56. package/dist/instance/Subtree.d.ts +30 -0
  57. package/dist/instance/abstract/DescendantNode.d.ts +76 -0
  58. package/dist/instance/abstract/InstanceNode.d.ts +107 -0
  59. package/dist/instance/children.d.ts +2 -0
  60. package/dist/instance/hierarchy.d.ts +12 -0
  61. package/dist/instance/identity.d.ts +7 -0
  62. package/dist/instance/index.d.ts +8 -0
  63. package/dist/instance/internal-api/EvaluationContext.d.ts +34 -0
  64. package/dist/instance/internal-api/InstanceConfig.d.ts +8 -0
  65. package/dist/instance/internal-api/SubscribableDependency.d.ts +59 -0
  66. package/dist/instance/internal-api/TranslationContext.d.ts +4 -0
  67. package/dist/instance/internal-api/ValueContext.d.ts +22 -0
  68. package/dist/instance/resource.d.ts +10 -0
  69. package/dist/instance/text/FormattedTextStub.d.ts +1 -0
  70. package/dist/instance/text/TextChunk.d.ts +11 -0
  71. package/dist/instance/text/TextRange.d.ts +10 -0
  72. package/dist/lib/dom/query.d.ts +20 -0
  73. package/dist/lib/reactivity/createChildrenState.d.ts +36 -0
  74. package/dist/lib/reactivity/createComputedExpression.d.ts +12 -0
  75. package/dist/lib/reactivity/createSelectItems.d.ts +16 -0
  76. package/dist/lib/reactivity/createValueState.d.ts +44 -0
  77. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +18 -0
  78. package/dist/lib/reactivity/node-state/createClientState.d.ts +9 -0
  79. package/dist/lib/reactivity/node-state/createCurrentState.d.ts +6 -0
  80. package/dist/lib/reactivity/node-state/createEngineState.d.ts +5 -0
  81. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +22 -0
  82. package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +6 -0
  83. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +139 -0
  84. package/dist/lib/reactivity/node-state/representations.d.ts +25 -0
  85. package/dist/lib/reactivity/scope.d.ts +23 -0
  86. package/dist/lib/reactivity/text/createFieldHint.d.ts +5 -0
  87. package/dist/lib/reactivity/text/createNodeLabel.d.ts +5 -0
  88. package/dist/lib/reactivity/text/createTextRange.d.ts +19 -0
  89. package/dist/lib/reactivity/types.d.ts +21 -0
  90. package/dist/lib/unique-id.d.ts +27 -0
  91. package/dist/lib/xpath/analysis.d.ts +22 -0
  92. package/dist/model/BindComputation.d.ts +30 -0
  93. package/dist/model/BindDefinition.d.ts +31 -0
  94. package/dist/model/BindElement.d.ts +6 -0
  95. package/dist/model/DescendentNodeDefinition.d.ts +25 -0
  96. package/dist/model/ModelBindMap.d.ts +15 -0
  97. package/dist/model/ModelDefinition.d.ts +10 -0
  98. package/dist/model/NodeDefinition.d.ts +74 -0
  99. package/dist/model/RepeatInstanceDefinition.d.ts +15 -0
  100. package/dist/model/RepeatSequenceDefinition.d.ts +19 -0
  101. package/dist/model/RepeatTemplateDefinition.d.ts +29 -0
  102. package/dist/model/RootDefinition.d.ts +24 -0
  103. package/dist/model/SubtreeDefinition.d.ts +14 -0
  104. package/dist/model/ValueNodeDefinition.d.ts +15 -0
  105. package/dist/solid.js +37273 -0
  106. package/dist/solid.js.map +1 -0
  107. package/package.json +87 -0
  108. package/src/XFormDOM.ts +224 -0
  109. package/src/XFormDataType.ts +64 -0
  110. package/src/XFormDefinition.ts +40 -0
  111. package/src/body/BodyDefinition.ts +202 -0
  112. package/src/body/BodyElementDefinition.ts +62 -0
  113. package/src/body/RepeatDefinition.ts +54 -0
  114. package/src/body/UnsupportedBodyElementDefinition.ts +17 -0
  115. package/src/body/control/ControlDefinition.ts +42 -0
  116. package/src/body/control/InputDefinition.ts +9 -0
  117. package/src/body/control/select/ItemDefinition.ts +31 -0
  118. package/src/body/control/select/ItemsetDefinition.ts +36 -0
  119. package/src/body/control/select/ItemsetNodesetContext.ts +26 -0
  120. package/src/body/control/select/ItemsetNodesetExpression.ts +8 -0
  121. package/src/body/control/select/ItemsetValueExpression.ts +11 -0
  122. package/src/body/control/select/SelectDefinition.ts +74 -0
  123. package/src/body/group/BaseGroupDefinition.ts +137 -0
  124. package/src/body/group/LogicalGroupDefinition.ts +11 -0
  125. package/src/body/group/PresentationGroupDefinition.ts +28 -0
  126. package/src/body/group/RepeatGroupDefinition.ts +91 -0
  127. package/src/body/group/StructuralGroupDefinition.ts +11 -0
  128. package/src/body/text/HintDefinition.ts +26 -0
  129. package/src/body/text/LabelDefinition.ts +54 -0
  130. package/src/body/text/TextElementDefinition.ts +97 -0
  131. package/src/body/text/TextElementOutputPart.ts +27 -0
  132. package/src/body/text/TextElementPart.ts +31 -0
  133. package/src/body/text/TextElementReferencePart.ts +21 -0
  134. package/src/body/text/TextElementStaticPart.ts +26 -0
  135. package/src/client/BaseNode.ts +180 -0
  136. package/src/client/EngineConfig.ts +83 -0
  137. package/src/client/FormLanguage.ts +77 -0
  138. package/src/client/GroupNode.ts +33 -0
  139. package/src/client/OpaqueReactiveObjectFactory.ts +100 -0
  140. package/src/client/README.md +39 -0
  141. package/src/client/RepeatInstanceNode.ts +41 -0
  142. package/src/client/RepeatRangeNode.ts +100 -0
  143. package/src/client/RootNode.ts +36 -0
  144. package/src/client/SelectNode.ts +69 -0
  145. package/src/client/StringNode.ts +46 -0
  146. package/src/client/SubtreeNode.ts +57 -0
  147. package/src/client/TextRange.ts +63 -0
  148. package/src/client/hierarchy.ts +63 -0
  149. package/src/client/index.ts +29 -0
  150. package/src/client/node-types.ts +10 -0
  151. package/src/expression/DependencyContext.ts +53 -0
  152. package/src/expression/DependentExpression.ts +102 -0
  153. package/src/index.ts +35 -0
  154. package/src/instance/Group.ts +82 -0
  155. package/src/instance/RepeatInstance.ts +164 -0
  156. package/src/instance/RepeatRange.ts +214 -0
  157. package/src/instance/Root.ts +264 -0
  158. package/src/instance/SelectField.ts +204 -0
  159. package/src/instance/StringField.ts +93 -0
  160. package/src/instance/Subtree.ts +79 -0
  161. package/src/instance/abstract/DescendantNode.ts +182 -0
  162. package/src/instance/abstract/InstanceNode.ts +257 -0
  163. package/src/instance/children.ts +52 -0
  164. package/src/instance/hierarchy.ts +54 -0
  165. package/src/instance/identity.ts +11 -0
  166. package/src/instance/index.ts +37 -0
  167. package/src/instance/internal-api/EvaluationContext.ts +41 -0
  168. package/src/instance/internal-api/InstanceConfig.ts +9 -0
  169. package/src/instance/internal-api/SubscribableDependency.ts +61 -0
  170. package/src/instance/internal-api/TranslationContext.ts +5 -0
  171. package/src/instance/internal-api/ValueContext.ts +27 -0
  172. package/src/instance/resource.ts +75 -0
  173. package/src/instance/text/FormattedTextStub.ts +8 -0
  174. package/src/instance/text/TextChunk.ts +20 -0
  175. package/src/instance/text/TextRange.ts +23 -0
  176. package/src/lib/dom/query.ts +49 -0
  177. package/src/lib/reactivity/createChildrenState.ts +60 -0
  178. package/src/lib/reactivity/createComputedExpression.ts +114 -0
  179. package/src/lib/reactivity/createSelectItems.ts +163 -0
  180. package/src/lib/reactivity/createValueState.ts +258 -0
  181. package/src/lib/reactivity/materializeCurrentStateChildren.ts +121 -0
  182. package/src/lib/reactivity/node-state/createClientState.ts +51 -0
  183. package/src/lib/reactivity/node-state/createCurrentState.ts +27 -0
  184. package/src/lib/reactivity/node-state/createEngineState.ts +18 -0
  185. package/src/lib/reactivity/node-state/createSharedNodeState.ts +79 -0
  186. package/src/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.ts +85 -0
  187. package/src/lib/reactivity/node-state/createSpecifiedState.ts +229 -0
  188. package/src/lib/reactivity/node-state/representations.ts +64 -0
  189. package/src/lib/reactivity/scope.ts +106 -0
  190. package/src/lib/reactivity/text/createFieldHint.ts +16 -0
  191. package/src/lib/reactivity/text/createNodeLabel.ts +16 -0
  192. package/src/lib/reactivity/text/createTextRange.ts +155 -0
  193. package/src/lib/reactivity/types.ts +27 -0
  194. package/src/lib/unique-id.ts +34 -0
  195. package/src/lib/xpath/analysis.ts +241 -0
  196. package/src/model/BindComputation.ts +88 -0
  197. package/src/model/BindDefinition.ts +104 -0
  198. package/src/model/BindElement.ts +8 -0
  199. package/src/model/DescendentNodeDefinition.ts +56 -0
  200. package/src/model/ModelBindMap.ts +71 -0
  201. package/src/model/ModelDefinition.ts +19 -0
  202. package/src/model/NodeDefinition.ts +146 -0
  203. package/src/model/RepeatInstanceDefinition.ts +39 -0
  204. package/src/model/RepeatSequenceDefinition.ts +53 -0
  205. package/src/model/RepeatTemplateDefinition.ts +150 -0
  206. package/src/model/RootDefinition.ts +121 -0
  207. package/src/model/SubtreeDefinition.ts +50 -0
  208. package/src/model/ValueNodeDefinition.ts +39 -0
@@ -0,0 +1,61 @@
1
+ import type { ReactiveScope } from '../../lib/reactivity/scope.ts';
2
+ import type { EvaluationContext } from './EvaluationContext.ts';
3
+
4
+ /**
5
+ * Provides a common interface to explicitly establish a reactive subscription
6
+ * to a form node, regardless of which node-specific details might trigger
7
+ * reactive updates. This is primarily intended for use in coordination with a
8
+ * node's {@link EvaluationContext} interface, so that computed expression
9
+ * evaluations can be re-executed when their dependencies update, without
10
+ * requiring a given evaluation to handle node-specific logic to determine
11
+ * when/whether updates have occurred.
12
+ *
13
+ * @example Given a form with these bindings:
14
+ *
15
+ * ```xml
16
+ * <bind nodeset="/data/field-1" />
17
+ * <bind nodeset="/data/field-2" type="int" />
18
+ * <bind nodeset="/data/group-a" relevant="/data/field-1 != ''" />
19
+ * <bind nodeset="/data/group-a/field-a-1" calculate="/data/field-2 * 2" />
20
+ * ```
21
+ *
22
+ * The field `/data/group-a/field-a-1`'s calculate has two dependencies:
23
+ *
24
+ * - `/data-field-2` (explicit)
25
+ * - `/data/group-a` (implicit: non-relevance is inherited)
26
+ *
27
+ * (This is an oversimplification, but it helps to illustrate the concept.)
28
+ *
29
+ * The first dependency's subscription is clearly concerned with that leaf
30
+ * node's value. In which case, that node's `subscribe` method should react to
31
+ * updates to its value (e.g. its `currentState.value` or the internal reactive
32
+ * representation thereof).
33
+ *
34
+ * Whereas the second dependency has no value (no parent nodes do); instead, the
35
+ * subscription would be concerned with that node's relevance. In which case,
36
+ * that node's `subscribe` method should react to updates to its relevance state
37
+ * (e.g. its `currentState.relevant` or the internal reactive represeentation
38
+ * thereof).
39
+ *
40
+ * @example Given a form with:
41
+ *
42
+ * ```xml
43
+ * <!-- model -->
44
+ * <bind nodeset="/data/repeat-a/field-a-1" calculate="position(..)" />
45
+ * <!-- body -->
46
+ * <repeat nodeset="/data/repeat-a" />
47
+ * ```
48
+ *
49
+ * The field `/data/repeat-a/field-a-1` has a dependency on its containing
50
+ * repeat instance. That node also doesn't have a value, and here the dynamic
51
+ * aspect of the dependency is the node's current position. Here the node's
52
+ * `subscribe` method should react to changes in its position (likely indirectly
53
+ * by reading its `contextReference`).
54
+ *
55
+ * @see {@link EvaluationContext}
56
+ * @see {@link EvaluationContext.contextReference}
57
+ */
58
+ export interface SubscribableDependency {
59
+ readonly scope: ReactiveScope;
60
+ readonly subscribe: VoidFunction;
61
+ }
@@ -0,0 +1,5 @@
1
+ import type { ActiveLanguage } from '../../index.ts';
2
+
3
+ export interface TranslationContext {
4
+ get activeLanguage(): ActiveLanguage;
5
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactiveScope } from '../../lib/reactivity/scope.ts';
2
+ import type { BindComputation } from '../../model/BindComputation.ts';
3
+ import type { EvaluationContext } from './EvaluationContext.ts';
4
+
5
+ export type InstanceValue = string;
6
+
7
+ interface ValueContextDefinitionBind {
8
+ readonly calculate: BindComputation<'calculate'> | null;
9
+ readonly readonly: BindComputation<'readonly'>;
10
+ }
11
+
12
+ export interface ValueContextDefinition {
13
+ readonly bind: ValueContextDefinitionBind;
14
+ readonly defaultValue: InstanceValue;
15
+ }
16
+
17
+ export interface ValueContext<RuntimeValue> extends EvaluationContext {
18
+ readonly scope: ReactiveScope;
19
+ readonly definition: ValueContextDefinition;
20
+ readonly contextNode: Element;
21
+
22
+ get isReadonly(): boolean;
23
+ get isRelevant(): boolean;
24
+
25
+ readonly encodeValue: (this: unknown, runtimeValue: RuntimeValue) => InstanceValue;
26
+ readonly decodeValue: (this: unknown, instanceValue: InstanceValue) => RuntimeValue;
27
+ }
@@ -0,0 +1,75 @@
1
+ import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
2
+ import { getBlobText } from '@getodk/common/lib/web-compat/blob.ts';
3
+ import type { FetchResource, FetchResourceResponse } from '../client/EngineConfig.ts';
4
+ import type { FormResource } from '../client/index.ts';
5
+
6
+ export type { FetchResource, FetchResourceResponse, FormResource };
7
+
8
+ export interface ResourceOptions {
9
+ readonly fetchResource: FetchResource;
10
+ }
11
+
12
+ const fetchTextFromURL = async (resource: URL, options: ResourceOptions): Promise<string> => {
13
+ const response = await options.fetchResource(resource);
14
+
15
+ return response.text();
16
+ };
17
+
18
+ const resourceXMLPrefix = '<';
19
+
20
+ type ResourceXMLPrefix = typeof resourceXMLPrefix;
21
+
22
+ type ResourceXML = `${ResourceXMLPrefix}${string}`;
23
+
24
+ class InvalidSourceXMLError extends Error {
25
+ constructor(readonly resourceText: string) {
26
+ super('Source text is not XML');
27
+ }
28
+ }
29
+
30
+ const isXML = (resourceText: string): resourceText is ResourceXML => {
31
+ return resourceText.startsWith(resourceXMLPrefix);
32
+ };
33
+
34
+ type AssertResourceTextIsXML = (resourceText: string) => asserts resourceText is ResourceXML;
35
+
36
+ const assertResourceTextIsXML: AssertResourceTextIsXML = (resourceText) => {
37
+ if (!isXML(resourceText)) {
38
+ throw new InvalidSourceXMLError(resourceText);
39
+ }
40
+ };
41
+
42
+ class InvalidFormResourceError extends Error {
43
+ constructor(readonly resource: FormResource) {
44
+ super('Invalid form resource');
45
+ }
46
+ }
47
+
48
+ export const retrieveSourceXMLResource = async (
49
+ resource: FormResource,
50
+ options: ResourceOptions
51
+ ): Promise<ResourceXML> => {
52
+ let text: string;
53
+
54
+ if (resource instanceof URL) {
55
+ text = await fetchTextFromURL(resource, options);
56
+ } else if (resource instanceof Blob) {
57
+ text = await getBlobText(resource);
58
+ } else if (typeof resource === 'string') {
59
+ const trimmed = resource.trim();
60
+
61
+ if (isXML(trimmed)) {
62
+ text = trimmed;
63
+ } else if (URL.canParse(trimmed)) {
64
+ text = await fetchTextFromURL(new URL(trimmed), options);
65
+ } else {
66
+ throw new InvalidFormResourceError(trimmed);
67
+ }
68
+ } else {
69
+ throw new UnreachableError(resource);
70
+ }
71
+
72
+ assertResourceTextIsXML(text);
73
+
74
+ return text;
75
+ };
@@ -0,0 +1,8 @@
1
+ export const FormattedTextStub = new Proxy({} as Record<PropertyKey, unknown>, {
2
+ get() {
3
+ throw new TypeError('Not implemented');
4
+ },
5
+ set() {
6
+ return false;
7
+ },
8
+ });
@@ -0,0 +1,20 @@
1
+ import type { ActiveLanguage } from '../../client/FormLanguage.ts';
2
+ import type { TextChunk as ClientTextChunk, TextChunkSource } from '../../client/TextRange.ts';
3
+ import type { TranslationContext } from '../internal-api/TranslationContext.ts';
4
+ import { FormattedTextStub } from './FormattedTextStub.ts';
5
+
6
+ export class TextChunk implements ClientTextChunk {
7
+ get formatted() {
8
+ return FormattedTextStub;
9
+ }
10
+
11
+ get language(): ActiveLanguage {
12
+ return this.context.activeLanguage;
13
+ }
14
+
15
+ constructor(
16
+ readonly context: TranslationContext,
17
+ readonly source: TextChunkSource,
18
+ readonly asString: string
19
+ ) {}
20
+ }
@@ -0,0 +1,23 @@
1
+ import type { TextRange as ClientTextRange, TextChunk } from '../../client/TextRange.ts';
2
+ import { FormattedTextStub } from './FormattedTextStub.ts';
3
+
4
+ export type TextRole = 'hint' | 'label';
5
+
6
+ export class TextRange<Role extends TextRole> implements ClientTextRange<Role> {
7
+ *[Symbol.iterator]() {
8
+ yield* this.chunks;
9
+ }
10
+
11
+ get formatted() {
12
+ return FormattedTextStub;
13
+ }
14
+
15
+ get asString(): string {
16
+ return this.chunks.map((chunk) => chunk.asString).join('');
17
+ }
18
+
19
+ constructor(
20
+ readonly role: Role,
21
+ protected readonly chunks: readonly TextChunk[]
22
+ ) {}
23
+ }
@@ -0,0 +1,49 @@
1
+ import { ScopedElementLookup } from '@getodk/common/lib/dom/compatibility.ts';
2
+ import type {
3
+ KnownAttributeLocalNamedElement,
4
+ LocalNamedElement,
5
+ } from '@getodk/common/types/dom.ts';
6
+ import type { SelectElement } from '../../body/control/select/SelectDefinition';
7
+
8
+ const hintLookup = new ScopedElementLookup(':scope > hint', 'hint');
9
+ const itemLookup = new ScopedElementLookup(':scope > item', 'item');
10
+ const itemsetLookup = new ScopedElementLookup(':scope > itemset[nodeset]', 'itemset[nodeset]');
11
+ const labelLookup = new ScopedElementLookup(':scope > label', 'label');
12
+ const repeatLookup = new ScopedElementLookup(':scope > repeat[nodeset]', 'repeat[nodeset]');
13
+ const valueLookup = new ScopedElementLookup(':scope > value', 'value');
14
+
15
+ export interface HintElement extends LocalNamedElement<'hint'> {}
16
+
17
+ export interface ItemElement extends LocalNamedElement<'item'> {}
18
+
19
+ export interface ItemsetElement extends KnownAttributeLocalNamedElement<'itemset', 'nodeset'> {}
20
+
21
+ export interface LabelElement extends LocalNamedElement<'label'> {}
22
+
23
+ export interface RepeatElement extends KnownAttributeLocalNamedElement<'repeat', 'nodeset'> {}
24
+
25
+ export interface ValueElement extends LocalNamedElement<'value'> {}
26
+
27
+ export const getHintElement = (parent: Element): HintElement | null => {
28
+ return hintLookup.getElement<HintElement>(parent);
29
+ };
30
+
31
+ export const getItemElements = (parent: SelectElement): readonly ItemElement[] => {
32
+ return Array.from(itemLookup.getElements<ItemElement>(parent));
33
+ };
34
+
35
+ export const getItemsetElement = (parent: Element): ItemsetElement | null => {
36
+ return itemsetLookup.getElement<ItemsetElement>(parent);
37
+ };
38
+
39
+ export const getLabelElement = (parent: Element): LabelElement | null => {
40
+ return labelLookup.getElement<LabelElement>(parent);
41
+ };
42
+
43
+ export const getRepeatElement = (parent: Element): RepeatElement | null => {
44
+ return repeatLookup.getElement<RepeatElement>(parent);
45
+ };
46
+
47
+ export const getValueElement = (parent: ItemElement | ItemsetElement): ValueElement | null => {
48
+ return valueLookup.getElement<ValueElement>(parent);
49
+ };
@@ -0,0 +1,60 @@
1
+ import { createMemo, createSignal, type Accessor, type Setter, type Signal } from 'solid-js';
2
+ import type { OpaqueReactiveObjectFactory } from '../../index.ts';
3
+ import type { AnyChildNode, AnyParentNode } from '../../instance/hierarchy.ts';
4
+ import type { NodeID } from '../../instance/identity.ts';
5
+ import type { materializeCurrentStateChildren } from './materializeCurrentStateChildren.ts';
6
+ import type { ClientState } from './node-state/createClientState.ts';
7
+ import type { CurrentState } from './node-state/createCurrentState.ts';
8
+ import type { EngineState } from './node-state/createEngineState.ts';
9
+
10
+ export interface ChildrenState<Child extends AnyChildNode> {
11
+ readonly children: Signal<readonly Child[]>;
12
+ readonly getChildren: Accessor<readonly Child[]>;
13
+ readonly setChildren: Setter<readonly Child[]>;
14
+ readonly childIds: Accessor<readonly NodeID[]>;
15
+ }
16
+
17
+ /**
18
+ * Creates a synchronized pair of:
19
+ *
20
+ * - Internal children state suitable for all parent node types
21
+ * - The same children state computed as an array of each child's {@link NodeID}
22
+ *
23
+ * This state is used, in tandem with {@link materializeCurrentStateChildren},
24
+ * to ensure children in **client-facing** state are not written into nested
25
+ * {@link OpaqueReactiveObjectFactory} calls.
26
+ *
27
+ * The produced {@link ChildrenState.children} (and its get/set convenience
28
+ * methods) signal is intended to be used to store the engine's children state,
29
+ * and update that state when appropriate (when appending children of any parent
30
+ * node during form initialization, and when appending repeat instances and
31
+ * their descendants subsequently during a form session).
32
+ *
33
+ * The produced {@link ChildrenState.childIds} memo is intended to be used to
34
+ * specify each parent node's `children` in an instance of {@link EngineState}.
35
+ * In so doing, the node's corresponding (internal, synchronized)
36
+ * {@link ClientState} will likewise store only the children's {@link NodeID}s.
37
+ *
38
+ * As a client reacts to changes in a given parent node's `children` state, that
39
+ * node's {@link CurrentState} should produce the child nodes corresponding to
40
+ * those {@link NodeID}s with the aforementioned
41
+ * {@link materializeCurrentStateChildren}.
42
+ */
43
+ export const createChildrenState = <Parent extends AnyParentNode, Child extends AnyChildNode>(
44
+ parent: Parent
45
+ ): ChildrenState<Child> => {
46
+ return parent.scope.runTask(() => {
47
+ const children = createSignal<readonly Child[]>([]);
48
+ const [getChildren, setChildren] = children;
49
+ const childIds = createMemo((): readonly NodeID[] => {
50
+ return getChildren().map((child) => child.nodeId);
51
+ });
52
+
53
+ return {
54
+ children,
55
+ getChildren,
56
+ setChildren,
57
+ childIds,
58
+ };
59
+ });
60
+ };
@@ -0,0 +1,114 @@
1
+ import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
2
+ import type { XFormsXPathEvaluator } from '@getodk/xpath';
3
+ import type { Accessor } from 'solid-js';
4
+ import { createMemo } from 'solid-js';
5
+ import type {
6
+ DependentExpression,
7
+ DependentExpressionResultType,
8
+ } from '../../expression/DependentExpression.ts';
9
+ import type { EvaluationContext } from '../../instance/internal-api/EvaluationContext.ts';
10
+ import type { SubscribableDependency } from '../../instance/internal-api/SubscribableDependency.ts';
11
+
12
+ interface ComputedExpressionResults {
13
+ readonly boolean: boolean;
14
+ readonly nodes: Node[];
15
+ readonly string: string;
16
+ }
17
+
18
+ // prettier-ignore
19
+ type EvaluatedExpression<
20
+ Type extends DependentExpressionResultType
21
+ > = ComputedExpressionResults[Type];
22
+
23
+ // prettier-ignore
24
+ type ExpressionEvaluator<
25
+ Type extends DependentExpressionResultType
26
+ > = () => EvaluatedExpression<Type>
27
+
28
+ const expressionEvaluator = <Type extends DependentExpressionResultType>(
29
+ evaluator: XFormsXPathEvaluator,
30
+ contextNode: Node,
31
+ type: Type,
32
+ expression: string
33
+ ): ExpressionEvaluator<Type> => {
34
+ const options = { contextNode };
35
+
36
+ switch (type) {
37
+ case 'boolean':
38
+ return (() => {
39
+ return evaluator.evaluateBoolean(expression, options);
40
+ }) as ExpressionEvaluator<Type>;
41
+
42
+ case 'nodes':
43
+ return (() => {
44
+ return evaluator.evaluateNodes(expression, options);
45
+ }) as ExpressionEvaluator<Type>;
46
+
47
+ case 'string':
48
+ return (() => {
49
+ return evaluator.evaluateString(expression, options);
50
+ }) as ExpressionEvaluator<Type>;
51
+
52
+ default:
53
+ throw new UnreachableError(type);
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Determines if an XPath expression will always produce the same value.
59
+ *
60
+ * @todo There are quite a few more cases than this, and it also likely belongs
61
+ * in another `lib` module.
62
+ */
63
+ const isConstantExpression = (expression: string): boolean => {
64
+ const normalized = expression.replaceAll(/\s/g, '');
65
+
66
+ return normalized === 'true()' || normalized === 'false()';
67
+ };
68
+
69
+ // prettier-ignore
70
+ type ComputedExpression<Type extends DependentExpressionResultType> = Accessor<
71
+ EvaluatedExpression<Type>
72
+ >;
73
+
74
+ export const createComputedExpression = <Type extends DependentExpressionResultType>(
75
+ context: EvaluationContext,
76
+ dependentExpression: DependentExpression<Type>
77
+ ): ComputedExpression<Type> => {
78
+ const { contextNode, evaluator, root, scope } = context;
79
+ const { expression, isTranslated, resultType } = dependentExpression;
80
+ const dependencyReferences = Array.from(dependentExpression.dependencyReferences);
81
+ const evaluateExpression = expressionEvaluator(evaluator, contextNode, resultType, expression);
82
+
83
+ return scope.runTask(() => {
84
+ if (isConstantExpression(expression)) {
85
+ return createMemo(evaluateExpression);
86
+ }
87
+
88
+ const getReferencedDependencies = createMemo(() => {
89
+ return dependencyReferences.flatMap((reference) => {
90
+ return context.getSubscribableDependencyByReference(reference) ?? [];
91
+ });
92
+ });
93
+
94
+ let getDependencies: Accessor<readonly SubscribableDependency[]>;
95
+
96
+ if (isTranslated) {
97
+ getDependencies = createMemo(() => {
98
+ return [root, ...getReferencedDependencies()];
99
+ });
100
+ } else {
101
+ getDependencies = getReferencedDependencies;
102
+ }
103
+
104
+ return createMemo(() => {
105
+ const dependencies = getDependencies();
106
+
107
+ dependencies.forEach((dependency) => {
108
+ dependency.subscribe();
109
+ });
110
+
111
+ return evaluateExpression();
112
+ });
113
+ });
114
+ };
@@ -0,0 +1,163 @@
1
+ import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
2
+ import type { XFormsXPathEvaluator } from '@getodk/xpath';
3
+ import type { Accessor } from 'solid-js';
4
+ import { createMemo } from 'solid-js';
5
+ import type { ItemDefinition } from '../../body/control/select/ItemDefinition.ts';
6
+ import type { ItemsetDefinition } from '../../body/control/select/ItemsetDefinition.ts';
7
+ import type { SelectItem } from '../../index.ts';
8
+ import type { SelectField } from '../../instance/SelectField.ts';
9
+ import type {
10
+ EvaluationContext,
11
+ EvaluationContextRoot,
12
+ } from '../../instance/internal-api/EvaluationContext.ts';
13
+ import type { SubscribableDependency } from '../../instance/internal-api/SubscribableDependency.ts';
14
+ import type { TextRange } from '../../instance/text/TextRange.ts';
15
+ import { createComputedExpression } from './createComputedExpression.ts';
16
+ import type { ReactiveScope } from './scope.ts';
17
+ import { createTextRange } from './text/createTextRange.ts';
18
+
19
+ const createSelectItemLabel = (
20
+ context: EvaluationContext,
21
+ definition: ItemDefinition
22
+ ): Accessor<TextRange<'label'>> => {
23
+ const { label, value } = definition;
24
+
25
+ return createTextRange(context, 'label', label, {
26
+ fallbackValue: value,
27
+ });
28
+ };
29
+
30
+ const createTranslatedStaticSelectItems = (
31
+ selectField: SelectField,
32
+ items: readonly ItemDefinition[]
33
+ ): Accessor<readonly SelectItem[]> => {
34
+ return selectField.scope.runTask(() => {
35
+ const labeledItems = items.map((item) => {
36
+ const { value } = item;
37
+ const label = createSelectItemLabel(selectField, item);
38
+
39
+ return () => ({
40
+ value,
41
+ label: label(),
42
+ });
43
+ });
44
+
45
+ return createMemo(() => {
46
+ return labeledItems.map((item) => item());
47
+ });
48
+ });
49
+ };
50
+
51
+ class ItemsetItemEvaluationContext implements EvaluationContext {
52
+ readonly scope: ReactiveScope;
53
+ readonly evaluator: XFormsXPathEvaluator;
54
+ readonly root: EvaluationContextRoot;
55
+
56
+ get contextReference(): string {
57
+ return this.selectField.contextReference;
58
+ }
59
+
60
+ constructor(
61
+ private readonly selectField: SelectField,
62
+ readonly contextNode: Node
63
+ ) {
64
+ this.scope = selectField.scope;
65
+ this.evaluator = selectField.evaluator;
66
+ this.root = selectField.root;
67
+ }
68
+
69
+ getSubscribableDependencyByReference(reference: string): SubscribableDependency | null {
70
+ return this.selectField.getSubscribableDependencyByReference(reference);
71
+ }
72
+ }
73
+
74
+ const createSelectItemsetItemLabel = (
75
+ context: EvaluationContext,
76
+ definition: ItemsetDefinition,
77
+ itemValue: Accessor<string>
78
+ ): Accessor<TextRange<'label'>> => {
79
+ const { label } = definition;
80
+
81
+ if (label == null) {
82
+ return createMemo(() => {
83
+ const value = itemValue();
84
+ const staticValueLabel = createTextRange(context, 'label', label, {
85
+ fallbackValue: value,
86
+ });
87
+
88
+ return staticValueLabel();
89
+ });
90
+ }
91
+
92
+ return createTextRange(context, 'label', label);
93
+ };
94
+
95
+ interface ItemsetItem {
96
+ label(): TextRange<'label'>;
97
+ value(): string;
98
+ }
99
+
100
+ const createItemsetItems = (
101
+ selectField: SelectField,
102
+ itemset: ItemsetDefinition
103
+ ): Accessor<readonly ItemsetItem[]> => {
104
+ return selectField.scope.runTask(() => {
105
+ const itemNodes = createComputedExpression(selectField, itemset.nodes);
106
+ const itemsCache = new UpsertableMap<Node, ItemsetItem>();
107
+
108
+ return createMemo(() => {
109
+ return itemNodes().map((itemNode) => {
110
+ return itemsCache.upsert(itemNode, () => {
111
+ const context = new ItemsetItemEvaluationContext(selectField, itemNode);
112
+ const value = createComputedExpression(context, itemset.value);
113
+ const label = createSelectItemsetItemLabel(context, itemset, value);
114
+
115
+ return {
116
+ label,
117
+ value,
118
+ };
119
+ });
120
+ });
121
+ });
122
+ });
123
+ };
124
+
125
+ const createItemset = (
126
+ selectField: SelectField,
127
+ itemset: ItemsetDefinition
128
+ ): Accessor<readonly SelectItem[]> => {
129
+ return selectField.scope.runTask(() => {
130
+ const itemsetItems = createItemsetItems(selectField, itemset);
131
+
132
+ return createMemo(() => {
133
+ return itemsetItems().map((item) => {
134
+ return {
135
+ label: item.label(),
136
+ value: item.value(),
137
+ };
138
+ });
139
+ });
140
+ });
141
+ };
142
+
143
+ /**
144
+ * Creates a reactive computation of a {@link SelectField}'s
145
+ * {@link SelectItem}s, in support of the field's `valueOptions`.
146
+ *
147
+ * - Selects defined with static `<item>`s will compute to an corresponding
148
+ * static list of items.
149
+ * - Selects defined with a computed `<itemset>` will compute to a reactive list
150
+ * of items.
151
+ * - Items of both will produce {@link SelectItem.label | labels} reactive to
152
+ * their appropriate dependencies (whether relative to the itemset item node,
153
+ * referencing a form's `itext` translations, etc).
154
+ */
155
+ export const createSelectItems = (selectField: SelectField): Accessor<readonly SelectItem[]> => {
156
+ const { items, itemset } = selectField.definition.bodyElement;
157
+
158
+ if (itemset == null) {
159
+ return createTranslatedStaticSelectItems(selectField, items);
160
+ }
161
+
162
+ return createItemset(selectField, itemset);
163
+ };