@getodk/xforms-engine 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/dist/body/BodyDefinition.d.ts +24 -7
  2. package/dist/body/BodyElementDefinition.d.ts +4 -3
  3. package/dist/body/RepeatElementDefinition.d.ts +19 -0
  4. package/dist/body/appearance/inputAppearanceParser.d.ts +4 -0
  5. package/dist/body/appearance/selectAppearanceParser.d.ts +4 -0
  6. package/dist/body/appearance/structureElementAppearanceParser.d.ts +4 -0
  7. package/dist/body/control/ControlDefinition.d.ts +4 -2
  8. package/dist/body/control/InputDefinition.d.ts +5 -0
  9. package/dist/body/control/select/ItemDefinition.d.ts +2 -2
  10. package/dist/body/control/select/ItemsetDefinition.d.ts +5 -4
  11. package/dist/body/control/select/SelectDefinition.d.ts +11 -1
  12. package/dist/body/group/BaseGroupDefinition.d.ts +4 -9
  13. package/dist/body/group/PresentationGroupDefinition.d.ts +1 -1
  14. package/dist/client/BaseNode.d.ts +74 -3
  15. package/dist/client/GroupNode.d.ts +7 -2
  16. package/dist/client/ModelValueNode.d.ts +37 -0
  17. package/dist/client/NodeAppearances.d.ts +15 -0
  18. package/dist/client/NoteNode.d.ts +53 -0
  19. package/dist/client/RootNode.d.ts +21 -0
  20. package/dist/client/SelectNode.d.ts +8 -3
  21. package/dist/client/StringNode.d.ts +8 -3
  22. package/dist/client/SubtreeNode.d.ts +3 -0
  23. package/dist/client/TextRange.d.ts +85 -2
  24. package/dist/client/constants.d.ts +9 -0
  25. package/dist/client/hierarchy.d.ts +14 -9
  26. package/dist/client/node-types.d.ts +2 -1
  27. package/dist/client/{RepeatRangeNode.d.ts → repeat/BaseRepeatRangeNode.d.ts} +19 -15
  28. package/dist/client/{RepeatInstanceNode.d.ts → repeat/RepeatInstanceNode.d.ts} +11 -7
  29. package/dist/client/repeat/RepeatRangeControlledNode.d.ts +19 -0
  30. package/dist/client/repeat/RepeatRangeUncontrolledNode.d.ts +20 -0
  31. package/dist/client/validation.d.ts +163 -0
  32. package/dist/expression/DependentExpression.d.ts +12 -8
  33. package/dist/index.d.ts +9 -4
  34. package/dist/index.js +3173 -960
  35. package/dist/index.js.map +1 -1
  36. package/dist/instance/Group.d.ts +6 -4
  37. package/dist/instance/ModelValue.d.ts +40 -0
  38. package/dist/instance/Note.d.ts +42 -0
  39. package/dist/instance/Root.d.ts +10 -23
  40. package/dist/instance/SelectField.d.ts +12 -6
  41. package/dist/instance/StringField.d.ts +13 -7
  42. package/dist/instance/Subtree.d.ts +3 -1
  43. package/dist/instance/abstract/DescendantNode.d.ts +16 -9
  44. package/dist/instance/abstract/InstanceNode.d.ts +28 -29
  45. package/dist/instance/hierarchy.d.ts +10 -5
  46. package/dist/instance/internal-api/EvaluationContext.d.ts +5 -4
  47. package/dist/instance/internal-api/ValidationContext.d.ts +21 -0
  48. package/dist/instance/internal-api/ValueContext.d.ts +2 -2
  49. package/dist/instance/repeat/BaseRepeatRange.d.ts +160 -0
  50. package/dist/instance/{RepeatInstance.d.ts → repeat/RepeatInstance.d.ts} +38 -13
  51. package/dist/instance/repeat/RepeatRangeControlled.d.ts +16 -0
  52. package/dist/instance/repeat/RepeatRangeUncontrolled.d.ts +35 -0
  53. package/dist/instance/text/TextRange.d.ts +4 -4
  54. package/dist/lib/TokenListParser.d.ts +84 -0
  55. package/dist/lib/dom/query.d.ts +5 -0
  56. package/dist/lib/reactivity/createComputedExpression.d.ts +6 -1
  57. package/dist/lib/reactivity/createNoteReadonlyThunk.d.ts +5 -0
  58. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +2 -1
  59. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +1 -1
  60. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +1 -1
  61. package/dist/lib/reactivity/text/createFieldHint.d.ts +3 -3
  62. package/dist/lib/reactivity/text/createNodeLabel.d.ts +2 -2
  63. package/dist/lib/reactivity/text/createNoteText.d.ts +25 -0
  64. package/dist/lib/reactivity/text/createTextRange.d.ts +5 -7
  65. package/dist/lib/reactivity/validation/createAggregatedViolations.d.ts +9 -0
  66. package/dist/lib/reactivity/validation/createValidation.d.ts +18 -0
  67. package/dist/model/BindDefinition.d.ts +4 -2
  68. package/dist/model/BindElement.d.ts +1 -0
  69. package/dist/model/DescendentNodeDefinition.d.ts +1 -2
  70. package/dist/model/{ValueNodeDefinition.d.ts → LeafNodeDefinition.d.ts} +3 -4
  71. package/dist/model/NodeDefinition.d.ts +16 -16
  72. package/dist/model/RepeatInstanceDefinition.d.ts +5 -6
  73. package/dist/model/RepeatRangeDefinition.d.ts +30 -0
  74. package/dist/model/RepeatTemplateDefinition.d.ts +6 -7
  75. package/dist/model/RootDefinition.d.ts +3 -1
  76. package/dist/model/SubtreeDefinition.d.ts +2 -2
  77. package/dist/parse/NoteNodeDefinition.d.ts +31 -0
  78. package/dist/parse/expression/RepeatCountControlExpression.d.ts +19 -0
  79. package/dist/parse/text/HintDefinition.d.ts +9 -0
  80. package/dist/parse/text/ItemLabelDefinition.d.ts +9 -0
  81. package/dist/parse/text/ItemsetLabelDefinition.d.ts +13 -0
  82. package/dist/parse/text/LabelDefinition.d.ts +15 -0
  83. package/dist/parse/text/MessageDefinition.d.ts +15 -0
  84. package/dist/parse/text/OutputChunkDefinition.d.ts +8 -0
  85. package/dist/parse/text/ReferenceChunkDefinition.d.ts +8 -0
  86. package/dist/parse/text/StaticTextChunkDefinition.d.ts +10 -0
  87. package/dist/parse/text/TranslationChunkDefinition.d.ts +9 -0
  88. package/dist/parse/text/abstract/TextChunkDefinition.d.ts +18 -0
  89. package/dist/parse/text/abstract/TextElementDefinition.d.ts +23 -0
  90. package/dist/parse/text/abstract/TextRangeDefinition.d.ts +35 -0
  91. package/dist/parse/xpath/dependency-analysis.d.ts +40 -0
  92. package/dist/parse/xpath/path-resolution.d.ts +70 -0
  93. package/dist/parse/xpath/predicate-analysis.d.ts +30 -0
  94. package/dist/parse/xpath/reference-parsing.d.ts +18 -0
  95. package/dist/parse/xpath/semantic-analysis.d.ts +98 -0
  96. package/dist/parse/xpath/syntax-traversal.d.ts +69 -0
  97. package/dist/solid.js +3174 -961
  98. package/dist/solid.js.map +1 -1
  99. package/package.json +14 -15
  100. package/src/XFormDOM.ts +81 -8
  101. package/src/body/BodyDefinition.ts +38 -23
  102. package/src/body/BodyElementDefinition.ts +4 -3
  103. package/src/body/RepeatElementDefinition.ts +58 -0
  104. package/src/body/appearance/inputAppearanceParser.ts +39 -0
  105. package/src/body/appearance/selectAppearanceParser.ts +38 -0
  106. package/src/body/appearance/structureElementAppearanceParser.ts +7 -0
  107. package/src/body/control/ControlDefinition.ts +8 -3
  108. package/src/body/control/InputDefinition.ts +13 -0
  109. package/src/body/control/select/ItemDefinition.ts +3 -3
  110. package/src/body/control/select/ItemsetDefinition.ts +29 -12
  111. package/src/body/control/select/ItemsetNodesetExpression.ts +1 -1
  112. package/src/body/control/select/SelectDefinition.ts +14 -5
  113. package/src/body/group/BaseGroupDefinition.ts +14 -51
  114. package/src/body/group/PresentationGroupDefinition.ts +1 -1
  115. package/src/client/BaseNode.ts +82 -8
  116. package/src/client/GroupNode.ts +8 -2
  117. package/src/client/ModelValueNode.ts +40 -0
  118. package/src/client/NodeAppearances.ts +22 -0
  119. package/src/client/NoteNode.ts +74 -0
  120. package/src/client/README.md +1 -0
  121. package/src/client/RootNode.ts +24 -0
  122. package/src/client/SelectNode.ts +9 -3
  123. package/src/client/StringNode.ts +9 -3
  124. package/src/client/SubtreeNode.ts +3 -0
  125. package/src/client/TextRange.ts +99 -2
  126. package/src/client/constants.ts +10 -0
  127. package/src/client/hierarchy.ts +30 -14
  128. package/src/client/node-types.ts +8 -1
  129. package/src/client/{RepeatRangeNode.ts → repeat/BaseRepeatRangeNode.ts} +20 -17
  130. package/src/client/{RepeatInstanceNode.ts → repeat/RepeatInstanceNode.ts} +13 -7
  131. package/src/client/repeat/RepeatRangeControlledNode.ts +20 -0
  132. package/src/client/repeat/RepeatRangeUncontrolledNode.ts +21 -0
  133. package/src/client/validation.ts +199 -0
  134. package/src/expression/DependentExpression.ts +45 -27
  135. package/src/index.ts +15 -8
  136. package/src/instance/Group.ts +24 -13
  137. package/src/instance/ModelValue.ts +104 -0
  138. package/src/instance/Note.ts +142 -0
  139. package/src/instance/Root.ts +29 -67
  140. package/src/instance/SelectField.ts +35 -13
  141. package/src/instance/StringField.ts +40 -13
  142. package/src/instance/Subtree.ts +19 -10
  143. package/src/instance/abstract/DescendantNode.ts +50 -49
  144. package/src/instance/abstract/InstanceNode.ts +89 -92
  145. package/src/instance/children.ts +47 -10
  146. package/src/instance/hierarchy.ts +21 -2
  147. package/src/instance/index.ts +1 -1
  148. package/src/instance/internal-api/EvaluationContext.ts +5 -6
  149. package/src/instance/internal-api/ValidationContext.ts +23 -0
  150. package/src/instance/internal-api/ValueContext.ts +2 -2
  151. package/src/instance/repeat/BaseRepeatRange.ts +347 -0
  152. package/src/instance/{RepeatInstance.ts → repeat/RepeatInstance.ts} +85 -36
  153. package/src/instance/repeat/RepeatRangeControlled.ts +82 -0
  154. package/src/instance/repeat/RepeatRangeUncontrolled.ts +67 -0
  155. package/src/instance/text/TextRange.ts +10 -4
  156. package/src/lib/TokenListParser.ts +156 -0
  157. package/src/lib/dom/query.ts +13 -0
  158. package/src/lib/reactivity/createChildrenState.ts +51 -6
  159. package/src/lib/reactivity/createComputedExpression.ts +23 -25
  160. package/src/lib/reactivity/createNoteReadonlyThunk.ts +33 -0
  161. package/src/lib/reactivity/createSelectItems.ts +25 -20
  162. package/src/lib/reactivity/createValueState.ts +6 -6
  163. package/src/lib/reactivity/materializeCurrentStateChildren.ts +3 -1
  164. package/src/lib/reactivity/node-state/createSharedNodeState.ts +1 -1
  165. package/src/lib/reactivity/text/createFieldHint.ts +9 -7
  166. package/src/lib/reactivity/text/createNodeLabel.ts +7 -5
  167. package/src/lib/reactivity/text/createNoteText.ts +72 -0
  168. package/src/lib/reactivity/text/createTextRange.ts +17 -90
  169. package/src/lib/reactivity/validation/createAggregatedViolations.ts +70 -0
  170. package/src/lib/reactivity/validation/createValidation.ts +196 -0
  171. package/src/model/BindComputation.ts +0 -4
  172. package/src/model/BindDefinition.ts +8 -6
  173. package/src/model/BindElement.ts +1 -0
  174. package/src/model/DescendentNodeDefinition.ts +1 -2
  175. package/src/model/{ValueNodeDefinition.ts → LeafNodeDefinition.ts} +5 -6
  176. package/src/model/ModelBindMap.ts +4 -0
  177. package/src/model/ModelDefinition.ts +1 -1
  178. package/src/model/NodeDefinition.ts +21 -21
  179. package/src/model/RepeatInstanceDefinition.ts +8 -13
  180. package/src/model/RepeatRangeDefinition.ts +94 -0
  181. package/src/model/RepeatTemplateDefinition.ts +10 -15
  182. package/src/model/RootDefinition.ts +12 -14
  183. package/src/model/SubtreeDefinition.ts +3 -3
  184. package/src/parse/NoteNodeDefinition.ts +70 -0
  185. package/src/parse/TODO.md +3 -0
  186. package/src/parse/expression/RepeatCountControlExpression.ts +44 -0
  187. package/src/parse/text/HintDefinition.ts +25 -0
  188. package/src/parse/text/ItemLabelDefinition.ts +25 -0
  189. package/src/parse/text/ItemsetLabelDefinition.ts +44 -0
  190. package/src/parse/text/LabelDefinition.ts +61 -0
  191. package/src/parse/text/MessageDefinition.ts +49 -0
  192. package/src/parse/text/OutputChunkDefinition.ts +25 -0
  193. package/src/parse/text/ReferenceChunkDefinition.ts +14 -0
  194. package/src/parse/text/StaticTextChunkDefinition.ts +19 -0
  195. package/src/parse/text/TranslationChunkDefinition.ts +38 -0
  196. package/src/parse/text/abstract/TextChunkDefinition.ts +38 -0
  197. package/src/parse/text/abstract/TextElementDefinition.ts +71 -0
  198. package/src/parse/text/abstract/TextRangeDefinition.ts +70 -0
  199. package/src/parse/xpath/dependency-analysis.ts +105 -0
  200. package/src/parse/xpath/path-resolution.ts +475 -0
  201. package/src/parse/xpath/predicate-analysis.ts +61 -0
  202. package/src/parse/xpath/reference-parsing.ts +90 -0
  203. package/src/parse/xpath/semantic-analysis.ts +466 -0
  204. package/src/parse/xpath/syntax-traversal.ts +129 -0
  205. package/dist/body/RepeatDefinition.d.ts +0 -16
  206. package/dist/body/group/RepeatGroupDefinition.d.ts +0 -13
  207. package/dist/body/text/HintDefinition.d.ts +0 -11
  208. package/dist/body/text/LabelDefinition.d.ts +0 -20
  209. package/dist/body/text/TextElementDefinition.d.ts +0 -33
  210. package/dist/body/text/TextElementOutputPart.d.ts +0 -13
  211. package/dist/body/text/TextElementPart.d.ts +0 -13
  212. package/dist/body/text/TextElementReferencePart.d.ts +0 -7
  213. package/dist/body/text/TextElementStaticPart.d.ts +0 -7
  214. package/dist/instance/RepeatRange.d.ts +0 -80
  215. package/dist/lib/xpath/analysis.d.ts +0 -23
  216. package/dist/model/RepeatSequenceDefinition.d.ts +0 -20
  217. package/src/body/RepeatDefinition.ts +0 -54
  218. package/src/body/group/RepeatGroupDefinition.ts +0 -91
  219. package/src/body/text/HintDefinition.ts +0 -26
  220. package/src/body/text/LabelDefinition.ts +0 -54
  221. package/src/body/text/TextElementDefinition.ts +0 -97
  222. package/src/body/text/TextElementOutputPart.ts +0 -27
  223. package/src/body/text/TextElementPart.ts +0 -31
  224. package/src/body/text/TextElementReferencePart.ts +0 -21
  225. package/src/body/text/TextElementStaticPart.ts +0 -26
  226. package/src/instance/RepeatRange.ts +0 -214
  227. package/src/lib/xpath/analysis.ts +0 -241
  228. package/src/model/RepeatSequenceDefinition.ts +0 -53
@@ -1,9 +1,14 @@
1
- import type { TextRange as ClientTextRange, TextChunk } from '../../client/TextRange.ts';
1
+ import type {
2
+ TextRange as ClientTextRange,
3
+ TextChunk,
4
+ TextOrigin,
5
+ TextRole,
6
+ } from '../../client/TextRange.ts';
2
7
  import { FormattedTextStub } from './FormattedTextStub.ts';
3
8
 
4
- export type TextRole = 'hint' | 'label';
5
-
6
- export class TextRange<Role extends TextRole> implements ClientTextRange<Role> {
9
+ export class TextRange<Role extends TextRole, Origin extends TextOrigin>
10
+ implements ClientTextRange<Role, Origin>
11
+ {
7
12
  *[Symbol.iterator]() {
8
13
  yield* this.chunks;
9
14
  }
@@ -17,6 +22,7 @@ export class TextRange<Role extends TextRole> implements ClientTextRange<Role> {
17
22
  }
18
23
 
19
24
  constructor(
25
+ readonly origin: Origin,
20
26
  readonly role: Role,
21
27
  protected readonly chunks: readonly TextChunk[]
22
28
  ) {}
@@ -0,0 +1,156 @@
1
+ import { xmlXPathWhitespaceSeparatedList } from '@getodk/common/lib/string/whitespace.ts';
2
+ import type { PartiallyKnownString } from '@getodk/common/types/string/PartiallyKnownString.ts';
3
+
4
+ type SymbolIterator = typeof Symbol.iterator;
5
+
6
+ type TokenListKey<CanonicalToken extends string> =
7
+ | PartiallyKnownString<CanonicalToken>
8
+ | SymbolIterator;
9
+
10
+ type TokenListIterator<CanonicalToken extends string> = IterableIterator<
11
+ PartiallyKnownString<CanonicalToken>
12
+ >;
13
+
14
+ /**
15
+ * @see {@link TokenListParser}
16
+ */
17
+ // prettier-ignore
18
+ export type TokenList<CanonicalToken extends string = string> = {
19
+ readonly [Key in TokenListKey<CanonicalToken>]:
20
+ Key extends SymbolIterator
21
+ ? () => TokenListIterator<CanonicalToken>
22
+ : boolean;
23
+ };
24
+
25
+ interface TokenListAlias<CanonicalToken extends string> {
26
+ readonly fromAlias: string;
27
+ readonly toCanonical: CanonicalToken;
28
+ }
29
+
30
+ type TokenAliases<CanonicalToken extends string> = ReadonlyArray<TokenListAlias<CanonicalToken>>;
31
+
32
+ interface TokenListParserOptions<CanonicalToken extends string> {
33
+ readonly aliases?: TokenAliases<CanonicalToken>;
34
+ }
35
+
36
+ type TokenListAttributeName = PartiallyKnownString<'appearance' | 'class'>;
37
+
38
+ /**
39
+ * Intended primarily for use in parsing these features:
40
+ *
41
+ * - {@link https://getodk.github.io/xforms-spec/#appearances | appearances} ({@link https://xlsform.org/en/#appearance | additional documentation})
42
+ *
43
+ * - {@link https://getodk.github.io/xforms-spec/#body-attributes | body `class` attribute}
44
+ *
45
+ * This class is named as a reference to {@link DOMTokenList}
46
+ * ({@link https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList | MDN}),
47
+ * with these similarities:
48
+ *
49
+ * - Represents a value whose serialization is a space-separated list.
50
+ * - Each member ("token") is a string (without whitespace).
51
+ * - Provides set-like semantics to determine presence of members.
52
+ * - Provides ordering semantics as determined by the serialization.
53
+ *
54
+ * This class differs from that prior art in that:
55
+ *
56
+ * - It provides a notion of "canonical" members. This is _mostly_ (but not
57
+ * entirely) intended as a convenience to client developers, automatically
58
+ * populating canonical/known tokens for a given parser type in
59
+ * editor-provided autocomplete, etc. **Importantly**, non-canonical tokens
60
+ * _are not ignored_ in either the {@link TokenList}'s types or runtime
61
+ * values.
62
+ *
63
+ * - Provided "canonical" members may also be used to specify
64
+ * {@link TokenListParserOptions.aliases | optional aliases}. (Example: when
65
+ * parsing "appearances", an alias might map an older deprecated appearance to
66
+ * a newer canonical equivalent.) **Importantly**, when a token matches an
67
+ * alias, both that alias _and the token as-specified_ will be present in the
68
+ * produced {@link TokenList}.
69
+ *
70
+ * - As a parser, it is intended to be read-only. The serialized format which it
71
+ * parses is _generally_ the source of truth (excepting e.g. aliases).
72
+ * Notably, and as mentioned above, ordering is determined by:
73
+ *
74
+ * - Iterating each member, as provided by the serialized representation
75
+ *
76
+ * - If that member corresponds to an alias, that alias is yielded first
77
+ *
78
+ * - Regardless of whether the member corresponds to an alias, the member is
79
+ * yielded as-specified
80
+ *
81
+ * - A parsed {@link TokenList} is intended to maximize convenience of read-only
82
+ * access. Despite many _conceptual similarities_, most of the
83
+ * {@link DOMTokenList} **interface** is eschewed in favor of two (mutually
84
+ * equivalent) access mechanisms:
85
+ *
86
+ * - `Iterable<Token>`, with the ordering semantics described above
87
+ * - `Record<Token, boolean>`
88
+ *
89
+ * \* This may change, as we refine requirements. In the future, we may
90
+ * introduce a notion of mutually exclusive tokens (e.g. "appearances" which
91
+ * cannot be used together), which may in turn utilize instance-defined ordering
92
+ * as part of that mechanism.
93
+ */
94
+ export class TokenListParser<
95
+ CanonicalToken extends string,
96
+ // Note: this is a separate type parameter so that specifying an alias does
97
+ // not cause it to be mistakenly inferred as a `CanonicalToken` which was
98
+ // not otherwise specified.
99
+ TokenAlias extends CanonicalToken = CanonicalToken,
100
+ > {
101
+ private readonly aliases: ReadonlyMap<string, CanonicalToken>;
102
+
103
+ constructor(
104
+ readonly canonicalTokens: readonly CanonicalToken[],
105
+ options?: TokenListParserOptions<TokenAlias>
106
+ ) {
107
+ this.aliases = new Map(
108
+ (options?.aliases ?? []).map(({ fromAlias, toCanonical }) => {
109
+ return [fromAlias, toCanonical];
110
+ })
111
+ );
112
+ }
113
+
114
+ parseFrom(element: Element, attributeName: TokenListAttributeName): TokenList<CanonicalToken> {
115
+ const serialized = element.getAttribute(attributeName) ?? '';
116
+ const specified = xmlXPathWhitespaceSeparatedList(serialized, {
117
+ ignoreEmpty: true,
118
+ });
119
+ const { aliases } = this;
120
+ const resolved = specified.flatMap((token) => {
121
+ const alias = aliases.get(token);
122
+
123
+ if (alias == null) {
124
+ return token;
125
+ }
126
+
127
+ return [alias, token];
128
+ });
129
+ const tokens = new Set(resolved);
130
+
131
+ return new Proxy(
132
+ {
133
+ [Symbol.iterator]() {
134
+ return resolved.values();
135
+ },
136
+ } satisfies TokenList<string> as TokenList<CanonicalToken>,
137
+ {
138
+ get(target, property, receiver) {
139
+ if (typeof property === 'symbol') {
140
+ return Reflect.get(target, property, receiver);
141
+ }
142
+
143
+ return tokens.has(property);
144
+ },
145
+
146
+ set() {
147
+ return false;
148
+ },
149
+ }
150
+ );
151
+ }
152
+ }
153
+
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ export type ParsedTokenList<Parser extends TokenListParser<any>> =
156
+ Parser extends TokenListParser<infer CanonicalToken> ? TokenList<CanonicalToken> : never;
@@ -9,6 +9,10 @@ const hintLookup = new ScopedElementLookup(':scope > hint', 'hint');
9
9
  const itemLookup = new ScopedElementLookup(':scope > item', 'item');
10
10
  const itemsetLookup = new ScopedElementLookup(':scope > itemset[nodeset]', 'itemset[nodeset]');
11
11
  const labelLookup = new ScopedElementLookup(':scope > label', 'label');
12
+ const repeatGroupLabelLookup = new ScopedElementLookup(
13
+ ':scope > label[form-definition-source="repeat-group"]',
14
+ 'label[form-definition-source="repeat-group"]'
15
+ );
12
16
  const repeatLookup = new ScopedElementLookup(':scope > repeat[nodeset]', 'repeat[nodeset]');
13
17
  const valueLookup = new ScopedElementLookup(':scope > value', 'value');
14
18
 
@@ -20,6 +24,11 @@ export interface ItemsetElement extends KnownAttributeLocalNamedElement<'itemset
20
24
 
21
25
  export interface LabelElement extends LocalNamedElement<'label'> {}
22
26
 
27
+ export interface RepeatGroupLabelElement extends LabelElement {
28
+ getAttribute(name: 'form-definition-source'): 'repeat-group';
29
+ getAttribute(name: string): string;
30
+ }
31
+
23
32
  export interface RepeatElement extends KnownAttributeLocalNamedElement<'repeat', 'nodeset'> {}
24
33
 
25
34
  export interface ValueElement extends LocalNamedElement<'value'> {}
@@ -40,6 +49,10 @@ export const getLabelElement = (parent: Element): LabelElement | null => {
40
49
  return labelLookup.getElement<LabelElement>(parent);
41
50
  };
42
51
 
52
+ export const getRepeatGroupLabelElement = (parent: Element): RepeatGroupLabelElement | null => {
53
+ return repeatGroupLabelLookup.getElement<RepeatGroupLabelElement>(parent);
54
+ };
55
+
43
56
  export const getRepeatElement = (parent: Element): RepeatElement | null => {
44
57
  return repeatLookup.getElement<RepeatElement>(parent);
45
58
  };
@@ -1,4 +1,5 @@
1
- import { createMemo, createSignal, type Accessor, type Setter, type Signal } from 'solid-js';
1
+ import type { Accessor, Setter, Signal } from 'solid-js';
2
+ import { createSignal } from 'solid-js';
2
3
  import type { OpaqueReactiveObjectFactory } from '../../index.ts';
3
4
  import type { AnyChildNode, AnyParentNode } from '../../instance/hierarchy.ts';
4
5
  import type { NodeID } from '../../instance/identity.ts';
@@ -44,11 +45,55 @@ export const createChildrenState = <Parent extends AnyParentNode, Child extends
44
45
  parent: Parent
45
46
  ): ChildrenState<Child> => {
46
47
  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
- });
48
+ const baseState = createSignal<readonly Child[]>([]);
49
+ const [getChildren, baseSetChildren] = baseState;
50
+
51
+ /**
52
+ * Note: this is clearly derived state. It would be obvious to use
53
+ * `createMemo`, which is exactly what a previous iteration did. This caused
54
+ * mysterious issues when clients:
55
+ *
56
+ * - Also used Solid-based reactivity
57
+ * - Accessed node children state within their own `createMemo` calls
58
+ *
59
+ * It's quite likely that there's a more robust and general solution, which
60
+ * may be applicable if we also generalize this approach to
61
+ * encode/materialize shared structured state (e.g. it may be applicable for
62
+ * select values, form language, probably much more in coming features).
63
+ *
64
+ * In the meantime, while this approach is marginally more complex, it is
65
+ * likely also slightly more efficient. We can revisit the tradeoff if/when
66
+ * those hypothetical generalizations become a priority.
67
+ */
68
+ const ids = createSignal<readonly NodeID[]>([]);
69
+ const [childIds, setChildIds] = ids;
70
+
71
+ type ChildrenSetterCallback = (prev: readonly Child[]) => readonly Child[];
72
+ type ChildrenOrSetterCallback = ChildrenSetterCallback | readonly Child[];
73
+
74
+ const setChildren: Setter<readonly Child[]> = (
75
+ valueOrSetterCallback: ChildrenOrSetterCallback
76
+ ) => {
77
+ let setterCallback: ChildrenSetterCallback;
78
+
79
+ if (typeof valueOrSetterCallback === 'function') {
80
+ setterCallback = valueOrSetterCallback;
81
+ } else {
82
+ setterCallback = (_) => valueOrSetterCallback;
83
+ }
84
+
85
+ return baseSetChildren((prev) => {
86
+ const result = setterCallback(prev);
87
+
88
+ setChildIds(() => {
89
+ return result.map((child) => child.nodeId);
90
+ });
91
+
92
+ return result;
93
+ });
94
+ };
95
+
96
+ const children: Signal<readonly Child[]> = [getChildren, setChildren];
52
97
 
53
98
  return {
54
99
  children,
@@ -8,10 +8,12 @@ import type {
8
8
  } from '../../expression/DependentExpression.ts';
9
9
  import type { EvaluationContext } from '../../instance/internal-api/EvaluationContext.ts';
10
10
  import type { SubscribableDependency } from '../../instance/internal-api/SubscribableDependency.ts';
11
+ import { isConstantExpression } from '../../parse/xpath/semantic-analysis.ts';
11
12
 
12
13
  interface ComputedExpressionResults {
13
14
  readonly boolean: boolean;
14
15
  readonly nodes: Node[];
16
+ readonly number: number;
15
17
  readonly string: string;
16
18
  }
17
19
 
@@ -44,6 +46,11 @@ const expressionEvaluator = <Type extends DependentExpressionResultType>(
44
46
  return evaluator.evaluateNodes(expression, options);
45
47
  }) as ExpressionEvaluator<Type>;
46
48
 
49
+ case 'number':
50
+ return (() => {
51
+ return evaluator.evaluateNumber(expression, options);
52
+ }) as ExpressionEvaluator<Type>;
53
+
47
54
  case 'string':
48
55
  return (() => {
49
56
  return evaluator.evaluateString(expression, options);
@@ -54,26 +61,19 @@ const expressionEvaluator = <Type extends DependentExpressionResultType>(
54
61
  }
55
62
  };
56
63
 
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
64
  // prettier-ignore
70
65
  type ComputedExpression<Type extends DependentExpressionResultType> = Accessor<
71
66
  EvaluatedExpression<Type>
72
67
  >;
73
68
 
69
+ interface CreateComputedExpressionOptions {
70
+ readonly arbitraryDependencies?: readonly SubscribableDependency[];
71
+ }
72
+
74
73
  export const createComputedExpression = <Type extends DependentExpressionResultType>(
75
74
  context: EvaluationContext,
76
- dependentExpression: DependentExpression<Type>
75
+ dependentExpression: DependentExpression<Type>,
76
+ options: CreateComputedExpressionOptions = {}
77
77
  ): ComputedExpression<Type> => {
78
78
  const { contextNode, evaluator, root, scope } = context;
79
79
  const { expression, isTranslated, resultType } = dependentExpression;
@@ -85,26 +85,24 @@ export const createComputedExpression = <Type extends DependentExpressionResultT
85
85
  return createMemo(evaluateExpression);
86
86
  }
87
87
 
88
+ const { arbitraryDependencies = [] } = options;
89
+
88
90
  const getReferencedDependencies = createMemo(() => {
89
91
  return dependencyReferences.flatMap((reference) => {
90
- return context.getSubscribableDependencyByReference(reference) ?? [];
92
+ return context.getSubscribableDependenciesByReference(reference) ?? [];
91
93
  });
92
94
  });
93
95
 
94
- let getDependencies: Accessor<readonly SubscribableDependency[]>;
96
+ return createMemo(() => {
97
+ if (isTranslated) {
98
+ root.subscribe();
99
+ }
95
100
 
96
- if (isTranslated) {
97
- getDependencies = createMemo(() => {
98
- return [root, ...getReferencedDependencies()];
101
+ arbitraryDependencies.forEach((dependency) => {
102
+ dependency.subscribe();
99
103
  });
100
- } else {
101
- getDependencies = getReferencedDependencies;
102
- }
103
-
104
- return createMemo(() => {
105
- const dependencies = getDependencies();
106
104
 
107
- dependencies.forEach((dependency) => {
105
+ getReferencedDependencies().forEach((dependency) => {
108
106
  dependency.subscribe();
109
107
  });
110
108
 
@@ -0,0 +1,33 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import type { EvaluationContext } from '../../instance/internal-api/EvaluationContext.ts';
3
+ import type { BindComputation } from '../../model/BindComputation.ts';
4
+ import { createComputedExpression } from './createComputedExpression.ts';
5
+
6
+ export const createNoteReadonlyThunk = (
7
+ context: EvaluationContext,
8
+ readonlyDefinition: BindComputation<'readonly'>
9
+ ): Accessor<true> => {
10
+ if (!readonlyDefinition.isConstantTruthyExpression()) {
11
+ throw new Error('Expected a static readonly expression');
12
+ }
13
+
14
+ let result = true;
15
+
16
+ if (import.meta.env.DEV) {
17
+ const { expression } = readonlyDefinition;
18
+
19
+ if (readonlyDefinition.dependencyReferences.size > 0) {
20
+ throw new Error(`Expected expression ${expression} to have no dependencies`);
21
+ }
22
+
23
+ const computedExpression = createComputedExpression(context, readonlyDefinition);
24
+
25
+ result = computedExpression();
26
+
27
+ if (result !== true) {
28
+ throw new Error(`Expected expression ${readonlyDefinition.expression} to return true`);
29
+ }
30
+ }
31
+
32
+ return () => result;
33
+ };
@@ -4,6 +4,7 @@ import type { Accessor } from 'solid-js';
4
4
  import { createMemo } from 'solid-js';
5
5
  import type { ItemDefinition } from '../../body/control/select/ItemDefinition.ts';
6
6
  import type { ItemsetDefinition } from '../../body/control/select/ItemsetDefinition.ts';
7
+ import type { TextRange as ClientTextRange } from '../../client/TextRange.ts';
7
8
  import type { SelectItem } from '../../index.ts';
8
9
  import type { SelectField } from '../../instance/SelectField.ts';
9
10
  import type {
@@ -11,20 +12,31 @@ import type {
11
12
  EvaluationContextRoot,
12
13
  } from '../../instance/internal-api/EvaluationContext.ts';
13
14
  import type { SubscribableDependency } from '../../instance/internal-api/SubscribableDependency.ts';
14
- import type { TextRange } from '../../instance/text/TextRange.ts';
15
+ import { TextChunk } from '../../instance/text/TextChunk.ts';
16
+ import { TextRange } from '../../instance/text/TextRange.ts';
15
17
  import { createComputedExpression } from './createComputedExpression.ts';
16
18
  import type { ReactiveScope } from './scope.ts';
17
19
  import { createTextRange } from './text/createTextRange.ts';
18
20
 
21
+ type DerivedItemLabel = ClientTextRange<'item-label', 'form-derived'>;
22
+
23
+ const derivedItemLabel = (context: EvaluationContext, value: string): DerivedItemLabel => {
24
+ const chunk = new TextChunk(context.root, 'static', value);
25
+
26
+ return new TextRange('form-derived', 'item-label', [chunk]);
27
+ };
28
+
19
29
  const createSelectItemLabel = (
20
30
  context: EvaluationContext,
21
31
  definition: ItemDefinition
22
- ): Accessor<TextRange<'label'>> => {
32
+ ): Accessor<ClientTextRange<'item-label'>> => {
23
33
  const { label, value } = definition;
24
34
 
25
- return createTextRange(context, 'label', label, {
26
- fallbackValue: value,
27
- });
35
+ if (label == null) {
36
+ return () => derivedItemLabel(context, value);
37
+ }
38
+
39
+ return createTextRange(context, 'item-label', label);
28
40
  };
29
41
 
30
42
  const createTranslatedStaticSelectItems = (
@@ -52,10 +64,7 @@ class ItemsetItemEvaluationContext implements EvaluationContext {
52
64
  readonly scope: ReactiveScope;
53
65
  readonly evaluator: XFormsXPathEvaluator;
54
66
  readonly root: EvaluationContextRoot;
55
-
56
- get contextReference(): string {
57
- return this.selectField.contextReference;
58
- }
67
+ readonly contextReference: Accessor<string>;
59
68
 
60
69
  constructor(
61
70
  private readonly selectField: SelectField,
@@ -64,10 +73,11 @@ class ItemsetItemEvaluationContext implements EvaluationContext {
64
73
  this.scope = selectField.scope;
65
74
  this.evaluator = selectField.evaluator;
66
75
  this.root = selectField.root;
76
+ this.contextReference = selectField.contextReference;
67
77
  }
68
78
 
69
- getSubscribableDependencyByReference(reference: string): SubscribableDependency | null {
70
- return this.selectField.getSubscribableDependencyByReference(reference);
79
+ getSubscribableDependenciesByReference(reference: string): readonly SubscribableDependency[] {
80
+ return this.selectField.getSubscribableDependenciesByReference(reference);
71
81
  }
72
82
  }
73
83
 
@@ -75,25 +85,20 @@ const createSelectItemsetItemLabel = (
75
85
  context: EvaluationContext,
76
86
  definition: ItemsetDefinition,
77
87
  itemValue: Accessor<string>
78
- ): Accessor<TextRange<'label'>> => {
88
+ ): Accessor<ClientTextRange<'item-label'>> => {
79
89
  const { label } = definition;
80
90
 
81
91
  if (label == null) {
82
92
  return createMemo(() => {
83
- const value = itemValue();
84
- const staticValueLabel = createTextRange(context, 'label', label, {
85
- fallbackValue: value,
86
- });
87
-
88
- return staticValueLabel();
93
+ return derivedItemLabel(context, itemValue());
89
94
  });
90
95
  }
91
96
 
92
- return createTextRange(context, 'label', label);
97
+ return createTextRange(context, 'item-label', label);
93
98
  };
94
99
 
95
100
  interface ItemsetItem {
96
- label(): TextRange<'label'>;
101
+ label(): ClientTextRange<'item-label'>;
97
102
  value(): string;
98
103
  }
99
104
 
@@ -72,7 +72,7 @@ const createPrimaryInstanceValueState = <RuntimeValue>(
72
72
 
73
73
  const persistedValueState = createSignal<PersistedValueState>(
74
74
  {
75
- isRelevant: context.isRelevant,
75
+ isRelevant: context.isRelevant(),
76
76
  value: initialValue,
77
77
  },
78
78
  {
@@ -91,7 +91,7 @@ const createPrimaryInstanceValueState = <RuntimeValue>(
91
91
  const [persistedValue, setValueForPersistence] = persistedValueState;
92
92
 
93
93
  createComputed(() => {
94
- const isRelevant = context.isRelevant;
94
+ const isRelevant = context.isRelevant();
95
95
 
96
96
  setValueForPersistence((persisted) => {
97
97
  return {
@@ -122,7 +122,7 @@ const createPrimaryInstanceValueState = <RuntimeValue>(
122
122
  const setPrimaryInstanceValue: SimpleAtomicStateSetter<string> = (value) => {
123
123
  // TODO: Check (error?) for non-relevant value change?
124
124
  const persisted = setValueForPersistence({
125
- isRelevant: context.isRelevant,
125
+ isRelevant: context.isRelevant(),
126
126
  value,
127
127
  });
128
128
 
@@ -189,8 +189,8 @@ const guardDownstreamReadonlyWrites = <RuntimeValue>(
189
189
  const [getValue, baseSetValue] = baseState;
190
190
 
191
191
  const setValue: SimpleAtomicStateSetter<RuntimeValue> = (value) => {
192
- if (context.isReadonly) {
193
- const reference = untrack(() => context.contextReference);
192
+ if (context.isReadonly()) {
193
+ const reference = untrack(() => context.contextReference());
194
194
 
195
195
  throw new Error(`Cannot write to readonly field: ${reference}`);
196
196
  }
@@ -215,7 +215,7 @@ const createCalculation = <RuntimeValue>(
215
215
  const calculate = createComputedExpression(context, calculateDefinition);
216
216
 
217
217
  createComputed(() => {
218
- if (context.isRelevant) {
218
+ if (context.isRelevant()) {
219
219
  const calculated = calculate();
220
220
  const value = context.decodeValue(calculated);
221
221
 
@@ -3,6 +3,7 @@ import type { NodeID } from '../../instance/identity.ts';
3
3
  import type { ChildrenState, createChildrenState } from './createChildrenState.ts';
4
4
  import type { ClientState } from './node-state/createClientState.ts';
5
5
  import type { CurrentState } from './node-state/createCurrentState.ts';
6
+ import type { ReactiveScope } from './scope.ts';
6
7
 
7
8
  interface InconsistentChildrenStateDetails {
8
9
  readonly missingIds: readonly NodeID[];
@@ -96,6 +97,7 @@ export const materializeCurrentStateChildren = <
96
97
  Child extends AnyChildNode,
97
98
  ParentState extends EncodedParentState,
98
99
  >(
100
+ scope: ReactiveScope,
99
101
  currentState: ParentState,
100
102
  childrenState: ChildrenState<Child>
101
103
  ): MaterializedChildren<ParentState, Child> => {
@@ -105,7 +107,7 @@ export const materializeCurrentStateChildren = <
105
107
  return new Proxy(proxyTarget, {
106
108
  get(_, key) {
107
109
  if (key === 'children') {
108
- const expectedChildIDs = currentState.children;
110
+ const expectedChildIDs = scope.runTask(() => currentState.children);
109
111
  const children = childrenState.getChildren();
110
112
 
111
113
  if (import.meta.env.DEV) {
@@ -31,7 +31,7 @@ export interface SharedNodeState<Spec extends StateSpec> {
31
31
  readonly setProperty: SetEnginePropertyState<Spec>;
32
32
  }
33
33
 
34
- interface SharedNodeStateOptions<
34
+ export interface SharedNodeStateOptions<
35
35
  Factory extends OpaqueReactiveObjectFactory,
36
36
  Spec extends StateSpec,
37
37
  > {
@@ -1,16 +1,18 @@
1
1
  import { type Accessor } from 'solid-js';
2
+ import type { TextRange } from '../../../client/TextRange.ts';
2
3
  import type { EvaluationContext } from '../../../instance/internal-api/EvaluationContext.ts';
3
- import { TextRange } from '../../../instance/text/TextRange.ts';
4
- import type { ValueNodeDefinition } from '../../../model/ValueNodeDefinition.ts';
4
+ import type { LeafNodeDefinition } from '../../../model/LeafNodeDefinition.ts';
5
5
  import { createTextRange } from './createTextRange.ts';
6
6
 
7
7
  export const createFieldHint = (
8
8
  context: EvaluationContext,
9
- definition: ValueNodeDefinition
10
- ): Accessor<TextRange<'hint'> | null> => {
9
+ definition: LeafNodeDefinition
10
+ ): Accessor<TextRange<'hint', 'form'> | null> => {
11
11
  const hintDefinition = definition.bodyElement?.hint ?? null;
12
12
 
13
- return createTextRange(context, 'hint', hintDefinition, {
14
- fallbackValue: null,
15
- });
13
+ if (hintDefinition == null) {
14
+ return () => null;
15
+ }
16
+
17
+ return createTextRange(context, 'hint', hintDefinition);
16
18
  };
@@ -1,16 +1,18 @@
1
1
  import { type Accessor } from 'solid-js';
2
+ import type { TextRange } from '../../../client/TextRange.ts';
2
3
  import type { EvaluationContext } from '../../../instance/internal-api/EvaluationContext.ts';
3
- import { TextRange } from '../../../instance/text/TextRange.ts';
4
4
  import type { AnyNodeDefinition } from '../../../model/NodeDefinition.ts';
5
5
  import { createTextRange } from './createTextRange.ts';
6
6
 
7
7
  export const createNodeLabel = (
8
8
  context: EvaluationContext,
9
9
  definition: AnyNodeDefinition
10
- ): Accessor<TextRange<'label'> | null> => {
10
+ ): Accessor<TextRange<'label', 'form'> | null> => {
11
11
  const labelDefinition = definition.bodyElement?.label ?? null;
12
12
 
13
- return createTextRange(context, 'label', labelDefinition, {
14
- fallbackValue: null,
15
- });
13
+ if (labelDefinition == null) {
14
+ return () => null;
15
+ }
16
+
17
+ return createTextRange(context, 'label', labelDefinition);
16
18
  };