@getodk/xforms-engine 0.16.1 → 0.17.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/client/InputNode.d.ts +8 -4
- package/dist/client/MarkdownNode.d.ts +3 -0
- package/dist/client/NoteNode.d.ts +6 -2
- package/dist/client/form/FormInstanceConfig.d.ts +4 -0
- package/dist/client/form/LoadFormResult.d.ts +5 -14
- package/dist/client/form/ResetFormInstance.d.ts +13 -0
- package/dist/entrypoints/FormResult/BaseFormResult.d.ts +1 -0
- package/dist/entrypoints/FormResult/BaseInstantiableFormResult.d.ts +2 -0
- package/dist/entrypoints/FormResult/FormFailureResult.d.ts +2 -0
- package/dist/entrypoints/createPotentiallyClientOwnedReactiveScope.d.ts +19 -0
- package/dist/index.js +21681 -25500
- package/dist/index.js.map +1 -1
- package/dist/instance/PrimaryInstance.d.ts +4 -1
- package/dist/instance/internal-api/AttributeContext.d.ts +1 -0
- package/dist/instance/internal-api/InstanceConfig.d.ts +2 -1
- package/dist/instance/internal-api/InstanceValueContext.d.ts +1 -0
- package/dist/instance/markdown/MarkdownNode.d.ts +14 -9
- package/dist/integration/xpath/static-dom/StaticDocument.d.ts +2 -0
- package/dist/lib/codecs/{Geopoint/Geopoint.d.ts → geolocation/Geolocation.d.ts} +11 -15
- package/dist/lib/codecs/geolocation/Geopoint.d.ts +7 -0
- package/dist/lib/codecs/geolocation/Geoshape.d.ts +7 -0
- package/dist/lib/codecs/geolocation/Geotrace.d.ts +7 -0
- package/dist/lib/codecs/geolocation/createGeolocationValueCodec.d.ts +3 -0
- package/dist/lib/codecs/getSharedValueCodec.d.ts +7 -5
- package/dist/lib/reactivity/text/createTextRange.d.ts +0 -2
- package/dist/parse/XFormDOM.d.ts +7 -1
- package/dist/parse/body/appearance/inputAppearanceParser.d.ts +1 -1
- package/dist/parse/model/ActionDefinition.d.ts +1 -1
- package/dist/parse/model/BindPreloadDefinition.d.ts +2 -1
- package/dist/parse/model/ModelActionMap.d.ts +3 -2
- package/dist/parse/model/ModelDefinition.d.ts +3 -5
- package/dist/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.d.ts +0 -17
- package/dist/parse/model/SecondaryInstance/sources/external-instance-csv-parser.d.ts +8 -0
- package/dist/parse/model/TranslationDefinitionMap.d.ts +4 -0
- package/dist/solid.js +21407 -25226
- package/dist/solid.js.map +1 -1
- package/package.json +2 -2
- package/src/client/InputNode.ts +11 -3
- package/src/client/MarkdownNode.ts +3 -0
- package/src/client/NoteNode.ts +9 -1
- package/src/client/form/FormInstanceConfig.ts +6 -0
- package/src/client/form/LoadFormResult.ts +5 -17
- package/src/client/form/ResetFormInstance.ts +17 -0
- package/src/entrypoints/FormInstance.ts +2 -0
- package/src/entrypoints/FormResult/BaseFormResult.ts +1 -0
- package/src/entrypoints/FormResult/BaseInstantiableFormResult.ts +10 -1
- package/src/entrypoints/FormResult/FormFailureResult.ts +3 -0
- package/src/entrypoints/createPotentiallyClientOwnedReactiveScope.ts +30 -0
- package/src/entrypoints/loadForm.ts +1 -31
- package/src/instance/InputControl.ts +3 -5
- package/src/instance/PrimaryInstance.ts +17 -2
- package/src/instance/attachments/buildAttributes.ts +21 -1
- package/src/instance/internal-api/AttributeContext.ts +1 -0
- package/src/instance/internal-api/InstanceConfig.ts +3 -0
- package/src/instance/internal-api/InstanceValueContext.ts +1 -0
- package/src/instance/markdown/MarkdownNode.ts +19 -7
- package/src/instance/text/markdownFormat.ts +4 -3
- package/src/integration/xpath/static-dom/StaticDocument.ts +2 -0
- package/src/lib/codecs/{Geopoint/Geopoint.ts → geolocation/Geolocation.ts} +43 -24
- package/src/lib/codecs/geolocation/Geopoint.ts +15 -0
- package/src/lib/codecs/geolocation/Geoshape.ts +36 -0
- package/src/lib/codecs/geolocation/Geotrace.ts +36 -0
- package/src/lib/codecs/geolocation/createGeolocationValueCodec.ts +18 -0
- package/src/lib/codecs/getSharedValueCodec.ts +37 -11
- package/src/lib/reactivity/createInstanceValueState.ts +64 -34
- package/src/lib/reactivity/text/createTextRange.ts +71 -45
- package/src/parse/XFormDOM.ts +22 -2
- package/src/parse/model/ActionDefinition.ts +6 -6
- package/src/parse/model/BindDefinition.ts +1 -1
- package/src/parse/model/BindPreloadDefinition.ts +21 -14
- package/src/parse/model/ModelActionMap.ts +30 -13
- package/src/parse/model/ModelDefinition.ts +5 -10
- package/src/parse/model/RootDefinition.ts +2 -1
- package/src/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.ts +2 -184
- package/src/parse/model/SecondaryInstance/sources/external-instance-csv-parser.ts +185 -0
- package/src/parse/model/TranslationDefinitionMap.ts +23 -0
- package/dist/lib/codecs/Geopoint/GeopointValueCodec.d.ts +0 -5
- package/dist/parse/model/generateItextChunks.d.ts +0 -5
- package/src/lib/codecs/Geopoint/GeopointValueCodec.ts +0 -20
- package/src/parse/model/generateItextChunks.ts +0 -61
package/src/parse/XFormDOM.ts
CHANGED
|
@@ -10,8 +10,13 @@ import type {
|
|
|
10
10
|
} from '@getodk/common/types/dom.ts';
|
|
11
11
|
import { DefaultEvaluator } from '@getodk/xpath';
|
|
12
12
|
|
|
13
|
+
export const SET_VALUE_LOCAL_NAME = 'setvalue';
|
|
14
|
+
export const SET_GEOPOINT_LOCAL_NAME = 'odk:setgeopoint';
|
|
15
|
+
|
|
13
16
|
interface DOMBindElement extends KnownAttributeLocalNamedElement<'bind', 'nodeset'> {}
|
|
14
|
-
interface DOMSetValueElement extends KnownAttributeLocalNamedElement<'setvalue', 'event'> {}
|
|
17
|
+
export interface DOMSetValueElement extends KnownAttributeLocalNamedElement<'setvalue', 'event'> {}
|
|
18
|
+
export interface DOMSetGeopointElement
|
|
19
|
+
extends KnownAttributeLocalNamedElement<'odk:setgeopoint', 'event'> {}
|
|
15
20
|
|
|
16
21
|
const getMetaElement = (primaryInstanceRoot: Element): Element | null => {
|
|
17
22
|
for (const child of primaryInstanceRoot.children) {
|
|
@@ -325,6 +330,15 @@ export class XFormDOM {
|
|
|
325
330
|
return new this(sourceXML, { isNormalized: false });
|
|
326
331
|
}
|
|
327
332
|
|
|
333
|
+
isInstanceID = (nodeset: string) => {
|
|
334
|
+
const meta = getMetaElement(this.primaryInstanceRoot);
|
|
335
|
+
const instanceId = meta && getMetaChildElement(meta, 'instanceID');
|
|
336
|
+
return (
|
|
337
|
+
instanceId &&
|
|
338
|
+
nodeset === `/${this.primaryInstanceRoot.nodeName}/${meta.nodeName}/${instanceId.nodeName}`
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
328
342
|
protected readonly normalizedXML: string;
|
|
329
343
|
|
|
330
344
|
// Commonly accessed landmark nodes
|
|
@@ -338,6 +352,7 @@ export class XFormDOM {
|
|
|
338
352
|
readonly model: Element;
|
|
339
353
|
readonly binds: readonly DOMBindElement[];
|
|
340
354
|
readonly setValues: readonly DOMSetValueElement[];
|
|
355
|
+
readonly setGeopoints: readonly DOMSetGeopointElement[];
|
|
341
356
|
readonly primaryInstance: Element;
|
|
342
357
|
readonly primaryInstanceRoot: Element;
|
|
343
358
|
|
|
@@ -371,11 +386,15 @@ export class XFormDOM {
|
|
|
371
386
|
}
|
|
372
387
|
);
|
|
373
388
|
const setValues: readonly DOMSetValueElement[] = evaluator.evaluateNodes<DOMSetValueElement>(
|
|
374
|
-
|
|
389
|
+
`./xf:${SET_VALUE_LOCAL_NAME}[@event]`,
|
|
375
390
|
{
|
|
376
391
|
contextNode: model,
|
|
377
392
|
}
|
|
378
393
|
);
|
|
394
|
+
const setGeopoints: readonly DOMSetGeopointElement[] =
|
|
395
|
+
evaluator.evaluateNodes<DOMSetGeopointElement>(`./${SET_GEOPOINT_LOCAL_NAME}[@event]`, {
|
|
396
|
+
contextNode: model,
|
|
397
|
+
});
|
|
379
398
|
|
|
380
399
|
const instances = evaluator.evaluateNodes<DOMInstanceElement>('./xf:instance', {
|
|
381
400
|
contextNode: model,
|
|
@@ -426,6 +445,7 @@ export class XFormDOM {
|
|
|
426
445
|
this.model = model;
|
|
427
446
|
this.binds = binds;
|
|
428
447
|
this.setValues = setValues;
|
|
448
|
+
this.setGeopoints = setGeopoints;
|
|
429
449
|
this.primaryInstance = primaryInstance;
|
|
430
450
|
this.primaryInstanceRoot = primaryInstanceRoot;
|
|
431
451
|
this.itextTranslationElements = itextTranslationElements;
|
|
@@ -4,12 +4,12 @@ import { type XFormEvent, XFORM_EVENT } from './Event.ts';
|
|
|
4
4
|
import type { ModelDefinition } from './ModelDefinition.ts';
|
|
5
5
|
|
|
6
6
|
export class ActionDefinition {
|
|
7
|
-
static getRef(model: ModelDefinition,
|
|
8
|
-
if (
|
|
9
|
-
return
|
|
7
|
+
static getRef(model: ModelDefinition, element: Element): string | null {
|
|
8
|
+
if (element.hasAttribute('ref')) {
|
|
9
|
+
return element.getAttribute('ref') ?? null;
|
|
10
10
|
}
|
|
11
|
-
if (
|
|
12
|
-
const bindId =
|
|
11
|
+
if (element.hasAttribute('bind')) {
|
|
12
|
+
const bindId = element.getAttribute('bind');
|
|
13
13
|
const bindDefinition = Array.from(model.binds.values()).find((definition) => {
|
|
14
14
|
return definition.bindElement.getAttribute('id') === bindId;
|
|
15
15
|
});
|
|
@@ -58,7 +58,7 @@ export class ActionDefinition {
|
|
|
58
58
|
const ref = ActionDefinition.getRef(model, element);
|
|
59
59
|
if (!ref) {
|
|
60
60
|
throw new Error(
|
|
61
|
-
|
|
61
|
+
`Invalid ${element.localName} element - you must define either "ref" or "bind" attribute`
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
this.ref = ref;
|
|
@@ -82,7 +82,7 @@ export class BindDefinition<T extends BindType = BindType> extends DependencyCon
|
|
|
82
82
|
const parentNodeset = nodeset.replace(/\/[^/]+$/, '');
|
|
83
83
|
|
|
84
84
|
this.parentNodeset = parentNodeset.length > 1 ? parentNodeset : null;
|
|
85
|
-
this.preload = BindPreloadDefinition.from(bindElement);
|
|
85
|
+
this.preload = BindPreloadDefinition.from(this, bindElement);
|
|
86
86
|
this.calculate = BindComputationExpression.forComputation(this, 'calculate');
|
|
87
87
|
this.readonly = BindComputationExpression.forComputation(this, 'readonly');
|
|
88
88
|
this.relevant = BindComputationExpression.forComputation(this, 'relevant');
|
|
@@ -2,6 +2,7 @@ import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
|
|
|
2
2
|
import type { PartiallyKnownString } from '@getodk/common/types/string/PartiallyKnownString.ts';
|
|
3
3
|
import type { AttributeContext } from '../../instance/internal-api/AttributeContext.ts';
|
|
4
4
|
import type { InstanceValueContext } from '../../instance/internal-api/InstanceValueContext.ts';
|
|
5
|
+
import type { BindDefinition } from './BindDefinition.ts';
|
|
5
6
|
import type { BindElement } from './BindElement.ts';
|
|
6
7
|
import { XFORM_EVENT, type XFormEvent } from './Event.ts';
|
|
7
8
|
|
|
@@ -71,20 +72,29 @@ const getPreloadInput = (bindElement: BindElement): AnyPreloadInput | null => {
|
|
|
71
72
|
* - {@link parameter}, an associated `jr:preloadParams` value
|
|
72
73
|
*/
|
|
73
74
|
export class BindPreloadDefinition<Type extends PreloadType> implements PreloadInput<Type> {
|
|
74
|
-
static from(
|
|
75
|
+
static from(
|
|
76
|
+
definition: BindDefinition,
|
|
77
|
+
bindElement: BindElement
|
|
78
|
+
): AnyBindPreloadDefinition | null {
|
|
75
79
|
const input = getPreloadInput(bindElement);
|
|
76
80
|
|
|
77
81
|
if (input == null) {
|
|
78
82
|
return null;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
const type = input.type;
|
|
86
|
+
const parameter = input.parameter;
|
|
87
|
+
let event;
|
|
88
|
+
if (definition.form.xformDOM.isInstanceID(bindElement.getAttribute('nodeset'))) {
|
|
89
|
+
event = XFORM_EVENT.odkInstanceLoad;
|
|
90
|
+
} else if (type === 'timestamp' && parameter === 'end') {
|
|
91
|
+
event = XFORM_EVENT.xformsRevalidate;
|
|
92
|
+
} else {
|
|
93
|
+
event = XFORM_EVENT.odkInstanceFirstLoad;
|
|
94
|
+
}
|
|
95
|
+
return new this(type, parameter, event);
|
|
82
96
|
}
|
|
83
97
|
|
|
84
|
-
readonly type: Type;
|
|
85
|
-
readonly parameter: PreloadParameter<Type>;
|
|
86
|
-
readonly event: XFormEvent;
|
|
87
|
-
|
|
88
98
|
getValue(context: AttributeContext | InstanceValueContext): string | undefined {
|
|
89
99
|
if (this.type === 'uid') {
|
|
90
100
|
return context.evaluator.evaluateString(PRELOAD_UID_EXPRESSION);
|
|
@@ -113,14 +123,11 @@ export class BindPreloadDefinition<Type extends PreloadType> implements PreloadI
|
|
|
113
123
|
return;
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
private constructor(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
? XFORM_EVENT.xformsRevalidate
|
|
122
|
-
: XFORM_EVENT.odkInstanceFirstLoad;
|
|
123
|
-
}
|
|
126
|
+
private constructor(
|
|
127
|
+
readonly type: Type,
|
|
128
|
+
readonly parameter: PreloadParameter<Type>,
|
|
129
|
+
readonly event: XFormEvent
|
|
130
|
+
) {}
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
// prettier-ignore
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DOMSetGeopointElement,
|
|
3
|
+
type DOMSetValueElement,
|
|
4
|
+
SET_GEOPOINT_LOCAL_NAME,
|
|
5
|
+
SET_VALUE_LOCAL_NAME,
|
|
6
|
+
} from '../XFormDOM.ts';
|
|
1
7
|
import { ActionDefinition } from './ActionDefinition.ts';
|
|
2
8
|
import { XFORM_EVENT } from './Event.ts';
|
|
3
9
|
import type { ModelDefinition } from './ModelDefinition.ts';
|
|
4
10
|
|
|
5
11
|
const REPEAT_REGEX = /(\[[^\]]*\])/gm;
|
|
6
12
|
|
|
7
|
-
export class ModelActionMap extends Map<string, ActionDefinition> {
|
|
13
|
+
export class ModelActionMap extends Map<string, ActionDefinition[]> {
|
|
8
14
|
static fromModel(model: ModelDefinition): ModelActionMap {
|
|
9
15
|
return new this(model);
|
|
10
16
|
}
|
|
@@ -14,24 +20,35 @@ export class ModelActionMap extends Map<string, ActionDefinition> {
|
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
protected constructor(model: ModelDefinition) {
|
|
17
|
-
super(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
|
|
21
|
-
throw new Error('Model contains "setvalue" element with "odk-new-repeat" event');
|
|
22
|
-
}
|
|
23
|
-
const key = ModelActionMap.getKey(action.ref);
|
|
24
|
-
return [key, action];
|
|
25
|
-
})
|
|
26
|
-
);
|
|
23
|
+
super();
|
|
24
|
+
this.addAll(model, model.form.xformDOM.setValues, SET_VALUE_LOCAL_NAME);
|
|
25
|
+
this.addAll(model, model.form.xformDOM.setGeopoints, SET_GEOPOINT_LOCAL_NAME);
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
override get(ref: string): ActionDefinition | undefined {
|
|
28
|
+
override get(ref: string): ActionDefinition[] | undefined {
|
|
30
29
|
return super.get(ModelActionMap.getKey(ref));
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
private addAll(
|
|
33
|
+
model: ModelDefinition,
|
|
34
|
+
elements: readonly DOMSetGeopointElement[] | readonly DOMSetValueElement[],
|
|
35
|
+
type: string
|
|
36
|
+
) {
|
|
37
|
+
for (const element of elements) {
|
|
38
|
+
const action = new ActionDefinition(model, element);
|
|
39
|
+
if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
|
|
40
|
+
throw new Error(`Model contains "${type}" element with "odk-new-repeat" event`);
|
|
41
|
+
}
|
|
42
|
+
this.add(action);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
add(action: ActionDefinition) {
|
|
34
47
|
const key = ModelActionMap.getKey(action.ref);
|
|
35
|
-
this.
|
|
48
|
+
if (this.has(key)) {
|
|
49
|
+
this.get(key)!.push(action);
|
|
50
|
+
} else {
|
|
51
|
+
this.set(key, [action]);
|
|
52
|
+
}
|
|
36
53
|
}
|
|
37
54
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { ActiveLanguage } from '../../client/FormLanguage.ts';
|
|
2
2
|
import { ErrorProductionDesignPendingError } from '../../error/ErrorProductionDesignPendingError.ts';
|
|
3
3
|
import type { StaticDocument } from '../../integration/xpath/static-dom/StaticDocument.ts';
|
|
4
|
-
import { TextChunkExpression } from '../expression/TextChunkExpression.ts';
|
|
5
4
|
import { parseStaticDocumentFromDOMSubtree } from '../shared/parseStaticDocumentFromDOMSubtree.ts';
|
|
6
5
|
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
7
|
-
import { generateItextChunks, type ChunkExpressionsByItextId } from './generateItextChunks.ts';
|
|
8
6
|
import { ItextTranslationsDefinition } from './ItextTranslationsDefinition.ts';
|
|
9
7
|
import { ModelActionMap } from './ModelActionMap.ts';
|
|
10
8
|
import { ModelBindMap } from './ModelBindMap.ts';
|
|
@@ -13,6 +11,7 @@ import type { NodeDefinitionMap } from './nodeDefinitionMap.ts';
|
|
|
13
11
|
import { nodeDefinitionMap } from './nodeDefinitionMap.ts';
|
|
14
12
|
import { RootDefinition } from './RootDefinition.ts';
|
|
15
13
|
import { SubmissionDefinition } from './SubmissionDefinition.ts';
|
|
14
|
+
import { TranslationDefinitionMap } from './TranslationDefinitionMap.ts';
|
|
16
15
|
|
|
17
16
|
type XformsRevalidateListener = () => void;
|
|
18
17
|
|
|
@@ -23,7 +22,7 @@ export class ModelDefinition {
|
|
|
23
22
|
readonly nodes: NodeDefinitionMap;
|
|
24
23
|
readonly instance: StaticDocument;
|
|
25
24
|
readonly itextTranslations: ItextTranslationsDefinition;
|
|
26
|
-
readonly
|
|
25
|
+
readonly itextElements: Map<string, Map<string, Element>>;
|
|
27
26
|
readonly xformsRevalidateListeners: Map<string, XformsRevalidateListener>;
|
|
28
27
|
|
|
29
28
|
constructor(readonly form: XFormDefinition) {
|
|
@@ -37,7 +36,7 @@ export class ModelDefinition {
|
|
|
37
36
|
this.root = new RootDefinition(form, this, submission, form.body.classes);
|
|
38
37
|
this.nodes = nodeDefinitionMap(this.root);
|
|
39
38
|
this.itextTranslations = ItextTranslationsDefinition.from(form.xformDOM);
|
|
40
|
-
this.
|
|
39
|
+
this.itextElements = new TranslationDefinitionMap(form.xformDOM.itextTranslationElements);
|
|
41
40
|
this.xformsRevalidateListeners = new Map();
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -69,12 +68,8 @@ export class ModelDefinition {
|
|
|
69
68
|
this.xformsRevalidateListeners.forEach((listener: XformsRevalidateListener) => listener());
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
itextId
|
|
74
|
-
activeLanguage: ActiveLanguage
|
|
75
|
-
): ReadonlyArray<TextChunkExpression<'string'>> {
|
|
76
|
-
const languageMap = this.itextChunks.get(activeLanguage.language);
|
|
77
|
-
return languageMap?.get(itextId) ?? [];
|
|
71
|
+
getItextElement(activeLanguage: ActiveLanguage, itextId: string): Element | undefined {
|
|
72
|
+
return this.itextElements.get(activeLanguage.language)?.get(itextId);
|
|
78
73
|
}
|
|
79
74
|
|
|
80
75
|
toJSON() {
|
|
@@ -3,6 +3,7 @@ import { NamespaceDeclarationMap } from '../../lib/names/NamespaceDeclarationMap
|
|
|
3
3
|
import { QualifiedName } from '../../lib/names/QualifiedName.ts';
|
|
4
4
|
import type { AnyBodyElementDefinition, BodyClassList } from '../body/BodyDefinition.ts';
|
|
5
5
|
import type { XFormDefinition } from '../XFormDefinition.ts';
|
|
6
|
+
import { SET_GEOPOINT_LOCAL_NAME, SET_VALUE_LOCAL_NAME } from '../XFormDOM.ts';
|
|
6
7
|
import { ActionDefinition } from './ActionDefinition.ts';
|
|
7
8
|
import { AttributeDefinitionMap } from './AttributeDefinitionMap.ts';
|
|
8
9
|
import { GroupDefinition } from './GroupDefinition.ts';
|
|
@@ -63,7 +64,7 @@ export class RootDefinition extends NodeDefinition<'root'> {
|
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
for (const child of bodyElement.element.children) {
|
|
66
|
-
if (child.nodeName ===
|
|
67
|
+
if (child.nodeName === SET_VALUE_LOCAL_NAME || child.nodeName === SET_GEOPOINT_LOCAL_NAME) {
|
|
67
68
|
const action = new ActionDefinition(this.model, child, source);
|
|
68
69
|
this.model.actions.add(action);
|
|
69
70
|
}
|
|
@@ -1,83 +1,9 @@
|
|
|
1
|
-
import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
|
|
2
|
-
import * as papa from 'papaparse';
|
|
3
|
-
import { ErrorProductionDesignPendingError } from '../../../../error/ErrorProductionDesignPendingError.ts';
|
|
4
1
|
import type { StaticElementOptions } from '../../../../integration/xpath/static-dom/StaticElement.ts';
|
|
5
2
|
import { defineSecondaryInstance } from '../defineSecondaryInstance.ts';
|
|
6
3
|
import type { SecondaryInstanceDefinition } from '../SecondaryInstancesDefinition.ts';
|
|
4
|
+
import { parseItems } from './external-instance-csv-parser.ts';
|
|
7
5
|
import { ExternalSecondaryInstanceSource } from './ExternalSecondaryInstanceSource.ts';
|
|
8
6
|
|
|
9
|
-
type CSVColumn = string;
|
|
10
|
-
type CSVRow = readonly CSVColumn[];
|
|
11
|
-
|
|
12
|
-
type AssertCSVRow = (columns: unknown) => asserts columns is CSVRow;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Based on {@link https://github.com/getodk/central-frontend/commit/29cebcc870c9be70ab0d222e3349e34639045d19}
|
|
16
|
-
*
|
|
17
|
-
* Central performs this check for header and rows. A comment is included there for the header check, but the logic is the same in both cases.
|
|
18
|
-
*/
|
|
19
|
-
const rejectNullCharacters = (cell: string) => {
|
|
20
|
-
if (cell.includes('\0')) {
|
|
21
|
-
throw new ErrorProductionDesignPendingError(`Failed to parse CSV: null character`);
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const assertCSVRow: AssertCSVRow = (columns) => {
|
|
26
|
-
if (!Array.isArray(columns)) {
|
|
27
|
-
throw new ErrorProductionDesignPendingError('Failed to parse CSV columns');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for (const [index, column] of columns.entries()) {
|
|
31
|
-
if (typeof column !== 'string') {
|
|
32
|
-
throw new ErrorProductionDesignPendingError(`Failed to parse CSV column at index ${index}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
rejectNullCharacters(column);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type AssertPapaparseSuccess = (
|
|
40
|
-
resourceURL: JRResourceURL,
|
|
41
|
-
errors: readonly papa.ParseError[]
|
|
42
|
-
) => asserts errors is readonly [];
|
|
43
|
-
|
|
44
|
-
const assertPapaparseSuccess: AssertPapaparseSuccess = (resourceURL, errors) => {
|
|
45
|
-
if (errors.length > 0) {
|
|
46
|
-
const cause = new AggregateError(errors);
|
|
47
|
-
throw new ErrorProductionDesignPendingError(
|
|
48
|
-
`Failed to parse CSV external secondary instance ${resourceURL.href}`,
|
|
49
|
-
{ cause }
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
interface ParsedCSVHeader {
|
|
55
|
-
readonly columns: CSVRow;
|
|
56
|
-
readonly errors: readonly [];
|
|
57
|
-
readonly meta: papa.ParseMeta;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface ParseCSVOptions {
|
|
61
|
-
readonly columns: readonly string[];
|
|
62
|
-
readonly delimiter: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const stripTrailingEmptyCells = (columns: CSVRow, row: CSVRow): CSVRow => {
|
|
66
|
-
const result = row.slice();
|
|
67
|
-
|
|
68
|
-
while (result.length > columns.length && result.at(-1) === '') {
|
|
69
|
-
result.pop();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
interface ParsedCSVRows {
|
|
76
|
-
readonly rows: readonly CSVRow[];
|
|
77
|
-
readonly errors: readonly [];
|
|
78
|
-
readonly meta: papa.ParseMeta;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
7
|
interface CSVExternalSecondaryInstanceItemColumn {
|
|
82
8
|
readonly columnName: string;
|
|
83
9
|
readonly cellValue: string;
|
|
@@ -120,116 +46,8 @@ const csvExternalSecondaryInstanceDefinition = (
|
|
|
120
46
|
};
|
|
121
47
|
|
|
122
48
|
export class CSVExternalSecondaryInstanceSource extends ExternalSecondaryInstanceSource<'csv'> {
|
|
123
|
-
/**
|
|
124
|
-
* Based on
|
|
125
|
-
* {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L79} (and {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L13}).
|
|
126
|
-
*
|
|
127
|
-
* The most significant deviations at time of writing:
|
|
128
|
-
*
|
|
129
|
-
* - we have already retrieved the CSV resource, so we are parsing the raw CSV data directly.
|
|
130
|
-
* - we have no need for asynchronous/streaming parsing at this point in the
|
|
131
|
-
* form initialization process, so we can dispense with those details of the
|
|
132
|
-
* {@link papa | papaparse} API/config.
|
|
133
|
-
*/
|
|
134
|
-
private parseCSVHeader(csvData: string): ParsedCSVHeader {
|
|
135
|
-
const { data, errors, meta } = papa.parse(csvData, {
|
|
136
|
-
delimitersToGuess: [',', ';', '\t', '|'],
|
|
137
|
-
download: false,
|
|
138
|
-
preview: 1,
|
|
139
|
-
});
|
|
140
|
-
const [columns = []] = data;
|
|
141
|
-
|
|
142
|
-
assertCSVRow(columns);
|
|
143
|
-
assertPapaparseSuccess(this.resourceURL, errors);
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
errors,
|
|
147
|
-
meta,
|
|
148
|
-
columns,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Largely based on {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L170}
|
|
154
|
-
*/
|
|
155
|
-
private parseCSVRows(csvData: string, options: ParseCSVOptions): ParsedCSVRows {
|
|
156
|
-
const { columns, delimiter } = options;
|
|
157
|
-
const { data, errors, meta } = papa.parse(csvData, {
|
|
158
|
-
delimiter,
|
|
159
|
-
download: false,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
assertPapaparseSuccess(this.resourceURL, errors);
|
|
163
|
-
|
|
164
|
-
const rowData = data.slice(1);
|
|
165
|
-
const lastRowIndex = rowData.length - 1;
|
|
166
|
-
|
|
167
|
-
let stripLastRow = false;
|
|
168
|
-
|
|
169
|
-
const rows = rowData.map((values, index) => {
|
|
170
|
-
assertCSVRow(values);
|
|
171
|
-
|
|
172
|
-
const rowIndex = index + 1;
|
|
173
|
-
|
|
174
|
-
// Central: Remove trailing empty cells.
|
|
175
|
-
const row = stripTrailingEmptyCells(columns, values);
|
|
176
|
-
|
|
177
|
-
// Central: Skip trailing empty rows and do not check them for warnings.
|
|
178
|
-
// Throw for an empty row that is not trailing.
|
|
179
|
-
if (row.every((cell) => cell === '')) {
|
|
180
|
-
if (index === lastRowIndex) {
|
|
181
|
-
stripLastRow = true;
|
|
182
|
-
} else {
|
|
183
|
-
throw new ErrorProductionDesignPendingError(
|
|
184
|
-
`Failed to parse CSV row ${rowIndex}: unexpected empty row`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Central: Throw if there are too many cells.
|
|
190
|
-
if (row.length > columns.length) {
|
|
191
|
-
throw new ErrorProductionDesignPendingError(
|
|
192
|
-
`Failed to parse CSV row ${rowIndex}: expected ${columns.length} columns, got ${row.length}`
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return row;
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
if (stripLastRow) {
|
|
200
|
-
rows.pop();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
errors,
|
|
205
|
-
meta,
|
|
206
|
-
rows,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
private toItems(
|
|
211
|
-
columns: CSVRow,
|
|
212
|
-
rows: readonly CSVRow[]
|
|
213
|
-
): readonly CSVExternalSecondaryInstanceItem[] {
|
|
214
|
-
return rows.map((row) => {
|
|
215
|
-
return columns.map((columnName, index) => {
|
|
216
|
-
return {
|
|
217
|
-
columnName,
|
|
218
|
-
cellValue: row[index] ?? '',
|
|
219
|
-
};
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
49
|
parseDefinition(): SecondaryInstanceDefinition {
|
|
225
|
-
const
|
|
226
|
-
const { columns, meta } = this.parseCSVHeader(csvData);
|
|
227
|
-
const { rows } = this.parseCSVRows(csvData, {
|
|
228
|
-
columns,
|
|
229
|
-
delimiter: meta.delimiter,
|
|
230
|
-
});
|
|
231
|
-
const items = this.toItems(columns, rows);
|
|
232
|
-
|
|
50
|
+
const items = parseItems(this.resourceURL, this.resource.data);
|
|
233
51
|
return csvExternalSecondaryInstanceDefinition(this.instanceId, items);
|
|
234
52
|
}
|
|
235
53
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL';
|
|
2
|
+
import * as papa from 'papaparse';
|
|
3
|
+
import { ErrorProductionDesignPendingError } from '../../../../error/ErrorProductionDesignPendingError';
|
|
4
|
+
|
|
5
|
+
type CSVColumn = string;
|
|
6
|
+
type CSVRow = readonly CSVColumn[];
|
|
7
|
+
type AssertCSVRow = (resourceURL: JRResourceURL, columns: unknown) => asserts columns is CSVRow;
|
|
8
|
+
|
|
9
|
+
interface ParsedCSVHeader {
|
|
10
|
+
readonly columns: CSVRow;
|
|
11
|
+
readonly errors: readonly [];
|
|
12
|
+
readonly meta: papa.ParseMeta;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ParseCSVOptions {
|
|
16
|
+
readonly columns: readonly string[];
|
|
17
|
+
readonly delimiter: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ParsedCSVRows {
|
|
21
|
+
readonly rows: readonly CSVRow[];
|
|
22
|
+
readonly errors: readonly [];
|
|
23
|
+
readonly meta: papa.ParseMeta;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CSVExternalSecondaryInstanceItemColumn {
|
|
27
|
+
readonly columnName: string;
|
|
28
|
+
readonly cellValue: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CSVExternalSecondaryInstanceItem = readonly CSVExternalSecondaryInstanceItemColumn[];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Based on {@link https://github.com/getodk/central-frontend/commit/29cebcc870c9be70ab0d222e3349e34639045d19}
|
|
35
|
+
*
|
|
36
|
+
* Central performs this check for header and rows. A comment is included there for the header check, but the logic is the same in both cases.
|
|
37
|
+
*/
|
|
38
|
+
const rejectNullCharacters = (resourceURL: JRResourceURL, cell: string) => {
|
|
39
|
+
if (cell.includes('\0')) {
|
|
40
|
+
throw new ErrorProductionDesignPendingError(
|
|
41
|
+
`Failed to parse CSV ${resourceURL.href}: null character`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const stripTrailingEmptyCells = (columns: CSVRow, row: CSVRow): CSVRow => {
|
|
47
|
+
const result = row.slice();
|
|
48
|
+
|
|
49
|
+
while (result.length > columns.length && result.at(-1) === '') {
|
|
50
|
+
result.pop();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const assertCSVRow: AssertCSVRow = (resourceURL: JRResourceURL, columns) => {
|
|
57
|
+
if (!Array.isArray(columns)) {
|
|
58
|
+
throw new ErrorProductionDesignPendingError(
|
|
59
|
+
`Failed to parse CSV ${resourceURL.href}: invalid columns`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [index, column] of columns.entries()) {
|
|
64
|
+
if (typeof column !== 'string') {
|
|
65
|
+
throw new ErrorProductionDesignPendingError(
|
|
66
|
+
`Failed to parse CSV ${resourceURL.href}: invalid column at ${index}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
rejectNullCharacters(resourceURL, column);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type AssertPapaparseSuccess = (
|
|
75
|
+
resourceURL: JRResourceURL,
|
|
76
|
+
errors: readonly papa.ParseError[]
|
|
77
|
+
) => asserts errors is readonly [];
|
|
78
|
+
|
|
79
|
+
const assertPapaparseSuccess: AssertPapaparseSuccess = (resourceURL, errors) => {
|
|
80
|
+
if (errors.length > 0) {
|
|
81
|
+
const cause = new AggregateError(errors);
|
|
82
|
+
throw new ErrorProductionDesignPendingError(`Failed to parse CSV ${resourceURL.href}`, {
|
|
83
|
+
cause,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Largely based on {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L170}
|
|
90
|
+
*/
|
|
91
|
+
const parseCSVRows = (
|
|
92
|
+
resourceURL: JRResourceURL,
|
|
93
|
+
csvData: string,
|
|
94
|
+
options: ParseCSVOptions
|
|
95
|
+
): ParsedCSVRows => {
|
|
96
|
+
const { columns, delimiter } = options;
|
|
97
|
+
const { data, errors, meta } = papa.parse(csvData, {
|
|
98
|
+
delimiter,
|
|
99
|
+
download: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assertPapaparseSuccess(resourceURL, errors);
|
|
103
|
+
|
|
104
|
+
const rowData = data.slice(1);
|
|
105
|
+
|
|
106
|
+
const rows = rowData.map((values, index) => {
|
|
107
|
+
assertCSVRow(resourceURL, values);
|
|
108
|
+
|
|
109
|
+
const rowIndex = index + 1;
|
|
110
|
+
|
|
111
|
+
// Central: Remove trailing empty cells.
|
|
112
|
+
const row = stripTrailingEmptyCells(columns, values);
|
|
113
|
+
|
|
114
|
+
// Central: Throw if there are too many cells.
|
|
115
|
+
if (row.length > columns.length) {
|
|
116
|
+
throw new ErrorProductionDesignPendingError(
|
|
117
|
+
`Failed to parse CSV ${resourceURL.href}: row ${rowIndex}, expected ${columns.length} columns, got ${row.length}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return row;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
errors,
|
|
126
|
+
meta,
|
|
127
|
+
rows,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const toItems = (
|
|
132
|
+
columns: CSVRow,
|
|
133
|
+
rows: readonly CSVRow[]
|
|
134
|
+
): readonly CSVExternalSecondaryInstanceItem[] => {
|
|
135
|
+
return rows.map((row) => {
|
|
136
|
+
return columns.map((columnName, index) => {
|
|
137
|
+
return {
|
|
138
|
+
columnName,
|
|
139
|
+
cellValue: row[index] ?? '',
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Based on
|
|
147
|
+
* {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L79} (and {@link https://github.com/getodk/central-frontend/blob/42c9277709e593480d1462e28b4be5f1364532b7/src/util/csv.js#L13}).
|
|
148
|
+
*
|
|
149
|
+
* The most significant deviations at time of writing:
|
|
150
|
+
*
|
|
151
|
+
* - we have already retrieved the CSV resource, so we are parsing the raw CSV data directly.
|
|
152
|
+
* - we have no need for asynchronous/streaming parsing at this point in the
|
|
153
|
+
* form initialization process, so we can dispense with those details of the
|
|
154
|
+
* {@link papa | papaparse} API/config.
|
|
155
|
+
*/
|
|
156
|
+
const parseCSVHeader = (resourceURL: JRResourceURL, csvData: string): ParsedCSVHeader => {
|
|
157
|
+
const { data, errors, meta } = papa.parse(csvData, {
|
|
158
|
+
delimitersToGuess: [',', ';', '\t', '|'],
|
|
159
|
+
download: false,
|
|
160
|
+
preview: 1,
|
|
161
|
+
});
|
|
162
|
+
const [columns = []] = data;
|
|
163
|
+
|
|
164
|
+
assertCSVRow(resourceURL, columns);
|
|
165
|
+
assertPapaparseSuccess(resourceURL, errors);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
errors,
|
|
169
|
+
meta,
|
|
170
|
+
columns,
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const parseItems = (
|
|
175
|
+
resourceURL: JRResourceURL,
|
|
176
|
+
data: string
|
|
177
|
+
): readonly CSVExternalSecondaryInstanceItem[] => {
|
|
178
|
+
const csvData = data.replace(/[\n\r]+$/, '');
|
|
179
|
+
const { columns, meta } = parseCSVHeader(resourceURL, csvData);
|
|
180
|
+
const { rows } = parseCSVRows(resourceURL, csvData, {
|
|
181
|
+
columns,
|
|
182
|
+
delimiter: meta.delimiter,
|
|
183
|
+
});
|
|
184
|
+
return toItems(columns, rows);
|
|
185
|
+
};
|