@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.
- package/dist/client/TextRange.d.ts +0 -9
- package/dist/index.js +227 -100
- package/dist/index.js.map +1 -1
- package/dist/instance/RankControl.d.ts +3 -2
- package/dist/instance/SelectControl.d.ts +3 -2
- package/dist/parse/body/control/ItemsetDefinition.d.ts +1 -1
- package/dist/parse/expression/BindComputationExpression.d.ts +1 -1
- package/dist/parse/expression/ItemsetNodesetExpression.d.ts +1 -2
- package/dist/parse/expression/TextChunkExpression.d.ts +9 -7
- package/dist/parse/expression/abstract/DependentExpression.d.ts +1 -15
- package/dist/parse/model/ModelDefinition.d.ts +6 -1
- package/dist/parse/model/generateItextChunks.d.ts +5 -0
- package/dist/parse/xpath/semantic-analysis.d.ts +1 -0
- package/dist/solid.js +227 -100
- package/dist/solid.js.map +1 -1
- package/package.json +2 -2
- package/src/client/TextRange.ts +0 -9
- package/src/instance/RankControl.ts +8 -2
- package/src/instance/SelectControl.ts +8 -2
- package/src/lib/reactivity/text/createTextRange.ts +30 -30
- package/src/parse/body/control/ItemsetDefinition.ts +2 -2
- package/src/parse/expression/BindComputationExpression.ts +2 -7
- package/src/parse/expression/ItemsetNodesetExpression.ts +2 -3
- package/src/parse/expression/ItemsetValueExpression.ts +1 -1
- package/src/parse/expression/RepeatCountControlExpression.ts +4 -4
- package/src/parse/expression/TextChunkExpression.ts +25 -35
- package/src/parse/expression/abstract/DependentExpression.ts +2 -38
- package/src/parse/model/ModelDefinition.ts +13 -0
- package/src/parse/model/generateItextChunks.ts +61 -0
- package/src/parse/text/ItemsetLabelDefinition.ts +4 -4
- package/src/parse/text/MessageDefinition.ts +4 -4
- package/src/parse/text/abstract/TextElementDefinition.ts +6 -7
- 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.
|
|
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.
|
|
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",
|
package/src/client/TextRange.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
6
|
-
super(
|
|
4
|
+
constructor(nodesetExpression: string) {
|
|
5
|
+
super('nodes', nodesetExpression);
|
|
7
6
|
}
|
|
8
7
|
}
|
|
@@ -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(
|
|
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(
|
|
35
|
+
return new this(fixedCountExpression);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
private constructor(
|
|
42
|
-
super(
|
|
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
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
30
|
+
literalValue = '',
|
|
31
|
+
options: TextChunkExpressionOptions = {}
|
|
29
32
|
) {
|
|
30
|
-
super(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
53
|
+
return new TextChunkExpression('string', element.getAttribute('value'), 'output');
|
|
64
54
|
}
|
|
65
55
|
|
|
66
|
-
static
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
34
|
-
if (
|
|
35
|
-
this.chunks = [
|
|
33
|
+
const translationChunk = TextChunkExpression.fromTranslation(refExpression);
|
|
34
|
+
if (translationChunk) {
|
|
35
|
+
this.chunks = [translationChunk];
|
|
36
36
|
} else {
|
|
37
|
-
this.chunks = [TextChunkExpression.fromReference(
|
|
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
|
|
33
|
-
if (
|
|
34
|
-
this.chunks = [
|
|
32
|
+
const translationChunk = TextChunkExpression.fromTranslation(message);
|
|
33
|
+
if (translationChunk) {
|
|
34
|
+
this.chunks = [translationChunk];
|
|
35
35
|
} else {
|
|
36
|
-
this.chunks = [TextChunkExpression.fromLiteral(
|
|
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(
|
|
29
|
+
return TextChunkExpression.fromOutput(childNode) ?? [];
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
if (isTextNode(childNode)) {
|
|
34
|
-
return TextChunkExpression.fromLiteral(
|
|
33
|
+
return TextChunkExpression.fromLiteral(childNode.data);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
return [];
|
|
38
37
|
});
|
|
39
38
|
} else {
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
this.chunks = [
|
|
39
|
+
const translationChunk = TextChunkExpression.fromTranslation(refExpression);
|
|
40
|
+
if (translationChunk) {
|
|
41
|
+
this.chunks = [translationChunk];
|
|
43
42
|
} else {
|
|
44
|
-
this.chunks = [TextChunkExpression.fromReference(
|
|
43
|
+
this.chunks = [TextChunkExpression.fromReference(refExpression)];
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
}
|