@getodk/xforms-engine 0.12.0 → 0.13.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 (33) hide show
  1. package/dist/client/TextRange.d.ts +0 -9
  2. package/dist/index.js +227 -100
  3. package/dist/index.js.map +1 -1
  4. package/dist/instance/RankControl.d.ts +3 -2
  5. package/dist/instance/SelectControl.d.ts +3 -2
  6. package/dist/parse/body/control/ItemsetDefinition.d.ts +1 -1
  7. package/dist/parse/expression/BindComputationExpression.d.ts +1 -1
  8. package/dist/parse/expression/ItemsetNodesetExpression.d.ts +1 -2
  9. package/dist/parse/expression/TextChunkExpression.d.ts +9 -7
  10. package/dist/parse/expression/abstract/DependentExpression.d.ts +1 -15
  11. package/dist/parse/model/ModelDefinition.d.ts +6 -1
  12. package/dist/parse/model/generateItextChunks.d.ts +5 -0
  13. package/dist/parse/xpath/semantic-analysis.d.ts +1 -0
  14. package/dist/solid.js +227 -100
  15. package/dist/solid.js.map +1 -1
  16. package/package.json +2 -2
  17. package/src/client/TextRange.ts +0 -9
  18. package/src/instance/RankControl.ts +8 -2
  19. package/src/instance/SelectControl.ts +8 -2
  20. package/src/lib/reactivity/text/createTextRange.ts +30 -30
  21. package/src/parse/body/control/ItemsetDefinition.ts +2 -2
  22. package/src/parse/expression/BindComputationExpression.ts +2 -7
  23. package/src/parse/expression/ItemsetNodesetExpression.ts +2 -3
  24. package/src/parse/expression/ItemsetValueExpression.ts +1 -1
  25. package/src/parse/expression/RepeatCountControlExpression.ts +4 -4
  26. package/src/parse/expression/TextChunkExpression.ts +25 -35
  27. package/src/parse/expression/abstract/DependentExpression.ts +2 -38
  28. package/src/parse/model/ModelDefinition.ts +13 -0
  29. package/src/parse/model/generateItextChunks.ts +61 -0
  30. package/src/parse/text/ItemsetLabelDefinition.ts +4 -4
  31. package/src/parse/text/MessageDefinition.ts +4 -4
  32. package/src/parse/text/abstract/TextElementDefinition.ts +6 -7
  33. package/src/parse/xpath/semantic-analysis.ts +37 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getodk/xforms-engine",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "XForms engine for ODK Web Forms",
6
6
  "type": "module",
@@ -62,7 +62,7 @@
62
62
  "devDependencies": {
63
63
  "@babel/core": "^7.28.0",
64
64
  "@getodk/tree-sitter-xpath": "0.2.0",
65
- "@getodk/xpath": "0.7.0",
65
+ "@getodk/xpath": "0.8.0",
66
66
  "@playwright/test": "^1.53.2",
67
67
  "@types/papaparse": "^5.3.16",
68
68
  "@vitest/browser": "^3.2.4",
@@ -48,15 +48,6 @@ import type { ActiveLanguage } from './FormLanguage.ts';
48
48
  * - `h:body//hint/@ref[not(is-translation-expr())]`
49
49
  *
50
50
  * (See notes above for clarification of `is-translation-expr()`.)
51
- *
52
- * @todo It's unclear whether this will all become simpler or more compelex when
53
- * we add support for outputs in translations. In theory, the actual translation
54
- * `<text>` nodes map quite well to the `TextRange` concept (i.e. they are a
55
- * range of static and output chunks, just like labels and hints). The potential
56
- * for complications arise from XPath implementation details being largely
57
- * opaque (as in, the `jr:itext` implementation is encapsulated in the `xpath`
58
- * package, and the engine doesn't really deal with itext translations at the
59
- * node level at all).
60
51
  */
61
52
  // prettier-ignore
62
53
  export type TextChunkSource =
@@ -1,4 +1,4 @@
1
- import { XPathNodeKindKey } from '@getodk/xpath';
1
+ import { type XPathChoiceNode, XPathNodeKindKey } from '@getodk/xpath';
2
2
  import type { Accessor } from 'solid-js';
3
3
  import { createMemo } from 'solid-js';
4
4
  import type { RankDefinition, RankItem, RankNode, RankValueOptions } from '../client/RankNode.ts';
@@ -67,7 +67,8 @@ export class RankControl
67
67
  XFormsXPathElement,
68
68
  EvaluationContext,
69
69
  ValidationContext,
70
- ClientReactiveSerializableValueNode
70
+ ClientReactiveSerializableValueNode,
71
+ XPathChoiceNode
71
72
  {
72
73
  static from(
73
74
  parent: GeneralParentNode,
@@ -205,4 +206,9 @@ export class RankControl
205
206
  this.setValueState(valuesInOrder);
206
207
  return this.root;
207
208
  }
209
+
210
+ getChoiceName(value: string): string | null {
211
+ const option = this.mapOptionsByValue().get(value);
212
+ return option?.label?.asString ?? null;
213
+ }
208
214
  }
@@ -1,4 +1,4 @@
1
- import { XPathNodeKindKey } from '@getodk/xpath';
1
+ import { type XPathChoiceNode, XPathNodeKindKey } from '@getodk/xpath';
2
2
  import type { Accessor } from 'solid-js';
3
3
  import { createMemo } from 'solid-js';
4
4
  import type {
@@ -61,7 +61,8 @@ export class SelectControl
61
61
  XFormsXPathElement,
62
62
  EvaluationContext,
63
63
  ValidationContext,
64
- ClientReactiveSerializableValueNode
64
+ ClientReactiveSerializableValueNode,
65
+ XPathChoiceNode
65
66
  {
66
67
  static from(
67
68
  parent: GeneralParentNode,
@@ -231,4 +232,9 @@ export class SelectControl
231
232
 
232
233
  return this.root;
233
234
  }
235
+
236
+ getChoiceName(value: string): string | null {
237
+ const option = this.mapOptionsByValue().get(value);
238
+ return option?.label?.asString ?? null;
239
+ }
234
240
  }
@@ -1,12 +1,13 @@
1
- import { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts';
1
+ import {
2
+ JRResourceURL,
3
+ type JRResourceURLString,
4
+ } from '@getodk/common/jr-resources/JRResourceURL.ts';
2
5
  import type { Accessor } from 'solid-js';
3
6
  import { createMemo } from 'solid-js';
4
7
  import type { TextRole } from '../../../client/TextRange.ts';
5
8
  import type { EvaluationContext } from '../../../instance/internal-api/EvaluationContext.ts';
6
9
  import { TextChunk } from '../../../instance/text/TextChunk.ts';
7
10
  import { TextRange, type MediaSources } from '../../../instance/text/TextRange.ts';
8
- import { isEngineXPathElement } from '../../../integration/xpath/adapter/kind.ts';
9
- import { StaticElement } from '../../../integration/xpath/static-dom/StaticElement.ts';
10
11
  import { type TextChunkExpression } from '../../../parse/expression/TextChunkExpression.ts';
11
12
  import type { TextRangeDefinition } from '../../../parse/text/abstract/TextRangeDefinition.ts';
12
13
  import { createComputedExpression } from '../createComputedExpression.ts';
@@ -25,45 +26,44 @@ interface ChunksAndMedia {
25
26
  * @param chunkExpressions - Array of text source expressions to process.
26
27
  * @returns An accessor for an object with all chunks and the first image (if any).
27
28
  */
28
- const createTextChunks = (
29
+ const createTextChunks = <Role extends TextRole>(
29
30
  context: EvaluationContext,
30
- chunkExpressions: ReadonlyArray<TextChunkExpression<'nodes' | 'string'>>
31
+ definition: TextRangeDefinition<Role>
31
32
  ): Accessor<ChunksAndMedia> => {
32
33
  return createMemo(() => {
33
34
  const chunks: TextChunk[] = [];
34
35
  const mediaSources: MediaSources = {};
35
36
 
37
+ let chunkExpressions: ReadonlyArray<TextChunkExpression<'string'>>;
38
+
39
+ if (definition.chunks[0]?.source === 'translation') {
40
+ const itextId = context.evaluator.evaluateString(definition.chunks[0].toString()!, {
41
+ contextNode: context.contextNode,
42
+ });
43
+ chunkExpressions = definition.form.model.getTranslationChunks(
44
+ itextId,
45
+ context.getActiveLanguage()
46
+ );
47
+ } else {
48
+ // only translations have 'nodes' chunks
49
+ chunkExpressions = definition.chunks as Array<TextChunkExpression<'string'>>;
50
+ }
51
+
36
52
  chunkExpressions.forEach((chunkExpression) => {
53
+ if (chunkExpression.resourceType) {
54
+ mediaSources[chunkExpression.resourceType] = JRResourceURL.from(
55
+ chunkExpression.stringValue as JRResourceURLString
56
+ );
57
+ return;
58
+ }
59
+
37
60
  if (chunkExpression.source === 'literal') {
38
61
  chunks.push(new TextChunk(context, chunkExpression.source, chunkExpression.stringValue));
39
62
  return;
40
63
  }
41
64
 
42
65
  const computed = createComputedExpression(context, chunkExpression)();
43
-
44
- if (typeof computed === 'string') {
45
- // not a translation expression
46
- chunks.push(new TextChunk(context, chunkExpression.source, computed));
47
- return;
48
- } else {
49
- // translation expression evaluates to an entire itext block, process forms separately
50
- computed.forEach((itextForm) => {
51
- if (isEngineXPathElement(itextForm) && itextForm instanceof StaticElement) {
52
- const formAttribute = itextForm.getAttributeValue('form');
53
-
54
- if (!formAttribute) {
55
- const defaultFormValue = itextForm.getXPathValue();
56
- chunks.push(new TextChunk(context, chunkExpression.source, defaultFormValue));
57
- } else if (['image', 'video', 'audio'].includes(formAttribute)) {
58
- const formValue = itextForm.getXPathValue();
59
-
60
- if (JRResourceURL.isJRResourceReference(formValue)) {
61
- mediaSources[formAttribute as keyof MediaSources] = JRResourceURL.from(formValue);
62
- }
63
- }
64
- }
65
- });
66
- }
66
+ chunks.push(new TextChunk(context, chunkExpression.source, computed));
67
67
  });
68
68
 
69
69
  return { chunks, mediaSources };
@@ -86,7 +86,7 @@ export const createTextRange = <Role extends TextRole>(
86
86
  definition: TextRangeDefinition<Role>
87
87
  ): ComputedFormTextRange<Role> => {
88
88
  return context.scope.runTask(() => {
89
- const textChunks = createTextChunks(context, definition.chunks);
89
+ const textChunks = createTextChunks(context, definition);
90
90
 
91
91
  return createMemo(() => {
92
92
  const chunks = textChunks();
@@ -6,8 +6,8 @@ import { ItemsetLabelDefinition } from '../../text/ItemsetLabelDefinition.ts';
6
6
  import type { XFormDefinition } from '../../XFormDefinition.ts';
7
7
  import { parseNodesetReference } from '../../xpath/reference-parsing.ts';
8
8
  import { BodyElementDefinition } from '../BodyElementDefinition.ts';
9
- import type { AnySelectControlDefinition } from './SelectControlDefinition.ts';
10
9
  import { RankControlDefinition } from './RankControlDefinition.ts';
10
+ import type { AnySelectControlDefinition } from './SelectControlDefinition.ts';
11
11
 
12
12
  export class ItemsetDefinition extends BodyElementDefinition<'itemset'> {
13
13
  override readonly category = 'support';
@@ -28,7 +28,7 @@ export class ItemsetDefinition extends BodyElementDefinition<'itemset'> {
28
28
 
29
29
  const nodesetExpression = parseNodesetReference(parent, element, 'nodeset');
30
30
 
31
- this.nodes = new ItemsetNodesetExpression(this, nodesetExpression);
31
+ this.nodes = new ItemsetNodesetExpression(nodesetExpression);
32
32
  this.reference = nodesetExpression;
33
33
 
34
34
  const valueElement = getValueElement(element);
@@ -47,18 +47,15 @@ export class BindComputationExpression<
47
47
  return null as BindComputationFactoryResult<Type>;
48
48
  }
49
49
 
50
- return new this(bind, computation, expression);
50
+ return new this(computation, expression);
51
51
  }
52
52
 
53
53
  readonly isDefaultExpression: boolean;
54
54
 
55
55
  protected constructor(
56
- bind: BindDefinition,
57
56
  readonly computation: Computation,
58
57
  expression: string | null
59
58
  ) {
60
- const ignoreContextReference = computation === 'constraint';
61
-
62
59
  let isDefaultExpression: boolean;
63
60
  let resolvedExpression: string;
64
61
 
@@ -75,9 +72,7 @@ export class BindComputationExpression<
75
72
  resolvedExpression = expression;
76
73
  }
77
74
 
78
- super(bind, bindComputationResultTypes[computation], resolvedExpression, {
79
- ignoreContextReference,
80
- });
75
+ super(bindComputationResultTypes[computation], resolvedExpression);
81
76
 
82
77
  this.isDefaultExpression = isDefaultExpression;
83
78
  }
@@ -1,8 +1,7 @@
1
- import type { ItemsetDefinition } from '../body/control/ItemsetDefinition.ts';
2
1
  import { DependentExpression } from './abstract/DependentExpression.ts';
3
2
 
4
3
  export class ItemsetNodesetExpression extends DependentExpression<'nodes'> {
5
- constructor(itemset: ItemsetDefinition, nodesetExpression: string) {
6
- super(itemset.parent, 'nodes', nodesetExpression);
4
+ constructor(nodesetExpression: string) {
5
+ super('nodes', nodesetExpression);
7
6
  }
8
7
  }
@@ -6,6 +6,6 @@ export class ItemsetValueExpression extends DependentExpression<'string'> {
6
6
  readonly itemset: ItemsetDefinition,
7
7
  expression: string
8
8
  ) {
9
- super(itemset, 'string', expression);
9
+ super('string', expression);
10
10
  }
11
11
  }
@@ -23,7 +23,7 @@ export class RepeatCountControlExpression extends DependentExpression<'number'>
23
23
  const { countExpression, noAddRemoveExpression } = bodyElement;
24
24
 
25
25
  if (countExpression != null) {
26
- return new this(bodyElement, countExpression);
26
+ return new this(countExpression);
27
27
  }
28
28
 
29
29
  if (noAddRemoveExpression != null && isConstantTruthyExpression(noAddRemoveExpression)) {
@@ -32,13 +32,13 @@ export class RepeatCountControlExpression extends DependentExpression<'number'>
32
32
  // repeat's template.
33
33
  const fixedCountExpression = String(Math.max(initialCount, 1));
34
34
 
35
- return new this(bodyElement, fixedCountExpression);
35
+ return new this(fixedCountExpression);
36
36
  }
37
37
 
38
38
  return null;
39
39
  }
40
40
 
41
- private constructor(context: RepeatElementDefinition, expression: string) {
42
- super(context, 'number', expression);
41
+ private constructor(expression: string) {
42
+ super('number', expression);
43
43
  }
44
44
  }
@@ -1,11 +1,14 @@
1
+ import type {
2
+ JRResourceURLString,
3
+ ResourceType,
4
+ } from '@getodk/common/jr-resources/JRResourceURL.ts';
1
5
  import type { KnownAttributeLocalNamedElement } from '@getodk/common/types/dom.ts';
2
6
  import type { TextChunkSource } from '../../client/TextRange.ts';
3
- import type { AnyTextRangeDefinition } from '../text/abstract/TextRangeDefinition.ts';
4
- import { isTranslationExpression } from '../xpath/semantic-analysis.ts';
7
+ import { getTranslationExpression } from '../xpath/semantic-analysis.ts';
5
8
  import { DependentExpression } from './abstract/DependentExpression.ts';
6
9
 
7
10
  interface TextChunkExpressionOptions {
8
- readonly isTranslated?: true;
11
+ readonly type?: ResourceType;
9
12
  }
10
13
 
11
14
  interface OutputElement extends KnownAttributeLocalNamedElement<'output', 'value'> {}
@@ -18,59 +21,46 @@ export class TextChunkExpression<T extends 'nodes' | 'string'> extends Dependent
18
21
  readonly source: TextChunkSource;
19
22
  // Set for the literal source, blank otherwise
20
23
  readonly stringValue: string;
24
+ readonly resourceType: ResourceType | null;
21
25
 
22
26
  constructor(
23
- context: AnyTextRangeDefinition,
24
27
  resultType: T,
25
28
  expression: string,
26
29
  source: TextChunkSource,
27
- options: TextChunkExpressionOptions = {},
28
- literalValue = ''
30
+ literalValue = '',
31
+ options: TextChunkExpressionOptions = {}
29
32
  ) {
30
- super(context, resultType, expression, {
31
- semanticDependencies: {
32
- translations: options.isTranslated,
33
- },
34
- ignoreContextReference: true,
35
- });
33
+ super(resultType, expression);
36
34
 
35
+ this.resourceType = options.type ?? null;
37
36
  this.source = source;
38
37
  this.stringValue = literalValue;
39
38
  }
40
39
 
41
- static fromLiteral(
42
- context: AnyTextRangeDefinition,
43
- stringValue: string
44
- ): TextChunkExpression<'string'> {
45
- return new TextChunkExpression(context, 'string', 'null', 'literal', {}, stringValue);
40
+ static fromLiteral(stringValue: string): TextChunkExpression<'string'> {
41
+ return new TextChunkExpression('string', 'null', 'literal', stringValue);
46
42
  }
47
43
 
48
- static fromReference(
49
- context: AnyTextRangeDefinition,
50
- ref: string
51
- ): TextChunkExpression<'string'> {
52
- return new TextChunkExpression(context, 'string', ref, 'reference');
44
+ static fromReference(ref: string): TextChunkExpression<'string'> {
45
+ return new TextChunkExpression('string', ref, 'reference');
53
46
  }
54
47
 
55
- static fromOutput(
56
- context: AnyTextRangeDefinition,
57
- element: Element
58
- ): TextChunkExpression<'string'> | null {
48
+ static fromOutput(element: Element): TextChunkExpression<'string'> | null {
59
49
  if (!isOutputElement(element)) {
60
50
  return null;
61
51
  }
62
52
 
63
- return new TextChunkExpression(context, 'string', element.getAttribute('value'), 'output');
53
+ return new TextChunkExpression('string', element.getAttribute('value'), 'output');
64
54
  }
65
55
 
66
- static fromTranslation(
67
- context: AnyTextRangeDefinition,
68
- maybeExpression: string
69
- ): TextChunkExpression<'nodes'> | null {
70
- if (isTranslationExpression(maybeExpression)) {
71
- return new TextChunkExpression(context, 'nodes', maybeExpression, 'translation', {
72
- isTranslated: true,
73
- });
56
+ static fromResource(url: JRResourceURLString, type: ResourceType): TextChunkExpression<'string'> {
57
+ return new TextChunkExpression('string', 'null', 'literal', url, { type });
58
+ }
59
+
60
+ static fromTranslation(maybeExpression: string): TextChunkExpression<'nodes'> | null {
61
+ const translationExpression = getTranslationExpression(maybeExpression);
62
+ if (translationExpression) {
63
+ return new TextChunkExpression('nodes', translationExpression, 'translation');
74
64
  }
75
65
 
76
66
  return null;
@@ -3,12 +3,7 @@ import type {
3
3
  ConstantExpression,
4
4
  ConstantTruthyExpression,
5
5
  } from '../../xpath/semantic-analysis.ts';
6
- import {
7
- isConstantExpression,
8
- isConstantTruthyExpression,
9
- isTranslationExpression,
10
- } from '../../xpath/semantic-analysis.ts';
11
- import type { DependencyContext } from './DependencyContext.ts';
6
+ import { isConstantExpression, isConstantTruthyExpression } from '../../xpath/semantic-analysis.ts';
12
7
 
13
8
  const evaluatorMethodsByResultType = {
14
9
  boolean: 'evaluateBoolean',
@@ -28,22 +23,6 @@ export type DependentExpressionResult<Type extends DependentExpressionResultType
28
23
  EngineXPathEvaluator[DependentExpressionEvaluatorMethod<Type>]
29
24
  >;
30
25
 
31
- interface SemanticDependencyOptions {
32
- /**
33
- * @default false
34
- */
35
- readonly translations?: boolean | undefined;
36
- }
37
-
38
- interface DependentExpressionOptions {
39
- /**
40
- * @default false
41
- */
42
- readonly ignoreContextReference?: boolean;
43
-
44
- readonly semanticDependencies?: SemanticDependencyOptions;
45
- }
46
-
47
26
  export interface ConstantDependentExpression<Type extends DependentExpressionResultType>
48
27
  extends DependentExpression<Type> {
49
28
  readonly expression: ConstantExpression;
@@ -60,10 +39,8 @@ export abstract class DependentExpression<Type extends DependentExpressionResult
60
39
  readonly constantTruthyExpression: ConstantTruthyExpression | null;
61
40
 
62
41
  constructor(
63
- context: DependencyContext,
64
42
  readonly resultType: Type,
65
- readonly expression: string,
66
- options: DependentExpressionOptions = {}
43
+ readonly expression: string
67
44
  ) {
68
45
  if (resultType === 'boolean' && isConstantTruthyExpression(expression)) {
69
46
  this.constantTruthyExpression = expression;
@@ -77,19 +54,6 @@ export abstract class DependentExpression<Type extends DependentExpressionResult
77
54
  }
78
55
 
79
56
  this.evaluatorMethod = evaluatorMethodsByResultType[resultType];
80
-
81
- const {
82
- semanticDependencies = {
83
- translations: false,
84
- },
85
- } = options;
86
-
87
- const isTranslated = semanticDependencies.translations && isTranslationExpression(expression);
88
-
89
- if (isTranslated) {
90
- this.isTranslated = true;
91
- context.isTranslated = true;
92
- }
93
57
  }
94
58
 
95
59
  isConstantExpression(): this is ConstantDependentExpression<Type> {
@@ -1,7 +1,10 @@
1
+ import type { ActiveLanguage } from '../../client/FormLanguage.ts';
1
2
  import { ErrorProductionDesignPendingError } from '../../error/ErrorProductionDesignPendingError.ts';
2
3
  import type { StaticDocument } from '../../integration/xpath/static-dom/StaticDocument.ts';
4
+ import { TextChunkExpression } from '../expression/TextChunkExpression.ts';
3
5
  import { parseStaticDocumentFromDOMSubtree } from '../shared/parseStaticDocumentFromDOMSubtree.ts';
4
6
  import type { XFormDefinition } from '../XFormDefinition.ts';
7
+ import { generateItextChunks, type ChunkExpressionsByItextId } from './generateItextChunks.ts';
5
8
  import { ItextTranslationsDefinition } from './ItextTranslationsDefinition.ts';
6
9
  import { ModelBindMap } from './ModelBindMap.ts';
7
10
  import type { AnyNodeDefinition } from './NodeDefinition.ts';
@@ -16,6 +19,7 @@ export class ModelDefinition {
16
19
  readonly nodes: NodeDefinitionMap;
17
20
  readonly instance: StaticDocument;
18
21
  readonly itextTranslations: ItextTranslationsDefinition;
22
+ readonly itextChunks: Map<string, ChunkExpressionsByItextId>;
19
23
 
20
24
  constructor(readonly form: XFormDefinition) {
21
25
  const submission = new SubmissionDefinition(form.xformDOM);
@@ -27,6 +31,7 @@ export class ModelDefinition {
27
31
  this.root = new RootDefinition(form, this, submission, form.body.classes);
28
32
  this.nodes = nodeDefinitionMap(this.root);
29
33
  this.itextTranslations = ItextTranslationsDefinition.from(form.xformDOM);
34
+ this.itextChunks = generateItextChunks(form.xformDOM.itextTranslationElements);
30
35
  }
31
36
 
32
37
  getNodeDefinition(nodeset: string): AnyNodeDefinition {
@@ -54,4 +59,12 @@ export class ModelDefinition {
54
59
 
55
60
  return rest;
56
61
  }
62
+
63
+ getTranslationChunks(
64
+ itextId: string,
65
+ activeLanguage: ActiveLanguage
66
+ ): ReadonlyArray<TextChunkExpression<'string'>> {
67
+ const languageMap = this.itextChunks.get(activeLanguage.language);
68
+ return languageMap?.get(itextId) ?? [];
69
+ }
57
70
  }
@@ -0,0 +1,61 @@
1
+ import {
2
+ isResourceType,
3
+ type JRResourceURLString,
4
+ type ResourceType,
5
+ } from '@getodk/common/jr-resources/JRResourceURL.ts';
6
+ import { isElementNode, isTextNode } from '@getodk/common/lib/dom/predicates.ts';
7
+ import { TextChunkExpression } from '../expression/TextChunkExpression.ts';
8
+ import type { DOMItextTranslationElement } from '../XFormDOM.ts';
9
+
10
+ const generateChunk = (node: Node): TextChunkExpression<'string'> | null => {
11
+ if (isElementNode(node)) {
12
+ return TextChunkExpression.fromOutput(node);
13
+ }
14
+ if (isTextNode(node)) {
15
+ const formAttribute = node.parentElement!.getAttribute('form') as ResourceType;
16
+ if (isResourceType(formAttribute)) {
17
+ return TextChunkExpression.fromResource(node.data as JRResourceURLString, formAttribute);
18
+ }
19
+ return TextChunkExpression.fromLiteral(node.data);
20
+ }
21
+ return null;
22
+ };
23
+
24
+ const generateChunksForValues = (valueElement: ChildNode): Array<TextChunkExpression<'string'>> => {
25
+ return Array.from(valueElement.childNodes)
26
+ .map((node) => generateChunk(node))
27
+ .filter((chunk) => chunk !== null);
28
+ };
29
+
30
+ const generateChunksForTranslation = (
31
+ textElement: Element
32
+ ): Array<TextChunkExpression<'string'>> => {
33
+ return Array.from(textElement.childNodes).flatMap((valueElement) =>
34
+ generateChunksForValues(valueElement)
35
+ );
36
+ };
37
+
38
+ const generateChunksForLanguage = (
39
+ translationElement: DOMItextTranslationElement
40
+ ): Map<string, ReadonlyArray<TextChunkExpression<'string'>>> => {
41
+ return new Map(
42
+ Array.from(translationElement.children).map((textElement) => {
43
+ const itextId = textElement.getAttribute('id');
44
+ return [itextId!, generateChunksForTranslation(textElement)] as const;
45
+ })
46
+ );
47
+ };
48
+
49
+ export interface ChunkExpressionsByItextId
50
+ extends Map<string, ReadonlyArray<TextChunkExpression<'string'>>> {}
51
+
52
+ export const generateItextChunks = (
53
+ translationElements: readonly DOMItextTranslationElement[]
54
+ ): Map<string, ChunkExpressionsByItextId> => {
55
+ return new Map(
56
+ translationElements.map((translationElement) => {
57
+ const lang = translationElement.getAttribute('lang');
58
+ return [lang, generateChunksForLanguage(translationElement)] as const;
59
+ })
60
+ );
61
+ };
@@ -30,11 +30,11 @@ export class ItemsetLabelDefinition extends TextRangeDefinition<'item-label'> {
30
30
  throw new Error('<itemset><label> missing ref attribute');
31
31
  }
32
32
 
33
- const expression = TextChunkExpression.fromTranslation(this, refExpression);
34
- if (expression != null) {
35
- this.chunks = [expression];
33
+ const translationChunk = TextChunkExpression.fromTranslation(refExpression);
34
+ if (translationChunk) {
35
+ this.chunks = [translationChunk];
36
36
  } else {
37
- this.chunks = [TextChunkExpression.fromReference(this, refExpression)];
37
+ this.chunks = [TextChunkExpression.fromReference(refExpression)];
38
38
  }
39
39
  }
40
40
  }
@@ -29,11 +29,11 @@ export class MessageDefinition<
29
29
  ) {
30
30
  super(bind.form, bind, null);
31
31
 
32
- const expression = TextChunkExpression.fromTranslation(this, message);
33
- if (expression != null) {
34
- this.chunks = [expression];
32
+ const translationChunk = TextChunkExpression.fromTranslation(message);
33
+ if (translationChunk) {
34
+ this.chunks = [translationChunk];
35
35
  } else {
36
- this.chunks = [TextChunkExpression.fromLiteral(this, message)];
36
+ this.chunks = [TextChunkExpression.fromLiteral(message)];
37
37
  }
38
38
  }
39
39
  }
@@ -21,27 +21,26 @@ export abstract class TextElementDefinition<
21
21
  constructor(form: XFormDefinition, owner: TextElementOwner, sourceNode: TextSourceNode<Role>) {
22
22
  super(form, owner, sourceNode);
23
23
 
24
- const context = this as AnyTextElementDefinition;
25
24
  const refExpression = parseNodesetReference(owner, sourceNode, 'ref');
26
25
 
27
26
  if (refExpression == null) {
28
27
  this.chunks = Array.from(sourceNode.childNodes).flatMap((childNode) => {
29
28
  if (isElementNode(childNode)) {
30
- return TextChunkExpression.fromOutput(context, childNode) ?? [];
29
+ return TextChunkExpression.fromOutput(childNode) ?? [];
31
30
  }
32
31
 
33
32
  if (isTextNode(childNode)) {
34
- return TextChunkExpression.fromLiteral(context, childNode.data);
33
+ return TextChunkExpression.fromLiteral(childNode.data);
35
34
  }
36
35
 
37
36
  return [];
38
37
  });
39
38
  } else {
40
- const expression = TextChunkExpression.fromTranslation(context, refExpression);
41
- if (expression != null) {
42
- this.chunks = [expression];
39
+ const translationChunk = TextChunkExpression.fromTranslation(refExpression);
40
+ if (translationChunk) {
41
+ this.chunks = [translationChunk];
43
42
  } else {
44
- this.chunks = [TextChunkExpression.fromReference(context, refExpression)];
43
+ this.chunks = [TextChunkExpression.fromReference(refExpression)];
45
44
  }
46
45
  }
47
46
  }