@getodk/xforms-engine 0.9.0 → 0.10.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 (37) hide show
  1. package/dist/client/SelectNode.d.ts +1 -0
  2. package/dist/client/TextRange.d.ts +4 -0
  3. package/dist/index.js +125 -133
  4. package/dist/index.js.map +1 -1
  5. package/dist/instance/SelectControl.d.ts +1 -0
  6. package/dist/instance/text/TextRange.d.ts +11 -1
  7. package/dist/lib/reactivity/text/createTextRange.d.ts +1 -2
  8. package/dist/parse/expression/TextChunkExpression.d.ts +16 -0
  9. package/dist/parse/text/ItemsetLabelDefinition.d.ts +2 -4
  10. package/dist/parse/text/MessageDefinition.d.ts +3 -7
  11. package/dist/parse/text/abstract/TextElementDefinition.d.ts +2 -8
  12. package/dist/parse/text/abstract/TextRangeDefinition.d.ts +2 -2
  13. package/dist/solid.js +125 -133
  14. package/dist/solid.js.map +1 -1
  15. package/package.json +2 -2
  16. package/src/client/SelectNode.ts +2 -0
  17. package/src/client/TextRange.ts +5 -1
  18. package/src/instance/SelectControl.ts +6 -0
  19. package/src/instance/text/TextRange.ts +21 -1
  20. package/src/lib/reactivity/text/createTextRange.ts +56 -43
  21. package/src/lib/reactivity/validation/createValidation.ts +1 -3
  22. package/src/parse/expression/TextChunkExpression.ts +78 -0
  23. package/src/parse/text/ItemsetLabelDefinition.ts +8 -12
  24. package/src/parse/text/MessageDefinition.ts +9 -16
  25. package/src/parse/text/abstract/TextElementDefinition.ts +10 -26
  26. package/src/parse/text/abstract/TextRangeDefinition.ts +2 -2
  27. package/src/parse/xpath/semantic-analysis.ts +7 -2
  28. package/dist/parse/expression/TextLiteralExpression.d.ts +0 -9
  29. package/dist/parse/expression/TextOutputExpression.d.ts +0 -7
  30. package/dist/parse/expression/TextReferenceExpression.d.ts +0 -7
  31. package/dist/parse/expression/TextTranslationExpression.d.ts +0 -8
  32. package/dist/parse/expression/abstract/TextChunkExpression.d.ts +0 -17
  33. package/src/parse/expression/TextLiteralExpression.ts +0 -19
  34. package/src/parse/expression/TextOutputExpression.ts +0 -25
  35. package/src/parse/expression/TextReferenceExpression.ts +0 -14
  36. package/src/parse/expression/TextTranslationExpression.ts +0 -38
  37. package/src/parse/expression/abstract/TextChunkExpression.ts +0 -38
@@ -13,6 +13,7 @@ export interface SelectItem {
13
13
  }
14
14
  export type SelectValueOptions = readonly SelectItem[];
15
15
  export interface SelectNodeState extends BaseValueNodeState<readonly string[]> {
16
+ get isSelectWithImages(): boolean;
16
17
  get children(): null;
17
18
  get valueOptions(): readonly SelectItem[];
18
19
  /**
@@ -1,3 +1,4 @@
1
+ import { JRResourceURL } from '../../../common/src/jr-resources/JRResourceURL.ts';
1
2
  import { ActiveLanguage } from './FormLanguage.ts';
2
3
  /**
3
4
  * **COMMENTARY**
@@ -135,4 +136,7 @@ export interface TextRange<Role extends TextRole, Origin extends TextOrigin = Te
135
136
  [Symbol.iterator](): Iterable<TextChunk>;
136
137
  get asString(): string;
137
138
  get formatted(): unknown;
139
+ get imageSource(): JRResourceURL | undefined;
140
+ get audioSource(): JRResourceURL | undefined;
141
+ get videoSource(): JRResourceURL | undefined;
138
142
  }
package/dist/index.js CHANGED
@@ -9759,7 +9759,7 @@ const substring = new StringFunction(
9759
9759
  return string2.substring(start, end);
9760
9760
  }
9761
9761
  );
9762
- const string$2 = new StringFunction(
9762
+ const string$1 = new StringFunction(
9763
9763
  "string",
9764
9764
  [{ arityType: "optional" }],
9765
9765
  (context, [expression]) => (expression?.evaluate(context) ?? context).toString()
@@ -9792,13 +9792,13 @@ const translate = new StringFunction(
9792
9792
  }
9793
9793
  );
9794
9794
 
9795
- const string$3 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
9795
+ const string$2 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
9796
9796
  __proto__: null,
9797
9797
  concat: concat$1,
9798
9798
  contains,
9799
9799
  normalizeSpace,
9800
9800
  startsWith,
9801
- string: string$2,
9801
+ string: string$1,
9802
9802
  stringLength,
9803
9803
  substring,
9804
9804
  substringAfter,
@@ -9810,7 +9810,7 @@ const fn$2 = new FunctionLibrary(FN_NAMESPACE_URI, [
9810
9810
  ...Object.values(boolean$2),
9811
9811
  ...Object.values(nodeset$1),
9812
9812
  ...Object.values(number$3),
9813
- ...Object.values(string$3)
9813
+ ...Object.values(string$2)
9814
9814
  ]);
9815
9815
 
9816
9816
  class BaseStep {
@@ -11590,21 +11590,21 @@ const enk = new FunctionLibrary(ENKETO_NAMESPACE_URI, [
11590
11590
  new FunctionAlias("format-date", formatDateTime)
11591
11591
  ]);
11592
11592
 
11593
- const itext = new StringFunction(
11593
+ const itext = new NodeSetFunction(
11594
11594
  "itext",
11595
11595
  [{ arityType: "required", typeHint: "string" }],
11596
11596
  (context, [itextIDExpression]) => {
11597
11597
  const itextID = itextIDExpression.evaluate(context).toString();
11598
- return XFormsXPathEvaluator.getDefaultTranslationText(context, itextID);
11598
+ return XFormsXPathEvaluator.getTranslationValues(context, itextID) ?? [];
11599
11599
  }
11600
11600
  );
11601
11601
 
11602
- const string$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
11602
+ const nodeSet = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
11603
11603
  __proto__: null,
11604
11604
  itext
11605
11605
  }, Symbol.toStringTag, { value: 'Module' }));
11606
11606
 
11607
- const jr$2 = new FunctionLibrary(JAVAROSA_NAMESPACE_URI, [...Object.values(string$1)]);
11607
+ const jr$2 = new FunctionLibrary(JAVAROSA_NAMESPACE_URI, [...Object.values(nodeSet)]);
11608
11608
 
11609
11609
  const booleanFromString = new BooleanFunction(
11610
11610
  "boolean-from-string",
@@ -19307,46 +19307,39 @@ class XFormsItextTranslations {
19307
19307
  return textMap.get(itextID) ?? null;
19308
19308
  }
19309
19309
  /**
19310
+ * Retrieves all translation value elements for a given Itext in the active language.
19311
+ *
19310
19312
  * @package
19311
19313
  *
19312
- * Here, "default" is meant as more precise language than "regular" as
19313
- * {@link https://getodk.github.io/xforms-spec/#languages | specified by ODK XForms}. In other words, this is equivalent to the following hypothetical XPath pseudo-code (whitespace added to improve structural clarity):
19314
+ * This method fetches the `text` element matching `itextID` and returns its child `value` elements,
19315
+ * which may have an optional `@form` attribute.
19316
+ *
19317
+ * The operation is conceptually similar to the following XPath query:
19314
19318
  *
19315
19319
  * ```xpath
19316
- * string(
19317
- * imaginary:itext-translation(
19318
- * xpath3-fn:environment-variable('activeLanguage')
19319
- * )
19320
- * /text[@id = $itextID]
19321
- * /value[not(@form)]
19320
+ * imaginary:itext-translation(
19321
+ * xpath3-fn:environment-variable('activeLanguage')
19322
19322
  * )
19323
+ * /text[@id = $itextID]
19324
+ * /value
19323
19325
  * ```
19324
19326
  *
19325
19327
  * Or alternately:
19326
19328
  *
19327
19329
  * ```xpath
19328
- * string(
19329
- * imaginary:itext-root()
19330
- * /translation[@lang = xpath3-fn:environment-variable('activeLanguage')]
19331
- * /text[@id = $itextID]
19332
- * /value[not(@form)]
19333
- * )
19330
+ * imaginary:itext-root()
19331
+ * /translation[@lang = xpath3-fn:environment-variable('activeLanguage')]
19332
+ * /text[@id = $itextID]
19333
+ * /value
19334
19334
  * ```
19335
- *
19336
- * @todo The above really feels like it adds some helpful clarity to how `jr:itext()` is designed to work, and the kinds of structures, state and input involved. Since there's already some discomfort around that API as specified, it's worth considering
19335
+ * Returns an array of `XFormsItextTranslationValueElement<T>`
19337
19336
  */
19338
- getDefaultTranslationText(itextID) {
19337
+ getTranslationValues(itextID) {
19339
19338
  const textElement = this.getTranslationTextElement(itextID);
19340
19339
  if (textElement == null) {
19341
- return "";
19342
- }
19343
- const { domProvider } = this;
19344
- for (const valueElement of domProvider.getChildrenByLocalName(textElement, "value")) {
19345
- if (!domProvider.hasLocalNamedAttribute(valueElement, "form")) {
19346
- return domProvider.getNodeValue(valueElement);
19347
- }
19340
+ return [];
19348
19341
  }
19349
- return "";
19342
+ return [...this.domProvider.getChildrenByLocalName(textElement, "value")];
19350
19343
  }
19351
19344
  getLanguages() {
19352
19345
  return this.languages;
@@ -19383,9 +19376,9 @@ class XFormsXPathEvaluator extends Evaluator {
19383
19376
  assertInternalXFormsXPathEvaluatorContext(context);
19384
19377
  return context.evaluator.secondaryInstancesById.get(id) ?? null;
19385
19378
  }
19386
- static getDefaultTranslationText(context, itextID) {
19379
+ static getTranslationValues(context, itextID) {
19387
19380
  assertInternalXFormsXPathEvaluatorContext(context);
19388
- return context.evaluator.itextTranslations.getDefaultTranslationText(itextID);
19381
+ return context.evaluator.itextTranslations.getTranslationValues(itextID);
19389
19382
  }
19390
19383
  rootNode;
19391
19384
  /**
@@ -19939,8 +19932,13 @@ const isTranslationFunctionCall = (syntaxNode) => {
19939
19932
  ]);
19940
19933
  };
19941
19934
  const isTranslationExpression = (expression) => {
19942
- const { rootNode } = expressionParser.parse(expression);
19943
- const functionCallNode = findTypedPrincipalExpressionNode(["function_call"], rootNode);
19935
+ let result;
19936
+ try {
19937
+ result = expressionParser.parse(expression);
19938
+ } catch {
19939
+ return false;
19940
+ }
19941
+ const functionCallNode = findTypedPrincipalExpressionNode(["function_call"], result.rootNode);
19944
19942
  if (functionCallNode == null) {
19945
19943
  return false;
19946
19944
  }
@@ -20104,77 +20102,43 @@ class DependentExpression {
20104
20102
  }
20105
20103
  }
20106
20104
 
20105
+ const isOutputElement = (element) => {
20106
+ return element.localName === "output" && element.hasAttribute("value");
20107
+ };
20107
20108
  class TextChunkExpression extends DependentExpression {
20109
+ source;
20110
+ // Set for the literal source, blank otherwise
20108
20111
  stringValue;
20109
- constructor(context, expression, options = {}) {
20110
- super(context, "string", expression, {
20112
+ constructor(context, resultType, expression, source, options = {}, literalValue = "") {
20113
+ super(context, resultType, expression, {
20111
20114
  semanticDependencies: {
20112
20115
  translations: options.isTranslated
20113
20116
  },
20114
20117
  ignoreContextReference: true
20115
20118
  });
20119
+ this.source = source;
20120
+ this.stringValue = literalValue;
20116
20121
  }
20117
- }
20118
-
20119
- class TextLiteralExpression extends TextChunkExpression {
20120
- constructor(context, stringValue) {
20121
- super(context, "null");
20122
- this.stringValue = stringValue;
20123
- }
20124
- static from(context, stringValue) {
20125
- return new this(context, stringValue);
20126
- }
20127
- source = "literal";
20128
- }
20129
-
20130
- const isOutputElement = (element) => {
20131
- return element.localName === "output" && element.hasAttribute("value");
20132
- };
20133
- class TextOutputExpression extends TextChunkExpression {
20134
- static from(context, element) {
20135
- if (isOutputElement(element)) {
20136
- return new this(context, element);
20137
- }
20138
- return null;
20139
- }
20140
- source = "output";
20141
- constructor(context, element) {
20142
- super(context, element.getAttribute("value"));
20143
- }
20144
- }
20145
-
20146
- class TextReferenceExpression extends TextChunkExpression {
20147
- static from(context, refExpression) {
20148
- return new this(context, refExpression);
20122
+ static fromLiteral(context, stringValue) {
20123
+ return new TextChunkExpression(context, "string", "null", "literal", {}, stringValue);
20149
20124
  }
20150
- source = "reference";
20151
- constructor(context, refExpression) {
20152
- super(context, refExpression);
20125
+ static fromReference(context, ref) {
20126
+ return new TextChunkExpression(context, "string", ref, "reference");
20153
20127
  }
20154
- }
20155
-
20156
- class TextTranslationExpression extends TextChunkExpression {
20157
- static fromMessage(context, maybeExpression) {
20158
- try {
20159
- expressionParser.parse(maybeExpression);
20160
- } catch {
20128
+ static fromOutput(context, element) {
20129
+ if (!isOutputElement(element)) {
20161
20130
  return null;
20162
20131
  }
20163
- if (isTranslationExpression(maybeExpression)) {
20164
- return new this(context, maybeExpression);
20165
- }
20166
- return null;
20132
+ return new TextChunkExpression(context, "string", element.getAttribute("value"), "output");
20167
20133
  }
20168
- static from(context, maybeExpression) {
20134
+ static fromTranslation(context, maybeExpression) {
20169
20135
  if (isTranslationExpression(maybeExpression)) {
20170
- return new this(context, maybeExpression);
20136
+ return new TextChunkExpression(context, "nodes", maybeExpression, "translation", {
20137
+ isTranslated: true
20138
+ });
20171
20139
  }
20172
20140
  return null;
20173
20141
  }
20174
- source = "translation";
20175
- constructor(context, expression) {
20176
- super(context, expression, { isTranslated: true });
20177
- }
20178
20142
  }
20179
20143
 
20180
20144
  const absolutePathNodeList = (pathNode) => {
@@ -20423,16 +20387,20 @@ class TextElementDefinition extends TextRangeDefinition {
20423
20387
  if (refExpression == null) {
20424
20388
  this.chunks = Array.from(sourceNode.childNodes).flatMap((childNode) => {
20425
20389
  if (isElementNode(childNode)) {
20426
- return TextOutputExpression.from(context, childNode) ?? [];
20390
+ return TextChunkExpression.fromOutput(context, childNode) ?? [];
20427
20391
  }
20428
20392
  if (isTextNode(childNode)) {
20429
- return TextLiteralExpression.from(context, childNode.data);
20393
+ return TextChunkExpression.fromLiteral(context, childNode.data);
20430
20394
  }
20431
20395
  return [];
20432
20396
  });
20433
20397
  } else {
20434
- const refChunk = TextTranslationExpression.from(context, refExpression) ?? TextReferenceExpression.from(context, refExpression);
20435
- this.chunks = [refChunk];
20398
+ const expression = TextChunkExpression.fromTranslation(context, refExpression);
20399
+ if (expression != null) {
20400
+ this.chunks = [expression];
20401
+ } else {
20402
+ this.chunks = [TextChunkExpression.fromReference(context, refExpression)];
20403
+ }
20436
20404
  }
20437
20405
  }
20438
20406
  }
@@ -20684,8 +20652,12 @@ class ItemsetLabelDefinition extends TextRangeDefinition {
20684
20652
  if (refExpression == null) {
20685
20653
  throw new Error("<itemset><label> missing ref attribute");
20686
20654
  }
20687
- const refChunk = TextTranslationExpression.from(this, refExpression) ?? TextReferenceExpression.from(this, refExpression);
20688
- this.chunks = [refChunk];
20655
+ const expression = TextChunkExpression.fromTranslation(this, refExpression);
20656
+ if (expression != null) {
20657
+ this.chunks = [expression];
20658
+ } else {
20659
+ this.chunks = [TextChunkExpression.fromReference(this, refExpression)];
20660
+ }
20689
20661
  }
20690
20662
  }
20691
20663
 
@@ -21593,8 +21565,12 @@ class MessageDefinition extends TextRangeDefinition {
21593
21565
  constructor(bind, role, message) {
21594
21566
  super(bind.form, bind, null);
21595
21567
  this.role = role;
21596
- const chunk = TextTranslationExpression.fromMessage(this, message) ?? TextLiteralExpression.from(this, message);
21597
- this.chunks = [chunk];
21568
+ const expression = TextChunkExpression.fromTranslation(this, message);
21569
+ if (expression != null) {
21570
+ this.chunks = [expression];
21571
+ } else {
21572
+ this.chunks = [TextChunkExpression.fromLiteral(this, message)];
21573
+ }
21598
21574
  }
21599
21575
  static from(bind, type) {
21600
21576
  const message = bind.bindElement.getAttributeNS(JAVAROSA_NAMESPACE_URI$1, type);
@@ -29479,10 +29455,11 @@ class TextChunk {
29479
29455
  }
29480
29456
 
29481
29457
  class TextRange {
29482
- constructor(origin, role, chunks) {
29458
+ constructor(origin, role, chunks, mediaSources) {
29483
29459
  this.origin = origin;
29484
29460
  this.role = role;
29485
29461
  this.chunks = chunks;
29462
+ this.mediaSources = mediaSources;
29486
29463
  }
29487
29464
  *[Symbol.iterator]() {
29488
29465
  yield* this.chunks;
@@ -29493,44 +29470,56 @@ class TextRange {
29493
29470
  get asString() {
29494
29471
  return this.chunks.map((chunk) => chunk.asString).join("");
29495
29472
  }
29473
+ get imageSource() {
29474
+ return this.mediaSources?.image;
29475
+ }
29476
+ get audioSource() {
29477
+ return this.mediaSources?.audio;
29478
+ }
29479
+ get videoSource() {
29480
+ return this.mediaSources?.video;
29481
+ }
29496
29482
  }
29497
29483
 
29498
- const createComputedTextChunk = (context, textSource) => {
29499
- const { source } = textSource;
29500
- if (source === "literal") {
29501
- const { stringValue } = textSource;
29502
- return {
29503
- source,
29504
- getText: () => stringValue
29505
- };
29506
- }
29507
- return context.scope.runTask(() => {
29508
- const getText = createComputedExpression(context, textSource, {
29509
- defaultValue: ""
29510
- });
29511
- return {
29512
- source,
29513
- getText
29514
- };
29515
- });
29516
- };
29517
- const createTextChunks = (context, textSources) => {
29518
- return context.scope.runTask(() => {
29519
- const chunkComputations = textSources.map((textSource) => {
29520
- return createComputedTextChunk(context, textSource);
29521
- });
29522
- return createMemo(() => {
29523
- return chunkComputations.map(({ source, getText }) => {
29524
- return new TextChunk(context, source, getText());
29525
- });
29484
+ const createTextChunks = (context, chunkExpressions) => {
29485
+ return createMemo(() => {
29486
+ const chunks = [];
29487
+ const mediaSources = {};
29488
+ chunkExpressions.forEach((chunkExpression) => {
29489
+ if (chunkExpression.source === "literal") {
29490
+ chunks.push(new TextChunk(context, chunkExpression.source, chunkExpression.stringValue));
29491
+ return;
29492
+ }
29493
+ const computed = createComputedExpression(context, chunkExpression)();
29494
+ if (typeof computed === "string") {
29495
+ chunks.push(new TextChunk(context, chunkExpression.source, computed));
29496
+ return;
29497
+ } else {
29498
+ computed.forEach((itextForm) => {
29499
+ if (isEngineXPathElement(itextForm) && itextForm instanceof StaticElement) {
29500
+ const formAttribute = itextForm.getAttributeValue("form");
29501
+ if (!formAttribute) {
29502
+ const defaultFormValue = itextForm.getXPathValue();
29503
+ chunks.push(new TextChunk(context, chunkExpression.source, defaultFormValue));
29504
+ } else if (["image", "video", "audio"].includes(formAttribute)) {
29505
+ const formValue = itextForm.getXPathValue();
29506
+ if (JRResourceURL.isJRResourceReference(formValue)) {
29507
+ mediaSources[formAttribute] = JRResourceURL.from(formValue);
29508
+ }
29509
+ }
29510
+ }
29511
+ });
29512
+ }
29526
29513
  });
29514
+ return { chunks, mediaSources };
29527
29515
  });
29528
29516
  };
29529
29517
  const createTextRange = (context, role, definition) => {
29530
29518
  return context.scope.runTask(() => {
29531
- const getTextChunks = createTextChunks(context, definition.chunks);
29519
+ const textChunks = createTextChunks(context, definition.chunks);
29532
29520
  return createMemo(() => {
29533
- return new TextRange("form", role, getTextChunks());
29521
+ const chunks = textChunks();
29522
+ return new TextRange("form", role, chunks.chunks, chunks.mediaSources);
29534
29523
  });
29535
29524
  });
29536
29525
  };
@@ -29697,8 +29686,7 @@ const createInstanceValueState = (context) => {
29697
29686
  const engineViolationMessage = (context, role) => {
29698
29687
  const messageText = VALIDATION_TEXT[role];
29699
29688
  const chunk = new TextChunk(context, "literal", messageText);
29700
- const message = new TextRange("engine", role, [chunk]);
29701
- return () => message;
29689
+ return () => new TextRange("engine", role, [chunk]);
29702
29690
  };
29703
29691
  const createViolationMessage = (context, role, definition) => {
29704
29692
  if (definition == null) {
@@ -30875,6 +30863,9 @@ class SelectControl extends ValueNode {
30875
30863
  this.appearances = definition.bodyElement.appearances;
30876
30864
  this.selectType = definition.bodyElement.type;
30877
30865
  const valueOptions = createItemCollection(this);
30866
+ const isSelectWithImages = this.scope.runTask(() => {
30867
+ return createMemo(() => valueOptions().some((item) => !!item.label.imageSource));
30868
+ });
30878
30869
  const mapOptionsByValue = this.scope.runTask(() => {
30879
30870
  return createMemo(() => {
30880
30871
  return new Map(valueOptions().map((item) => [item.value, item]));
@@ -30909,7 +30900,8 @@ class SelectControl extends ValueNode {
30909
30900
  children: null,
30910
30901
  valueOptions,
30911
30902
  value: valueState,
30912
- instanceValue: this.getInstanceValue
30903
+ instanceValue: this.getInstanceValue,
30904
+ isSelectWithImages
30913
30905
  },
30914
30906
  this.instanceConfig
30915
30907
  );