@getodk/xforms-engine 0.1.1 → 0.2.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 (101) hide show
  1. package/dist/body/BodyDefinition.d.ts +24 -7
  2. package/dist/body/RepeatElementDefinition.d.ts +19 -0
  3. package/dist/body/appearance/inputAppearanceParser.d.ts +4 -0
  4. package/dist/body/appearance/selectAppearanceParser.d.ts +4 -0
  5. package/dist/body/appearance/structureElementAppearanceParser.d.ts +4 -0
  6. package/dist/body/control/ControlDefinition.d.ts +2 -0
  7. package/dist/body/control/InputDefinition.d.ts +5 -0
  8. package/dist/body/control/select/SelectDefinition.d.ts +11 -1
  9. package/dist/body/group/BaseGroupDefinition.d.ts +3 -8
  10. package/dist/body/text/LabelDefinition.d.ts +2 -0
  11. package/dist/body/text/TextElementDefinition.d.ts +3 -3
  12. package/dist/client/BaseNode.d.ts +6 -1
  13. package/dist/client/GroupNode.d.ts +5 -2
  14. package/dist/client/NodeAppearances.d.ts +15 -0
  15. package/dist/client/RepeatInstanceNode.d.ts +3 -0
  16. package/dist/client/RepeatRangeNode.d.ts +5 -2
  17. package/dist/client/RootNode.d.ts +19 -0
  18. package/dist/client/SelectNode.d.ts +3 -0
  19. package/dist/client/StringNode.d.ts +3 -0
  20. package/dist/client/SubtreeNode.d.ts +1 -0
  21. package/dist/index.js +624 -368
  22. package/dist/index.js.map +1 -1
  23. package/dist/instance/Group.d.ts +3 -3
  24. package/dist/instance/RepeatInstance.d.ts +26 -2
  25. package/dist/instance/RepeatRange.d.ts +84 -5
  26. package/dist/instance/Root.d.ts +8 -23
  27. package/dist/instance/SelectField.d.ts +2 -2
  28. package/dist/instance/StringField.d.ts +2 -2
  29. package/dist/instance/Subtree.d.ts +1 -1
  30. package/dist/instance/abstract/DescendantNode.d.ts +12 -4
  31. package/dist/instance/abstract/InstanceNode.d.ts +26 -29
  32. package/dist/instance/internal-api/EvaluationContext.d.ts +5 -4
  33. package/dist/instance/internal-api/ValueContext.d.ts +2 -2
  34. package/dist/lib/TokenListParser.d.ts +84 -0
  35. package/dist/lib/dom/query.d.ts +5 -0
  36. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +2 -1
  37. package/dist/model/DescendentNodeDefinition.d.ts +1 -2
  38. package/dist/model/NodeDefinition.d.ts +12 -12
  39. package/dist/model/RepeatInstanceDefinition.d.ts +5 -6
  40. package/dist/model/{RepeatSequenceDefinition.d.ts → RepeatRangeDefinition.d.ts} +4 -4
  41. package/dist/model/RepeatTemplateDefinition.d.ts +6 -7
  42. package/dist/model/RootDefinition.d.ts +3 -1
  43. package/dist/model/SubtreeDefinition.d.ts +2 -2
  44. package/dist/model/ValueNodeDefinition.d.ts +2 -3
  45. package/dist/solid.js +625 -369
  46. package/dist/solid.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/XFormDOM.ts +81 -8
  49. package/src/body/BodyDefinition.ts +38 -23
  50. package/src/body/RepeatElementDefinition.ts +70 -0
  51. package/src/body/appearance/inputAppearanceParser.ts +39 -0
  52. package/src/body/appearance/selectAppearanceParser.ts +38 -0
  53. package/src/body/appearance/structureElementAppearanceParser.ts +7 -0
  54. package/src/body/control/ControlDefinition.ts +4 -0
  55. package/src/body/control/InputDefinition.ts +13 -0
  56. package/src/body/control/select/SelectDefinition.ts +14 -5
  57. package/src/body/group/BaseGroupDefinition.ts +11 -49
  58. package/src/body/text/LabelDefinition.ts +15 -1
  59. package/src/body/text/TextElementDefinition.ts +5 -5
  60. package/src/client/BaseNode.ts +9 -1
  61. package/src/client/GroupNode.ts +6 -2
  62. package/src/client/NodeAppearances.ts +22 -0
  63. package/src/client/RepeatInstanceNode.ts +4 -0
  64. package/src/client/RepeatRangeNode.ts +6 -2
  65. package/src/client/RootNode.ts +22 -0
  66. package/src/client/SelectNode.ts +4 -0
  67. package/src/client/StringNode.ts +4 -0
  68. package/src/client/SubtreeNode.ts +1 -0
  69. package/src/instance/Group.ts +14 -9
  70. package/src/instance/RepeatInstance.ts +59 -15
  71. package/src/instance/RepeatRange.ts +133 -15
  72. package/src/instance/Root.ts +20 -64
  73. package/src/instance/SelectField.ts +7 -7
  74. package/src/instance/StringField.ts +8 -7
  75. package/src/instance/Subtree.ts +10 -7
  76. package/src/instance/abstract/DescendantNode.ts +45 -43
  77. package/src/instance/abstract/InstanceNode.ts +69 -86
  78. package/src/instance/children.ts +17 -7
  79. package/src/instance/index.ts +1 -1
  80. package/src/instance/internal-api/EvaluationContext.ts +5 -6
  81. package/src/instance/internal-api/ValueContext.ts +2 -2
  82. package/src/lib/TokenListParser.ts +156 -0
  83. package/src/lib/dom/query.ts +13 -0
  84. package/src/lib/reactivity/createChildrenState.ts +51 -6
  85. package/src/lib/reactivity/createComputedExpression.ts +1 -1
  86. package/src/lib/reactivity/createSelectItems.ts +4 -6
  87. package/src/lib/reactivity/createValueState.ts +6 -6
  88. package/src/lib/reactivity/materializeCurrentStateChildren.ts +3 -1
  89. package/src/model/DescendentNodeDefinition.ts +1 -2
  90. package/src/model/ModelDefinition.ts +1 -1
  91. package/src/model/NodeDefinition.ts +12 -12
  92. package/src/model/RepeatInstanceDefinition.ts +8 -13
  93. package/src/model/{RepeatSequenceDefinition.ts → RepeatRangeDefinition.ts} +6 -6
  94. package/src/model/RepeatTemplateDefinition.ts +10 -15
  95. package/src/model/RootDefinition.ts +6 -12
  96. package/src/model/SubtreeDefinition.ts +3 -3
  97. package/src/model/ValueNodeDefinition.ts +2 -3
  98. package/dist/body/RepeatDefinition.d.ts +0 -16
  99. package/dist/body/group/RepeatGroupDefinition.d.ts +0 -13
  100. package/src/body/RepeatDefinition.ts +0 -54
  101. package/src/body/group/RepeatGroupDefinition.ts +0 -91
@@ -3,10 +3,10 @@ import type { XFormDefinition } from '../../XFormDefinition.ts';
3
3
  import { type AnyDependentExpression } from '../../expression/DependentExpression.ts';
4
4
  import type { AnyGroupElementDefinition } from '../BodyDefinition.ts';
5
5
  import { BodyElementDefinition } from '../BodyElementDefinition.ts';
6
- import type { InputDefinition } from '../control/InputDefinition.ts';
6
+ import type { RepeatElementDefinition } from '../RepeatElementDefinition.ts';
7
+ import type { AnyControlDefinition } from '../control/ControlDefinition.ts';
7
8
  import type { ItemDefinition } from '../control/select/ItemDefinition.ts';
8
9
  import type { ItemsetDefinition } from '../control/select/ItemsetDefinition.ts';
9
- import type { AnySelectDefinition } from '../control/select/SelectDefinition.ts';
10
10
  import { TextElementOutputPart } from './TextElementOutputPart.ts';
11
11
  import { TextElementReferencePart } from './TextElementReferencePart.ts';
12
12
  import { TextElementStaticPart } from './TextElementStaticPart.ts';
@@ -18,11 +18,11 @@ export interface TextElement extends Element {
18
18
  }
19
19
 
20
20
  export type TextElementOwner =
21
+ | AnyControlDefinition
21
22
  | AnyGroupElementDefinition
22
- | AnySelectDefinition
23
- | InputDefinition
24
23
  | ItemDefinition
25
- | ItemsetDefinition;
24
+ | ItemsetDefinition
25
+ | RepeatElementDefinition;
26
26
 
27
27
  export type TextElementChild = TextElementOutputPart | TextElementStaticPart;
28
28
 
@@ -1,7 +1,9 @@
1
+ import type { TokenListParser } from '../lib/TokenListParser.ts';
1
2
  import type { AnyNodeDefinition } from '../model/NodeDefinition.ts';
2
- import type { InstanceNodeType } from './node-types.js';
3
+ import type { NodeAppearances } from './NodeAppearances.ts';
3
4
  import type { OpaqueReactiveObjectFactory } from './OpaqueReactiveObjectFactory.ts';
4
5
  import type { TextRange } from './TextRange.ts';
6
+ import type { InstanceNodeType } from './node-types.ts';
5
7
 
6
8
  export interface BaseNodeState {
7
9
  /**
@@ -126,6 +128,12 @@ export interface BaseNode {
126
128
  */
127
129
  readonly nodeId: FormNodeID;
128
130
 
131
+ /**
132
+ * @see {@link TokenListParser} for details.
133
+ */
134
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
135
+ readonly appearances: NodeAppearances<any> | null;
136
+
129
137
  /**
130
138
  * Each node has a definition which specifies aspects of the node defined in
131
139
  * the form. These aspects include (but are not limited to) the node's data
@@ -1,6 +1,7 @@
1
- import type { NonRepeatGroupElementDefinition } from '../body/BodyDefinition.ts';
1
+ import type { AnyGroupElementDefinition } from '../body/BodyDefinition.ts';
2
2
  import type { SubtreeDefinition } from '../model/SubtreeDefinition.ts';
3
3
  import type { BaseNode, BaseNodeState } from './BaseNode.ts';
4
+ import type { NodeAppearances } from './NodeAppearances.ts';
4
5
  import type { RootNode } from './RootNode.ts';
5
6
  import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts';
6
7
 
@@ -14,9 +15,11 @@ export interface GroupNodeState extends BaseNodeState {
14
15
  // TODO: as with `SubtreeNode`'s `SubtreeDefinition`, there is a naming
15
16
  // inconsistency emerging here.
16
17
  export interface GroupDefinition extends SubtreeDefinition {
17
- readonly bodyElement: NonRepeatGroupElementDefinition;
18
+ readonly bodyElement: AnyGroupElementDefinition;
18
19
  }
19
20
 
21
+ export type GroupNodeAppearances = NodeAppearances<GroupDefinition>;
22
+
20
23
  /**
21
24
  * A node corresponding to an XForms `<group>`.
22
25
  */
@@ -26,6 +29,7 @@ export interface GroupDefinition extends SubtreeDefinition {
26
29
  // for context.
27
30
  export interface GroupNode extends BaseNode {
28
31
  readonly nodeType: 'group';
32
+ readonly appearances: GroupNodeAppearances;
29
33
  readonly definition: GroupDefinition;
30
34
  readonly root: RootNode;
31
35
  readonly parent: GeneralParentNode;
@@ -0,0 +1,22 @@
1
+ import type { ParsedTokenList } from '../lib/TokenListParser.ts';
2
+ import type { NodeDefinition } from '../model/NodeDefinition.ts';
3
+
4
+ /**
5
+ * - Provides a means to distinguish between internal and client-facing names
6
+ * for the same {@link ParsedTokenList} types.
7
+ *
8
+ * - Anticipates some iteration on both parsed ("definition") types and
9
+ * client-facing node types, which may not happen in tandem.
10
+ */
11
+ // prettier-ignore
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ export type NodeAppearances<Definition extends NodeDefinition<any>> =
14
+ Definition extends {
15
+ readonly bodyElement: {
16
+ readonly appearances:
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ infer Appearances extends ParsedTokenList<any>
19
+ };
20
+ }
21
+ ? Appearances
22
+ : null;
@@ -1,6 +1,7 @@
1
1
  import type { RepeatInstanceDefinition } from '../model/RepeatInstanceDefinition.ts';
2
2
  import type { RepeatTemplateDefinition } from '../model/RepeatTemplateDefinition.ts';
3
3
  import type { BaseNode, BaseNodeState } from './BaseNode.ts';
4
+ import type { NodeAppearances } from './NodeAppearances.ts';
4
5
  import type { RepeatRangeNode } from './RepeatRangeNode.ts';
5
6
  import type { RootNode } from './RootNode.ts';
6
7
  import type { GeneralChildNode } from './hierarchy.ts';
@@ -22,8 +23,11 @@ export type RepeatDefinition =
22
23
  | RepeatInstanceDefinition
23
24
  | RepeatTemplateDefinition;
24
25
 
26
+ export type RepeatInstanceNodeAppearances = NodeAppearances<RepeatDefinition>;
27
+
25
28
  export interface RepeatInstanceNode extends BaseNode {
26
29
  readonly nodeType: 'repeat-instance';
30
+ readonly appearances: RepeatInstanceNodeAppearances;
27
31
  readonly definition: RepeatDefinition;
28
32
  readonly root: RootNode;
29
33
 
@@ -1,5 +1,6 @@
1
- import type { RepeatSequenceDefinition } from '../model/RepeatSequenceDefinition.ts';
1
+ import type { RepeatRangeDefinition } from '../model/RepeatRangeDefinition.ts';
2
2
  import type { BaseNode, BaseNodeState } from './BaseNode.ts';
3
+ import type { NodeAppearances } from './NodeAppearances.ts';
3
4
  import type { RepeatInstanceNode } from './RepeatInstanceNode.ts';
4
5
  import type { RootNode } from './RootNode.ts';
5
6
  import type { TextRange } from './TextRange.ts';
@@ -23,6 +24,8 @@ export interface RepeatRangeNodeState extends BaseNodeState {
23
24
  get value(): null;
24
25
  }
25
26
 
27
+ export type RepeatRangeNodeAppearances = NodeAppearances<RepeatRangeDefinition>;
28
+
26
29
  /**
27
30
  * Represents a contiguous set of zero or more {@link RepeatInstanceNode}s
28
31
  * (accessed by its
@@ -89,7 +92,8 @@ export interface RepeatRangeNodeState extends BaseNodeState {
89
92
  */
90
93
  export interface RepeatRangeNode extends BaseNode {
91
94
  readonly nodeType: 'repeat-range';
92
- readonly definition: RepeatSequenceDefinition;
95
+ readonly appearances: RepeatRangeNodeAppearances;
96
+ readonly definition: RepeatRangeDefinition;
93
97
  readonly root: RootNode;
94
98
  readonly parent: GeneralParentNode;
95
99
  readonly currentState: RepeatRangeNodeState;
@@ -1,3 +1,4 @@
1
+ import type { BodyClassList } from '../body/BodyDefinition.ts';
1
2
  import type { RootDefinition } from '../model/RootDefinition.ts';
2
3
  import type { BaseNode, BaseNodeState } from './BaseNode.ts';
3
4
  import type { ActiveLanguage, FormLanguage, FormLanguages } from './FormLanguage.ts';
@@ -21,6 +22,27 @@ export interface RootNodeState extends BaseNodeState {
21
22
 
22
23
  export interface RootNode extends BaseNode {
23
24
  readonly nodeType: 'root';
25
+
26
+ /**
27
+ * @todo this along with {@link classes} is... awkward.
28
+ */
29
+ readonly appearances: null;
30
+
31
+ /**
32
+ * @todo This is another odd deviation in {@link RootNode}. Unlike
33
+ * {@link languages}, it doesn't feel particularly **essential**. While it
34
+ * would deviate from XForms spec terminology, it seems like it _might be
35
+ * reasonable_ to instead convey `<h:body class="...">` as
36
+ * {@link RootNode.appearances} in the client interface. They do have slightly
37
+ * different spec semantics (i.e. a body class can be anything, to trigger
38
+ * styling in a form UI). But the **most likely anticipated** use case in Web
39
+ * Forms would be the "pages" class, and perhaps "theme-grid". The former is
40
+ * definitely conceptually similar to a XForms `appearance` (albeit
41
+ * form-global, which is not a spec concept). The latter does as well, and we
42
+ * already anticipate applying that concept in non-form-global ways.
43
+ */
44
+ readonly classes: BodyClassList;
45
+
24
46
  readonly definition: RootDefinition;
25
47
  readonly root: RootNode;
26
48
  readonly parent: null;
@@ -1,6 +1,7 @@
1
1
  import type { AnySelectDefinition } from '../body/control/select/SelectDefinition.ts';
2
2
  import type { ValueNodeDefinition } from '../model/ValueNodeDefinition.ts';
3
3
  import type { BaseNode, BaseNodeState } from './BaseNode.ts';
4
+ import type { NodeAppearances } from './NodeAppearances.ts';
4
5
  import type { RootNode } from './RootNode.ts';
5
6
  import type { StringNode } from './StringNode.ts';
6
7
  import type { TextRange } from './TextRange.ts';
@@ -38,8 +39,11 @@ export interface SelectDefinition extends ValueNodeDefinition {
38
39
  readonly bodyElement: AnySelectDefinition;
39
40
  }
40
41
 
42
+ export type SelectNodeAppearances = NodeAppearances<SelectDefinition>;
43
+
41
44
  export interface SelectNode extends BaseNode {
42
45
  readonly nodeType: 'select';
46
+ readonly appearances: SelectNodeAppearances;
43
47
  readonly definition: SelectDefinition;
44
48
  readonly root: RootNode;
45
49
  readonly parent: GeneralParentNode;
@@ -1,6 +1,7 @@
1
1
  import type { InputDefinition } from '../body/control/InputDefinition.ts';
2
2
  import type { ValueNodeDefinition } from '../model/ValueNodeDefinition.ts';
3
3
  import type { BaseNode, BaseNodeState } from './BaseNode.ts';
4
+ import type { NodeAppearances } from './NodeAppearances.ts';
4
5
  import type { RootNode } from './RootNode.ts';
5
6
  import type { GeneralParentNode } from './hierarchy.ts';
6
7
 
@@ -21,6 +22,8 @@ export interface StringDefinition extends ValueNodeDefinition {
21
22
  readonly bodyElement: InputDefinition | null;
22
23
  }
23
24
 
25
+ export type StringNodeAppearances = NodeAppearances<StringDefinition>;
26
+
24
27
  /**
25
28
  * A node which can be assigned a string/text value. A string node **MAY**
26
29
  * correspond to form field defined as an XForms `<input>`, which a user-facing
@@ -30,6 +33,7 @@ export interface StringDefinition extends ValueNodeDefinition {
30
33
  */
31
34
  export interface StringNode extends BaseNode {
32
35
  readonly nodeType: 'string';
36
+ readonly appearances: StringNodeAppearances;
33
37
  readonly definition: StringDefinition;
34
38
  readonly root: RootNode;
35
39
  readonly parent: GeneralParentNode;
@@ -50,6 +50,7 @@ export interface SubtreeDefinition extends BaseSubtreeDefinition {
50
50
  // TODO: directly test presentation of non-group subtree children/descendants
51
51
  export interface SubtreeNode extends BaseNode {
52
52
  readonly nodeType: 'subtree';
53
+ readonly appearances: null;
53
54
  readonly definition: SubtreeDefinition;
54
55
  readonly root: RootNode;
55
56
  readonly parent: GeneralParentNode;
@@ -1,5 +1,5 @@
1
1
  import type { Accessor } from 'solid-js';
2
- import type { GroupDefinition, GroupNode } from '../client/GroupNode.ts';
2
+ import type { GroupDefinition, GroupNode, GroupNodeAppearances } from '../client/GroupNode.ts';
3
3
  import type { TextRange } from '../index.ts';
4
4
  import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
5
5
  import { createChildrenState } from '../lib/reactivity/createChildrenState.ts';
@@ -38,13 +38,15 @@ export class Group
38
38
  protected override engineState: EngineState<GroupStateSpec>;
39
39
 
40
40
  // GroupNode
41
- readonly currentState: MaterializedChildren<CurrentState<GroupStateSpec>, GeneralChildNode>;
42
-
43
41
  readonly nodeType = 'group';
42
+ readonly appearances: GroupNodeAppearances;
43
+ readonly currentState: MaterializedChildren<CurrentState<GroupStateSpec>, GeneralChildNode>;
44
44
 
45
45
  constructor(parent: GeneralParentNode, definition: GroupDefinition) {
46
46
  super(parent, definition);
47
47
 
48
+ this.appearances = definition.bodyElement.appearances;
49
+
48
50
  const childrenState = createChildrenState<Group, GeneralChildNode>(this);
49
51
 
50
52
  this.childrenState = childrenState;
@@ -52,7 +54,10 @@ export class Group
52
54
  const state = createSharedNodeState(
53
55
  this.scope,
54
56
  {
55
- ...this.buildSharedStateSpec(parent, definition),
57
+ reference: this.contextReference,
58
+ readonly: this.isReadonly,
59
+ relevant: this.isRelevant,
60
+ required: this.isRequired,
56
61
 
57
62
  label: createNodeLabel(this, definition),
58
63
  hint: null,
@@ -67,15 +72,15 @@ export class Group
67
72
 
68
73
  this.state = state;
69
74
  this.engineState = state.engineState;
70
- this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
75
+ this.currentState = materializeCurrentStateChildren(
76
+ this.scope,
77
+ state.currentState,
78
+ childrenState
79
+ );
71
80
 
72
81
  childrenState.setChildren(buildChildren(this));
73
82
  }
74
83
 
75
- protected computeReference(parent: GeneralParentNode): string {
76
- return this.computeChildStepReference(parent);
77
- }
78
-
79
84
  getChildren(): readonly GeneralChildNode[] {
80
85
  return this.childrenState.getChildren();
81
86
  }
@@ -1,6 +1,10 @@
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';
3
+ import type {
4
+ RepeatDefinition,
5
+ RepeatInstanceNode,
6
+ RepeatInstanceNodeAppearances,
7
+ } from '../client/RepeatInstanceNode.ts';
4
8
  import type { TextRange } from '../index.ts';
5
9
  import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
6
10
  import { createChildrenState } from '../lib/reactivity/createChildrenState.ts';
@@ -46,9 +50,39 @@ export class RepeatInstance
46
50
  protected readonly state: SharedNodeState<RepeatInstanceStateSpec>;
47
51
  protected override engineState: EngineState<RepeatInstanceStateSpec>;
48
52
 
53
+ /**
54
+ * @todo Should we special case repeat `readonly` inheritance the same way
55
+ * we do for `relevant`?
56
+ *
57
+ * @see {@link hasNonRelevantAncestor}
58
+ */
59
+ declare readonly hasReadonlyAncestor: Accessor<boolean>;
60
+
61
+ /**
62
+ * A repeat instance can inherit non-relevance, just like any other node. That
63
+ * inheritance is derived from the repeat instance's parent node in the
64
+ * primary instance XML/DOM tree (and would be semantically expected to do so
65
+ * even if we move away from that implementation detail).
66
+ *
67
+ * Since {@link RepeatInstance.parent} is a {@link RepeatRange}, which is a
68
+ * runtime data model fiction that does not exist in that hierarchy, we pass
69
+ * this call through, allowing the {@link RepeatRange} to check the actual
70
+ * primary instance parent node's relevance state.
71
+ *
72
+ * @todo Should we apply similar reasoning in {@link hasReadonlyAncestor}?
73
+ */
74
+ override readonly hasNonRelevantAncestor: Accessor<boolean> = () => {
75
+ return this.parent.hasNonRelevantAncestor();
76
+ };
77
+
49
78
  // RepeatInstanceNode
50
79
  readonly nodeType = 'repeat-instance';
51
80
 
81
+ /**
82
+ * @see {@link RepeatRange.appearances}
83
+ */
84
+ readonly appearances: RepeatInstanceNodeAppearances;
85
+
52
86
  readonly currentState: MaterializedChildren<
53
87
  CurrentState<RepeatInstanceStateSpec>,
54
88
  GeneralChildNode
@@ -59,7 +93,20 @@ export class RepeatInstance
59
93
  definition: RepeatDefinition,
60
94
  options: RepeatInstanceOptions
61
95
  ) {
62
- super(parent, definition);
96
+ const { precedingInstance } = options;
97
+ const precedingIndex = precedingInstance?.currentIndex ?? (() => -1);
98
+ const initialIndex = precedingIndex() + 1;
99
+ const [currentIndex, setCurrentIndex] = createSignal(initialIndex);
100
+
101
+ super(parent, definition, {
102
+ computeReference: (): string => {
103
+ const currentPosition = currentIndex() + 1;
104
+
105
+ return `${parent.contextReference()}[${currentPosition}]`;
106
+ },
107
+ });
108
+
109
+ this.appearances = definition.bodyElement.appearances;
63
110
 
64
111
  const childrenState = createChildrenState<RepeatInstance, GeneralChildNode>(this);
65
112
 
@@ -67,18 +114,17 @@ export class RepeatInstance
67
114
 
68
115
  options.precedingPrimaryInstanceNode.after(this.contextNode);
69
116
 
70
- const { precedingInstance } = options;
71
- const precedingIndex = precedingInstance?.currentIndex ?? (() => -1);
72
- const initialIndex = precedingIndex() + 1;
73
- const [currentIndex, setCurrentIndex] = createSignal(initialIndex);
74
-
75
117
  this.currentIndex = currentIndex;
76
118
 
77
119
  const state = createSharedNodeState(
78
120
  this.scope,
79
121
  {
80
- ...this.buildSharedStateSpec(parent, definition),
122
+ reference: this.contextReference,
123
+ readonly: this.isReadonly,
124
+ relevant: this.isRelevant,
125
+ required: this.isRequired,
81
126
 
127
+ // TODO: only-child <group><label>
82
128
  label: createNodeLabel(this, definition),
83
129
  hint: null,
84
130
  children: childrenState.childIds,
@@ -92,7 +138,11 @@ export class RepeatInstance
92
138
 
93
139
  this.state = state;
94
140
  this.engineState = state.engineState;
95
- this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
141
+ this.currentState = materializeCurrentStateChildren(
142
+ this.scope,
143
+ state.currentState,
144
+ childrenState
145
+ );
96
146
 
97
147
  // Maintain current index state, updating as the parent range's children
98
148
  // state is changed. Notable Solid reactivity nuances:
@@ -118,12 +168,6 @@ export class RepeatInstance
118
168
  childrenState.setChildren(buildChildren(this));
119
169
  }
120
170
 
121
- protected computeReference(parent: RepeatRange): string {
122
- const currentPosition = this.currentIndex() + 1;
123
-
124
- return `${parent.contextReference}[${currentPosition}]`;
125
- }
126
-
127
171
  protected override initializeContextNode(parentContextNode: Element, nodeName: string): Element {
128
172
  return this.createContextNode(parentContextNode, nodeName);
129
173
  }
@@ -1,8 +1,9 @@
1
1
  import { insertAtIndex } from '@getodk/common/lib/array/insert.ts';
2
2
  import type { Accessor } from 'solid-js';
3
- import type { RepeatRangeNode } from '../client/RepeatRangeNode.ts';
3
+ import type { RepeatRangeNode, RepeatRangeNodeAppearances } from '../client/RepeatRangeNode.ts';
4
4
  import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
5
5
  import { createChildrenState } from '../lib/reactivity/createChildrenState.ts';
6
+ import { createComputedExpression } from '../lib/reactivity/createComputedExpression.ts';
6
7
  import type { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
7
8
  import { materializeCurrentStateChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
8
9
  import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
@@ -10,7 +11,7 @@ import type { EngineState } from '../lib/reactivity/node-state/createEngineState
10
11
  import type { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
11
12
  import { createSharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
12
13
  import { createNodeLabel } from '../lib/reactivity/text/createNodeLabel.ts';
13
- import type { RepeatSequenceDefinition } from '../model/RepeatSequenceDefinition.ts';
14
+ import type { RepeatRangeDefinition } from '../model/RepeatRangeDefinition.ts';
14
15
  import type { RepeatDefinition } from './RepeatInstance.ts';
15
16
  import { RepeatInstance } from './RepeatInstance.ts';
16
17
  import type { Root } from './Root.ts';
@@ -31,7 +32,7 @@ interface RepeatRangeStateSpec extends DescendantNodeSharedStateSpec {
31
32
  }
32
33
 
33
34
  export class RepeatRange
34
- extends DescendantNode<RepeatSequenceDefinition, RepeatRangeStateSpec, RepeatInstance>
35
+ extends DescendantNode<RepeatRangeDefinition, RepeatRangeStateSpec, RepeatInstance>
35
36
  implements RepeatRangeNode, EvaluationContext, SubscribableDependency
36
37
  {
37
38
  /**
@@ -64,22 +65,144 @@ export class RepeatRange
64
65
  protected readonly state: SharedNodeState<RepeatRangeStateSpec>;
65
66
  protected override engineState: EngineState<RepeatRangeStateSpec>;
66
67
 
68
+ /**
69
+ * @todo Should we special case repeat `readonly` state the same way
70
+ * we do for `relevant`?
71
+ *
72
+ * @see {@link isSelfRelevant}
73
+ */
74
+ declare isSelfReadonly: Accessor<boolean>;
75
+
76
+ private readonly emptyRangeEvaluationContext: EvaluationContext & {
77
+ readonly contextNode: Comment;
78
+ };
79
+
80
+ /**
81
+ * @see {@link isSelfRelevant}
82
+ */
83
+ private readonly isEmptyRangeSelfRelevant: Accessor<boolean>;
84
+
85
+ /**
86
+ * A repeat range does not exist in the primary instance tree. A `relevant`
87
+ * expression applies to each {@link RepeatInstance} child of the repeat
88
+ * range. Determining whether a repeat range itself "is relevant" isn't a
89
+ * concept the spec addresses, but it may be used by clients to determine
90
+ * whether to allow interaction with the range (e.g. by adding a repeat
91
+ * instance, or presenting the range's label when empty).
92
+ *
93
+ * As a naive first pass, it seems like the heuristic for this should be:
94
+ *
95
+ * 1. Does the repeat range have any repeat instance children?
96
+ *
97
+ * - If yes, go to 2.
98
+ * - If no, go to 3.
99
+ *
100
+ * 2. Does one or more of those children return `true` for the node's
101
+ * `relevant` expression (i.e. is the repeat instance "self relevant")?
102
+ *
103
+ * 3. Does the relevant expression return `true` for the repeat range itself
104
+ * (where, at least for now, the context of that evaluation would be the
105
+ * repeat range's {@link anchorNode} to ensure correct relative expressions
106
+ * resolve correctly)?
107
+ *
108
+ * @todo While (3) is proactively implemented, there isn't presently a test
109
+ * exercising it. It felt best for now to surface this for discussion in
110
+ * review to validate that it's going in the right direction.
111
+ *
112
+ * @todo While (2) **is actually tested**, the tests currently in place behave
113
+ * the same way with only the logic for (3), regardless of whether the repeat
114
+ * range actually has any repeat instance children. It's unclear (a) if that's
115
+ * a preferable simplification and (b) how that might affect performance (in
116
+ * theory it could vary depending on form structure and runtime state).
117
+ */
118
+ override readonly isSelfRelevant: Accessor<boolean> = () => {
119
+ const instances = this.childrenState.getChildren();
120
+
121
+ if (instances.length > 0) {
122
+ return instances.some((instance) => instance.isSelfRelevant());
123
+ }
124
+
125
+ return this.isEmptyRangeSelfRelevant();
126
+ };
127
+
67
128
  // RepeatRangeNode
68
129
  readonly nodeType = 'repeat-range';
69
130
 
131
+ /**
132
+ * @todo RepeatRange*, RepeatInstance* (and RepeatTemplate*) all share the
133
+ * same body element, and thus all share the same definition `bodyElement`. As
134
+ * such, they also all share the same `appearances`. At time of writing,
135
+ * `web-forms` (Vue UI package) treats a `RepeatRangeNode`...
136
+ *
137
+ * - ... as a group, if the node has a label (i.e.
138
+ * `<group><label/><repeat/></group>`)
139
+ * - ... effectively as a fragment containing only its instances, otherwise
140
+ *
141
+ * We now collapse `<group><repeat>` into `<repeat>`, and no longer treat
142
+ * "repeat group" as a concept (after parsing). According to the spec, these
143
+ * appearances **are supposed to** come from that "repeat group" in the form
144
+ * definition. In practice, many forms do define appearances directly on a
145
+ * repeat element. The engine currently produces an error if both are defined
146
+ * simultaneously, but otherwise makes no distinction between appearances in
147
+ * these form definition shapes:
148
+ *
149
+ * ```xml
150
+ * <group ref="/data/rep1" appearance="...">
151
+ * <repeat nodeset="/data/rep1"/>
152
+ * </group>
153
+ *
154
+ * <group ref="/data/rep1">
155
+ * <repeat nodeset="/data/rep1"/ appearance="...">
156
+ * </group>
157
+ *
158
+ * <repeat nodeset="/data/rep1"/ appearance="...">
159
+ * ```
160
+ *
161
+ * All of the above creates considerable ambiguity about where "repeat
162
+ * appearances" should apply, under which circumstances.
163
+ */
164
+ readonly appearances: RepeatRangeNodeAppearances;
165
+
70
166
  readonly currentState: MaterializedChildren<CurrentState<RepeatRangeStateSpec>, RepeatInstance>;
71
167
 
72
- constructor(parent: GeneralParentNode, definition: RepeatSequenceDefinition) {
168
+ constructor(parent: GeneralParentNode, definition: RepeatRangeDefinition) {
73
169
  super(parent, definition);
74
170
 
171
+ this.appearances = definition.bodyElement.appearances;
172
+
75
173
  const childrenState = createChildrenState<RepeatRange, RepeatInstance>(this);
76
174
 
77
175
  this.childrenState = childrenState;
78
176
 
177
+ this.anchorNode = this.contextNode.ownerDocument.createComment(
178
+ `Begin repeat range: ${definition.nodeset}`
179
+ );
180
+ this.contextNode.append(this.anchorNode);
181
+
182
+ this.emptyRangeEvaluationContext = {
183
+ scope: this.scope,
184
+ evaluator: this.evaluator,
185
+ root: this.root,
186
+ contextReference: this.contextReference,
187
+ contextNode: this.anchorNode,
188
+
189
+ getSubscribableDependenciesByReference: (reference) => {
190
+ return this.getSubscribableDependenciesByReference(reference);
191
+ },
192
+ };
193
+
194
+ this.isEmptyRangeSelfRelevant = createComputedExpression(
195
+ this.emptyRangeEvaluationContext,
196
+ definition.bind.relevant
197
+ );
198
+
79
199
  const state = createSharedNodeState(
80
200
  this.scope,
81
201
  {
82
- ...this.buildSharedStateSpec(parent, definition),
202
+ reference: this.contextReference,
203
+ readonly: this.isReadonly,
204
+ relevant: this.isRelevant,
205
+ required: this.isRequired,
83
206
 
84
207
  label: createNodeLabel(this, definition),
85
208
  hint: null,
@@ -92,14 +215,13 @@ export class RepeatRange
92
215
  }
93
216
  );
94
217
 
95
- this.anchorNode = this.contextNode.ownerDocument.createComment(
96
- `Begin repeat range: ${definition.nodeset}`
97
- );
98
- this.contextNode.append(this.anchorNode);
99
-
100
218
  this.state = state;
101
219
  this.engineState = state.engineState;
102
- this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
220
+ this.currentState = materializeCurrentStateChildren(
221
+ this.scope,
222
+ state.currentState,
223
+ childrenState
224
+ );
103
225
 
104
226
  definition.instances.forEach((instanceDefinition, index) => {
105
227
  const afterIndex = index - 1;
@@ -116,10 +238,6 @@ export class RepeatRange
116
238
  return parentContextNode;
117
239
  }
118
240
 
119
- protected computeReference(parent: GeneralParentNode): string {
120
- return this.computeChildStepReference(parent);
121
- }
122
-
123
241
  getInstanceIndex(instance: RepeatInstance): number {
124
242
  return this.engineState.children.indexOf(instance.nodeId);
125
243
  }