@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
@@ -0,0 +1,347 @@
1
+ import { insertAtIndex } from '@getodk/common/lib/array/insert.ts';
2
+ import { untrack, type Accessor } from 'solid-js';
3
+ import type { NodeAppearances } from '../../client/NodeAppearances.ts';
4
+ import type { BaseRepeatRangeNode } from '../../client/repeat/BaseRepeatRangeNode.ts';
5
+ import type { TextRange } from '../../client/TextRange.ts';
6
+ import type { AncestorNodeValidationState } from '../../client/validation.ts';
7
+ import type { ChildrenState } from '../../lib/reactivity/createChildrenState.ts';
8
+ import { createChildrenState } from '../../lib/reactivity/createChildrenState.ts';
9
+ import { createComputedExpression } from '../../lib/reactivity/createComputedExpression.ts';
10
+ import type { MaterializedChildren } from '../../lib/reactivity/materializeCurrentStateChildren.ts';
11
+ import { materializeCurrentStateChildren } from '../../lib/reactivity/materializeCurrentStateChildren.ts';
12
+ import type { CurrentState } from '../../lib/reactivity/node-state/createCurrentState.ts';
13
+ import type { EngineState } from '../../lib/reactivity/node-state/createEngineState.ts';
14
+ import type { SharedNodeState } from '../../lib/reactivity/node-state/createSharedNodeState.ts';
15
+ import { createSharedNodeState } from '../../lib/reactivity/node-state/createSharedNodeState.ts';
16
+ import { createNodeLabel } from '../../lib/reactivity/text/createNodeLabel.ts';
17
+ import type {
18
+ AnyRepeatRangeDefinition,
19
+ ControlledRepeatRangeDefinition,
20
+ } from '../../model/RepeatRangeDefinition.ts';
21
+ import type {
22
+ AnyDescendantNode,
23
+ DescendantNodeParent,
24
+ DescendantNodeSharedStateSpec,
25
+ } from '../abstract/DescendantNode.ts';
26
+ import { DescendantNode } from '../abstract/DescendantNode.ts';
27
+ import type { RepeatRange } from '../hierarchy.ts';
28
+ import type { NodeID } from '../identity.ts';
29
+ import type { EvaluationContext } from '../internal-api/EvaluationContext.ts';
30
+ import type { SubscribableDependency } from '../internal-api/SubscribableDependency.ts';
31
+ import { RepeatInstance, type RepeatDefinition } from './RepeatInstance.ts';
32
+
33
+ interface RepeatRangeStateSpec extends DescendantNodeSharedStateSpec {
34
+ readonly hint: null;
35
+ readonly label: Accessor<TextRange<'label'> | null>;
36
+ readonly children: Accessor<readonly NodeID[]>;
37
+ readonly valueOptions: null;
38
+ readonly value: null;
39
+ }
40
+
41
+ // prettier-ignore
42
+ type BaseRepeatRangeNodeType<Definition extends AnyRepeatRangeDefinition> =
43
+ Definition extends ControlledRepeatRangeDefinition
44
+ ? 'repeat-range:controlled'
45
+ : 'repeat-range:uncontrolled';
46
+
47
+ export abstract class BaseRepeatRange<Definition extends AnyRepeatRangeDefinition>
48
+ extends DescendantNode<Definition, RepeatRangeStateSpec, RepeatInstance>
49
+ implements BaseRepeatRangeNode, EvaluationContext, SubscribableDependency
50
+ {
51
+ /**
52
+ * A repeat range doesn't have a corresponding primary instance element of its
53
+ * own, and its instances are appended to the range's parent element. During
54
+ * creation of the initial primary instance state and DOM trees, we _could_
55
+ * reliably append all of the range's instances in order as the definition
56
+ * tree is recursed. But that would fail to handle some instance addition
57
+ * cases afterwards.
58
+ *
59
+ * Most notably, we need to know where in the primary instance tree to append
60
+ * instances created for a range which is currently empty. As a lucky
61
+ * coincidence, this need coincides with the ability to add instances at any
62
+ * arbitrary index within the range. In each case, we can reference a primary
63
+ * instance DOM node which will become the new instance's preceding sibling.
64
+ * Where the range is empty, we use this {@link Comment} node (itself created
65
+ * and appended during range initialization) in lieu of a nonexistent
66
+ * preceding instance's {@link contextNode}.
67
+ *
68
+ * @todo We likely want to remove these during submission serialization.
69
+ * @todo Can we use a
70
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Range | DOM Range}
71
+ * instead?
72
+ */
73
+ protected readonly anchorNode: Comment;
74
+
75
+ protected readonly childrenState: ChildrenState<RepeatInstance>;
76
+
77
+ /**
78
+ * Provides an {@link EvaluationContext} from which to evaluate expressions
79
+ * where some LocationPath sub-expressions may be **relative to the repeat
80
+ * range itself**. This is useful for evaluation of expressions where:
81
+ *
82
+ * - the expression is typically contextualized to any of its
83
+ * {@link RepeatInstance} children, but it presently has none (i.e.
84
+ * `relevant`)
85
+ *
86
+ * - the expression is conceptually intended to be evaluated in the context of
87
+ * the repeat range itself (i.e. `jr:count`)
88
+ */
89
+ protected readonly selfEvaluationContext: EvaluationContext & {
90
+ readonly contextNode: Comment;
91
+ };
92
+
93
+ /**
94
+ * @see {@link isSelfRelevant}
95
+ */
96
+ protected readonly isEmptyRangeSelfRelevant: Accessor<boolean>;
97
+
98
+ // InstanceNode
99
+ protected readonly state: SharedNodeState<RepeatRangeStateSpec>;
100
+ protected override engineState: EngineState<RepeatRangeStateSpec>;
101
+
102
+ // DescendantNode
103
+ /**
104
+ * @todo Should we special case repeat `readonly` state the same way
105
+ * we do for `relevant`?
106
+ *
107
+ * @see {@link isSelfRelevant}
108
+ */
109
+ declare isSelfReadonly: Accessor<boolean>;
110
+
111
+ /**
112
+ * A repeat range does not exist in the primary instance tree. A `relevant`
113
+ * expression applies to each {@link RepeatInstance} child of the repeat
114
+ * range. Determining whether a repeat range itself "is relevant" isn't a
115
+ * concept the spec addresses, but it may be used by clients to determine
116
+ * whether to allow interaction with the range (e.g. by adding a repeat
117
+ * instance, or presenting the range's label when empty).
118
+ *
119
+ * As a naive first pass, it seems like the heuristic for this should be:
120
+ *
121
+ * 1. Does the repeat range have any repeat instance children?
122
+ *
123
+ * - If yes, go to 2.
124
+ * - If no, go to 3.
125
+ *
126
+ * 2. Does one or more of those children return `true` for the node's
127
+ * `relevant` expression (i.e. is the repeat instance "self relevant")?
128
+ *
129
+ * 3. Does the relevant expression return `true` for the repeat range itself
130
+ * (where, at least for now, the context of that evaluation would be the
131
+ * repeat range's {@link anchorNode} to ensure correct relative expressions
132
+ * resolve correctly)?
133
+ *
134
+ * @todo While (3) is proactively implemented, there isn't presently a test
135
+ * exercising it. It felt best for now to surface this for discussion in
136
+ * review to validate that it's going in the right direction.
137
+ *
138
+ * @todo While (2) **is actually tested**, the tests currently in place behave
139
+ * the same way with only the logic for (3), regardless of whether the repeat
140
+ * range actually has any repeat instance children. It's unclear (a) if that's
141
+ * a preferable simplification and (b) how that might affect performance (in
142
+ * theory it could vary depending on form structure and runtime state).
143
+ */
144
+ override readonly isSelfRelevant: Accessor<boolean> = () => {
145
+ const instances = this.childrenState.getChildren();
146
+
147
+ if (instances.length > 0) {
148
+ return instances.some((instance) => instance.isSelfRelevant());
149
+ }
150
+
151
+ return this.isEmptyRangeSelfRelevant();
152
+ };
153
+
154
+ // BaseRepeatRangeNode
155
+ abstract override readonly nodeType: BaseRepeatRangeNodeType<Definition>;
156
+
157
+ /**
158
+ * @todo RepeatRange*, RepeatInstance* (and RepeatTemplate*) all share the
159
+ * same body element, and thus all share the same definition `bodyElement`. As
160
+ * such, they also all share the same `appearances`. At time of writing,
161
+ * `web-forms` (Vue UI package) treats a `RepeatRangeNode`...
162
+ *
163
+ * - ... as a group, if the node has a label (i.e.
164
+ * `<group><label/><repeat/></group>`)
165
+ * - ... effectively as a fragment containing only its instances, otherwise
166
+ *
167
+ * We now collapse `<group><repeat>` into `<repeat>`, and no longer treat
168
+ * "repeat group" as a concept (after parsing). According to the spec, these
169
+ * appearances **are supposed to** come from that "repeat group" in the form
170
+ * definition. In practice, many forms do define appearances directly on a
171
+ * repeat element. The engine currently produces an error if both are defined
172
+ * simultaneously, but otherwise makes no distinction between appearances in
173
+ * these form definition shapes:
174
+ *
175
+ * ```xml
176
+ * <group ref="/data/rep1" appearance="...">
177
+ * <repeat nodeset="/data/rep1"/>
178
+ * </group>
179
+ *
180
+ * <group ref="/data/rep1">
181
+ * <repeat nodeset="/data/rep1"/ appearance="...">
182
+ * </group>
183
+ *
184
+ * <repeat nodeset="/data/rep1"/ appearance="...">
185
+ * ```
186
+ *
187
+ * All of the above creates considerable ambiguity about where "repeat
188
+ * appearances" should apply, under which circumstances.
189
+ */
190
+ abstract override readonly appearances: NodeAppearances<Definition>;
191
+
192
+ readonly currentState: MaterializedChildren<CurrentState<RepeatRangeStateSpec>, RepeatInstance>;
193
+
194
+ abstract override readonly validationState: AncestorNodeValidationState;
195
+
196
+ constructor(parent: DescendantNodeParent<Definition>, definition: Definition) {
197
+ super(parent, definition);
198
+
199
+ const repeatRange = this as AnyDescendantNode as RepeatRange;
200
+
201
+ const childrenState = createChildrenState<RepeatRange, RepeatInstance>(repeatRange);
202
+
203
+ this.childrenState = childrenState;
204
+
205
+ this.anchorNode = this.contextNode.ownerDocument.createComment(
206
+ `Begin repeat range: ${definition.nodeset}`
207
+ );
208
+ this.contextNode.append(this.anchorNode);
209
+
210
+ this.selfEvaluationContext = {
211
+ scope: this.scope,
212
+ evaluator: this.evaluator,
213
+ root: this.root,
214
+ contextReference: this.contextReference,
215
+ contextNode: this.anchorNode,
216
+
217
+ getSubscribableDependenciesByReference: (reference) => {
218
+ return repeatRange.getSubscribableDependenciesByReference(reference);
219
+ },
220
+ };
221
+
222
+ this.isEmptyRangeSelfRelevant = createComputedExpression(
223
+ this.selfEvaluationContext,
224
+ definition.bind.relevant
225
+ );
226
+
227
+ const sharedStateOptions = {
228
+ clientStateFactory: this.engineConfig.stateFactory,
229
+ };
230
+
231
+ const state = createSharedNodeState(
232
+ this.scope,
233
+ {
234
+ reference: this.contextReference,
235
+ readonly: this.isReadonly,
236
+ relevant: this.isRelevant,
237
+ required: this.isRequired,
238
+
239
+ label: createNodeLabel(this, definition),
240
+ hint: null,
241
+ children: childrenState.childIds,
242
+ valueOptions: null,
243
+ value: null,
244
+ },
245
+ sharedStateOptions
246
+ );
247
+
248
+ this.state = state;
249
+ this.engineState = state.engineState;
250
+ this.currentState = materializeCurrentStateChildren(
251
+ this.scope,
252
+ state.currentState,
253
+ childrenState
254
+ );
255
+ }
256
+
257
+ protected getLastIndex(): number {
258
+ return this.engineState.children.length - 1;
259
+ }
260
+
261
+ protected override initializeContextNode(parentContextNode: Element): Element {
262
+ return parentContextNode;
263
+ }
264
+
265
+ getInstanceIndex(instance: RepeatInstance): number {
266
+ return this.engineState.children.indexOf(instance.nodeId);
267
+ }
268
+
269
+ private createChildren(
270
+ afterIndex: number,
271
+ definitions: readonly RepeatDefinition[]
272
+ ): readonly RepeatInstance[] {
273
+ return this.scope.runTask(() => {
274
+ let initialPrecedingInstance: RepeatInstance | null;
275
+
276
+ if (afterIndex === -1) {
277
+ initialPrecedingInstance = null;
278
+ } else {
279
+ const instance = untrack(() => this.childrenState.getChildren()[afterIndex]);
280
+
281
+ if (instance == null) {
282
+ throw new Error(`No repeat instance at index ${afterIndex}`);
283
+ }
284
+
285
+ initialPrecedingInstance = instance;
286
+ }
287
+
288
+ const repeatRange = this as AnyDescendantNode as RepeatRange;
289
+
290
+ return definitions.reduce<RepeatInstance[]>((acc, definition) => {
291
+ const precedingInstance = acc[acc.length - 1] ?? initialPrecedingInstance;
292
+ const precedingPrimaryInstanceNode = precedingInstance?.contextNode ?? this.anchorNode;
293
+ const newInstance = new RepeatInstance(repeatRange, definition, {
294
+ precedingPrimaryInstanceNode,
295
+ precedingInstance,
296
+ });
297
+
298
+ acc.push(newInstance);
299
+
300
+ return acc;
301
+ }, []);
302
+ });
303
+ }
304
+
305
+ protected addChildren(
306
+ afterIndex: number,
307
+ definitions: readonly RepeatDefinition[]
308
+ ): readonly RepeatInstance[] {
309
+ return this.scope.runTask(() => {
310
+ const initialIndex = afterIndex + 1;
311
+ const newInstances = this.createChildren(afterIndex, definitions);
312
+
313
+ return this.childrenState.setChildren((currentInstances) => {
314
+ return insertAtIndex(currentInstances, initialIndex, newInstances);
315
+ });
316
+ });
317
+ }
318
+
319
+ protected removeChildren(startIndex: number, count: number): readonly RepeatInstance[] {
320
+ return this.scope.runTask(() => {
321
+ return this.childrenState.setChildren((currentInstances) => {
322
+ const updatedInstances = currentInstances.slice();
323
+ const removedInstances = updatedInstances.splice(startIndex, count);
324
+
325
+ removedInstances.forEach((instance) => {
326
+ instance.remove();
327
+ });
328
+
329
+ return updatedInstances;
330
+ });
331
+ });
332
+ }
333
+
334
+ override subscribe(): void {
335
+ super.subscribe();
336
+
337
+ // Subscribing to children can support reactive expressions dependent on the
338
+ // repeat range itself (e.g. `count()`).
339
+ this.childrenState.getChildren().forEach((child) => {
340
+ child.subscribe();
341
+ });
342
+ }
343
+
344
+ getChildren(): readonly RepeatInstance[] {
345
+ return this.childrenState.getChildren();
346
+ }
347
+ }
@@ -1,24 +1,29 @@
1
1
  import type { Accessor } from 'solid-js';
2
2
  import { createComputed, createSignal, on } from 'solid-js';
3
- import type { RepeatDefinition, RepeatInstanceNode } from '../client/RepeatInstanceNode.ts';
4
- import type { TextRange } from '../index.ts';
5
- import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
6
- import { createChildrenState } from '../lib/reactivity/createChildrenState.ts';
7
- import type { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
8
- import { materializeCurrentStateChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
9
- import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
10
- import type { EngineState } from '../lib/reactivity/node-state/createEngineState.ts';
11
- import type { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
12
- import { createSharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
13
- import { createNodeLabel } from '../lib/reactivity/text/createNodeLabel.ts';
14
- import type { RepeatRange } from './RepeatRange.ts';
15
- import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts';
16
- import { DescendantNode } from './abstract/DescendantNode.ts';
17
- import { buildChildren } from './children.ts';
18
- import type { AnyChildNode, GeneralChildNode } from './hierarchy.ts';
19
- import type { NodeID } from './identity.ts';
20
- import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
21
- import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts';
3
+ import type {
4
+ RepeatDefinition,
5
+ RepeatInstanceNode,
6
+ RepeatInstanceNodeAppearances,
7
+ } from '../../client/repeat/RepeatInstanceNode.ts';
8
+ import type { TextRange } from '../../client/TextRange.ts';
9
+ import type { AncestorNodeValidationState } from '../../client/validation.ts';
10
+ import type { ChildrenState } from '../../lib/reactivity/createChildrenState.ts';
11
+ import { createChildrenState } from '../../lib/reactivity/createChildrenState.ts';
12
+ import type { MaterializedChildren } from '../../lib/reactivity/materializeCurrentStateChildren.ts';
13
+ import { materializeCurrentStateChildren } from '../../lib/reactivity/materializeCurrentStateChildren.ts';
14
+ import type { CurrentState } from '../../lib/reactivity/node-state/createCurrentState.ts';
15
+ import type { EngineState } from '../../lib/reactivity/node-state/createEngineState.ts';
16
+ import type { SharedNodeState } from '../../lib/reactivity/node-state/createSharedNodeState.ts';
17
+ import { createSharedNodeState } from '../../lib/reactivity/node-state/createSharedNodeState.ts';
18
+ import { createNodeLabel } from '../../lib/reactivity/text/createNodeLabel.ts';
19
+ import { createAggregatedViolations } from '../../lib/reactivity/validation/createAggregatedViolations.ts';
20
+ import type { DescendantNodeSharedStateSpec } from '../abstract/DescendantNode.ts';
21
+ import { DescendantNode } from '../abstract/DescendantNode.ts';
22
+ import { buildChildren } from '../children.ts';
23
+ import type { AnyChildNode, GeneralChildNode, RepeatRange } from '../hierarchy.ts';
24
+ import type { NodeID } from '../identity.ts';
25
+ import type { EvaluationContext } from '../internal-api/EvaluationContext.ts';
26
+ import type { SubscribableDependency } from '../internal-api/SubscribableDependency.ts';
22
27
 
23
28
  export type { RepeatDefinition };
24
29
 
@@ -46,20 +51,64 @@ export class RepeatInstance
46
51
  protected readonly state: SharedNodeState<RepeatInstanceStateSpec>;
47
52
  protected override engineState: EngineState<RepeatInstanceStateSpec>;
48
53
 
54
+ /**
55
+ * @todo Should we special case repeat `readonly` inheritance the same way
56
+ * we do for `relevant`?
57
+ *
58
+ * @see {@link hasNonRelevantAncestor}
59
+ */
60
+ declare readonly hasReadonlyAncestor: Accessor<boolean>;
61
+
62
+ /**
63
+ * A repeat instance can inherit non-relevance, just like any other node. That
64
+ * inheritance is derived from the repeat instance's parent node in the
65
+ * primary instance XML/DOM tree (and would be semantically expected to do so
66
+ * even if we move away from that implementation detail).
67
+ *
68
+ * Since {@link RepeatInstance.parent} is a {@link RepeatRange}, which is a
69
+ * runtime data model fiction that does not exist in that hierarchy, we pass
70
+ * this call through, allowing the {@link RepeatRange} to check the actual
71
+ * primary instance parent node's relevance state.
72
+ *
73
+ * @todo Should we apply similar reasoning in {@link hasReadonlyAncestor}?
74
+ */
75
+ override readonly hasNonRelevantAncestor: Accessor<boolean> = () => {
76
+ return this.parent.hasNonRelevantAncestor();
77
+ };
78
+
49
79
  // RepeatInstanceNode
50
80
  readonly nodeType = 'repeat-instance';
51
81
 
82
+ /**
83
+ * @see {@link RepeatRange.appearances}
84
+ */
85
+ readonly appearances: RepeatInstanceNodeAppearances;
86
+
52
87
  readonly currentState: MaterializedChildren<
53
88
  CurrentState<RepeatInstanceStateSpec>,
54
89
  GeneralChildNode
55
90
  >;
91
+ readonly validationState: AncestorNodeValidationState;
56
92
 
57
93
  constructor(
58
94
  override readonly parent: RepeatRange,
59
95
  definition: RepeatDefinition,
60
96
  options: RepeatInstanceOptions
61
97
  ) {
62
- super(parent, definition);
98
+ const { precedingInstance } = options;
99
+ const precedingIndex = precedingInstance?.currentIndex ?? (() => -1);
100
+ const initialIndex = precedingIndex() + 1;
101
+ const [currentIndex, setCurrentIndex] = createSignal(initialIndex);
102
+
103
+ super(parent, definition, {
104
+ computeReference: (): string => {
105
+ const currentPosition = currentIndex() + 1;
106
+
107
+ return `${parent.contextReference()}[${currentPosition}]`;
108
+ },
109
+ });
110
+
111
+ this.appearances = definition.bodyElement.appearances;
63
112
 
64
113
  const childrenState = createChildrenState<RepeatInstance, GeneralChildNode>(this);
65
114
 
@@ -67,32 +116,37 @@ export class RepeatInstance
67
116
 
68
117
  options.precedingPrimaryInstanceNode.after(this.contextNode);
69
118
 
70
- const { precedingInstance } = options;
71
- const precedingIndex = precedingInstance?.currentIndex ?? (() => -1);
72
- const initialIndex = precedingIndex() + 1;
73
- const [currentIndex, setCurrentIndex] = createSignal(initialIndex);
74
-
75
119
  this.currentIndex = currentIndex;
76
120
 
121
+ const sharedStateOptions = {
122
+ clientStateFactory: this.engineConfig.stateFactory,
123
+ };
124
+
77
125
  const state = createSharedNodeState(
78
126
  this.scope,
79
127
  {
80
- ...this.buildSharedStateSpec(parent, definition),
128
+ reference: this.contextReference,
129
+ readonly: this.isReadonly,
130
+ relevant: this.isRelevant,
131
+ required: this.isRequired,
81
132
 
133
+ // TODO: only-child <group><label>
82
134
  label: createNodeLabel(this, definition),
83
135
  hint: null,
84
136
  children: childrenState.childIds,
85
137
  valueOptions: null,
86
138
  value: null,
87
139
  },
88
- {
89
- clientStateFactory: this.engineConfig.stateFactory,
90
- }
140
+ sharedStateOptions
91
141
  );
92
142
 
93
143
  this.state = state;
94
144
  this.engineState = state.engineState;
95
- this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
145
+ this.currentState = materializeCurrentStateChildren(
146
+ this.scope,
147
+ state.currentState,
148
+ childrenState
149
+ );
96
150
 
97
151
  // Maintain current index state, updating as the parent range's children
98
152
  // state is changed. Notable Solid reactivity nuances:
@@ -116,12 +170,7 @@ export class RepeatInstance
116
170
  });
117
171
 
118
172
  childrenState.setChildren(buildChildren(this));
119
- }
120
-
121
- protected computeReference(parent: RepeatRange): string {
122
- const currentPosition = this.currentIndex() + 1;
123
-
124
- return `${parent.contextReference}[${currentPosition}]`;
173
+ this.validationState = createAggregatedViolations(this, sharedStateOptions);
125
174
  }
126
175
 
127
176
  protected override initializeContextNode(parentContextNode: Element, nodeName: string): Element {
@@ -0,0 +1,82 @@
1
+ import { createComputed } from 'solid-js';
2
+ import type { RepeatRangeNodeAppearances } from '../../client/repeat/BaseRepeatRangeNode.ts';
3
+ import type { RepeatRangeControlledNode } from '../../client/repeat/RepeatRangeControlledNode.ts';
4
+ import type { AncestorNodeValidationState } from '../../client/validation.ts';
5
+ import { createComputedExpression } from '../../lib/reactivity/createComputedExpression.ts';
6
+ import { createAggregatedViolations } from '../../lib/reactivity/validation/createAggregatedViolations.ts';
7
+ import type { ControlledRepeatRangeDefinition } from '../../model/RepeatRangeDefinition.ts';
8
+ import type { GeneralParentNode } from '../hierarchy.ts';
9
+ import type { EvaluationContext } from '../internal-api/EvaluationContext.ts';
10
+ import type { SubscribableDependency } from '../internal-api/SubscribableDependency.ts';
11
+ import { BaseRepeatRange } from './BaseRepeatRange.ts';
12
+ import type { RepeatDefinition } from './RepeatInstance.ts';
13
+
14
+ export class RepeatRangeControlled
15
+ extends BaseRepeatRange<ControlledRepeatRangeDefinition>
16
+ implements RepeatRangeControlledNode, EvaluationContext, SubscribableDependency
17
+ {
18
+ // RepeatRangeControlledNode
19
+ readonly nodeType = 'repeat-range:controlled';
20
+
21
+ readonly appearances: RepeatRangeNodeAppearances;
22
+
23
+ readonly validationState: AncestorNodeValidationState;
24
+
25
+ constructor(parent: GeneralParentNode, definition: ControlledRepeatRangeDefinition) {
26
+ super(parent, definition);
27
+
28
+ this.appearances = definition.bodyElement.appearances;
29
+
30
+ this.initializeControlledChildrenState(definition);
31
+
32
+ this.validationState = createAggregatedViolations(this, {
33
+ clientStateFactory: this.engineConfig.stateFactory,
34
+ });
35
+ }
36
+
37
+ private initializeControlledChildrenState(definition: ControlledRepeatRangeDefinition): void {
38
+ this.scope.runTask(() => {
39
+ const { count, instances, template } = definition;
40
+ const computeCount = createComputedExpression(this.selfEvaluationContext, count);
41
+
42
+ createComputed<number>((previousCount) => {
43
+ let currentCount = computeCount();
44
+
45
+ if (Number.isFinite(currentCount) && currentCount < 0) {
46
+ currentCount = 0;
47
+ }
48
+
49
+ if (
50
+ currentCount === previousCount ||
51
+ // TODO: the intent of this check is to defer a count update when the
52
+ // count expression produces a blank value. This "feels right" when
53
+ // the count is directly controlled by the user (i.e. entering a
54
+ // number in an input), but probably does not make sense in every
55
+ // scenario! For instance, when a referenced node's relevance changes.
56
+ Number.isNaN(currentCount)
57
+ ) {
58
+ return previousCount;
59
+ }
60
+
61
+ if (currentCount > previousCount) {
62
+ const delta = currentCount - previousCount;
63
+ const definitions = Array<RepeatDefinition>(delta)
64
+ .fill(template)
65
+ .map((baseDefinition, index) => {
66
+ const instanceIndex = previousCount + index;
67
+
68
+ return instances[instanceIndex] ?? baseDefinition;
69
+ });
70
+
71
+ this.addChildren(previousCount - 1, definitions);
72
+ } else {
73
+ const delta = previousCount - currentCount;
74
+
75
+ this.removeChildren(currentCount, delta);
76
+ }
77
+
78
+ return currentCount;
79
+ }, 0);
80
+ });
81
+ }
82
+ }
@@ -0,0 +1,67 @@
1
+ import type { RepeatRangeNodeAppearances } from '../../client/repeat/BaseRepeatRangeNode.ts';
2
+ import type { RepeatRangeUncontrolledNode } from '../../client/repeat/RepeatRangeUncontrolledNode.ts';
3
+ import type { AncestorNodeValidationState } from '../../client/validation.ts';
4
+ import { createAggregatedViolations } from '../../lib/reactivity/validation/createAggregatedViolations.ts';
5
+ import type { UncontrolledRepeatRangeDefinition } from '../../model/RepeatRangeDefinition.ts';
6
+ import type { GeneralParentNode } from '../hierarchy.ts';
7
+ import type { EvaluationContext } from '../internal-api/EvaluationContext.ts';
8
+ import type { SubscribableDependency } from '../internal-api/SubscribableDependency.ts';
9
+ import type { Root } from '../Root.ts';
10
+ import { BaseRepeatRange } from './BaseRepeatRange.ts';
11
+ import { RepeatInstance } from './RepeatInstance.ts';
12
+
13
+ export class RepeatRangeUncontrolled
14
+ extends BaseRepeatRange<UncontrolledRepeatRangeDefinition>
15
+ implements RepeatRangeUncontrolledNode, EvaluationContext, SubscribableDependency
16
+ {
17
+ // RepeatRangeUncontrolledNode
18
+ readonly nodeType = 'repeat-range:uncontrolled';
19
+
20
+ readonly appearances: RepeatRangeNodeAppearances;
21
+
22
+ readonly validationState: AncestorNodeValidationState;
23
+
24
+ constructor(parent: GeneralParentNode, definition: UncontrolledRepeatRangeDefinition) {
25
+ super(parent, definition);
26
+
27
+ this.appearances = definition.bodyElement.appearances;
28
+
29
+ this.addChildren(-1, definition.instances);
30
+
31
+ this.validationState = createAggregatedViolations(this, {
32
+ clientStateFactory: this.engineConfig.stateFactory,
33
+ });
34
+ }
35
+
36
+ // RepeatRangeUncontrolledNode
37
+ addInstances(afterIndex = this.getLastIndex(), count = 1): Root {
38
+ const definitions = Array(count).fill(this.definition.template);
39
+
40
+ this.addChildren(afterIndex, definitions);
41
+
42
+ return this.root;
43
+ }
44
+
45
+ /**
46
+ * Removes the {@link RepeatInstance}s corresponding to the specified range of
47
+ * indexes, and then removes those repeat instances from the repeat range's
48
+ * own children state in that order:
49
+ *
50
+ * 1. Identify the set of {@link RepeatInstance}s to be removed.
51
+ *
52
+ * 2. For each {@link RepeatInstance} pending removal, perform that node's
53
+ * removal logic. @see {@link RepeatInstance.remove} for more detail.
54
+ *
55
+ * 3. Finalize update to the repeat range's own {@link childrenState},
56
+ * omitting those {@link RepeatInstance}s which were removed.
57
+ *
58
+ * This ordering ensures a consistent representation of state is established
59
+ * prior to any downstream reactive updates, and ensures that removed nodes'
60
+ * reactivity is cleaned up.
61
+ */
62
+ removeInstances(startIndex: number, count = 1): Root {
63
+ this.removeChildren(startIndex, count);
64
+
65
+ return this.root;
66
+ }
67
+ }