@getodk/xforms-engine 0.7.0 → 0.9.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 (81) hide show
  1. package/dist/client/InputNode.d.ts +4 -2
  2. package/dist/client/NoteNode.d.ts +4 -2
  3. package/dist/client/RootNode.d.ts +4 -4
  4. package/dist/client/UploadNode.d.ts +53 -0
  5. package/dist/client/attachments/InstanceAttachmentMeta.d.ts +8 -0
  6. package/dist/client/attachments/InstanceAttachmentsConfig.d.ts +8 -0
  7. package/dist/client/form/FormInstanceConfig.d.ts +2 -0
  8. package/dist/client/hierarchy.d.ts +4 -5
  9. package/dist/client/index.d.ts +5 -2
  10. package/dist/client/node-types.d.ts +2 -3
  11. package/dist/client/serialization/InstanceData.d.ts +7 -5
  12. package/dist/client/serialization/InstancePayloadOptions.d.ts +3 -2
  13. package/dist/error/UploadValueTypeError.d.ts +8 -0
  14. package/dist/index.js +16938 -29914
  15. package/dist/index.js.map +1 -1
  16. package/dist/instance/PrimaryInstance.d.ts +2 -3
  17. package/dist/instance/UploadControl.d.ts +58 -0
  18. package/dist/instance/attachments/InstanceAttachment.d.ts +42 -0
  19. package/dist/instance/attachments/InstanceAttachmentsState.d.ts +9 -0
  20. package/dist/instance/hierarchy.d.ts +6 -7
  21. package/dist/instance/internal-api/InstanceAttachmentContext.d.ts +19 -0
  22. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -0
  23. package/dist/instance/internal-api/serialization/ClientReactiveSerializableInstance.d.ts +2 -0
  24. package/dist/integration/xpath/adapter/traversal.d.ts +2 -2
  25. package/dist/lib/codecs/DateValueCodec.d.ts +7 -0
  26. package/dist/lib/codecs/getSharedValueCodec.d.ts +3 -2
  27. package/dist/lib/reactivity/createInstanceAttachment.d.ts +8 -0
  28. package/dist/lib/resource-helpers.d.ts +2 -0
  29. package/dist/parse/body/BodyDefinition.d.ts +2 -1
  30. package/dist/parse/body/control/UploadControlDefinition.d.ts +2 -0
  31. package/dist/parse/model/BindTypeDefinition.d.ts +2 -7
  32. package/dist/parse/text/abstract/TextElementDefinition.d.ts +1 -1
  33. package/dist/solid.js +16935 -29912
  34. package/dist/solid.js.map +1 -1
  35. package/package.json +16 -14
  36. package/src/client/InputNode.ts +4 -0
  37. package/src/client/NoteNode.ts +4 -0
  38. package/src/client/RootNode.ts +8 -3
  39. package/src/client/UploadNode.ts +78 -0
  40. package/src/client/attachments/InstanceAttachmentMeta.ts +10 -0
  41. package/src/client/attachments/InstanceAttachmentsConfig.ts +13 -0
  42. package/src/client/form/FormInstanceConfig.ts +3 -0
  43. package/src/client/hierarchy.ts +5 -8
  44. package/src/client/index.ts +4 -2
  45. package/src/client/node-types.ts +2 -5
  46. package/src/client/serialization/InstanceData.ts +14 -6
  47. package/src/client/serialization/InstancePayloadOptions.ts +3 -2
  48. package/src/entrypoints/FormInstance.ts +3 -2
  49. package/src/error/UploadValueTypeError.ts +13 -0
  50. package/src/instance/PrimaryInstance.ts +4 -5
  51. package/src/instance/UploadControl.ts +184 -0
  52. package/src/instance/attachments/InstanceAttachment.ts +69 -0
  53. package/src/instance/attachments/InstanceAttachmentsState.ts +18 -0
  54. package/src/instance/children/buildChildren.ts +5 -8
  55. package/src/instance/hierarchy.ts +6 -9
  56. package/src/instance/input/InstanceAttachmentMap.ts +33 -21
  57. package/src/instance/internal-api/InstanceAttachmentContext.ts +20 -0
  58. package/src/instance/internal-api/InstanceConfig.ts +3 -0
  59. package/src/instance/internal-api/serialization/ClientReactiveSerializableInstance.ts +2 -0
  60. package/src/integration/xpath/adapter/traversal.ts +4 -2
  61. package/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts +72 -8
  62. package/src/lib/codecs/DateValueCodec.ts +94 -0
  63. package/src/lib/codecs/getSharedValueCodec.ts +5 -3
  64. package/src/lib/reactivity/createInstanceAttachment.ts +212 -0
  65. package/src/lib/resource-helpers.ts +33 -0
  66. package/src/parse/XFormDOM.ts +1 -3
  67. package/src/parse/body/BodyDefinition.ts +4 -0
  68. package/src/parse/body/control/UploadControlDefinition.ts +42 -0
  69. package/src/parse/model/BindDefinition.ts +1 -1
  70. package/src/parse/model/BindTypeDefinition.ts +68 -26
  71. package/src/parse/model/ModelBindMap.ts +0 -5
  72. package/src/parse/model/SecondaryInstance/sources/ExternalSecondaryInstanceResource.ts +1 -30
  73. package/src/parse/text/abstract/TextElementDefinition.ts +2 -2
  74. package/dist/client/unsupported/UnsupportedControlNode.d.ts +0 -30
  75. package/dist/client/unsupported/UploadNode.d.ts +0 -9
  76. package/dist/instance/unsupported/UploadControl.d.ts +0 -34
  77. package/dist/lib/codecs/TempUnsupportedControlCodec.d.ts +0 -7
  78. package/src/client/unsupported/UnsupportedControlNode.ts +0 -36
  79. package/src/client/unsupported/UploadNode.ts +0 -14
  80. package/src/instance/unsupported/UploadControl.ts +0 -120
  81. package/src/lib/codecs/TempUnsupportedControlCodec.ts +0 -32
@@ -0,0 +1,212 @@
1
+ import { createMemo, createSignal } from 'solid-js';
2
+ import type { FormNodeID } from '../../client/identity.ts';
3
+ import { ErrorProductionDesignPendingError } from '../../error/ErrorProductionDesignPendingError.ts';
4
+ import type { InstanceAttachmentFileName } from '../../instance/attachments/InstanceAttachment.ts';
5
+ import { InstanceAttachment } from '../../instance/attachments/InstanceAttachment.ts';
6
+ import type { InstanceAttachmentContext } from '../../instance/internal-api/InstanceAttachmentContext.ts';
7
+ import type { DecodeInstanceValue } from '../../instance/internal-api/InstanceValueContext.ts';
8
+ import type { SimpleAtomicStateSetter } from './types.ts';
9
+
10
+ type FileNameExtension = `.${string}`;
11
+
12
+ type AssertFileNameExtension = (value: string) => asserts value is FileNameExtension;
13
+
14
+ const assertFileNameExtension: AssertFileNameExtension = (value) => {
15
+ if (!value.startsWith('.')) {
16
+ throw new ErrorProductionDesignPendingError('Expected file name extension to start with "."');
17
+ }
18
+ };
19
+
20
+ interface SplitFileNameResult {
21
+ readonly basename: string;
22
+ readonly extension: FileNameExtension | null;
23
+ }
24
+
25
+ const EXTENSION_PATTERN = /\.[^.]+?$/;
26
+
27
+ interface SearchPatternResult extends Array<string> {
28
+ readonly 0?: string;
29
+ readonly index?: number;
30
+ }
31
+
32
+ const searchPattern = (pattern: RegExp, string: string): SearchPatternResult => {
33
+ return pattern.exec(string) ?? [];
34
+ };
35
+
36
+ const splitFileName = (fileName: string): SplitFileNameResult => {
37
+ const extensionMatches = searchPattern(EXTENSION_PATTERN, fileName);
38
+ const [extension = null] = extensionMatches;
39
+ const basename = fileName.slice(0, extensionMatches.index);
40
+
41
+ if (extension == null) {
42
+ return {
43
+ basename,
44
+ extension,
45
+ };
46
+ }
47
+
48
+ assertFileNameExtension(extension);
49
+
50
+ return {
51
+ basename,
52
+ extension,
53
+ };
54
+ };
55
+
56
+ export type InstanceAttachmentRuntimeValue = File | null;
57
+
58
+ export type InstanceAttachmentFormDataEntry = readonly [
59
+ key: InstanceAttachmentFileName,
60
+ value: NonNullable<InstanceAttachmentRuntimeValue>,
61
+ ];
62
+
63
+ interface InstanceAttachmentValueOptions {
64
+ readonly nodeId: FormNodeID;
65
+ readonly writtenAt: Date | null;
66
+ readonly file: InstanceAttachmentRuntimeValue;
67
+ }
68
+
69
+ interface BaseInstanceAttachmentState {
70
+ readonly computedName: string | null;
71
+ readonly intrinsicName: string | null;
72
+ readonly file: InstanceAttachmentRuntimeValue;
73
+ }
74
+
75
+ interface BlankInstanceAttachmentState extends BaseInstanceAttachmentState {
76
+ readonly computedName: null;
77
+ readonly intrinsicName: null;
78
+ readonly file: null;
79
+ }
80
+
81
+ interface NonBlankInstanceAttachmentState extends BaseInstanceAttachmentState {
82
+ readonly computedName: string | null;
83
+ readonly intrinsicName: string;
84
+ readonly file: NonNullable<InstanceAttachmentRuntimeValue>;
85
+ }
86
+
87
+ // prettier-ignore
88
+ type InstanceAttachmentState =
89
+ | BlankInstanceAttachmentState
90
+ | NonBlankInstanceAttachmentState;
91
+
92
+ const instanceAttachmentState = (
93
+ context: InstanceAttachmentContext,
94
+ options: InstanceAttachmentValueOptions
95
+ ): InstanceAttachmentState => {
96
+ const { nodeId, file, writtenAt } = options;
97
+
98
+ // No file -> no intrinsic name, no name to compute
99
+ if (file == null) {
100
+ return {
101
+ computedName: null,
102
+ intrinsicName: null,
103
+ file: null,
104
+ };
105
+ }
106
+
107
+ const intrinsicName = file.name;
108
+
109
+ // File exists, not written by client -> preserve instance input name
110
+ if (writtenAt == null) {
111
+ return {
112
+ computedName: null,
113
+ intrinsicName,
114
+ file,
115
+ };
116
+ }
117
+
118
+ // File was written by client, name is computed as configured by client
119
+ const { basename, extension } = splitFileName(intrinsicName);
120
+ const computedName = context.instanceConfig.computeAttachmentName({
121
+ nodeId,
122
+ writtenAt,
123
+ basename,
124
+ extension,
125
+ });
126
+
127
+ return {
128
+ computedName,
129
+ intrinsicName,
130
+ file,
131
+ };
132
+ };
133
+
134
+ export const createInstanceAttachment = (
135
+ context: InstanceAttachmentContext
136
+ ): InstanceAttachment => {
137
+ return context.scope.runTask(() => {
138
+ const { rootDocument, nodeId } = context;
139
+ const { attachments } = rootDocument;
140
+ const file = attachments.getInitialFileValue(context.instanceNode);
141
+ const initialState = instanceAttachmentState(context, {
142
+ nodeId,
143
+ file,
144
+ writtenAt: null,
145
+ });
146
+
147
+ const [getState, setState] = createSignal<InstanceAttachmentState>(initialState);
148
+
149
+ const decodeInstanceValue: DecodeInstanceValue = (value) => {
150
+ const { computedName, intrinsicName } = getState();
151
+
152
+ if (value === '') {
153
+ if (computedName != null || intrinsicName != null) {
154
+ throw new ErrorProductionDesignPendingError(
155
+ `Unexpected file name reference. Expected one of "${computedName}", "${intrinsicName}", got: ""`
156
+ );
157
+ }
158
+ }
159
+
160
+ if (value === intrinsicName) {
161
+ return computedName ?? intrinsicName;
162
+ }
163
+
164
+ if (value === computedName) {
165
+ return computedName;
166
+ }
167
+
168
+ throw new ErrorProductionDesignPendingError(
169
+ `Unexpected file name reference. Expected one of "${computedName}", "${intrinsicName}", got: "${value}"`
170
+ );
171
+ };
172
+
173
+ const getValue = createMemo(() => {
174
+ const { computedName, file: currentFile } = getState();
175
+
176
+ if (computedName == null) {
177
+ return currentFile;
178
+ }
179
+
180
+ return new File([currentFile], computedName, {
181
+ type: currentFile.type,
182
+ });
183
+ });
184
+ const setValue: SimpleAtomicStateSetter<InstanceAttachmentRuntimeValue> = (value) => {
185
+ const updatedState = instanceAttachmentState(context, {
186
+ nodeId,
187
+ file: value,
188
+ writtenAt: new Date(),
189
+ });
190
+
191
+ return setState(updatedState).file;
192
+ };
193
+ const valueState = [getValue, setValue] as const;
194
+
195
+ const getFileName = createMemo(() => {
196
+ const { computedName, intrinsicName } = getState();
197
+
198
+ return computedName ?? intrinsicName;
199
+ });
200
+ const getInstanceValue = createMemo(() => getFileName() ?? '');
201
+
202
+ return InstanceAttachment.init(context, {
203
+ getFileName,
204
+ getInstanceValue,
205
+ decodeInstanceValue,
206
+
207
+ getValue,
208
+ setValue,
209
+ valueState,
210
+ });
211
+ });
212
+ };
@@ -0,0 +1,33 @@
1
+ import type { FetchResourceResponse } from '../client/resources.ts';
2
+
3
+ const CONTENT_TYPE_CHARSET_PATTERN = /\s*;\s*charset\s*=.*$/;
4
+
5
+ const stripContentTypeCharset = (contentType: string): string => {
6
+ return contentType.replace(CONTENT_TYPE_CHARSET_PATTERN, '');
7
+ };
8
+
9
+ export const getResponseContentType = (response: FetchResourceResponse): string | null => {
10
+ const { headers } = response;
11
+
12
+ if (headers == null) {
13
+ return null;
14
+ }
15
+
16
+ const contentType = headers.get('content-type');
17
+
18
+ if (contentType != null) {
19
+ return stripContentTypeCharset(contentType);
20
+ }
21
+
22
+ if (headers instanceof Headers) {
23
+ return contentType;
24
+ }
25
+
26
+ for (const [header, value] of headers) {
27
+ if (header.toLowerCase() === 'content-type') {
28
+ return stripContentTypeCharset(value);
29
+ }
30
+ }
31
+
32
+ return null;
33
+ };
@@ -104,9 +104,7 @@ const normalizeDefaultMetaBindings = (
104
104
  let meta = getMetaElement(primaryInstanceRoot);
105
105
  let instanceID = getMetaChildElement(meta, 'instanceID');
106
106
 
107
- if (meta == null) {
108
- meta = createNamespacedChildElement(primaryInstanceRoot, OPENROSA_XFORMS_NAMESPACE_URI, 'meta');
109
- }
107
+ meta ??= createNamespacedChildElement(primaryInstanceRoot, OPENROSA_XFORMS_NAMESPACE_URI, 'meta');
110
108
 
111
109
  if (instanceID == null) {
112
110
  instanceID = createNamespacedChildElement(meta, meta.namespaceURI, 'instanceID');
@@ -207,6 +207,10 @@ export class BodyDefinition extends DependencyContext implements BodyElementPare
207
207
  return this.elementsByReference.get(reference) ?? null;
208
208
  }
209
209
 
210
+ getBodyElementType(reference: BodyElementReference): AnyBodyElementType | null {
211
+ return this.elementsByReference.getBodyElementType(reference);
212
+ }
213
+
210
214
  getChildElementDefinitions(
211
215
  form: XFormDefinition,
212
216
  parent: BodyElementParentContext,
@@ -1,3 +1,5 @@
1
+ import type { UploadMediaOptions, UploadNodeOptions } from '../../../client/UploadNode.ts';
2
+ import { ErrorProductionDesignPendingError } from '../../../error/ErrorProductionDesignPendingError.ts';
1
3
  import type { XFormDefinition } from '../../XFormDefinition.ts';
2
4
  import {
3
5
  unknownAppearanceParser,
@@ -6,6 +8,44 @@ import {
6
8
  import type { BodyElementParentContext } from '../BodyDefinition.ts';
7
9
  import { ControlDefinition } from './ControlDefinition.ts';
8
10
 
11
+ const MEDIATYPE_PATTERN = /^([^/]+)\/([^/]+)$/;
12
+
13
+ // prettier-ignore
14
+ type MediaTypeMatches =
15
+ | readonly [$0: string, $1: string, $2: string];
16
+
17
+ const parseUploadMediaOptions = (element: Element): UploadMediaOptions => {
18
+ const mediaType = element.getAttribute('mediatype')?.trim();
19
+
20
+ if (mediaType == null || mediaType === '') {
21
+ return {
22
+ accept: '*',
23
+ type: null,
24
+ subtype: null,
25
+ };
26
+ }
27
+
28
+ const matches = MEDIATYPE_PATTERN.exec(mediaType) as MediaTypeMatches | null;
29
+
30
+ if (matches == null) {
31
+ throw new ErrorProductionDesignPendingError(`Unsupported mediatype: ${mediaType}`);
32
+ }
33
+
34
+ const [accept, type, subtype] = matches;
35
+
36
+ return {
37
+ accept,
38
+ type,
39
+ subtype,
40
+ };
41
+ };
42
+
43
+ const parseUploadNodeOptions = (element: Element): UploadNodeOptions => {
44
+ const media = parseUploadMediaOptions(element);
45
+
46
+ return { media };
47
+ };
48
+
9
49
  export class UploadControlDefinition extends ControlDefinition<'upload'> {
10
50
  static override isCompatible(localName: string): boolean {
11
51
  return localName === 'upload';
@@ -13,11 +53,13 @@ export class UploadControlDefinition extends ControlDefinition<'upload'> {
13
53
 
14
54
  readonly type = 'upload';
15
55
  readonly appearances: UnknownAppearanceDefinition;
56
+ readonly options: UploadNodeOptions;
16
57
 
17
58
  constructor(form: XFormDefinition, parent: BodyElementParentContext, element: Element) {
18
59
  super(form, parent, element);
19
60
 
20
61
  this.appearances = unknownAppearanceParser.parseFrom(element, 'appearance');
62
+ this.options = parseUploadNodeOptions(element);
21
63
  }
22
64
 
23
65
  override toJSON(): object {
@@ -80,7 +80,7 @@ export class BindDefinition<T extends BindType = BindType> extends DependencyCon
80
80
  ) {
81
81
  super();
82
82
 
83
- this.type = BindTypeDefinition.from(bindElement);
83
+ this.type = BindTypeDefinition.from(form, nodeset, bindElement);
84
84
 
85
85
  const parentNodeset = nodeset.replace(/\/[^/]+$/, '');
86
86
 
@@ -1,19 +1,14 @@
1
1
  import { XSD_NAMESPACE_URI, XSD_PREFIX } from '@getodk/common/constants/xmlns.ts';
2
+ import type { AnyBodyElementType } from '../body/BodyDefinition.ts';
3
+ import type { XFormDefinition } from '../XFormDefinition.ts';
2
4
  import { parseQualifiedNameExpression } from '../xpath/semantic-analysis.ts';
3
5
  import type { BindElement } from './BindElement.ts';
4
6
 
5
- /**
6
- * As specified by {@link https://getodk.github.io/xforms-spec/#bind-attributes}
7
- */
8
- export const DEFAULT_BIND_TYPE = 'string';
9
-
10
- export type DefaultBindType = typeof DEFAULT_BIND_TYPE;
11
-
12
7
  /**
13
8
  * As specified by {@link https://getodk.github.io/xforms-spec/#data-types}
14
9
  */
15
10
  const BIND_TYPES = [
16
- DEFAULT_BIND_TYPE,
11
+ 'string',
17
12
  'int',
18
13
  'boolean',
19
14
  'decimal',
@@ -32,6 +27,47 @@ type BindTypes = typeof BIND_TYPES;
32
27
 
33
28
  export type BindType = BindTypes[number];
34
29
 
30
+ type BindTypeDefaultOverridesByBodyType = Partial<Readonly<Record<AnyBodyElementType, BindType>>>;
31
+
32
+ /**
33
+ * As specified by {@link https://getodk.github.io/xforms-spec/#bind-attributes}
34
+ */
35
+ const SPEC_BIND_TYPE_DEFAULT = 'string' satisfies BindType;
36
+
37
+ /**
38
+ * If an `<upload>` is not explicitly associated with any `<bind type>`, we can do one of two things:
39
+ *
40
+ * - correct: default to {@link SPEC_BIND_TYPE_DEFAULT | the spec default regardless of control type}
41
+ * - more useful and robust: default to "binary", which is the only type the spec allows for an `<upload>` control
42
+ *
43
+ * Asked which would be preferable, @lognaturel responded:
44
+ *
45
+ * > I think we should assume in the spirit of doing the most useful thing!
46
+ */
47
+ const UPLOAD_BIND_TYPE_DEFAULT = 'binary' satisfies BindType;
48
+
49
+ const BODY_BIND_TYPE_DEFAULT_OVERRIDES = {
50
+ upload: UPLOAD_BIND_TYPE_DEFAULT,
51
+ } as const satisfies BindTypeDefaultOverridesByBodyType;
52
+
53
+ type BodyBindTypeDefaultOverrides = typeof BODY_BIND_TYPE_DEFAULT_OVERRIDES;
54
+
55
+ type BodyBindTypeDefaultOverride = keyof BodyBindTypeDefaultOverrides;
56
+
57
+ const isBodyBindTypeDefaultOverride = (
58
+ bodyElementType: AnyBodyElementType | null
59
+ ): bodyElementType is BodyBindTypeDefaultOverride => {
60
+ return bodyElementType != null && bodyElementType in BODY_BIND_TYPE_DEFAULT_OVERRIDES;
61
+ };
62
+
63
+ const resolveDefaultBindType = (bodyElementType: AnyBodyElementType | null): BindType => {
64
+ if (isBodyBindTypeDefaultOverride(bodyElementType)) {
65
+ return BODY_BIND_TYPE_DEFAULT_OVERRIDES[bodyElementType];
66
+ }
67
+
68
+ return SPEC_BIND_TYPE_DEFAULT;
69
+ };
70
+
35
71
  const isBindType = (value: string): value is BindType => {
36
72
  return BIND_TYPES.includes(value as BindType);
37
73
  };
@@ -42,14 +78,10 @@ const resolveSupportedBindType = (sourceType: string): BindType | null => {
42
78
 
43
79
  type BindTypeAliasMapping = Readonly<Record<string, BindType>>;
44
80
 
45
- /**
46
- * @todo should we make these aliases explicit (rather than relying on {@link resolveUnsupportedBindType})?
47
- *
48
- * - select1
49
- * - rank
50
- * - odk:rank
51
- */
52
81
  const BIND_TYPE_ALIASES: BindTypeAliasMapping = {
82
+ select1: SPEC_BIND_TYPE_DEFAULT,
83
+ rank: SPEC_BIND_TYPE_DEFAULT,
84
+ 'odk:rank': SPEC_BIND_TYPE_DEFAULT,
53
85
  integer: 'int',
54
86
  };
55
87
 
@@ -65,7 +97,7 @@ const resolveAliasedBindType = (sourceType: string): BindType | null => {
65
97
  * @todo Should we warn on this fallback?
66
98
  */
67
99
  const resolveUnsupportedBindType = (_unsupportedType: string): BindType => {
68
- return DEFAULT_BIND_TYPE;
100
+ return SPEC_BIND_TYPE_DEFAULT;
69
101
  };
70
102
 
71
103
  interface BindDataTypeNamespaceResolver {
@@ -144,7 +176,15 @@ const resolveNamespacedBindType = (
144
176
  return null;
145
177
  };
146
178
 
147
- const resolveBindType = (bindElement: BindElement, sourceType: string): BindType => {
179
+ const resolveBindType = (
180
+ bodyElementType: AnyBodyElementType | null,
181
+ bindElement: BindElement,
182
+ sourceType: string | null
183
+ ): BindType => {
184
+ if (sourceType == null) {
185
+ return resolveDefaultBindType(bodyElementType);
186
+ }
187
+
148
188
  return (
149
189
  resolveSupportedBindType(sourceType) ??
150
190
  resolveAliasedBindType(sourceType) ??
@@ -154,20 +194,22 @@ const resolveBindType = (bindElement: BindElement, sourceType: string): BindType
154
194
  };
155
195
 
156
196
  export class BindTypeDefinition<T extends BindType = BindType> {
157
- static from<T extends BindType>(bindElement: BindElement): BindTypeDefinition<T> {
197
+ static from<T extends BindType>(
198
+ form: XFormDefinition,
199
+ nodeset: string,
200
+ bindElement: BindElement
201
+ ): BindTypeDefinition<T> {
202
+ const bodyElementType = form.body.getBodyElementType(nodeset);
158
203
  const sourceType = bindElement.getAttribute('type');
159
-
160
- if (sourceType == null) {
161
- return new this(sourceType, DEFAULT_BIND_TYPE);
162
- }
163
-
164
- const resolved = resolveBindType(bindElement, sourceType);
204
+ const resolved = resolveBindType(
205
+ bodyElementType,
206
+ bindElement,
207
+ sourceType
208
+ ) satisfies BindType as T;
165
209
 
166
210
  return new this(sourceType, resolved);
167
211
  }
168
212
 
169
- private constructor(source: null, resolved: DefaultBindType);
170
- private constructor(source: string, resolved: BindType);
171
213
  private constructor(
172
214
  readonly source: string | null,
173
215
  readonly resolved: T
@@ -1,7 +1,6 @@
1
1
  import type { XFormDefinition } from '../XFormDefinition.ts';
2
2
  import { BindDefinition } from './BindDefinition.ts';
3
3
  import type { BindElement, BindNodeset } from './BindElement.ts';
4
- import { DEFAULT_BIND_TYPE } from './BindTypeDefinition.ts';
5
4
  import type { ModelDefinition } from './ModelDefinition.ts';
6
5
 
7
6
  class ArtificialBindElement implements BindElement {
@@ -16,10 +15,6 @@ class ArtificialBindElement implements BindElement {
16
15
  return this.ancestorNodeset;
17
16
  }
18
17
 
19
- if (name === 'type') {
20
- return DEFAULT_BIND_TYPE;
21
- }
22
-
23
18
  return null;
24
19
  }
25
20
 
@@ -2,6 +2,7 @@ import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts
2
2
  import type { MissingResourceBehavior } from '../../../../client/constants.ts';
3
3
  import type { FetchResource, FetchResourceResponse } from '../../../../client/resources.ts';
4
4
  import { ErrorProductionDesignPendingError } from '../../../../error/ErrorProductionDesignPendingError.ts';
5
+ import { getResponseContentType } from '../../../../lib/resource-helpers.ts';
5
6
  import { FormAttachmentResource } from '../../../attachments/FormAttachmentResource.ts';
6
7
  import type { ExternalSecondaryInstanceSourceFormat } from './SecondaryInstanceSource.ts';
7
8
 
@@ -13,36 +14,6 @@ const assertResponseSuccess = (resourceURL: JRResourceURL, response: FetchResour
13
14
  }
14
15
  };
15
16
 
16
- const stripContentTypeCharset = (contentType: string): string => {
17
- return contentType.replace(/;charset=.*$/, '');
18
- };
19
-
20
- const getResponseContentType = (response: FetchResourceResponse): string | null => {
21
- const { headers } = response;
22
-
23
- if (headers == null) {
24
- return null;
25
- }
26
-
27
- const contentType = headers.get('content-type');
28
-
29
- if (contentType != null) {
30
- return stripContentTypeCharset(contentType);
31
- }
32
-
33
- if (headers instanceof Headers) {
34
- return contentType;
35
- }
36
-
37
- for (const [header, value] of headers) {
38
- if (header.toLowerCase() === 'content-type') {
39
- return stripContentTypeCharset(value);
40
- }
41
- }
42
-
43
- return null;
44
- };
45
-
46
17
  interface ExternalSecondaryInstanceResourceMetadata<
47
18
  Format extends ExternalSecondaryInstanceSourceFormat = ExternalSecondaryInstanceSourceFormat,
48
19
  > {
@@ -21,8 +21,8 @@ export type RefAttributeChunk =
21
21
 
22
22
  // prettier-ignore
23
23
  type TextElementChildChunk =
24
- | TextOutputExpression
25
- | TextLiteralExpression;
24
+ | TextLiteralExpression
25
+ | TextOutputExpression;
26
26
 
27
27
  // prettier-ignore
28
28
  type TextElementChunks =
@@ -1,30 +0,0 @@
1
- import { UnknownAppearanceDefinition } from '../../parse/body/appearance/unknownAppearanceParser.ts';
2
- import { UploadControlDefinition } from '../../parse/body/control/UploadControlDefinition.ts';
3
- import { LeafNodeDefinition } from '../../parse/model/LeafNodeDefinition.ts';
4
- import { BaseNode, BaseNodeState } from '../BaseNode.ts';
5
- import { RootNode } from '../RootNode.ts';
6
- import { GeneralParentNode } from '../hierarchy.ts';
7
- import { UnsupportedControlNodeType } from '../node-types.ts';
8
- import { LeafNodeValidationState } from '../validation.ts';
9
- export interface UnsupportedControlNodeState extends BaseNodeState {
10
- get children(): null;
11
- get valueOptions(): unknown;
12
- get value(): unknown;
13
- }
14
- export type UnsupportedControlElementDefinition = UploadControlDefinition;
15
- export interface UnsupportedControlDefinition extends LeafNodeDefinition {
16
- readonly bodyElement: UnsupportedControlElementDefinition;
17
- }
18
- /**
19
- * Stub node, for form controls pending further engine support.
20
- */
21
- export interface UnsupportedControlNode extends BaseNode {
22
- readonly nodeType: UnsupportedControlNodeType;
23
- readonly appearances: UnknownAppearanceDefinition;
24
- readonly definition: UnsupportedControlDefinition;
25
- readonly root: RootNode;
26
- readonly parent: GeneralParentNode;
27
- readonly currentState: UnsupportedControlNodeState;
28
- readonly validationState: LeafNodeValidationState;
29
- setValue?(value: never): never;
30
- }
@@ -1,9 +0,0 @@
1
- import { UploadControlDefinition } from '../../parse/body/control/UploadControlDefinition.ts';
2
- import { UnsupportedControlDefinition, UnsupportedControlNode } from './UnsupportedControlNode.ts';
3
- export interface UploadNodeDefinition extends UnsupportedControlDefinition {
4
- readonly bodyElement: UploadControlDefinition;
5
- }
6
- export interface UploadNode extends UnsupportedControlNode {
7
- readonly nodeType: 'upload';
8
- readonly definition: UploadNodeDefinition;
9
- }
@@ -1,34 +0,0 @@
1
- import { XPathNodeKindKey } from '@getodk/xpath';
2
- import { Accessor } from 'solid-js';
3
- import { TextRange } from '../../client/TextRange.ts';
4
- import { ValueType } from '../../client/ValueType.ts';
5
- import { UploadNode, UploadNodeDefinition } from '../../client/unsupported/UploadNode.ts';
6
- import { XFormsXPathElement } from '../../integration/xpath/adapter/XFormsXPathNode.ts';
7
- import { StaticLeafElement } from '../../integration/xpath/static-dom/StaticElement.ts';
8
- import { TempUnsupportedInputValue, TempUnsupportedRuntimeValue } from '../../lib/codecs/TempUnsupportedControlCodec.ts';
9
- import { CurrentState } from '../../lib/reactivity/node-state/createCurrentState.ts';
10
- import { EngineState } from '../../lib/reactivity/node-state/createEngineState.ts';
11
- import { SharedNodeState } from '../../lib/reactivity/node-state/createSharedNodeState.ts';
12
- import { UnknownAppearanceDefinition } from '../../parse/body/appearance/unknownAppearanceParser.ts';
13
- import { ValueNode, ValueNodeStateSpec } from '../abstract/ValueNode.ts';
14
- import { GeneralParentNode } from '../hierarchy.ts';
15
- import { EvaluationContext } from '../internal-api/EvaluationContext.ts';
16
- import { ValidationContext } from '../internal-api/ValidationContext.ts';
17
- import { ClientReactiveSerializableValueNode } from '../internal-api/serialization/ClientReactiveSerializableValueNode.ts';
18
- interface UploadControlStateSpec extends ValueNodeStateSpec<TempUnsupportedRuntimeValue> {
19
- readonly label: Accessor<TextRange<'label'> | null>;
20
- readonly hint: Accessor<TextRange<'hint'> | null>;
21
- readonly valueOptions: null;
22
- }
23
- export declare class UploadControl extends ValueNode<ValueType, UploadNodeDefinition, TempUnsupportedRuntimeValue, TempUnsupportedInputValue> implements UploadNode, XFormsXPathElement, EvaluationContext, ValidationContext, ClientReactiveSerializableValueNode {
24
- readonly [XPathNodeKindKey] = "element";
25
- protected readonly state: SharedNodeState<UploadControlStateSpec>;
26
- protected readonly engineState: EngineState<UploadControlStateSpec>;
27
- readonly nodeType = "upload";
28
- readonly appearances: UnknownAppearanceDefinition;
29
- readonly nodeOptions: null;
30
- readonly currentState: CurrentState<UploadControlStateSpec>;
31
- constructor(parent: GeneralParentNode, instanceNode: StaticLeafElement | null, definition: UploadNodeDefinition);
32
- setValue(_: never): never;
33
- }
34
- export {};
@@ -1,7 +0,0 @@
1
- import { ValueType } from '../../client/ValueType.ts';
2
- import { ValueCodec } from './ValueCodec.ts';
3
- export type TempUnsupportedRuntimeValue = unknown;
4
- export type TempUnsupportedInputValue = unknown;
5
- export declare class TempUnsupportedControlCodec<V extends ValueType> extends ValueCodec<V, TempUnsupportedRuntimeValue, TempUnsupportedInputValue> {
6
- constructor(valueType: V);
7
- }