@getodk/xforms-engine 0.16.0 → 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/AttributeNode.d.ts +4 -3
- 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 +22150 -25908
- package/dist/index.js.map +1 -1
- package/dist/instance/Attribute.d.ts +11 -23
- package/dist/instance/Group.d.ts +3 -0
- package/dist/instance/InputControl.d.ts +3 -0
- package/dist/instance/ModelValue.d.ts +4 -0
- package/dist/instance/Note.d.ts +4 -0
- package/dist/instance/PrimaryInstance.d.ts +7 -1
- package/dist/instance/RangeControl.d.ts +4 -0
- package/dist/instance/RankControl.d.ts +5 -1
- package/dist/instance/Root.d.ts +3 -0
- package/dist/instance/SelectControl.d.ts +5 -1
- package/dist/instance/TriggerControl.d.ts +4 -0
- package/dist/instance/UploadControl.d.ts +3 -0
- package/dist/instance/abstract/DescendantNode.d.ts +5 -4
- package/dist/instance/abstract/InstanceNode.d.ts +4 -3
- package/dist/instance/hierarchy.d.ts +2 -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/instance/repeat/RepeatInstance.d.ts +2 -0
- package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +1 -1
- package/dist/integration/xpath/adapter/kind.d.ts +5 -3
- package/dist/integration/xpath/adapter/traversal.d.ts +3 -3
- package/dist/integration/xpath/static-dom/StaticAttribute.d.ts +1 -0
- 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/AttributeDefinition.d.ts +2 -0
- 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 +21608 -25366
- package/dist/solid.js.map +1 -1
- package/package.json +2 -2
- package/src/client/AttributeNode.ts +4 -3
- 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/Attribute.ts +38 -54
- package/src/instance/Group.ts +12 -4
- package/src/instance/InputControl.ts +15 -9
- package/src/instance/ModelValue.ts +13 -4
- package/src/instance/Note.ts +13 -4
- package/src/instance/PrimaryInstance.ts +29 -6
- package/src/instance/RangeControl.ts +13 -4
- package/src/instance/RankControl.ts +14 -5
- package/src/instance/Root.ts +12 -4
- package/src/instance/SelectControl.ts +14 -5
- package/src/instance/TriggerControl.ts +13 -4
- package/src/instance/UploadControl.ts +13 -3
- package/src/instance/abstract/DescendantNode.ts +4 -3
- package/src/instance/abstract/InstanceNode.ts +5 -3
- package/src/instance/attachments/buildAttributes.ts +26 -2
- package/src/instance/children/childrenInitOptions.ts +2 -1
- package/src/instance/hierarchy.ts +2 -0
- 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/repeat/RepeatInstance.ts +11 -3
- package/src/instance/text/markdownFormat.ts +4 -3
- package/src/integration/xpath/adapter/XFormsXPathNode.ts +1 -0
- package/src/integration/xpath/adapter/engineDOMAdapter.ts +2 -2
- package/src/integration/xpath/adapter/kind.ts +6 -1
- package/src/integration/xpath/adapter/names.ts +1 -0
- package/src/integration/xpath/adapter/traversal.ts +5 -6
- package/src/integration/xpath/static-dom/StaticAttribute.ts +1 -0
- 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 +90 -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/AttributeDefinition.ts +7 -0
- 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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DOMItextTranslationElement } from '../XFormDOM.ts';
|
|
2
|
+
|
|
3
|
+
const generateChunksForLanguage = (
|
|
4
|
+
translationElement: DOMItextTranslationElement
|
|
5
|
+
): Map<string, Element> => {
|
|
6
|
+
return new Map(
|
|
7
|
+
Array.from(translationElement.children).map((textElement) => {
|
|
8
|
+
const itextId = textElement.getAttribute('id');
|
|
9
|
+
return [itextId!, textElement] as const;
|
|
10
|
+
})
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class TranslationDefinitionMap extends Map<string, Map<string, Element>> {
|
|
15
|
+
constructor(translationElements: readonly DOMItextTranslationElement[]) {
|
|
16
|
+
super(
|
|
17
|
+
translationElements.map((translationElement) => {
|
|
18
|
+
const lang = translationElement.getAttribute('lang');
|
|
19
|
+
return [lang, generateChunksForLanguage(translationElement)] as const;
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { TextChunkExpression } from '../expression/TextChunkExpression.ts';
|
|
2
|
-
import { DOMItextTranslationElement } from '../XFormDOM.ts';
|
|
3
|
-
export interface ChunkExpressionsByItextId extends Map<string, ReadonlyArray<TextChunkExpression<'string'>>> {
|
|
4
|
-
}
|
|
5
|
-
export declare const generateItextChunks: (translationElements: readonly DOMItextTranslationElement[]) => Map<string, ChunkExpressionsByItextId>;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type CodecDecoder, type CodecEncoder, ValueCodec } from '../ValueCodec.ts';
|
|
2
|
-
import { Geopoint, type GeopointInputValue, type GeopointRuntimeValue } from './Geopoint.ts';
|
|
3
|
-
|
|
4
|
-
export class GeopointValueCodec extends ValueCodec<
|
|
5
|
-
'geopoint',
|
|
6
|
-
GeopointRuntimeValue,
|
|
7
|
-
GeopointInputValue
|
|
8
|
-
> {
|
|
9
|
-
constructor() {
|
|
10
|
-
const encodeValue: CodecEncoder<GeopointInputValue> = (value) => {
|
|
11
|
-
return Geopoint.toCoordinatesString(value);
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const decodeValue: CodecDecoder<GeopointRuntimeValue> = (value: string) => {
|
|
15
|
-
return Geopoint.parseString(value);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
super('geopoint', encodeValue, decodeValue);
|
|
19
|
-
}
|
|
20
|
-
}
|