@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.
Files changed (124) hide show
  1. package/dist/client/AttributeNode.d.ts +4 -3
  2. package/dist/client/InputNode.d.ts +8 -4
  3. package/dist/client/MarkdownNode.d.ts +3 -0
  4. package/dist/client/NoteNode.d.ts +6 -2
  5. package/dist/client/form/FormInstanceConfig.d.ts +4 -0
  6. package/dist/client/form/LoadFormResult.d.ts +5 -14
  7. package/dist/client/form/ResetFormInstance.d.ts +13 -0
  8. package/dist/entrypoints/FormResult/BaseFormResult.d.ts +1 -0
  9. package/dist/entrypoints/FormResult/BaseInstantiableFormResult.d.ts +2 -0
  10. package/dist/entrypoints/FormResult/FormFailureResult.d.ts +2 -0
  11. package/dist/entrypoints/createPotentiallyClientOwnedReactiveScope.d.ts +19 -0
  12. package/dist/index.js +22150 -25908
  13. package/dist/index.js.map +1 -1
  14. package/dist/instance/Attribute.d.ts +11 -23
  15. package/dist/instance/Group.d.ts +3 -0
  16. package/dist/instance/InputControl.d.ts +3 -0
  17. package/dist/instance/ModelValue.d.ts +4 -0
  18. package/dist/instance/Note.d.ts +4 -0
  19. package/dist/instance/PrimaryInstance.d.ts +7 -1
  20. package/dist/instance/RangeControl.d.ts +4 -0
  21. package/dist/instance/RankControl.d.ts +5 -1
  22. package/dist/instance/Root.d.ts +3 -0
  23. package/dist/instance/SelectControl.d.ts +5 -1
  24. package/dist/instance/TriggerControl.d.ts +4 -0
  25. package/dist/instance/UploadControl.d.ts +3 -0
  26. package/dist/instance/abstract/DescendantNode.d.ts +5 -4
  27. package/dist/instance/abstract/InstanceNode.d.ts +4 -3
  28. package/dist/instance/hierarchy.d.ts +2 -1
  29. package/dist/instance/internal-api/AttributeContext.d.ts +1 -0
  30. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -1
  31. package/dist/instance/internal-api/InstanceValueContext.d.ts +1 -0
  32. package/dist/instance/markdown/MarkdownNode.d.ts +14 -9
  33. package/dist/instance/repeat/RepeatInstance.d.ts +2 -0
  34. package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +1 -1
  35. package/dist/integration/xpath/adapter/kind.d.ts +5 -3
  36. package/dist/integration/xpath/adapter/traversal.d.ts +3 -3
  37. package/dist/integration/xpath/static-dom/StaticAttribute.d.ts +1 -0
  38. package/dist/integration/xpath/static-dom/StaticDocument.d.ts +2 -0
  39. package/dist/lib/codecs/{Geopoint/Geopoint.d.ts → geolocation/Geolocation.d.ts} +11 -15
  40. package/dist/lib/codecs/geolocation/Geopoint.d.ts +7 -0
  41. package/dist/lib/codecs/geolocation/Geoshape.d.ts +7 -0
  42. package/dist/lib/codecs/geolocation/Geotrace.d.ts +7 -0
  43. package/dist/lib/codecs/geolocation/createGeolocationValueCodec.d.ts +3 -0
  44. package/dist/lib/codecs/getSharedValueCodec.d.ts +7 -5
  45. package/dist/lib/reactivity/text/createTextRange.d.ts +0 -2
  46. package/dist/parse/XFormDOM.d.ts +7 -1
  47. package/dist/parse/body/appearance/inputAppearanceParser.d.ts +1 -1
  48. package/dist/parse/model/ActionDefinition.d.ts +1 -1
  49. package/dist/parse/model/AttributeDefinition.d.ts +2 -0
  50. package/dist/parse/model/BindPreloadDefinition.d.ts +2 -1
  51. package/dist/parse/model/ModelActionMap.d.ts +3 -2
  52. package/dist/parse/model/ModelDefinition.d.ts +3 -5
  53. package/dist/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.d.ts +0 -17
  54. package/dist/parse/model/SecondaryInstance/sources/external-instance-csv-parser.d.ts +8 -0
  55. package/dist/parse/model/TranslationDefinitionMap.d.ts +4 -0
  56. package/dist/solid.js +21608 -25366
  57. package/dist/solid.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/client/AttributeNode.ts +4 -3
  60. package/src/client/InputNode.ts +11 -3
  61. package/src/client/MarkdownNode.ts +3 -0
  62. package/src/client/NoteNode.ts +9 -1
  63. package/src/client/form/FormInstanceConfig.ts +6 -0
  64. package/src/client/form/LoadFormResult.ts +5 -17
  65. package/src/client/form/ResetFormInstance.ts +17 -0
  66. package/src/entrypoints/FormInstance.ts +2 -0
  67. package/src/entrypoints/FormResult/BaseFormResult.ts +1 -0
  68. package/src/entrypoints/FormResult/BaseInstantiableFormResult.ts +10 -1
  69. package/src/entrypoints/FormResult/FormFailureResult.ts +3 -0
  70. package/src/entrypoints/createPotentiallyClientOwnedReactiveScope.ts +30 -0
  71. package/src/entrypoints/loadForm.ts +1 -31
  72. package/src/instance/Attribute.ts +38 -54
  73. package/src/instance/Group.ts +12 -4
  74. package/src/instance/InputControl.ts +15 -9
  75. package/src/instance/ModelValue.ts +13 -4
  76. package/src/instance/Note.ts +13 -4
  77. package/src/instance/PrimaryInstance.ts +29 -6
  78. package/src/instance/RangeControl.ts +13 -4
  79. package/src/instance/RankControl.ts +14 -5
  80. package/src/instance/Root.ts +12 -4
  81. package/src/instance/SelectControl.ts +14 -5
  82. package/src/instance/TriggerControl.ts +13 -4
  83. package/src/instance/UploadControl.ts +13 -3
  84. package/src/instance/abstract/DescendantNode.ts +4 -3
  85. package/src/instance/abstract/InstanceNode.ts +5 -3
  86. package/src/instance/attachments/buildAttributes.ts +26 -2
  87. package/src/instance/children/childrenInitOptions.ts +2 -1
  88. package/src/instance/hierarchy.ts +2 -0
  89. package/src/instance/internal-api/AttributeContext.ts +1 -0
  90. package/src/instance/internal-api/InstanceConfig.ts +3 -0
  91. package/src/instance/internal-api/InstanceValueContext.ts +1 -0
  92. package/src/instance/markdown/MarkdownNode.ts +19 -7
  93. package/src/instance/repeat/RepeatInstance.ts +11 -3
  94. package/src/instance/text/markdownFormat.ts +4 -3
  95. package/src/integration/xpath/adapter/XFormsXPathNode.ts +1 -0
  96. package/src/integration/xpath/adapter/engineDOMAdapter.ts +2 -2
  97. package/src/integration/xpath/adapter/kind.ts +6 -1
  98. package/src/integration/xpath/adapter/names.ts +1 -0
  99. package/src/integration/xpath/adapter/traversal.ts +5 -6
  100. package/src/integration/xpath/static-dom/StaticAttribute.ts +1 -0
  101. package/src/integration/xpath/static-dom/StaticDocument.ts +2 -0
  102. package/src/lib/codecs/{Geopoint/Geopoint.ts → geolocation/Geolocation.ts} +43 -24
  103. package/src/lib/codecs/geolocation/Geopoint.ts +15 -0
  104. package/src/lib/codecs/geolocation/Geoshape.ts +36 -0
  105. package/src/lib/codecs/geolocation/Geotrace.ts +36 -0
  106. package/src/lib/codecs/geolocation/createGeolocationValueCodec.ts +18 -0
  107. package/src/lib/codecs/getSharedValueCodec.ts +37 -11
  108. package/src/lib/reactivity/createInstanceValueState.ts +90 -34
  109. package/src/lib/reactivity/text/createTextRange.ts +71 -45
  110. package/src/parse/XFormDOM.ts +22 -2
  111. package/src/parse/model/ActionDefinition.ts +6 -6
  112. package/src/parse/model/AttributeDefinition.ts +7 -0
  113. package/src/parse/model/BindDefinition.ts +1 -1
  114. package/src/parse/model/BindPreloadDefinition.ts +21 -14
  115. package/src/parse/model/ModelActionMap.ts +30 -13
  116. package/src/parse/model/ModelDefinition.ts +5 -10
  117. package/src/parse/model/RootDefinition.ts +2 -1
  118. package/src/parse/model/SecondaryInstance/sources/CSVExternalSecondaryInstance.ts +2 -184
  119. package/src/parse/model/SecondaryInstance/sources/external-instance-csv-parser.ts +185 -0
  120. package/src/parse/model/TranslationDefinitionMap.ts +23 -0
  121. package/dist/lib/codecs/Geopoint/GeopointValueCodec.d.ts +0 -5
  122. package/dist/parse/model/generateItextChunks.d.ts +0 -5
  123. package/src/lib/codecs/Geopoint/GeopointValueCodec.ts +0 -20
  124. 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(bindElement: BindElement): AnyBindPreloadDefinition | null {
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
- return new this(input);
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(input: PreloadInput<Type>) {
117
- this.type = input.type;
118
- this.parameter = input.parameter;
119
- this.event =
120
- this.type === 'timestamp' && this.parameter === 'end'
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
- model.form.xformDOM.setValues.map((setValueElement) => {
19
- const action = new ActionDefinition(model, setValueElement);
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.set(key, action);
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 itextChunks: Map<string, ChunkExpressionsByItextId>;
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.itextChunks = generateItextChunks(form.xformDOM.itextTranslationElements);
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
- getTranslationChunks(
73
- itextId: string,
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 === 'setvalue') {
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 csvData = this.resource.data.replace(/[\n\r]+$/, '');
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 { ValueCodec } from '../ValueCodec.ts';
2
- import { GeopointInputValue, GeopointRuntimeValue } from './Geopoint.ts';
3
- export declare class GeopointValueCodec extends ValueCodec<'geopoint', GeopointRuntimeValue, GeopointInputValue> {
4
- constructor();
5
- }
@@ -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
- }