@getodk/xforms-engine 0.1.0 → 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.
- package/dist/.vite/manifest.json +1 -0
- package/dist/XFormDOM.d.ts +1 -0
- package/dist/XFormDataType.d.ts +2 -1
- package/dist/XFormDefinition.d.ts +1 -0
- package/dist/body/BodyDefinition.d.ts +27 -9
- package/dist/body/BodyElementDefinition.d.ts +5 -4
- package/dist/body/RepeatElementDefinition.d.ts +19 -0
- package/dist/body/UnsupportedBodyElementDefinition.d.ts +3 -2
- package/dist/body/appearance/inputAppearanceParser.d.ts +4 -0
- package/dist/body/appearance/selectAppearanceParser.d.ts +4 -0
- package/dist/body/appearance/structureElementAppearanceParser.d.ts +4 -0
- package/dist/body/control/ControlDefinition.d.ts +5 -2
- package/dist/body/control/InputDefinition.d.ts +6 -0
- package/dist/body/control/select/ItemDefinition.d.ts +4 -3
- package/dist/body/control/select/ItemsetDefinition.d.ts +4 -3
- package/dist/body/control/select/ItemsetNodesetContext.d.ts +3 -2
- package/dist/body/control/select/ItemsetNodesetExpression.d.ts +2 -1
- package/dist/body/control/select/ItemsetValueExpression.d.ts +2 -1
- package/dist/body/control/select/SelectDefinition.d.ts +16 -5
- package/dist/body/group/BaseGroupDefinition.d.ts +6 -10
- package/dist/body/group/LogicalGroupDefinition.d.ts +1 -0
- package/dist/body/group/PresentationGroupDefinition.d.ts +3 -2
- package/dist/body/group/StructuralGroupDefinition.d.ts +1 -0
- package/dist/body/text/HintDefinition.d.ts +4 -4
- package/dist/body/text/LabelDefinition.d.ts +9 -7
- package/dist/body/text/TextElementDefinition.d.ts +9 -8
- package/dist/body/text/TextElementOutputPart.d.ts +2 -1
- package/dist/body/text/TextElementPart.d.ts +5 -4
- package/dist/body/text/TextElementReferencePart.d.ts +2 -1
- package/dist/body/text/TextElementStaticPart.d.ts +2 -1
- package/dist/client/BaseNode.d.ts +9 -3
- package/dist/client/EngineConfig.d.ts +2 -1
- package/dist/client/GroupNode.d.ts +10 -6
- package/dist/client/NodeAppearances.d.ts +15 -0
- package/dist/client/RepeatInstanceNode.d.ts +10 -6
- package/dist/client/RepeatRangeNode.d.ts +11 -7
- package/dist/client/RootNode.d.ts +24 -4
- package/dist/client/SelectNode.d.ts +10 -6
- package/dist/client/StringNode.d.ts +9 -5
- package/dist/client/SubtreeNode.d.ts +6 -4
- package/dist/client/TextRange.d.ts +2 -1
- package/dist/client/hierarchy.d.ts +9 -8
- package/dist/client/index.d.ts +3 -2
- package/dist/expression/DependencyContext.d.ts +2 -1
- package/dist/expression/DependentExpression.d.ts +3 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1882 -1757
- package/dist/index.js.map +1 -1
- package/dist/instance/Group.d.ts +15 -15
- package/dist/instance/RepeatInstance.d.ts +39 -15
- package/dist/instance/RepeatRange.d.ts +98 -20
- package/dist/instance/Root.d.ts +25 -39
- package/dist/instance/SelectField.d.ts +17 -17
- package/dist/instance/StringField.d.ts +17 -17
- package/dist/instance/Subtree.d.ts +13 -13
- package/dist/instance/abstract/DescendantNode.d.ts +26 -18
- package/dist/instance/abstract/InstanceNode.d.ts +44 -46
- package/dist/instance/children.d.ts +2 -1
- package/dist/instance/hierarchy.d.ts +8 -7
- package/dist/instance/index.d.ts +4 -3
- package/dist/instance/internal-api/EvaluationContext.d.ts +10 -8
- package/dist/instance/internal-api/InstanceConfig.d.ts +3 -2
- package/dist/instance/internal-api/SubscribableDependency.d.ts +2 -1
- package/dist/instance/internal-api/TranslationContext.d.ts +2 -1
- package/dist/instance/internal-api/ValueContext.d.ts +6 -5
- package/dist/instance/resource.d.ts +3 -2
- package/dist/instance/text/TextChunk.d.ts +4 -3
- package/dist/instance/text/TextRange.d.ts +2 -1
- package/dist/lib/TokenListParser.d.ts +84 -0
- package/dist/lib/dom/query.d.ts +8 -2
- package/dist/lib/reactivity/createChildrenState.d.ts +4 -3
- package/dist/lib/reactivity/createComputedExpression.d.ts +4 -3
- package/dist/lib/reactivity/createSelectItems.d.ts +4 -3
- package/dist/lib/reactivity/createValueState.d.ts +3 -2
- package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +6 -4
- package/dist/lib/reactivity/node-state/createClientState.d.ts +7 -6
- package/dist/lib/reactivity/node-state/createCurrentState.d.ts +5 -4
- package/dist/lib/reactivity/node-state/createEngineState.d.ts +4 -3
- package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +7 -6
- package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +2 -1
- package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +3 -2
- package/dist/lib/reactivity/node-state/representations.d.ts +2 -1
- package/dist/lib/reactivity/scope.d.ts +2 -1
- package/dist/lib/reactivity/text/createFieldHint.d.ts +4 -3
- package/dist/lib/reactivity/text/createNodeLabel.d.ts +4 -3
- package/dist/lib/reactivity/text/createTextRange.d.ts +6 -5
- package/dist/lib/reactivity/types.d.ts +2 -1
- package/dist/lib/xpath/analysis.d.ts +2 -1
- package/dist/model/BindComputation.d.ts +2 -1
- package/dist/model/BindDefinition.d.ts +6 -5
- package/dist/model/DescendentNodeDefinition.d.ts +6 -6
- package/dist/model/ModelBindMap.d.ts +4 -3
- package/dist/model/ModelDefinition.d.ts +2 -1
- package/dist/model/NodeDefinition.d.ts +20 -19
- package/dist/model/RepeatInstanceDefinition.d.ts +7 -7
- package/dist/model/{RepeatSequenceDefinition.d.ts → RepeatRangeDefinition.d.ts} +7 -6
- package/dist/model/RepeatTemplateDefinition.d.ts +8 -8
- package/dist/model/RootDefinition.d.ts +8 -5
- package/dist/model/SubtreeDefinition.d.ts +5 -4
- package/dist/model/ValueNodeDefinition.d.ts +5 -5
- package/dist/solid.js +1873 -1751
- package/dist/solid.js.map +1 -1
- package/package.json +14 -18
- package/src/XFormDOM.ts +81 -8
- package/src/body/BodyDefinition.ts +38 -23
- package/src/body/RepeatElementDefinition.ts +70 -0
- package/src/body/appearance/inputAppearanceParser.ts +39 -0
- package/src/body/appearance/selectAppearanceParser.ts +38 -0
- package/src/body/appearance/structureElementAppearanceParser.ts +7 -0
- package/src/body/control/ControlDefinition.ts +4 -0
- package/src/body/control/InputDefinition.ts +13 -0
- package/src/body/control/select/SelectDefinition.ts +14 -5
- package/src/body/group/BaseGroupDefinition.ts +11 -49
- package/src/body/text/LabelDefinition.ts +15 -1
- package/src/body/text/TextElementDefinition.ts +5 -5
- package/src/client/BaseNode.ts +9 -1
- package/src/client/GroupNode.ts +6 -2
- package/src/client/NodeAppearances.ts +22 -0
- package/src/client/RepeatInstanceNode.ts +4 -0
- package/src/client/RepeatRangeNode.ts +6 -2
- package/src/client/RootNode.ts +22 -0
- package/src/client/SelectNode.ts +4 -0
- package/src/client/StringNode.ts +4 -0
- package/src/client/SubtreeNode.ts +1 -0
- package/src/instance/Group.ts +14 -9
- package/src/instance/RepeatInstance.ts +59 -15
- package/src/instance/RepeatRange.ts +133 -15
- package/src/instance/Root.ts +20 -64
- package/src/instance/SelectField.ts +7 -7
- package/src/instance/StringField.ts +8 -7
- package/src/instance/Subtree.ts +10 -7
- package/src/instance/abstract/DescendantNode.ts +45 -43
- package/src/instance/abstract/InstanceNode.ts +69 -86
- package/src/instance/children.ts +17 -7
- package/src/instance/index.ts +1 -1
- package/src/instance/internal-api/EvaluationContext.ts +5 -6
- package/src/instance/internal-api/ValueContext.ts +2 -2
- package/src/lib/TokenListParser.ts +156 -0
- package/src/lib/dom/query.ts +13 -0
- package/src/lib/reactivity/createChildrenState.ts +51 -6
- package/src/lib/reactivity/createComputedExpression.ts +1 -1
- package/src/lib/reactivity/createSelectItems.ts +4 -6
- package/src/lib/reactivity/createValueState.ts +6 -6
- package/src/lib/reactivity/materializeCurrentStateChildren.ts +3 -1
- package/src/model/DescendentNodeDefinition.ts +1 -2
- package/src/model/ModelDefinition.ts +1 -1
- package/src/model/NodeDefinition.ts +12 -12
- package/src/model/RepeatInstanceDefinition.ts +8 -13
- package/src/model/{RepeatSequenceDefinition.ts → RepeatRangeDefinition.ts} +6 -6
- package/src/model/RepeatTemplateDefinition.ts +10 -15
- package/src/model/RootDefinition.ts +6 -12
- package/src/model/SubtreeDefinition.ts +3 -3
- package/src/model/ValueNodeDefinition.ts +2 -3
- package/dist/body/RepeatDefinition.d.ts +0 -15
- package/dist/body/group/RepeatGroupDefinition.d.ts +0 -12
- package/src/body/RepeatDefinition.ts +0 -54
- package/src/body/group/RepeatGroupDefinition.ts +0 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getodk/xforms-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "XForms engine for ODK Web Forms",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": "^18.
|
|
32
|
+
"node": "^18.20.3 || ^20.13.1 || ^22.2.0",
|
|
33
33
|
"yarn": "1.22.19"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
@@ -54,30 +54,26 @@
|
|
|
54
54
|
"test:types": "tsc --project ./tsconfig.json --emitDeclarationOnly false --noEmit"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"solid-js": "^1.8.
|
|
57
|
+
"solid-js": "^1.8.17"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@babel/core": "^7.
|
|
61
|
-
"@getodk/tree-sitter-xpath": "0.1.
|
|
62
|
-
"@getodk/xpath": "0.1.
|
|
63
|
-
"@playwright/test": "^1.
|
|
64
|
-
"@
|
|
65
|
-
"@vitest/browser": "^1.3.1",
|
|
60
|
+
"@babel/core": "^7.24.6",
|
|
61
|
+
"@getodk/tree-sitter-xpath": "0.1.1",
|
|
62
|
+
"@getodk/xpath": "0.1.2",
|
|
63
|
+
"@playwright/test": "^1.44.1",
|
|
64
|
+
"@vitest/browser": "^1.6.0",
|
|
66
65
|
"babel-plugin-transform-jsbi-to-bigint": "^1.4.0",
|
|
67
66
|
"http-server": "^14.1.1",
|
|
68
67
|
"jsdom": "^24.0.0",
|
|
69
|
-
"typedoc": "^0.25.
|
|
68
|
+
"typedoc": "^0.25.13",
|
|
70
69
|
"unplugin-fonts": "^1.1.1",
|
|
71
|
-
"vite": "^5.
|
|
72
|
-
"vite-plugin-
|
|
73
|
-
"vite-plugin-
|
|
74
|
-
"
|
|
75
|
-
"vite-plugin-solid": "^2.10.1",
|
|
76
|
-
"vitest": "^1.3.1",
|
|
77
|
-
"vitest-github-actions-reporter": "^0.11.1"
|
|
70
|
+
"vite": "^5.2.11",
|
|
71
|
+
"vite-plugin-dts": "^3.9.1",
|
|
72
|
+
"vite-plugin-no-bundle": "^4.0.0",
|
|
73
|
+
"vitest": "^1.6.0"
|
|
78
74
|
},
|
|
79
75
|
"peerDependencies": {
|
|
80
|
-
"solid-js": "^1.8.
|
|
76
|
+
"solid-js": "^1.8.17"
|
|
81
77
|
},
|
|
82
78
|
"peerDependenciesMeta": {
|
|
83
79
|
"solid-js": {
|
package/src/XFormDOM.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { XFORMS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
|
|
1
|
+
import { XFORMS_NAMESPACE_URI, XMLNS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
|
|
2
2
|
import { XFormsXPathEvaluator } from '@getodk/xpath';
|
|
3
3
|
|
|
4
4
|
const domParser = new DOMParser();
|
|
@@ -41,7 +41,83 @@ const normalizeBodyRefNodesetAttributes = (body: Element): void => {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const
|
|
44
|
+
const normalizeRepeatGroupAttributes = (group: Element, repeat: Element): void => {
|
|
45
|
+
for (const groupAttribute of group.attributes) {
|
|
46
|
+
const { localName, namespaceURI, nodeName, value } = groupAttribute;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
// Don't propagate namespace declarations (which appear as attributes in
|
|
50
|
+
// the browser/XML DOM, either named `xmlns` or with an `xmlns` prefix,
|
|
51
|
+
// always in the XMLNS namespace).
|
|
52
|
+
namespaceURI === XMLNS_NAMESPACE_URI ||
|
|
53
|
+
// Don't propagate `ref`, it has been normalized as `nodeset` on the
|
|
54
|
+
// repeat element.
|
|
55
|
+
localName === 'ref' ||
|
|
56
|
+
// TODO: this accommodates tests of this normalization process, where
|
|
57
|
+
// certain nodes of interest are given an `id` attribute, and looked up
|
|
58
|
+
// for the purpose of asserting what was normalized about them. It's
|
|
59
|
+
// unclear if there's a generally expected behavior around the attribute.
|
|
60
|
+
localName === 'id'
|
|
61
|
+
) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// TODO: The `appearance` attribute is propagated from
|
|
66
|
+
// `<group appearance><repeat>` to `<repeat appearance>`. But we presently
|
|
67
|
+
// bail if both elements define the attribute.
|
|
68
|
+
//
|
|
69
|
+
// The spec is clear that the attribute is only supported on `<group>` and
|
|
70
|
+
// control elements, which would suggest it should not be present on a
|
|
71
|
+
// `<repeat>` element directly. But many form fixtures (in e.g. Enketo)
|
|
72
|
+
// do have `<repeat apperance>`.
|
|
73
|
+
//
|
|
74
|
+
// It may be reasonable to relax this by:
|
|
75
|
+
//
|
|
76
|
+
// - Detecting if they share the same appearances, treated as a no-op.
|
|
77
|
+
//
|
|
78
|
+
// - Assume they're both meant to apply, and concatenate.
|
|
79
|
+
if (
|
|
80
|
+
localName === 'appearance' &&
|
|
81
|
+
namespaceURI === XFORMS_NAMESPACE_URI &&
|
|
82
|
+
repeat.hasAttribute(localName)
|
|
83
|
+
) {
|
|
84
|
+
const ref = group.getAttribute('ref');
|
|
85
|
+
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Failed to normalize conflicting "appearances" attribute of group/repeat "${ref}"`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
repeat.setAttributeNS(namespaceURI, nodeName, value);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const normalizeRepeatGroupLabel = (group: Element, repeat: Element): void => {
|
|
96
|
+
const groupLabel = Array.from(group.children).find((child) => {
|
|
97
|
+
return child.localName === 'label';
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (groupLabel == null) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const repeatLabel = groupLabel.cloneNode(true) as Element;
|
|
105
|
+
|
|
106
|
+
repeatLabel.setAttribute('form-definition-source', 'repeat-group');
|
|
107
|
+
|
|
108
|
+
repeat.prepend(repeatLabel);
|
|
109
|
+
|
|
110
|
+
groupLabel.remove();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const unwrapRepeatGroup = (group: Element, repeat: Element): void => {
|
|
114
|
+
normalizeRepeatGroupAttributes(group, repeat);
|
|
115
|
+
normalizeRepeatGroupLabel(group, repeat);
|
|
116
|
+
|
|
117
|
+
group.replaceWith(repeat);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const normalizeRepeatGroups = (body: Element): void => {
|
|
45
121
|
const repeats = body.querySelectorAll('repeat');
|
|
46
122
|
|
|
47
123
|
for (const repeat of repeats) {
|
|
@@ -63,11 +139,8 @@ const normalizeRepeatGroups = (xformDocument: XMLDocument, body: Element): void
|
|
|
63
139
|
}
|
|
64
140
|
}
|
|
65
141
|
|
|
66
|
-
if (group
|
|
67
|
-
group
|
|
68
|
-
group.setAttribute('ref', repeatNodeset);
|
|
69
|
-
repeat.before(group);
|
|
70
|
-
group.append(repeat);
|
|
142
|
+
if (group != null) {
|
|
143
|
+
unwrapRepeatGroup(group, repeat);
|
|
71
144
|
}
|
|
72
145
|
}
|
|
73
146
|
};
|
|
@@ -113,7 +186,7 @@ const parseNormalizedXForm = (
|
|
|
113
186
|
normalizedXML = sourceXML;
|
|
114
187
|
} else {
|
|
115
188
|
normalizeBodyRefNodesetAttributes(body);
|
|
116
|
-
normalizeRepeatGroups(
|
|
189
|
+
normalizeRepeatGroups(body);
|
|
117
190
|
|
|
118
191
|
normalizedXML = html.outerHTML;
|
|
119
192
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
2
2
|
import { DependencyContext } from '../expression/DependencyContext.ts';
|
|
3
|
+
import type { ParsedTokenList } from '../lib/TokenListParser.ts';
|
|
4
|
+
import { TokenListParser } from '../lib/TokenListParser.ts';
|
|
5
|
+
import { RepeatElementDefinition } from './RepeatElementDefinition.ts';
|
|
3
6
|
import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts';
|
|
4
7
|
import { ControlDefinition } from './control/ControlDefinition.ts';
|
|
5
8
|
import { InputDefinition } from './control/InputDefinition.ts';
|
|
@@ -7,7 +10,6 @@ import type { AnySelectDefinition } from './control/select/SelectDefinition.ts';
|
|
|
7
10
|
import { SelectDefinition } from './control/select/SelectDefinition.ts';
|
|
8
11
|
import { LogicalGroupDefinition } from './group/LogicalGroupDefinition.ts';
|
|
9
12
|
import { PresentationGroupDefinition } from './group/PresentationGroupDefinition.ts';
|
|
10
|
-
import { RepeatGroupDefinition } from './group/RepeatGroupDefinition.ts';
|
|
11
13
|
import { StructuralGroupDefinition } from './group/StructuralGroupDefinition.ts';
|
|
12
14
|
|
|
13
15
|
export interface BodyElementParentContext {
|
|
@@ -15,20 +17,24 @@ export interface BodyElementParentContext {
|
|
|
15
17
|
readonly element: Element;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
// prettier-ignore
|
|
21
|
+
export type ControlElementDefinition =
|
|
22
|
+
| AnySelectDefinition
|
|
23
|
+
| InputDefinition;
|
|
24
|
+
|
|
18
25
|
type SupportedBodyElementDefinition =
|
|
19
26
|
// eslint-disable-next-line @typescript-eslint/sort-type-constituents
|
|
20
|
-
|
|
|
27
|
+
| RepeatElementDefinition
|
|
21
28
|
| LogicalGroupDefinition
|
|
22
29
|
| PresentationGroupDefinition
|
|
23
30
|
| StructuralGroupDefinition
|
|
24
|
-
|
|
|
25
|
-
| AnySelectDefinition;
|
|
31
|
+
| ControlElementDefinition;
|
|
26
32
|
|
|
27
33
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
34
|
type BodyElementDefinitionConstructor = new (...args: any[]) => SupportedBodyElementDefinition;
|
|
29
35
|
|
|
30
36
|
const BodyElementDefinitionConstructors = [
|
|
31
|
-
|
|
37
|
+
RepeatElementDefinition,
|
|
32
38
|
LogicalGroupDefinition,
|
|
33
39
|
PresentationGroupDefinition,
|
|
34
40
|
StructuralGroupDefinition,
|
|
@@ -49,11 +55,6 @@ export type AnyGroupElementDefinition = Extract<
|
|
|
49
55
|
{ readonly type: `${string}-group` }
|
|
50
56
|
>;
|
|
51
57
|
|
|
52
|
-
export type NonRepeatGroupElementDefinition = Exclude<
|
|
53
|
-
AnyGroupElementDefinition,
|
|
54
|
-
{ readonly type: 'repeat-group' }
|
|
55
|
-
>;
|
|
56
|
-
|
|
57
58
|
const isGroupElementDefinition = (
|
|
58
59
|
element: AnyBodyElementDefinition
|
|
59
60
|
): element is AnyGroupElementDefinition => {
|
|
@@ -96,13 +97,13 @@ class BodyElementMap extends Map<BodyElementReference, AnyBodyElementDefinition>
|
|
|
96
97
|
for (const element of elements) {
|
|
97
98
|
const { reference } = element;
|
|
98
99
|
|
|
99
|
-
if (element instanceof
|
|
100
|
+
if (element instanceof RepeatElementDefinition) {
|
|
100
101
|
if (reference == null) {
|
|
101
|
-
throw new Error('Missing reference for repeat
|
|
102
|
+
throw new Error('Missing reference for repeat');
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
this.set(reference, element);
|
|
105
|
-
this.mapElementsByReference(element.
|
|
106
|
+
this.mapElementsByReference(element.children);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
if (
|
|
@@ -140,6 +141,10 @@ class BodyElementMap extends Map<BodyElementReference, AnyBodyElementDefinition>
|
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
const bodyClassParser = new TokenListParser(['pages' /*, 'theme-grid' */]);
|
|
145
|
+
|
|
146
|
+
export type BodyClassList = ParsedTokenList<typeof bodyClassParser>;
|
|
147
|
+
|
|
143
148
|
export class BodyDefinition extends DependencyContext {
|
|
144
149
|
static getChildElementDefinitions(
|
|
145
150
|
form: XFormDefinition,
|
|
@@ -161,6 +166,25 @@ export class BodyDefinition extends DependencyContext {
|
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
readonly element: Element;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @todo this class is already an oddity in that it's **like** an element
|
|
172
|
+
* definition, but it isn't one itself. Adding this property here emphasizes
|
|
173
|
+
* that awkwardness. It also extends the applicable scope where instances of
|
|
174
|
+
* this class are accessed. While it's still ephemeral, it's anticipated that
|
|
175
|
+
* this extension might cause some disomfort. If so, the most plausible
|
|
176
|
+
* alternative is an additional refactor to:
|
|
177
|
+
*
|
|
178
|
+
* 1. Introduce a `BodyElementDefinition` sublass for `<h:body>`.
|
|
179
|
+
* 2. Disambiguate the respective names of those, in some reasonable way.
|
|
180
|
+
* 3. Add a layer of indirection between this class and that new body element
|
|
181
|
+
* definition's class.
|
|
182
|
+
* 4. At that point, we may as well prioritize the little bit of grunt work to
|
|
183
|
+
* pass the `BodyDefinition` instance by reference rather than assigning it
|
|
184
|
+
* to anything.
|
|
185
|
+
*/
|
|
186
|
+
readonly classes: BodyClassList;
|
|
187
|
+
|
|
164
188
|
readonly elements: readonly AnyBodyElementDefinition[];
|
|
165
189
|
|
|
166
190
|
protected readonly elementsByReference: BodyElementMap;
|
|
@@ -176,6 +200,7 @@ export class BodyDefinition extends DependencyContext {
|
|
|
176
200
|
|
|
177
201
|
this.reference = form.rootReference;
|
|
178
202
|
this.element = element;
|
|
203
|
+
this.classes = bodyClassParser.parseFrom(element, 'class');
|
|
179
204
|
this.elements = BodyDefinition.getChildElementDefinitions(form, this, element);
|
|
180
205
|
this.elementsByReference = new BodyElementMap(this.elements);
|
|
181
206
|
}
|
|
@@ -184,16 +209,6 @@ export class BodyDefinition extends DependencyContext {
|
|
|
184
209
|
return this.elementsByReference.get(reference) ?? null;
|
|
185
210
|
}
|
|
186
211
|
|
|
187
|
-
getRepeatGroup(reference: string): RepeatGroupDefinition | null {
|
|
188
|
-
const element = this.getBodyElement(reference);
|
|
189
|
-
|
|
190
|
-
if (element?.type === 'repeat-group') {
|
|
191
|
-
return element;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
212
|
toJSON() {
|
|
198
213
|
const { form, ...rest } = this;
|
|
199
214
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
|
|
2
|
+
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
3
|
+
import type { BodyElementDefinitionArray, BodyElementParentContext } from './BodyDefinition.ts';
|
|
4
|
+
import { BodyDefinition } from './BodyDefinition.ts';
|
|
5
|
+
import { BodyElementDefinition } from './BodyElementDefinition.ts';
|
|
6
|
+
import type { StructureElementAppearanceDefinition } from './appearance/structureElementAppearanceParser.ts';
|
|
7
|
+
import { structureElementAppearanceParser } from './appearance/structureElementAppearanceParser.ts';
|
|
8
|
+
import { LabelDefinition } from './text/LabelDefinition.ts';
|
|
9
|
+
|
|
10
|
+
export class RepeatElementDefinition extends BodyElementDefinition<'repeat'> {
|
|
11
|
+
static override isCompatible(localName: string): boolean {
|
|
12
|
+
return localName === 'repeat';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override readonly category = 'structure';
|
|
16
|
+
readonly type = 'repeat';
|
|
17
|
+
override readonly reference: string;
|
|
18
|
+
readonly appearances: StructureElementAppearanceDefinition;
|
|
19
|
+
override readonly label: LabelDefinition | null;
|
|
20
|
+
|
|
21
|
+
// TODO: this will fall into the growing category of non-`BindExpression`
|
|
22
|
+
// cases which have roughly the same design story.
|
|
23
|
+
readonly countExpression: string | null;
|
|
24
|
+
|
|
25
|
+
readonly isFixedCount: boolean;
|
|
26
|
+
|
|
27
|
+
readonly children: BodyElementDefinitionArray;
|
|
28
|
+
|
|
29
|
+
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
|
|
30
|
+
super(form, parent, element);
|
|
31
|
+
|
|
32
|
+
this.label = LabelDefinition.forRepeatGroup(form, this);
|
|
33
|
+
|
|
34
|
+
const reference = element.getAttribute('nodeset');
|
|
35
|
+
|
|
36
|
+
if (reference == null) {
|
|
37
|
+
throw new Error('Invalid repeat: missing `nodeset` reference');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.reference = reference;
|
|
41
|
+
this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
|
|
42
|
+
this.countExpression = element.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'count');
|
|
43
|
+
|
|
44
|
+
const childElements = Array.from(element.children).filter((childElement) => {
|
|
45
|
+
const { localName } = childElement;
|
|
46
|
+
|
|
47
|
+
return localName !== 'label' && localName !== 'group-label';
|
|
48
|
+
});
|
|
49
|
+
const children = BodyDefinition.getChildElementDefinitions(form, this, element, childElements);
|
|
50
|
+
|
|
51
|
+
this.children = children;
|
|
52
|
+
|
|
53
|
+
// Spec says this can be either `true()` or `false()`. That said, it
|
|
54
|
+
// could also presumably be `true ( )` or whatever.
|
|
55
|
+
const noAddRemove =
|
|
56
|
+
element
|
|
57
|
+
.getAttributeNS(JAVAROSA_NAMESPACE_URI, 'noAddRemove')
|
|
58
|
+
?.trim()
|
|
59
|
+
.replaceAll(/\s+/g, '') ?? 'false()';
|
|
60
|
+
|
|
61
|
+
// TODO: **probably** safe to disregard anything else?
|
|
62
|
+
this.isFixedCount = noAddRemove === 'true()';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override toJSON() {
|
|
66
|
+
const { form, parent, ...rest } = this;
|
|
67
|
+
|
|
68
|
+
return rest;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts';
|
|
2
|
+
|
|
3
|
+
export const inputAppearanceParser = new TokenListParser([
|
|
4
|
+
'multiline',
|
|
5
|
+
'numbers',
|
|
6
|
+
'url',
|
|
7
|
+
'thousand-sep',
|
|
8
|
+
|
|
9
|
+
// date (TODO: data types)
|
|
10
|
+
'no-calendar',
|
|
11
|
+
'month-year',
|
|
12
|
+
'year',
|
|
13
|
+
// date > calendars
|
|
14
|
+
'ethiopian',
|
|
15
|
+
'coptic',
|
|
16
|
+
'islamic',
|
|
17
|
+
'bikram-sambat',
|
|
18
|
+
'myanmar',
|
|
19
|
+
'persian',
|
|
20
|
+
|
|
21
|
+
// geo (TODO: data types)
|
|
22
|
+
'placement-map',
|
|
23
|
+
'maps',
|
|
24
|
+
|
|
25
|
+
// image/media (TODO: move to eventual `<upload>`?)
|
|
26
|
+
'hidden-answer',
|
|
27
|
+
'annotate',
|
|
28
|
+
'draw',
|
|
29
|
+
'signature',
|
|
30
|
+
'new-front',
|
|
31
|
+
'new',
|
|
32
|
+
'front',
|
|
33
|
+
|
|
34
|
+
// *?
|
|
35
|
+
'printer', // Note: actual usage uses `printer:...` (like `ex:...`).
|
|
36
|
+
'masked',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export type InputAppearanceDefinition = ParsedTokenList<typeof inputAppearanceParser>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts';
|
|
2
|
+
|
|
3
|
+
export const selectAppearanceParser = new TokenListParser(
|
|
4
|
+
[
|
|
5
|
+
// From XLSForm Docs:
|
|
6
|
+
'compact',
|
|
7
|
+
'horizontal',
|
|
8
|
+
'horizontal-compact',
|
|
9
|
+
'label',
|
|
10
|
+
'list-nolabel',
|
|
11
|
+
'minimal',
|
|
12
|
+
|
|
13
|
+
// From Collect `Appearances.kt`:
|
|
14
|
+
'columns',
|
|
15
|
+
'columns-1',
|
|
16
|
+
'columns-2',
|
|
17
|
+
'columns-3',
|
|
18
|
+
'columns-4',
|
|
19
|
+
'columns-5',
|
|
20
|
+
// Note: Collect supports arbitrary columns-n. Technically we do too (we parse
|
|
21
|
+
// out any appearance, not just those we know about). But we'll only include
|
|
22
|
+
// types/defaults up to 5.
|
|
23
|
+
'columns-pack',
|
|
24
|
+
'autocomplete',
|
|
25
|
+
|
|
26
|
+
// TODO: these are `<select1>` only
|
|
27
|
+
'likert',
|
|
28
|
+
'quick',
|
|
29
|
+
'quickcompact',
|
|
30
|
+
'map',
|
|
31
|
+
// "quick map"
|
|
32
|
+
],
|
|
33
|
+
{
|
|
34
|
+
aliases: [{ fromAlias: 'search', toCanonical: 'autocomplete' }],
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export type SelectAppearanceDefinition = ParsedTokenList<typeof selectAppearanceParser>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TokenListParser, type ParsedTokenList } from '../../lib/TokenListParser.ts';
|
|
2
|
+
|
|
3
|
+
export const structureElementAppearanceParser = new TokenListParser(['field-list', 'table-list']);
|
|
4
|
+
|
|
5
|
+
export type StructureElementAppearanceDefinition = ParsedTokenList<
|
|
6
|
+
typeof structureElementAppearanceParser
|
|
7
|
+
>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { XFormDefinition } from '../../XFormDefinition.ts';
|
|
2
|
+
import type { ParsedTokenList } from '../../lib/TokenListParser.ts';
|
|
2
3
|
import type { BodyElementParentContext } from '../BodyDefinition.ts';
|
|
3
4
|
import { BodyElementDefinition } from '../BodyElementDefinition.ts';
|
|
4
5
|
import { HintDefinition } from '../text/HintDefinition.ts';
|
|
@@ -23,6 +24,9 @@ export abstract class ControlDefinition<
|
|
|
23
24
|
override readonly label: LabelDefinition | null;
|
|
24
25
|
override readonly hint: HintDefinition | null;
|
|
25
26
|
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
abstract readonly appearances: ParsedTokenList<any>;
|
|
29
|
+
|
|
26
30
|
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
|
|
27
31
|
super(form, parent, element);
|
|
28
32
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import type { XFormDefinition } from '../../XFormDefinition.ts';
|
|
2
|
+
import type { BodyElementParentContext } from '../BodyDefinition.ts';
|
|
3
|
+
import {
|
|
4
|
+
inputAppearanceParser,
|
|
5
|
+
type InputAppearanceDefinition,
|
|
6
|
+
} from '../appearance/inputAppearanceParser.ts';
|
|
1
7
|
import { ControlDefinition } from './ControlDefinition.ts';
|
|
2
8
|
|
|
3
9
|
export class InputDefinition extends ControlDefinition<'input'> {
|
|
@@ -6,4 +12,11 @@ export class InputDefinition extends ControlDefinition<'input'> {
|
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
readonly type = 'input';
|
|
15
|
+
readonly appearances: InputAppearanceDefinition;
|
|
16
|
+
|
|
17
|
+
constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
|
|
18
|
+
super(form, parent, element);
|
|
19
|
+
|
|
20
|
+
this.appearances = inputAppearanceParser.parseFrom(element, 'appearance');
|
|
21
|
+
}
|
|
9
22
|
}
|
|
@@ -3,14 +3,21 @@ import type { LocalNamedElement } from '@getodk/common/types/dom.ts';
|
|
|
3
3
|
import type { XFormDefinition } from '../../../XFormDefinition.ts';
|
|
4
4
|
import { getItemElements, getItemsetElement } from '../../../lib/dom/query.ts';
|
|
5
5
|
import type { AnyBodyElementDefinition, BodyElementParentContext } from '../../BodyDefinition.ts';
|
|
6
|
+
import type { SelectAppearanceDefinition } from '../../appearance/selectAppearanceParser.ts';
|
|
7
|
+
import { selectAppearanceParser } from '../../appearance/selectAppearanceParser.ts';
|
|
6
8
|
import { ControlDefinition } from '../ControlDefinition.ts';
|
|
7
9
|
import { ItemDefinition } from './ItemDefinition.ts';
|
|
8
10
|
import { ItemsetDefinition } from './ItemsetDefinition.ts';
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @todo We were previously a bit overzealous about introducing `<rank>` support
|
|
14
|
+
* here. It'll likely still fit, but we should approach it with more intention.
|
|
15
|
+
*
|
|
16
|
+
* @todo `<trigger>` is *almost* reasonable to support here too. The main
|
|
17
|
+
* hesitation is that its single, implicit "item" does not have a distinct
|
|
18
|
+
* <label>, and presumably has different UX **and translation** considerations.
|
|
19
|
+
*/
|
|
20
|
+
const selectLocalNames = new Set([/* 'rank', */ 'select', 'select1'] as const);
|
|
14
21
|
|
|
15
22
|
export type SelectType = CollectionValues<typeof selectLocalNames>;
|
|
16
23
|
|
|
@@ -34,6 +41,7 @@ export class SelectDefinition<Type extends SelectType> extends ControlDefinition
|
|
|
34
41
|
|
|
35
42
|
override readonly type: Type;
|
|
36
43
|
override readonly element: SelectElement;
|
|
44
|
+
readonly appearances: SelectAppearanceDefinition;
|
|
37
45
|
|
|
38
46
|
readonly itemset: ItemsetDefinition | null;
|
|
39
47
|
readonly items: readonly ItemDefinition[];
|
|
@@ -45,8 +53,9 @@ export class SelectDefinition<Type extends SelectType> extends ControlDefinition
|
|
|
45
53
|
|
|
46
54
|
super(form, parent, element);
|
|
47
55
|
|
|
48
|
-
this.element = element;
|
|
49
56
|
this.type = element.localName as Type;
|
|
57
|
+
this.element = element;
|
|
58
|
+
this.appearances = selectAppearanceParser.parseFrom(element, 'appearance');
|
|
50
59
|
|
|
51
60
|
const itemsetElement = getItemsetElement(element);
|
|
52
61
|
const itemElements = getItemElements(element);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
|
|
2
|
-
import type { XFormDOM } from '../../XFormDOM.ts';
|
|
3
2
|
import type { XFormDefinition } from '../../XFormDefinition.ts';
|
|
4
|
-
import { getLabelElement
|
|
3
|
+
import { getLabelElement } from '../../lib/dom/query.ts';
|
|
5
4
|
import {
|
|
6
5
|
BodyDefinition,
|
|
7
6
|
type BodyElementDefinitionArray,
|
|
8
7
|
type BodyElementParentContext,
|
|
9
8
|
} from '../BodyDefinition.ts';
|
|
10
9
|
import { BodyElementDefinition } from '../BodyElementDefinition.ts';
|
|
10
|
+
import type { StructureElementAppearanceDefinition } from '../appearance/structureElementAppearanceParser.ts';
|
|
11
|
+
import { structureElementAppearanceParser } from '../appearance/structureElementAppearanceParser.ts';
|
|
11
12
|
import { LabelDefinition } from '../text/LabelDefinition.ts';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -23,10 +24,6 @@ import { LabelDefinition } from '../text/LabelDefinition.ts';
|
|
|
23
24
|
* - `presentation-group` is a group with a `<label>` child; its usage here
|
|
24
25
|
* differs from the spec language in that `presentation-group` does **not**
|
|
25
26
|
* have a `ref`
|
|
26
|
-
* - `repeat-group` is not mentioned by the spec; it is an extension of
|
|
27
|
-
* `logical-group`, wherein its `ref` is the same as its immediate `<repeat>`
|
|
28
|
-
* child's `nodeset` (usage of each attribute is normalized during
|
|
29
|
-
* initialization, in {@link XFormDOM})
|
|
30
27
|
* - `structural-group` is any `<group>` element which does not satisfy any of
|
|
31
28
|
* the other usage scenarios; this isn't exactly the terminology used, but is
|
|
32
29
|
* the most closely fitting name for the concept where the other sceanarios
|
|
@@ -34,20 +31,17 @@ import { LabelDefinition } from '../text/LabelDefinition.ts';
|
|
|
34
31
|
*
|
|
35
32
|
* A more succinct decision tree:
|
|
36
33
|
*
|
|
37
|
-
* - `<group ref="$ref"><repeat nodeset="$ref">` -> `repeat-group`, else
|
|
38
34
|
* - `<group ref="$ref">` -> `logical-group`, else
|
|
39
35
|
* - `<group><label>` -> `presentation-group`, else
|
|
40
36
|
* - `<group>` -> `structural-group`
|
|
41
37
|
*/
|
|
42
|
-
export type GroupType =
|
|
43
|
-
| 'logical-group'
|
|
44
|
-
| 'presentation-group'
|
|
45
|
-
| 'repeat-group'
|
|
46
|
-
| 'structural-group';
|
|
38
|
+
export type GroupType = 'logical-group' | 'presentation-group' | 'structural-group';
|
|
47
39
|
|
|
48
40
|
export abstract class BaseGroupDefinition<
|
|
49
41
|
Type extends GroupType,
|
|
50
42
|
> extends BodyElementDefinition<Type> {
|
|
43
|
+
// TODO: does this really accomplish anything? It seems highly unlikely it
|
|
44
|
+
// has enough performance benefit to outweigh its memory and lookup costs.
|
|
51
45
|
private static groupTypes = new UpsertableMap<Element, GroupType | null>();
|
|
52
46
|
|
|
53
47
|
protected static getGroupType(localName: string, element: Element): GroupType | null {
|
|
@@ -56,20 +50,8 @@ export abstract class BaseGroupDefinition<
|
|
|
56
50
|
return null;
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (ref != null) {
|
|
62
|
-
const repeat = getRepeatElement(element);
|
|
63
|
-
|
|
64
|
-
if (repeat == null) {
|
|
65
|
-
return 'logical-group';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (repeat.getAttribute('nodeset') === ref) {
|
|
69
|
-
return 'repeat-group';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
throw new Error('Unexpected <repeat> child of unrelated <group>');
|
|
53
|
+
if (element.hasAttribute('ref')) {
|
|
54
|
+
return 'logical-group';
|
|
73
55
|
}
|
|
74
56
|
|
|
75
57
|
const label = getLabelElement(element);
|
|
@@ -87,6 +69,7 @@ export abstract class BaseGroupDefinition<
|
|
|
87
69
|
readonly children: BodyElementDefinitionArray;
|
|
88
70
|
|
|
89
71
|
override readonly reference: string | null;
|
|
72
|
+
readonly appearances: StructureElementAppearanceDefinition;
|
|
90
73
|
override readonly label: LabelDefinition | null;
|
|
91
74
|
|
|
92
75
|
constructor(
|
|
@@ -99,6 +82,7 @@ export abstract class BaseGroupDefinition<
|
|
|
99
82
|
|
|
100
83
|
this.children = children ?? this.getChildren(element);
|
|
101
84
|
this.reference = element.getAttribute('ref');
|
|
85
|
+
this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
|
|
102
86
|
this.label = LabelDefinition.forGroup(form, this);
|
|
103
87
|
}
|
|
104
88
|
|
|
@@ -107,31 +91,9 @@ export abstract class BaseGroupDefinition<
|
|
|
107
91
|
const children = Array.from(element.children).filter((child) => {
|
|
108
92
|
const childName = child.localName;
|
|
109
93
|
|
|
110
|
-
return childName !== 'label'
|
|
94
|
+
return childName !== 'label';
|
|
111
95
|
});
|
|
112
96
|
|
|
113
97
|
return BodyDefinition.getChildElementDefinitions(form, this, element, children);
|
|
114
98
|
}
|
|
115
99
|
}
|
|
116
|
-
|
|
117
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
118
|
-
export const repeatGroup = <T extends BaseGroupDefinition<any>>(
|
|
119
|
-
groupDefinition: T
|
|
120
|
-
): Extract<T, BaseGroupDefinition<'repeat-group'>> | null => {
|
|
121
|
-
if (groupDefinition.type === 'repeat-group') {
|
|
122
|
-
return groupDefinition as Extract<T, BaseGroupDefinition<'repeat-group'>>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return null;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
-
export const nonRepeatGroup = <T extends BaseGroupDefinition<any>>(
|
|
130
|
-
groupDefinition: T
|
|
131
|
-
): Exclude<T, BaseGroupDefinition<'repeat-group'>> | null => {
|
|
132
|
-
if (groupDefinition.type === 'repeat-group') {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return groupDefinition as Exclude<T, BaseGroupDefinition<'repeat-group'>>;
|
|
137
|
-
};
|