@getodk/xforms-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +44 -0
  2. package/dist/.vite/manifest.json +7 -0
  3. package/dist/XFormDOM.d.ts +31 -0
  4. package/dist/XFormDataType.d.ts +26 -0
  5. package/dist/XFormDefinition.d.ts +14 -0
  6. package/dist/body/BodyDefinition.d.ts +52 -0
  7. package/dist/body/BodyElementDefinition.d.ts +32 -0
  8. package/dist/body/RepeatDefinition.d.ts +15 -0
  9. package/dist/body/UnsupportedBodyElementDefinition.d.ts +10 -0
  10. package/dist/body/control/ControlDefinition.d.ts +16 -0
  11. package/dist/body/control/InputDefinition.d.ts +5 -0
  12. package/dist/body/control/select/ItemDefinition.d.ts +13 -0
  13. package/dist/body/control/select/ItemsetDefinition.d.ts +16 -0
  14. package/dist/body/control/select/ItemsetNodesetContext.d.ts +11 -0
  15. package/dist/body/control/select/ItemsetNodesetExpression.d.ts +5 -0
  16. package/dist/body/control/select/ItemsetValueExpression.d.ts +6 -0
  17. package/dist/body/control/select/SelectDefinition.d.ts +23 -0
  18. package/dist/body/group/BaseGroupDefinition.d.ts +46 -0
  19. package/dist/body/group/LogicalGroupDefinition.d.ts +6 -0
  20. package/dist/body/group/PresentationGroupDefinition.d.ts +11 -0
  21. package/dist/body/group/RepeatGroupDefinition.d.ts +12 -0
  22. package/dist/body/group/StructuralGroupDefinition.d.ts +6 -0
  23. package/dist/body/text/HintDefinition.d.ts +11 -0
  24. package/dist/body/text/LabelDefinition.d.ts +20 -0
  25. package/dist/body/text/TextElementDefinition.d.ts +32 -0
  26. package/dist/body/text/TextElementOutputPart.d.ts +12 -0
  27. package/dist/body/text/TextElementPart.d.ts +12 -0
  28. package/dist/body/text/TextElementReferencePart.d.ts +6 -0
  29. package/dist/body/text/TextElementStaticPart.d.ts +6 -0
  30. package/dist/client/BaseNode.d.ts +138 -0
  31. package/dist/client/EngineConfig.d.ts +78 -0
  32. package/dist/client/FormLanguage.d.ts +63 -0
  33. package/dist/client/GroupNode.d.ts +24 -0
  34. package/dist/client/OpaqueReactiveObjectFactory.d.ts +70 -0
  35. package/dist/client/RepeatInstanceNode.d.ts +28 -0
  36. package/dist/client/RepeatRangeNode.d.ts +94 -0
  37. package/dist/client/RootNode.d.ts +31 -0
  38. package/dist/client/SelectNode.d.ts +60 -0
  39. package/dist/client/StringNode.d.ts +41 -0
  40. package/dist/client/SubtreeNode.d.ts +52 -0
  41. package/dist/client/TextRange.d.ts +55 -0
  42. package/dist/client/hierarchy.d.ts +48 -0
  43. package/dist/client/index.d.ts +11 -0
  44. package/dist/client/node-types.d.ts +1 -0
  45. package/dist/expression/DependencyContext.d.ts +12 -0
  46. package/dist/expression/DependentExpression.d.ts +43 -0
  47. package/dist/index.d.ts +16 -0
  48. package/dist/index.js +37622 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/instance/Group.d.ts +31 -0
  51. package/dist/instance/RepeatInstance.d.ts +60 -0
  52. package/dist/instance/RepeatRange.d.ts +81 -0
  53. package/dist/instance/Root.d.ts +70 -0
  54. package/dist/instance/SelectField.d.ts +45 -0
  55. package/dist/instance/StringField.d.ts +39 -0
  56. package/dist/instance/Subtree.d.ts +30 -0
  57. package/dist/instance/abstract/DescendantNode.d.ts +76 -0
  58. package/dist/instance/abstract/InstanceNode.d.ts +107 -0
  59. package/dist/instance/children.d.ts +2 -0
  60. package/dist/instance/hierarchy.d.ts +12 -0
  61. package/dist/instance/identity.d.ts +7 -0
  62. package/dist/instance/index.d.ts +8 -0
  63. package/dist/instance/internal-api/EvaluationContext.d.ts +34 -0
  64. package/dist/instance/internal-api/InstanceConfig.d.ts +8 -0
  65. package/dist/instance/internal-api/SubscribableDependency.d.ts +59 -0
  66. package/dist/instance/internal-api/TranslationContext.d.ts +4 -0
  67. package/dist/instance/internal-api/ValueContext.d.ts +22 -0
  68. package/dist/instance/resource.d.ts +10 -0
  69. package/dist/instance/text/FormattedTextStub.d.ts +1 -0
  70. package/dist/instance/text/TextChunk.d.ts +11 -0
  71. package/dist/instance/text/TextRange.d.ts +10 -0
  72. package/dist/lib/dom/query.d.ts +20 -0
  73. package/dist/lib/reactivity/createChildrenState.d.ts +36 -0
  74. package/dist/lib/reactivity/createComputedExpression.d.ts +12 -0
  75. package/dist/lib/reactivity/createSelectItems.d.ts +16 -0
  76. package/dist/lib/reactivity/createValueState.d.ts +44 -0
  77. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +18 -0
  78. package/dist/lib/reactivity/node-state/createClientState.d.ts +9 -0
  79. package/dist/lib/reactivity/node-state/createCurrentState.d.ts +6 -0
  80. package/dist/lib/reactivity/node-state/createEngineState.d.ts +5 -0
  81. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +22 -0
  82. package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +6 -0
  83. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +139 -0
  84. package/dist/lib/reactivity/node-state/representations.d.ts +25 -0
  85. package/dist/lib/reactivity/scope.d.ts +23 -0
  86. package/dist/lib/reactivity/text/createFieldHint.d.ts +5 -0
  87. package/dist/lib/reactivity/text/createNodeLabel.d.ts +5 -0
  88. package/dist/lib/reactivity/text/createTextRange.d.ts +19 -0
  89. package/dist/lib/reactivity/types.d.ts +21 -0
  90. package/dist/lib/unique-id.d.ts +27 -0
  91. package/dist/lib/xpath/analysis.d.ts +22 -0
  92. package/dist/model/BindComputation.d.ts +30 -0
  93. package/dist/model/BindDefinition.d.ts +31 -0
  94. package/dist/model/BindElement.d.ts +6 -0
  95. package/dist/model/DescendentNodeDefinition.d.ts +25 -0
  96. package/dist/model/ModelBindMap.d.ts +15 -0
  97. package/dist/model/ModelDefinition.d.ts +10 -0
  98. package/dist/model/NodeDefinition.d.ts +74 -0
  99. package/dist/model/RepeatInstanceDefinition.d.ts +15 -0
  100. package/dist/model/RepeatSequenceDefinition.d.ts +19 -0
  101. package/dist/model/RepeatTemplateDefinition.d.ts +29 -0
  102. package/dist/model/RootDefinition.d.ts +24 -0
  103. package/dist/model/SubtreeDefinition.d.ts +14 -0
  104. package/dist/model/ValueNodeDefinition.d.ts +15 -0
  105. package/dist/solid.js +37273 -0
  106. package/dist/solid.js.map +1 -0
  107. package/package.json +87 -0
  108. package/src/XFormDOM.ts +224 -0
  109. package/src/XFormDataType.ts +64 -0
  110. package/src/XFormDefinition.ts +40 -0
  111. package/src/body/BodyDefinition.ts +202 -0
  112. package/src/body/BodyElementDefinition.ts +62 -0
  113. package/src/body/RepeatDefinition.ts +54 -0
  114. package/src/body/UnsupportedBodyElementDefinition.ts +17 -0
  115. package/src/body/control/ControlDefinition.ts +42 -0
  116. package/src/body/control/InputDefinition.ts +9 -0
  117. package/src/body/control/select/ItemDefinition.ts +31 -0
  118. package/src/body/control/select/ItemsetDefinition.ts +36 -0
  119. package/src/body/control/select/ItemsetNodesetContext.ts +26 -0
  120. package/src/body/control/select/ItemsetNodesetExpression.ts +8 -0
  121. package/src/body/control/select/ItemsetValueExpression.ts +11 -0
  122. package/src/body/control/select/SelectDefinition.ts +74 -0
  123. package/src/body/group/BaseGroupDefinition.ts +137 -0
  124. package/src/body/group/LogicalGroupDefinition.ts +11 -0
  125. package/src/body/group/PresentationGroupDefinition.ts +28 -0
  126. package/src/body/group/RepeatGroupDefinition.ts +91 -0
  127. package/src/body/group/StructuralGroupDefinition.ts +11 -0
  128. package/src/body/text/HintDefinition.ts +26 -0
  129. package/src/body/text/LabelDefinition.ts +54 -0
  130. package/src/body/text/TextElementDefinition.ts +97 -0
  131. package/src/body/text/TextElementOutputPart.ts +27 -0
  132. package/src/body/text/TextElementPart.ts +31 -0
  133. package/src/body/text/TextElementReferencePart.ts +21 -0
  134. package/src/body/text/TextElementStaticPart.ts +26 -0
  135. package/src/client/BaseNode.ts +180 -0
  136. package/src/client/EngineConfig.ts +83 -0
  137. package/src/client/FormLanguage.ts +77 -0
  138. package/src/client/GroupNode.ts +33 -0
  139. package/src/client/OpaqueReactiveObjectFactory.ts +100 -0
  140. package/src/client/README.md +39 -0
  141. package/src/client/RepeatInstanceNode.ts +41 -0
  142. package/src/client/RepeatRangeNode.ts +100 -0
  143. package/src/client/RootNode.ts +36 -0
  144. package/src/client/SelectNode.ts +69 -0
  145. package/src/client/StringNode.ts +46 -0
  146. package/src/client/SubtreeNode.ts +57 -0
  147. package/src/client/TextRange.ts +63 -0
  148. package/src/client/hierarchy.ts +63 -0
  149. package/src/client/index.ts +29 -0
  150. package/src/client/node-types.ts +10 -0
  151. package/src/expression/DependencyContext.ts +53 -0
  152. package/src/expression/DependentExpression.ts +102 -0
  153. package/src/index.ts +35 -0
  154. package/src/instance/Group.ts +82 -0
  155. package/src/instance/RepeatInstance.ts +164 -0
  156. package/src/instance/RepeatRange.ts +214 -0
  157. package/src/instance/Root.ts +264 -0
  158. package/src/instance/SelectField.ts +204 -0
  159. package/src/instance/StringField.ts +93 -0
  160. package/src/instance/Subtree.ts +79 -0
  161. package/src/instance/abstract/DescendantNode.ts +182 -0
  162. package/src/instance/abstract/InstanceNode.ts +257 -0
  163. package/src/instance/children.ts +52 -0
  164. package/src/instance/hierarchy.ts +54 -0
  165. package/src/instance/identity.ts +11 -0
  166. package/src/instance/index.ts +37 -0
  167. package/src/instance/internal-api/EvaluationContext.ts +41 -0
  168. package/src/instance/internal-api/InstanceConfig.ts +9 -0
  169. package/src/instance/internal-api/SubscribableDependency.ts +61 -0
  170. package/src/instance/internal-api/TranslationContext.ts +5 -0
  171. package/src/instance/internal-api/ValueContext.ts +27 -0
  172. package/src/instance/resource.ts +75 -0
  173. package/src/instance/text/FormattedTextStub.ts +8 -0
  174. package/src/instance/text/TextChunk.ts +20 -0
  175. package/src/instance/text/TextRange.ts +23 -0
  176. package/src/lib/dom/query.ts +49 -0
  177. package/src/lib/reactivity/createChildrenState.ts +60 -0
  178. package/src/lib/reactivity/createComputedExpression.ts +114 -0
  179. package/src/lib/reactivity/createSelectItems.ts +163 -0
  180. package/src/lib/reactivity/createValueState.ts +258 -0
  181. package/src/lib/reactivity/materializeCurrentStateChildren.ts +121 -0
  182. package/src/lib/reactivity/node-state/createClientState.ts +51 -0
  183. package/src/lib/reactivity/node-state/createCurrentState.ts +27 -0
  184. package/src/lib/reactivity/node-state/createEngineState.ts +18 -0
  185. package/src/lib/reactivity/node-state/createSharedNodeState.ts +79 -0
  186. package/src/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.ts +85 -0
  187. package/src/lib/reactivity/node-state/createSpecifiedState.ts +229 -0
  188. package/src/lib/reactivity/node-state/representations.ts +64 -0
  189. package/src/lib/reactivity/scope.ts +106 -0
  190. package/src/lib/reactivity/text/createFieldHint.ts +16 -0
  191. package/src/lib/reactivity/text/createNodeLabel.ts +16 -0
  192. package/src/lib/reactivity/text/createTextRange.ts +155 -0
  193. package/src/lib/reactivity/types.ts +27 -0
  194. package/src/lib/unique-id.ts +34 -0
  195. package/src/lib/xpath/analysis.ts +241 -0
  196. package/src/model/BindComputation.ts +88 -0
  197. package/src/model/BindDefinition.ts +104 -0
  198. package/src/model/BindElement.ts +8 -0
  199. package/src/model/DescendentNodeDefinition.ts +56 -0
  200. package/src/model/ModelBindMap.ts +71 -0
  201. package/src/model/ModelDefinition.ts +19 -0
  202. package/src/model/NodeDefinition.ts +146 -0
  203. package/src/model/RepeatInstanceDefinition.ts +39 -0
  204. package/src/model/RepeatSequenceDefinition.ts +53 -0
  205. package/src/model/RepeatTemplateDefinition.ts +150 -0
  206. package/src/model/RootDefinition.ts +121 -0
  207. package/src/model/SubtreeDefinition.ts +50 -0
  208. package/src/model/ValueNodeDefinition.ts +39 -0
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@getodk/xforms-engine",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "description": "XForms engine for ODK Web Forms",
6
+ "type": "module",
7
+ "author": "getodk",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/getodk/web-forms",
11
+ "directory": "packages/xforms-engine"
12
+ },
13
+ "bugs": "https://github.com/getodk/web-forms/issues",
14
+ "homepage": "https://getodk.org/",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "solid": "./dist/solid.js",
20
+ "import": "./dist/index.js",
21
+ "browser": "./dist/index.js",
22
+ "development": "./src/index.ts",
23
+ "default": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src",
29
+ "README.md"
30
+ ],
31
+ "engines": {
32
+ "node": "^18.19.1 || ^20.11.1",
33
+ "yarn": "1.22.19"
34
+ },
35
+ "scripts": {
36
+ "build": "npm-run-all -nl build:*",
37
+ "build:clean": "rimraf dist/",
38
+ "build:js": "vite build",
39
+ "build:solid": "export VITE_BUILD_TARGET=solid && vite build",
40
+ "dev": "vite",
41
+ "docs": "npm-run-all -nl docs:*",
42
+ "docs:clean": "rimraf api-docs/",
43
+ "docs:api": "typedoc --readme none --out api-docs --entryPoints \"src/**/*\"",
44
+ "docs:serve": "http-server api-docs -o modules/client",
45
+ "test": "npm-run-all --print-name --print-label test-node:* test-browser:*",
46
+ "test-node:jsdom": "vitest run",
47
+ "test-browser:chromium": "BROWSER_NAME=chromium vitest run",
48
+ "test-browser:firefox": "BROWSER_NAME=firefox vitest run",
49
+ "test-browser:webkit": "BROWSER_NAME=webkit vitest run",
50
+ "test-watch:jsdom": "vitest",
51
+ "test-watch:chromium": "BROWSER_NAME=chromium vitest",
52
+ "test-watch:firefox": "BROWSER_NAME=firefox vitest",
53
+ "test-watch:webkit": "BROWSER_NAME=webkit vitest",
54
+ "test:types": "tsc --project ./tsconfig.json --emitDeclarationOnly false --noEmit"
55
+ },
56
+ "dependencies": {
57
+ "solid-js": "^1.8.3"
58
+ },
59
+ "devDependencies": {
60
+ "@babel/core": "^7.23.2",
61
+ "@getodk/tree-sitter-xpath": "0.1.0",
62
+ "@getodk/xpath": "0.1.0",
63
+ "@playwright/test": "^1.42.1",
64
+ "@suid/vite-plugin": "^0.1.5",
65
+ "@vitest/browser": "^1.3.1",
66
+ "babel-plugin-transform-jsbi-to-bigint": "^1.4.0",
67
+ "http-server": "^14.1.1",
68
+ "jsdom": "^24.0.0",
69
+ "typedoc": "^0.25.12",
70
+ "unplugin-fonts": "^1.1.1",
71
+ "vite": "^5.1.5",
72
+ "vite-plugin-babel": "^1.2.0",
73
+ "vite-plugin-dts": "^3.7.3",
74
+ "vite-plugin-no-bundle": "^3.0.0",
75
+ "vite-plugin-solid": "^2.10.1",
76
+ "vitest": "^1.3.1",
77
+ "vitest-github-actions-reporter": "^0.11.1"
78
+ },
79
+ "peerDependencies": {
80
+ "solid-js": "^1.8.3"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "solid-js": {
84
+ "optional": true
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,224 @@
1
+ import { XFORMS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
+ import { XFormsXPathEvaluator } from '@getodk/xpath';
3
+
4
+ const domParser = new DOMParser();
5
+
6
+ const openingTag = (element: Element) => element.outerHTML.replace(/>(.|\n)*/, '>');
7
+
8
+ /**
9
+ * Per ODK XForms spec:
10
+ *
11
+ * > To link a body element with its corresponding data node and binding, both
12
+ * > `nodeset` and `ref` attributes can be used. The convention that is helpful is
13
+ * > the one used in XLSForms: use `nodeset="/some/path"` for `<repeat>` and
14
+ * > `<itemset>` elements and use `ref="/some/path"` for everything else.
15
+ */
16
+ const normalizeBodyRefNodesetAttributes = (body: Element): void => {
17
+ const referenceElements = body.querySelectorAll(
18
+ 'itemset[ref], repeat[ref], *[nodeset]:not(itemset, repeat)'
19
+ );
20
+
21
+ for (const element of referenceElements) {
22
+ switch (element.localName) {
23
+ case 'itemset':
24
+ case 'repeat': {
25
+ // Non-null assertion safe by selector
26
+ const ref = element.getAttribute('ref')!;
27
+
28
+ element.setAttribute('nodeset', ref);
29
+ element.removeAttribute('ref');
30
+ break;
31
+ }
32
+
33
+ default: {
34
+ // Non-null assertion safe by selector
35
+ const nodeset = element.getAttribute('nodeset')!;
36
+
37
+ element.setAttribute('ref', nodeset);
38
+ element.removeAttribute('nodeset');
39
+ }
40
+ }
41
+ }
42
+ };
43
+
44
+ const normalizeRepeatGroups = (xformDocument: XMLDocument, body: Element): void => {
45
+ const repeats = body.querySelectorAll('repeat');
46
+
47
+ for (const repeat of repeats) {
48
+ // Non-null assertion safe because `querySelectorAll` returns descendants
49
+ const parent = repeat.parentElement!;
50
+ const repeatNodeset = repeat.getAttribute('nodeset');
51
+
52
+ if (repeatNodeset == null) {
53
+ throw new Error('Found <repeat> without `nodeset` attribute');
54
+ }
55
+
56
+ let group: Element | null = null;
57
+
58
+ if (parent.localName === 'group') {
59
+ const groupRef = parent.getAttribute('ref');
60
+
61
+ if (groupRef === repeatNodeset) {
62
+ group = parent;
63
+ }
64
+ }
65
+
66
+ if (group == null) {
67
+ group = xformDocument.createElementNS(XFORMS_NAMESPACE_URI, 'group');
68
+ group.setAttribute('ref', repeatNodeset);
69
+ repeat.before(group);
70
+ group.append(repeat);
71
+ }
72
+ }
73
+ };
74
+
75
+ interface NormalizedXForm {
76
+ readonly xformDocument: XMLDocument;
77
+ readonly rootEvaluator: XFormsXPathEvaluator;
78
+ readonly html: Element;
79
+ readonly body: Element;
80
+ readonly normalizedXML: string;
81
+ }
82
+
83
+ interface XFormDOMNormalizationOptions {
84
+ readonly isNormalized: boolean;
85
+ }
86
+
87
+ /**
88
+ * Performs preprocess operations to normalize certain aspects of an XForm
89
+ * structure for consistency when building up its runtime representations.
90
+ * Currently this preprocessing:
91
+ *
92
+ * - Ensures consistent use of `ref` and `nodeset` where ambiguous in the
93
+ * ODK XForms spec
94
+ * - Ensures `<repeat>` body elements are always enclosed by a `<group>`
95
+ * with the same `ref`
96
+ */
97
+ const parseNormalizedXForm = (
98
+ sourceXML: string,
99
+ options: XFormDOMNormalizationOptions
100
+ ): NormalizedXForm => {
101
+ const xformDocument: XMLDocument = domParser.parseFromString(sourceXML, 'text/xml');
102
+ const rootEvaluator = new XFormsXPathEvaluator({
103
+ rootNode: xformDocument,
104
+ });
105
+ const html = rootEvaluator.evaluateNonNullElement('/h:html');
106
+ const body = rootEvaluator.evaluateNonNullElement('./h:body', {
107
+ contextNode: html,
108
+ });
109
+
110
+ let normalizedXML: string;
111
+
112
+ if (options.isNormalized) {
113
+ normalizedXML = sourceXML;
114
+ } else {
115
+ normalizeBodyRefNodesetAttributes(body);
116
+ normalizeRepeatGroups(xformDocument, body);
117
+
118
+ normalizedXML = html.outerHTML;
119
+ }
120
+
121
+ return {
122
+ xformDocument,
123
+ rootEvaluator,
124
+ html,
125
+ body,
126
+ normalizedXML,
127
+ };
128
+ };
129
+
130
+ export class XFormDOM {
131
+ static from(sourceXML: string) {
132
+ return new this(sourceXML, { isNormalized: false });
133
+ }
134
+
135
+ protected readonly normalizedXML: string;
136
+
137
+ // XPath
138
+ readonly rootEvaluator: XFormsXPathEvaluator;
139
+ readonly primaryInstanceEvaluator: XFormsXPathEvaluator;
140
+
141
+ // Commonly accessed landmark nodes
142
+ readonly xformDocument: XMLDocument;
143
+
144
+ readonly html: Element;
145
+
146
+ readonly head: Element;
147
+ readonly title: Element;
148
+
149
+ readonly model: Element;
150
+ readonly primaryInstance: Element;
151
+ readonly primaryInstanceRoot: Element;
152
+
153
+ readonly body: Element;
154
+
155
+ protected constructor(
156
+ protected readonly sourceXML: string,
157
+ options: XFormDOMNormalizationOptions
158
+ ) {
159
+ const normalizedXForm: NormalizedXForm = parseNormalizedXForm(sourceXML, options);
160
+ const { xformDocument, html, body, rootEvaluator, normalizedXML } = normalizedXForm;
161
+ const head = rootEvaluator.evaluateNonNullElement('./h:head', {
162
+ contextNode: html,
163
+ });
164
+ const title = rootEvaluator.evaluateNonNullElement('./h:title', {
165
+ contextNode: head,
166
+ });
167
+ const model = rootEvaluator.evaluateNonNullElement('./xf:model', {
168
+ contextNode: head,
169
+ });
170
+ // TODO: Evidently primary instance root will not always have an id
171
+ const primaryInstanceRoot = rootEvaluator.evaluateNonNullElement('./xf:instance/*[@id]', {
172
+ contextNode: model,
173
+ });
174
+ // TODO: invert primary instance/root lookups
175
+ const primaryInstance = rootEvaluator.evaluateNonNullElement('..', {
176
+ contextNode: primaryInstanceRoot,
177
+ });
178
+
179
+ this.normalizedXML = normalizedXML;
180
+ this.rootEvaluator = rootEvaluator;
181
+ this.primaryInstanceEvaluator = new XFormsXPathEvaluator({
182
+ rootNode: primaryInstance,
183
+ });
184
+ this.xformDocument = xformDocument;
185
+ this.html = html;
186
+ this.head = head;
187
+ this.title = title;
188
+ this.model = model;
189
+ this.primaryInstance = primaryInstance;
190
+ this.primaryInstanceRoot = primaryInstanceRoot;
191
+ this.body = body;
192
+ }
193
+
194
+ // TODO: anticipating this will be an entry point for edits as well
195
+ createInstance(): XFormDOM {
196
+ return new XFormDOM(this.normalizedXML, { isNormalized: true });
197
+ }
198
+
199
+ toJSON() {
200
+ const {
201
+ rootEvaluator,
202
+ primaryInstanceEvaluator,
203
+ html,
204
+ head,
205
+ title,
206
+ model,
207
+ primaryInstance,
208
+ primaryInstanceRoot,
209
+ xformDocument,
210
+ ...rest
211
+ } = this;
212
+
213
+ return {
214
+ ...rest,
215
+ xformDocument: '#document',
216
+ html: openingTag(html),
217
+ head: openingTag(head),
218
+ title: openingTag(title),
219
+ model: openingTag(model),
220
+ primaryInstance: openingTag(primaryInstance),
221
+ primaryInstanceRoot: openingTag(primaryInstanceRoot),
222
+ };
223
+ }
224
+ }
@@ -0,0 +1,64 @@
1
+ import type { CollectionValues } from '@getodk/common/types/collections/CollectionValues.ts';
2
+
3
+ /**
4
+ * Like JavaRosa. Presumably for explicit types which aren't impelemnted?
5
+ */
6
+ const UNSUPPORTED_DATA_TYPE = 'UNSUPPORTED';
7
+
8
+ export type UnsupportedDataType = typeof UNSUPPORTED_DATA_TYPE;
9
+
10
+ /**
11
+ * Like JavaRosa. Presumably for e.g. groups with explicit binds (`relevant` etc)?
12
+ */
13
+ const NULL_DATA_TYPE = 'NULL';
14
+
15
+ export type NullDataType = typeof NULL_DATA_TYPE;
16
+
17
+ /**
18
+ * As in ODK XForms Spec.
19
+ *
20
+ * TODO: it's unclear why JavaRosa's `DataType` hews closely to the spec, but
21
+ * has certain differences (e.g. string -> TEXT, int -> INTEGER). It's also
22
+ * not immediately clear how additive types like CHOICE and MULTIPLE_ITEMS
23
+ * square with the underlying spec types.
24
+ */
25
+ export const XFORM_SPEC_DATA_TYPES = [
26
+ 'string',
27
+ 'int',
28
+ 'boolean',
29
+ 'decimal',
30
+ 'date',
31
+ 'time',
32
+ 'dateTime',
33
+ 'geopoint',
34
+ 'geotrace',
35
+ 'geoshape',
36
+ 'binary',
37
+ 'barcode',
38
+ 'intent',
39
+ ] as const;
40
+
41
+ const isSupportedDataType = (bindType: string): bindType is XFormSpecDataType =>
42
+ XFORM_SPEC_DATA_TYPES.includes(bindType as XFormSpecDataType);
43
+
44
+ export type XFormSpecDataType = CollectionValues<typeof XFORM_SPEC_DATA_TYPES>;
45
+
46
+ export type XFormDataType = NullDataType | UnsupportedDataType | XFormSpecDataType;
47
+
48
+ const DEFAULT_XFORM_DATA_TYPE = 'string';
49
+
50
+ export type DefaultXFormDataType = typeof DEFAULT_XFORM_DATA_TYPE;
51
+
52
+ // TODO: groups -> NULL?
53
+ // TODO: XSD namespace
54
+ export const bindDataType = (bindType: string | null): XFormDataType => {
55
+ if (bindType == null) {
56
+ return DEFAULT_XFORM_DATA_TYPE;
57
+ }
58
+
59
+ if (isSupportedDataType(bindType)) {
60
+ return bindType;
61
+ }
62
+
63
+ return UNSUPPORTED_DATA_TYPE;
64
+ };
@@ -0,0 +1,40 @@
1
+ import { XFormDOM } from './XFormDOM.ts';
2
+ import { BodyDefinition } from './body/BodyDefinition.ts';
3
+ import { ModelDefinition } from './model/ModelDefinition.ts';
4
+
5
+ export class XFormDefinition {
6
+ readonly xformDOM: XFormDOM;
7
+ readonly xformDocument: XMLDocument;
8
+
9
+ readonly id: string;
10
+ readonly title: string;
11
+
12
+ readonly rootReference: string;
13
+
14
+ readonly body: BodyDefinition;
15
+ readonly model: ModelDefinition;
16
+
17
+ constructor(readonly sourceXML: string) {
18
+ const xformDOM = XFormDOM.from(sourceXML);
19
+
20
+ this.xformDOM = xformDOM;
21
+
22
+ const { primaryInstanceRoot, title, xformDocument } = xformDOM;
23
+ const id = primaryInstanceRoot.getAttribute('id');
24
+
25
+ if (id == null) {
26
+ throw new Error('Primary instance root has no id');
27
+ }
28
+
29
+ this.xformDocument = xformDocument;
30
+ this.id = id;
31
+ this.title = title.textContent ?? '';
32
+
33
+ // TODO: highly unlikely primary instance root will need a namespace prefix
34
+ // but noting it just in case there is such weird usage...
35
+ this.rootReference = `/${primaryInstanceRoot.localName}`;
36
+
37
+ this.body = new BodyDefinition(this);
38
+ this.model = new ModelDefinition(this);
39
+ }
40
+ }
@@ -0,0 +1,202 @@
1
+ import type { XFormDefinition } from '../XFormDefinition.ts';
2
+ import { DependencyContext } from '../expression/DependencyContext.ts';
3
+ import { UnsupportedBodyElementDefinition } from './UnsupportedBodyElementDefinition.ts';
4
+ import { ControlDefinition } from './control/ControlDefinition.ts';
5
+ import { InputDefinition } from './control/InputDefinition.ts';
6
+ import type { AnySelectDefinition } from './control/select/SelectDefinition.ts';
7
+ import { SelectDefinition } from './control/select/SelectDefinition.ts';
8
+ import { LogicalGroupDefinition } from './group/LogicalGroupDefinition.ts';
9
+ import { PresentationGroupDefinition } from './group/PresentationGroupDefinition.ts';
10
+ import { RepeatGroupDefinition } from './group/RepeatGroupDefinition.ts';
11
+ import { StructuralGroupDefinition } from './group/StructuralGroupDefinition.ts';
12
+
13
+ export interface BodyElementParentContext {
14
+ readonly reference: string | null;
15
+ readonly element: Element;
16
+ }
17
+
18
+ type SupportedBodyElementDefinition =
19
+ // eslint-disable-next-line @typescript-eslint/sort-type-constituents
20
+ | RepeatGroupDefinition
21
+ | LogicalGroupDefinition
22
+ | PresentationGroupDefinition
23
+ | StructuralGroupDefinition
24
+ | InputDefinition
25
+ | AnySelectDefinition;
26
+
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ type BodyElementDefinitionConstructor = new (...args: any[]) => SupportedBodyElementDefinition;
29
+
30
+ const BodyElementDefinitionConstructors = [
31
+ RepeatGroupDefinition,
32
+ LogicalGroupDefinition,
33
+ PresentationGroupDefinition,
34
+ StructuralGroupDefinition,
35
+ InputDefinition,
36
+ SelectDefinition,
37
+ ] as const satisfies readonly BodyElementDefinitionConstructor[];
38
+
39
+ export type AnyBodyElementDefinition =
40
+ | SupportedBodyElementDefinition
41
+ | UnsupportedBodyElementDefinition;
42
+
43
+ export type BodyElementDefinitionArray = readonly AnyBodyElementDefinition[];
44
+
45
+ export type AnyBodyElementType = AnyBodyElementDefinition['type'];
46
+
47
+ export type AnyGroupElementDefinition = Extract<
48
+ AnyBodyElementDefinition,
49
+ { readonly type: `${string}-group` }
50
+ >;
51
+
52
+ export type NonRepeatGroupElementDefinition = Exclude<
53
+ AnyGroupElementDefinition,
54
+ { readonly type: 'repeat-group' }
55
+ >;
56
+
57
+ const isGroupElementDefinition = (
58
+ element: AnyBodyElementDefinition
59
+ ): element is AnyGroupElementDefinition => {
60
+ return element.type.endsWith('-group');
61
+ };
62
+
63
+ export const groupElementDefinition = (
64
+ element: AnyBodyElementDefinition
65
+ ): AnyGroupElementDefinition | null => {
66
+ return isGroupElementDefinition(element) ? element : null;
67
+ };
68
+
69
+ export type AnyControlElementDefinition = Extract<
70
+ AnyBodyElementDefinition,
71
+ { readonly category: 'control' }
72
+ >;
73
+
74
+ const isControlElementDefinition = (
75
+ element: AnyBodyElementDefinition
76
+ ): element is AnyControlElementDefinition => {
77
+ return element.category === 'control';
78
+ };
79
+
80
+ export const controlElementDefinition = (
81
+ element: AnyBodyElementDefinition
82
+ ): AnyControlElementDefinition | null => {
83
+ return isControlElementDefinition(element) ? element : null;
84
+ };
85
+
86
+ type BodyElementReference = string;
87
+
88
+ class BodyElementMap extends Map<BodyElementReference, AnyBodyElementDefinition> {
89
+ constructor(elements: BodyElementDefinitionArray) {
90
+ super();
91
+
92
+ this.mapElementsByReference(elements);
93
+ }
94
+
95
+ protected mapElementsByReference(elements: BodyElementDefinitionArray) {
96
+ for (const element of elements) {
97
+ const { reference } = element;
98
+
99
+ if (element instanceof RepeatGroupDefinition) {
100
+ if (reference == null) {
101
+ throw new Error('Missing reference for repeat/repeat group');
102
+ }
103
+
104
+ this.set(reference, element);
105
+ this.mapElementsByReference(element.repeatChildren);
106
+ }
107
+
108
+ if (
109
+ element instanceof LogicalGroupDefinition ||
110
+ element instanceof PresentationGroupDefinition ||
111
+ element instanceof StructuralGroupDefinition
112
+ ) {
113
+ if (reference != null) {
114
+ this.set(reference, element);
115
+ }
116
+
117
+ this.mapElementsByReference(element.children);
118
+ }
119
+
120
+ if (element instanceof ControlDefinition) {
121
+ this.set(element.reference, element);
122
+ }
123
+ }
124
+ }
125
+
126
+ override set(reference: BodyElementReference, element: AnyBodyElementDefinition) {
127
+ if (this.has(reference)) {
128
+ throw new Error(`Multiple body elements for reference: ${reference}`);
129
+ }
130
+
131
+ return super.set(reference, element);
132
+ }
133
+
134
+ getBodyElementType(reference: BodyElementReference): AnyBodyElementType | null {
135
+ return this.get(reference)?.type ?? null;
136
+ }
137
+
138
+ toJSON() {
139
+ return Object.fromEntries(this.entries());
140
+ }
141
+ }
142
+
143
+ export class BodyDefinition extends DependencyContext {
144
+ static getChildElementDefinitions(
145
+ form: XFormDefinition,
146
+ parent: BodyElementParentContext,
147
+ parentElement: Element,
148
+ children: readonly Element[] = Array.from(parentElement.children)
149
+ ): readonly AnyBodyElementDefinition[] {
150
+ return Array.from(children).map((element) => {
151
+ const { localName } = element;
152
+
153
+ for (const Constructor of BodyElementDefinitionConstructors) {
154
+ if (Constructor.isCompatible(localName, element)) {
155
+ return new Constructor(form, parent, element);
156
+ }
157
+ }
158
+
159
+ return new UnsupportedBodyElementDefinition(form, parent, element);
160
+ });
161
+ }
162
+
163
+ readonly element: Element;
164
+ readonly elements: readonly AnyBodyElementDefinition[];
165
+
166
+ protected readonly elementsByReference: BodyElementMap;
167
+
168
+ // DependencyContext
169
+ readonly parentReference = null;
170
+ readonly reference: string;
171
+
172
+ constructor(protected readonly form: XFormDefinition) {
173
+ super();
174
+
175
+ const { body: element } = form.xformDOM;
176
+
177
+ this.reference = form.rootReference;
178
+ this.element = element;
179
+ this.elements = BodyDefinition.getChildElementDefinitions(form, this, element);
180
+ this.elementsByReference = new BodyElementMap(this.elements);
181
+ }
182
+
183
+ getBodyElement(reference: string): AnyBodyElementDefinition | null {
184
+ return this.elementsByReference.get(reference) ?? null;
185
+ }
186
+
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
+ toJSON() {
198
+ const { form, ...rest } = this;
199
+
200
+ return rest;
201
+ }
202
+ }
@@ -0,0 +1,62 @@
1
+ import type { XFormDefinition } from '../XFormDefinition.ts';
2
+ import { DependencyContext } from '../expression/DependencyContext.ts';
3
+ import type { BodyElementParentContext } from './BodyDefinition.ts';
4
+ import type { HintDefinition } from './text/HintDefinition.ts';
5
+ import type { LabelDefinition } from './text/LabelDefinition.ts';
6
+
7
+ /**
8
+ * These category names roughly correspond to each of the ODK XForms spec's
9
+ * {@link https://getodk.github.io/xforms-spec/#body-elements | Body Elements}
10
+ * tables.
11
+ */
12
+ type BodyElementCategory = 'control' | 'structure' | 'support' | 'UNSUPPORTED';
13
+
14
+ export abstract class BodyElementDefinition<Type extends string> extends DependencyContext {
15
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
16
+ // @ts-ignore
17
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
+ static isCompatible(localName: string, element: Element): boolean {
19
+ throw new Error('Must be overridden by BodyElementDefinition subclass');
20
+ }
21
+
22
+ abstract readonly category: BodyElementCategory;
23
+ abstract readonly type: Type;
24
+ readonly hint: HintDefinition | null = null;
25
+ readonly label: LabelDefinition | null = null;
26
+
27
+ readonly reference: string | null = null;
28
+ readonly parentReference: string | null;
29
+
30
+ protected constructor(
31
+ protected readonly form: XFormDefinition,
32
+ readonly parent: BodyElementParentContext,
33
+ readonly element: Element
34
+ ) {
35
+ super();
36
+ this.parentReference = parent.reference;
37
+ }
38
+
39
+ toJSON(): object {
40
+ const { form, parent, ...rest } = this;
41
+
42
+ return rest;
43
+ }
44
+ }
45
+
46
+ type BodyElementDefinitionClass = Pick<
47
+ typeof BodyElementDefinition,
48
+ keyof typeof BodyElementDefinition
49
+ >;
50
+
51
+ // prettier-ignore
52
+ export type BodyElementDefinitionConstructor =
53
+ & BodyElementDefinitionClass
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ & (new (form: XFormDefinition, element: Element) => BodyElementDefinition<any>);
56
+
57
+ type BodyElementDefinitionInstance = InstanceType<BodyElementDefinitionConstructor>;
58
+
59
+ export type TypedBodyElementDefinition<Type extends string> = Extract<
60
+ BodyElementDefinitionInstance,
61
+ { readonly type: Type }
62
+ >;