@bpmn-io/form-js-viewer 0.12.1 → 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 (44) hide show
  1. package/README.md +0 -1
  2. package/dist/assets/form-js-base.css +779 -0
  3. package/dist/assets/form-js.css +2693 -63
  4. package/dist/index.cjs +679 -311
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.es.js +676 -312
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/types/Form.d.ts +34 -0
  9. package/dist/types/core/FormLayouter.d.ts +64 -0
  10. package/dist/types/core/index.d.ts +5 -5
  11. package/dist/types/{core → features/expression-language}/ConditionChecker.d.ts +5 -13
  12. package/dist/types/features/expression-language/FeelExpressionLanguage.d.ts +39 -0
  13. package/dist/types/features/expression-language/FeelersTemplating.d.ts +26 -0
  14. package/dist/types/features/expression-language/index.d.ts +11 -0
  15. package/dist/types/features/index.d.ts +4 -0
  16. package/dist/types/features/markdown/MarkdownRenderer.d.ts +15 -0
  17. package/dist/types/features/markdown/index.d.ts +7 -0
  18. package/dist/types/import/Importer.d.ts +3 -1
  19. package/dist/types/import/index.d.ts +1 -1
  20. package/dist/types/index.d.ts +4 -3
  21. package/dist/types/render/components/Sanitizer.d.ts +10 -1
  22. package/dist/types/render/components/Util.d.ts +1 -12
  23. package/dist/types/render/components/form-fields/Checklist.d.ts +1 -1
  24. package/dist/types/render/components/form-fields/Datetime.d.ts +1 -1
  25. package/dist/types/render/components/form-fields/Radio.d.ts +1 -1
  26. package/dist/types/render/components/form-fields/Select.d.ts +1 -1
  27. package/dist/types/render/components/form-fields/Taglist.d.ts +1 -1
  28. package/dist/types/render/components/index.d.ts +14 -14
  29. package/dist/types/render/components/util/valuesUtil.d.ts +2 -0
  30. package/dist/types/render/context/FormRenderContext.d.ts +4 -2
  31. package/dist/types/render/hooks/index.d.ts +6 -0
  32. package/dist/types/render/hooks/useCondition.d.ts +2 -3
  33. package/dist/types/render/hooks/useExpressionEvaluation.d.ts +9 -0
  34. package/dist/types/render/hooks/useFilteredFormData.d.ts +6 -0
  35. package/dist/types/render/hooks/useService.d.ts +1 -1
  36. package/dist/types/render/hooks/useTemplateEvaluation.d.ts +16 -0
  37. package/dist/types/render/index.d.ts +2 -2
  38. package/dist/types/util/index.d.ts +2 -2
  39. package/dist/types/util/injector.d.ts +1 -1
  40. package/package.json +11 -7
  41. package/dist/assets/flatpickr/light.css +0 -809
  42. package/dist/types/render/hooks/useEvaluation.d.ts +0 -6
  43. package/dist/types/render/hooks/useExpressionValue.d.ts +0 -5
  44. package/dist/types/util/feel.d.ts +0 -15
package/dist/index.es.js CHANGED
@@ -1,17 +1,143 @@
1
1
  import Ids from 'ids';
2
- import { isString, isArray, isFunction, isNumber, bind, assign, isNil, get, isUndefined, set, findIndex, isObject } from 'min-dash';
3
- import { unaryTest, evaluate, parseUnaryTests, parseExpressions } from 'feelin';
2
+ import { isString, isArray, isFunction, isNumber, bind, assign, isNil, groupBy, flatten, get, isUndefined, set, findIndex, isObject } from 'min-dash';
3
+ import { evaluate, parseExpressions, parseUnaryTests, unaryTest } from 'feelin';
4
+ import { evaluate as evaluate$1 } from 'feelers';
5
+ import showdown from 'showdown';
4
6
  import Big from 'big.js';
5
- import snarkdown from '@bpmn-io/snarkdown';
6
7
  import classNames from 'classnames';
7
8
  import { jsx, jsxs, Fragment as Fragment$1 } from 'preact/jsx-runtime';
8
- import { useContext, useState, useEffect, useRef, useCallback, useMemo } from 'preact/hooks';
9
+ import { useContext, useState, useEffect, useMemo, useRef, useCallback } from 'preact/hooks';
9
10
  import { createContext, createElement, Fragment, render } from 'preact';
10
11
  import React, { createPortal } from 'preact/compat';
11
12
  import flatpickr from 'flatpickr';
12
13
  import Markup from 'preact-markup';
13
14
  import { Injector } from 'didi';
14
15
 
16
+ class FeelExpressionLanguage {
17
+ constructor(eventBus) {
18
+ this._eventBus = eventBus;
19
+ }
20
+
21
+ /**
22
+ * Determines if the given string is a FEEL expression.
23
+ *
24
+ * @param {string} value
25
+ * @returns {boolean}
26
+ *
27
+ */
28
+ isExpression(value) {
29
+ return isString(value) && value.startsWith('=');
30
+ }
31
+
32
+ /**
33
+ * Retrieve variable names from a given FEEL expression.
34
+ *
35
+ * @param {string} expression
36
+ * @param {object} [options]
37
+ * @param {string} [options.type]
38
+ *
39
+ * @returns {string[]}
40
+ */
41
+ getVariableNames(expression, options = {}) {
42
+ const {
43
+ type = 'expression'
44
+ } = options;
45
+ if (!this.isExpression(expression)) {
46
+ return [];
47
+ }
48
+ if (type === 'unaryTest') {
49
+ return this._getUnaryVariableNames(expression);
50
+ } else if (type === 'expression') {
51
+ return this._getExpressionVariableNames(expression);
52
+ }
53
+ throw new Error('Unknown expression type: ' + options.type);
54
+ }
55
+
56
+ /**
57
+ * Evaluate an expression.
58
+ *
59
+ * @param {string} expression
60
+ * @param {import('../../types').Data} [data]
61
+ *
62
+ * @returns {any}
63
+ */
64
+ evaluate(expression, data = {}) {
65
+ if (!expression) {
66
+ return null;
67
+ }
68
+ if (!isString(expression) || !expression.startsWith('=')) {
69
+ return null;
70
+ }
71
+ try {
72
+ const result = evaluate(expression.slice(1), data);
73
+ return result;
74
+ } catch (error) {
75
+ this._eventBus.fire('error', {
76
+ error
77
+ });
78
+ return null;
79
+ }
80
+ }
81
+ _getExpressionVariableNames(expression) {
82
+ const tree = parseExpressions(expression);
83
+ const cursor = tree.cursor();
84
+ const variables = new Set();
85
+ do {
86
+ const node = cursor.node;
87
+ if (node.type.name === 'VariableName') {
88
+ variables.add(expression.slice(node.from, node.to));
89
+ }
90
+ } while (cursor.next());
91
+ return Array.from(variables);
92
+ }
93
+ _getUnaryVariableNames(unaryTest) {
94
+ const tree = parseUnaryTests(unaryTest);
95
+ const cursor = tree.cursor();
96
+ const variables = new Set();
97
+ do {
98
+ const node = cursor.node;
99
+ if (node.type.name === 'VariableName') {
100
+ variables.add(unaryTest.slice(node.from, node.to));
101
+ }
102
+ } while (cursor.next());
103
+ return Array.from(variables);
104
+ }
105
+ }
106
+ FeelExpressionLanguage.$inject = ['eventBus'];
107
+
108
+ class FeelersTemplating {
109
+ constructor() {}
110
+ isTemplate(value) {
111
+ return isString(value) && (value.startsWith('=') || /{{/.test(value));
112
+ }
113
+
114
+ /**
115
+ * Evaluate a template.
116
+ *
117
+ * @param {string} template
118
+ * @param {Object<string, any>} context
119
+ * @param {Object} options
120
+ * @param {boolean} [options.debug = false]
121
+ * @param {boolean} [options.strict = false]
122
+ * @param {Function} [options.buildDebugString]
123
+ *
124
+ * @returns
125
+ */
126
+ evaluate(template, context = {}, options = {}) {
127
+ const {
128
+ debug = false,
129
+ strict = false,
130
+ buildDebugString = err => ' {{⚠}} '
131
+ } = options;
132
+ return evaluate$1(template, context, {
133
+ debug,
134
+ strict,
135
+ buildDebugString
136
+ });
137
+ }
138
+ }
139
+ FeelersTemplating.$inject = [];
140
+
15
141
  /**
16
142
  * @typedef {object} Condition
17
143
  * @property {string} [hide]
@@ -50,7 +176,7 @@ class ConditionChecker {
50
176
  * Check if given condition is met. Returns null for invalid/missing conditions.
51
177
  *
52
178
  * @param {string} condition
53
- * @param {import('../types').Data} [data]
179
+ * @param {import('../../types').Data} [data]
54
180
  *
55
181
  * @returns {boolean|null}
56
182
  */
@@ -87,32 +213,6 @@ class ConditionChecker {
87
213
  const result = this.check(condition.hide, data);
88
214
  return result === true;
89
215
  }
90
-
91
- /**
92
- * Evaluate an expression.
93
- *
94
- * @param {string} expression
95
- * @param {import('../types').Data} [data]
96
- *
97
- * @returns {any}
98
- */
99
- evaluate(expression, data = {}) {
100
- if (!expression) {
101
- return null;
102
- }
103
- if (!isString(expression) || !expression.startsWith('=')) {
104
- return null;
105
- }
106
- try {
107
- const result = evaluate(expression.slice(1), data);
108
- return result;
109
- } catch (error) {
110
- this._eventBus.fire('error', {
111
- error
112
- });
113
- return null;
114
- }
115
- }
116
216
  _getConditions() {
117
217
  const formFields = this._formFieldRegistry.getAll();
118
218
  return formFields.reduce((conditions, formField) => {
@@ -132,6 +232,38 @@ class ConditionChecker {
132
232
  }
133
233
  ConditionChecker.$inject = ['formFieldRegistry', 'eventBus'];
134
234
 
235
+ var ExpressionLanguageModule = {
236
+ __init__: ['expressionLanguage', 'templating', 'conditionChecker'],
237
+ expressionLanguage: ['type', FeelExpressionLanguage],
238
+ templating: ['type', FeelersTemplating],
239
+ conditionChecker: ['type', ConditionChecker]
240
+ };
241
+
242
+ // bootstrap showdown to support github flavored markdown
243
+ showdown.setFlavor('github');
244
+ class MarkdownRenderer {
245
+ constructor() {
246
+ this._converter = new showdown.Converter();
247
+ }
248
+
249
+ /**
250
+ * Render markdown to HTML.
251
+ *
252
+ * @param {string} markdown - The markdown to render
253
+ *
254
+ * @returns {string} HTML
255
+ */
256
+ render(markdown) {
257
+ return this._converter.makeHtml(markdown);
258
+ }
259
+ }
260
+ MarkdownRenderer.$inject = [];
261
+
262
+ var MarkdownModule = {
263
+ __init__: ['markdownRenderer'],
264
+ markdownRenderer: ['type', MarkdownRenderer]
265
+ };
266
+
135
267
  var FN_REF = '__fn';
136
268
  var DEFAULT_PRIORITY = 1000;
137
269
  var slice = Array.prototype.slice;
@@ -704,44 +836,148 @@ class FormFieldRegistry {
704
836
  FormFieldRegistry.$inject = ['eventBus'];
705
837
 
706
838
  /**
707
- * Retrieve variable names from given FEEL unary test.
708
- *
709
- * @param {string} unaryTest
710
- * @returns {string[]}
839
+ * @typedef { { id: String, components: Array<String> } } FormRow
840
+ * @typedef { { formFieldId: String, rows: Array<FormRow> } } FormRows
711
841
  */
712
- function getVariableNames(unaryTest) {
713
- const tree = parseUnaryTests(unaryTest);
714
- const cursor = tree.cursor();
715
- const variables = new Set();
716
- do {
717
- const node = cursor.node;
718
- if (node.type.name === 'VariableName') {
719
- variables.add(unaryTest.slice(node.from, node.to));
720
- }
721
- } while (cursor.next());
722
- return Array.from(variables);
723
- }
724
842
 
725
843
  /**
726
- * Retrieve variable names from given FEEL expression.
844
+ * Maintains the Form layout in a given structure, for example
845
+ *
846
+ * [
847
+ * {
848
+ * formFieldId: 'FormField_1',
849
+ * rows: [
850
+ * { id: 'Row_1', components: [ 'Text_1', 'Textdield_1', ... ] }
851
+ * ]
852
+ * }
853
+ * ]
727
854
  *
728
- * @param {string} expression
729
- * @returns {string[]}
730
855
  */
731
- function getExpressionVariableNames(expression) {
732
- const tree = parseExpressions(expression);
733
- const cursor = tree.cursor();
734
- const variables = new Set();
735
- do {
736
- const node = cursor.node;
737
- if (node.type.name === 'VariableName') {
738
- variables.add(expression.slice(node.from, node.to));
856
+ class FormLayouter {
857
+ constructor(eventBus) {
858
+ /** @type Array<FormRows> */
859
+ this._rows = [];
860
+ this._ids = new Ids([32, 36, 1]);
861
+ this._eventBus = eventBus;
862
+ }
863
+
864
+ /**
865
+ * @param {FormRow} row
866
+ */
867
+ addRow(formFieldId, row) {
868
+ let rowsPerComponent = this._rows.find(r => r.formFieldId === formFieldId);
869
+ if (!rowsPerComponent) {
870
+ rowsPerComponent = {
871
+ formFieldId,
872
+ rows: []
873
+ };
874
+ this._rows.push(rowsPerComponent);
875
+ }
876
+ rowsPerComponent.rows.push(row);
877
+ }
878
+
879
+ /**
880
+ * @param {String} id
881
+ * @returns {FormRow}
882
+ */
883
+ getRow(id) {
884
+ const rows = allRows(this._rows);
885
+ return rows.find(r => r.id === id);
886
+ }
887
+
888
+ /**
889
+ * @param {any} formField
890
+ * @returns {FormRow}
891
+ */
892
+ getRowForField(formField) {
893
+ return allRows(this._rows).find(r => {
894
+ const {
895
+ components
896
+ } = r;
897
+ return components.includes(formField.id);
898
+ });
899
+ }
900
+
901
+ /**
902
+ * @param {String} formFieldId
903
+ * @returns { Array<FormRow> }
904
+ */
905
+ getRows(formFieldId) {
906
+ const rowsForField = this._rows.find(r => formFieldId === r.formFieldId);
907
+ if (!rowsForField) {
908
+ return [];
909
+ }
910
+ return rowsForField.rows;
911
+ }
912
+
913
+ /**
914
+ * @returns {string}
915
+ */
916
+ nextRowId() {
917
+ return this._ids.nextPrefixed('Row_');
918
+ }
919
+
920
+ /**
921
+ * @param {any} formField
922
+ */
923
+ calculateLayout(formField) {
924
+ const {
925
+ type,
926
+ components
927
+ } = formField;
928
+ if (type !== 'default' || !components) {
929
+ return;
930
+ }
931
+
932
+ // (1) calculate rows order (by component order)
933
+ const rowsInOrder = groupByRow(components, this._ids);
934
+ Object.entries(rowsInOrder).forEach(([id, components]) => {
935
+ // (2) add fields to rows
936
+ this.addRow(formField.id, {
937
+ id: id,
938
+ components: components.map(c => c.id)
939
+ });
940
+ });
941
+
942
+ // (3) traverse through nested components
943
+ components.forEach(field => this.calculateLayout(field));
944
+
945
+ // (4) fire event to notify interested parties
946
+ this._eventBus.fire('form.layoutCalculated', {
947
+ rows: this._rows
948
+ });
949
+ }
950
+ clear() {
951
+ this._rows = [];
952
+ this._ids.clear();
953
+
954
+ // fire event to notify interested parties
955
+ this._eventBus.fire('form.layoutCleared');
956
+ }
957
+ }
958
+ FormLayouter.$inject = ['eventBus'];
959
+
960
+ // helpers //////
961
+
962
+ function groupByRow(components, ids) {
963
+ return groupBy(components, c => {
964
+ // mitigate missing row by creating new (handle legacy)
965
+ const {
966
+ layout
967
+ } = c;
968
+ if (!layout || !layout.row) {
969
+ return ids.nextPrefixed('Row_');
739
970
  }
740
- } while (cursor.next());
741
- return Array.from(variables);
971
+ return layout.row;
972
+ });
742
973
  }
743
- function isExpression$1(value) {
744
- return isString(value) && value.startsWith('=');
974
+
975
+ /**
976
+ * @param {Array<FormRows>} formRows
977
+ * @returns {Array<FormRow>}
978
+ */
979
+ function allRows(formRows) {
980
+ return flatten(formRows.map(c => c.rows));
745
981
  }
746
982
 
747
983
  // config ///////////////////
@@ -880,7 +1116,7 @@ function clone(data, replacer) {
880
1116
  *
881
1117
  * @return {string[]}
882
1118
  */
883
- function getSchemaVariables(schema) {
1119
+ function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLanguage(null)) {
884
1120
  if (!schema.components) {
885
1121
  return [];
886
1122
  }
@@ -901,15 +1137,17 @@ function getSchemaVariables(schema) {
901
1137
  variables = [...variables, valuesKey];
902
1138
  }
903
1139
  if (conditional && conditional.hide) {
904
- // cut off initial '='
905
- const conditionVariables = getVariableNames(conditional.hide.slice(1));
1140
+ const conditionVariables = expressionLanguage.getVariableNames(conditional.hide, {
1141
+ type: 'unaryTest'
1142
+ });
906
1143
  variables = [...variables, ...conditionVariables];
907
1144
  }
908
1145
  EXPRESSION_PROPERTIES.forEach(prop => {
909
1146
  const property = component[prop];
910
- if (property && isExpression$1(property)) {
911
- // cut off initial '='
912
- const expressionVariables = getExpressionVariableNames(property.slice(1));
1147
+ if (property && expressionLanguage.isExpression(property)) {
1148
+ const expressionVariables = expressionLanguage.getVariableNames(property, {
1149
+ type: 'expression'
1150
+ });
913
1151
  variables = [...variables, ...expressionVariables];
914
1152
  }
915
1153
  });
@@ -925,10 +1163,12 @@ class Importer {
925
1163
  * @constructor
926
1164
  * @param { import('../core').FormFieldRegistry } formFieldRegistry
927
1165
  * @param { import('../render/FormFields').default } formFields
1166
+ * @param { import('../core').FormLayouter } formLayouter
928
1167
  */
929
- constructor(formFieldRegistry, formFields) {
1168
+ constructor(formFieldRegistry, formFields, formLayouter) {
930
1169
  this._formFieldRegistry = formFieldRegistry;
931
1170
  this._formFields = formFields;
1171
+ this._formLayouter = formLayouter;
932
1172
  }
933
1173
 
934
1174
  /**
@@ -945,8 +1185,10 @@ class Importer {
945
1185
  // TODO: Add warnings - https://github.com/bpmn-io/form-js/issues/289
946
1186
  const warnings = [];
947
1187
  try {
1188
+ this._formLayouter.clear();
948
1189
  const importedSchema = this.importFormField(clone(schema)),
949
1190
  initializedData = this.initializeFieldValues(clone(data));
1191
+ this._formLayouter.calculateLayout(clone(importedSchema));
950
1192
  return {
951
1193
  warnings,
952
1194
  schema: importedSchema,
@@ -1050,126 +1292,11 @@ class Importer {
1050
1292
  }, data);
1051
1293
  }
1052
1294
  }
1053
- Importer.$inject = ['formFieldRegistry', 'formFields'];
1054
-
1055
- var importModule = {
1056
- importer: ['type', Importer]
1057
- };
1058
-
1059
- const NODE_TYPE_TEXT = 3,
1060
- NODE_TYPE_ELEMENT = 1;
1061
- const ALLOWED_NODES = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'em', 'a', 'p', 'div', 'ul', 'ol', 'li', 'hr', 'blockquote', 'img', 'pre', 'code', 'br', 'strong'];
1062
- const ALLOWED_ATTRIBUTES = ['align', 'alt', 'class', 'href', 'id', 'name', 'rel', 'target', 'src'];
1063
- const ALLOWED_URI_PATTERN = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
1064
- const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape
1065
- const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
1066
-
1067
- const FORM_ELEMENT = document.createElement('form');
1068
-
1069
- /**
1070
- * Sanitize a HTML string and return the cleaned, safe version.
1071
- *
1072
- * @param {string} html
1073
- * @return {string}
1074
- */
1075
- function sanitizeHTML(html) {
1076
- const doc = new DOMParser().parseFromString(`<!DOCTYPE html>\n<html><body><div>${html}`, 'text/html');
1077
- doc.normalize();
1078
- const element = doc.body.firstChild;
1079
- if (element) {
1080
- sanitizeNode( /** @type Element */element);
1081
- return new XMLSerializer().serializeToString(element);
1082
- } else {
1083
- // handle the case that document parsing
1084
- // does not work at all, due to HTML gibberish
1085
- return '';
1086
- }
1087
- }
1088
- function sanitizeImageSource(src) {
1089
- const valid = ALLOWED_IMAGE_SRC_PATTERN.test(src);
1090
- return valid ? src : '';
1091
- }
1092
-
1093
- /**
1094
- * Recursively sanitize a HTML node, potentially
1095
- * removing it, its children or attributes.
1096
- *
1097
- * Inspired by https://github.com/developit/snarkdown/issues/70
1098
- * and https://github.com/cure53/DOMPurify. Simplified
1099
- * for our use-case.
1100
- *
1101
- * @param {Element} node
1102
- */
1103
- function sanitizeNode(node) {
1104
- // allow text nodes
1105
- if (node.nodeType === NODE_TYPE_TEXT) {
1106
- return;
1107
- }
1108
-
1109
- // disallow all other nodes but Element
1110
- if (node.nodeType !== NODE_TYPE_ELEMENT) {
1111
- return node.remove();
1112
- }
1113
- const lcTag = node.tagName.toLowerCase();
1114
-
1115
- // disallow non-whitelisted tags
1116
- if (!ALLOWED_NODES.includes(lcTag)) {
1117
- return node.remove();
1118
- }
1119
- const attributes = node.attributes;
1120
-
1121
- // clean attributes
1122
- for (let i = attributes.length; i--;) {
1123
- const attribute = attributes[i];
1124
- const name = attribute.name;
1125
- const lcName = name.toLowerCase();
1126
-
1127
- // normalize node value
1128
- const value = attribute.value.trim();
1129
- node.removeAttribute(name);
1130
- const valid = isValidAttribute(lcTag, lcName, value);
1131
- if (valid) {
1132
- node.setAttribute(name, value);
1133
- }
1134
- }
1135
-
1136
- // force noopener on target="_blank" links
1137
- if (lcTag === 'a' && node.getAttribute('target') === '_blank' && node.getAttribute('rel') !== 'noopener') {
1138
- node.setAttribute('rel', 'noopener');
1139
- }
1140
- for (let i = node.childNodes.length; i--;) {
1141
- sanitizeNode( /** @type Element */node.childNodes[i]);
1142
- }
1143
- }
1144
-
1145
- /**
1146
- * Validates attributes for validity.
1147
- *
1148
- * @param {string} lcTag
1149
- * @param {string} lcName
1150
- * @param {string} value
1151
- * @return {boolean}
1152
- */
1153
- function isValidAttribute(lcTag, lcName, value) {
1154
- // disallow most attributes based on whitelist
1155
- if (!ALLOWED_ATTRIBUTES.includes(lcName)) {
1156
- return false;
1157
- }
1158
-
1159
- // disallow "DOM clobbering" / polution of document and wrapping form elements
1160
- if ((lcName === 'id' || lcName === 'name') && (value in document || value in FORM_ELEMENT)) {
1161
- return false;
1162
- }
1163
- if (lcName === 'target' && value !== '_blank') {
1164
- return false;
1165
- }
1166
-
1167
- // allow valid url links only
1168
- if (lcName === 'href' && !ALLOWED_URI_PATTERN.test(value.replace(ATTR_WHITESPACE_PATTERN, ''))) {
1169
- return false;
1170
- }
1171
- return true;
1172
- }
1295
+ Importer.$inject = ['formFieldRegistry', 'formFields', 'formLayouter'];
1296
+
1297
+ var importModule = {
1298
+ importer: ['type', Importer]
1299
+ };
1173
1300
 
1174
1301
  function formFieldClasses(type, {
1175
1302
  errors = [],
@@ -1183,35 +1310,23 @@ function formFieldClasses(type, {
1183
1310
  'fjs-disabled': disabled
1184
1311
  });
1185
1312
  }
1313
+ function gridColumnClasses(formField) {
1314
+ const {
1315
+ layout = {}
1316
+ } = formField;
1317
+ const {
1318
+ columns
1319
+ } = layout;
1320
+ return classNames('fjs-layout-column', `cds--col${columns ? '-lg-' + columns : ''}`,
1321
+ // always fall back to top-down on smallest screens
1322
+ 'cds--col-sm-16', 'cds--col-md-16');
1323
+ }
1186
1324
  function prefixId(id, formId) {
1187
1325
  if (formId) {
1188
1326
  return `fjs-form-${formId}-${id}`;
1189
1327
  }
1190
1328
  return `fjs-form-${id}`;
1191
1329
  }
1192
- function markdownToHTML(markdown) {
1193
- const htmls = markdown.toString().split(/(?:\r?\n){2,}/).map(line => /^((\d+.)|[><\s#-*])/.test(line) ? snarkdown(line) : `<p>${snarkdown(line)}</p>`);
1194
- return htmls.join('\n\n');
1195
- }
1196
-
1197
- // see https://github.com/developit/snarkdown/issues/70
1198
- function safeMarkdown(markdown) {
1199
- const html = markdownToHTML(markdown);
1200
- return sanitizeHTML(html);
1201
- }
1202
-
1203
- /**
1204
- * Sanitizes an image source to ensure we only allow for data URI and links
1205
- * that start with http(s).
1206
- *
1207
- * Note: Most browsers anyway do not support script execution in <img> elements.
1208
- *
1209
- * @param {string} src
1210
- * @returns {string}
1211
- */
1212
- function safeImageSource(src) {
1213
- return sanitizeImageSource(src);
1214
- }
1215
1330
 
1216
1331
  const type$b = 'button';
1217
1332
  function Button(props) {
@@ -1246,10 +1361,31 @@ const FormRenderContext = createContext({
1246
1361
  return null;
1247
1362
  },
1248
1363
  Children: props => {
1249
- return props.children;
1364
+ return jsx("div", {
1365
+ class: props.class,
1366
+ children: props.children
1367
+ });
1250
1368
  },
1251
1369
  Element: props => {
1252
- return props.children;
1370
+ return jsx("div", {
1371
+ class: props.class,
1372
+ children: props.children
1373
+ });
1374
+ },
1375
+ Row: props => {
1376
+ return jsx("div", {
1377
+ class: props.class,
1378
+ children: props.children
1379
+ });
1380
+ },
1381
+ Column: props => {
1382
+ if (props.field.type === 'default') {
1383
+ return props.children;
1384
+ }
1385
+ return jsx("div", {
1386
+ class: props.class,
1387
+ children: props.children
1388
+ });
1253
1389
  }
1254
1390
  });
1255
1391
  var FormRenderContext$1 = FormRenderContext;
@@ -1380,7 +1516,53 @@ Checkbox.sanitizeValue = ({
1380
1516
  }) => value === true;
1381
1517
  Checkbox.group = 'selection';
1382
1518
 
1383
- function useService (type, strict) {
1519
+ // parses the options data from the provided form field and form data
1520
+ function getValuesData(formField, formData) {
1521
+ const {
1522
+ valuesKey,
1523
+ values
1524
+ } = formField;
1525
+ return valuesKey ? get(formData, [valuesKey]) : values;
1526
+ }
1527
+
1528
+ // transforms the provided options into a normalized format, trimming invalid options
1529
+ function normalizeValuesData(valuesData) {
1530
+ return valuesData.filter(_isValueSomething).map(v => _normalizeValueData(v)).filter(v => v);
1531
+ }
1532
+ function _normalizeValueData(valueData) {
1533
+ if (_isAllowedValue(valueData)) {
1534
+ // if a primitive is provided, use it as label and value
1535
+ return {
1536
+ value: valueData,
1537
+ label: `${valueData}`
1538
+ };
1539
+ }
1540
+ if (typeof valueData === 'object') {
1541
+ if (!valueData.label && _isAllowedValue(valueData.value)) {
1542
+ // if no label is provided, use the value as label
1543
+ return {
1544
+ value: valueData.value,
1545
+ label: `${valueData.value}`
1546
+ };
1547
+ }
1548
+ if (_isValueSomething(valueData.value) && _isAllowedValue(valueData.label)) {
1549
+ // if both value and label are provided, use them as is, in this scenario, the value may also be an object
1550
+ return valueData;
1551
+ }
1552
+ }
1553
+ return null;
1554
+ }
1555
+ function _isAllowedValue(value) {
1556
+ return _isReadableType(value) && _isValueSomething(value);
1557
+ }
1558
+ function _isReadableType(value) {
1559
+ return ['number', 'string', 'boolean'].includes(typeof value);
1560
+ }
1561
+ function _isValueSomething(value) {
1562
+ return value || value === 0 || value === false;
1563
+ }
1564
+
1565
+ function useService(type, strict) {
1384
1566
  const {
1385
1567
  getService
1386
1568
  } = useContext(FormContext$1);
@@ -1421,22 +1603,30 @@ function useValuesAsync (field) {
1421
1603
  const initialData = useService('form')._getState().initialData;
1422
1604
  useEffect(() => {
1423
1605
  let values = [];
1606
+
1607
+ // dynamic values
1424
1608
  if (valuesKey !== undefined) {
1425
1609
  const keyedValues = (initialData || {})[valuesKey];
1426
1610
  if (keyedValues && Array.isArray(keyedValues)) {
1427
1611
  values = keyedValues;
1428
1612
  }
1429
- } else if (staticValues !== undefined) {
1613
+ }
1614
+
1615
+ // static values
1616
+ else if (staticValues !== undefined) {
1430
1617
  values = Array.isArray(staticValues) ? staticValues : [];
1431
1618
  } else {
1432
- setValuesGetter(getErrorState('No values source defined in the form definition'));
1619
+ setValuesGetter(buildErrorState('No values source defined in the form definition'));
1433
1620
  return;
1434
1621
  }
1622
+
1623
+ // normalize data to support primitives and partially defined objects
1624
+ values = normalizeValuesData(values);
1435
1625
  setValuesGetter(buildLoadedState(values));
1436
1626
  }, [valuesKey, staticValues, initialData]);
1437
1627
  return valuesGetter;
1438
1628
  }
1439
- const getErrorState = error => ({
1629
+ const buildErrorState = error => ({
1440
1630
  values: [],
1441
1631
  error,
1442
1632
  state: LOAD_STATES.ERROR
@@ -1629,12 +1819,8 @@ function sanitizeSingleSelectValue(options) {
1629
1819
  data,
1630
1820
  value
1631
1821
  } = options;
1632
- const {
1633
- valuesKey,
1634
- values
1635
- } = formField;
1636
1822
  try {
1637
- const validValues = (valuesKey ? get(data, [valuesKey]) : values).map(v => v.value) || [];
1823
+ const validValues = normalizeValuesData(getValuesData(formField, data)).map(v => v.value);
1638
1824
  return validValues.includes(value) ? value : null;
1639
1825
  } catch (error) {
1640
1826
  // use default value in case of formatting error
@@ -1648,12 +1834,8 @@ function sanitizeMultiSelectValue(options) {
1648
1834
  data,
1649
1835
  value
1650
1836
  } = options;
1651
- const {
1652
- valuesKey,
1653
- values
1654
- } = formField;
1655
1837
  try {
1656
- const validValues = (valuesKey ? get(data, [valuesKey]) : values).map(v => v.value) || [];
1838
+ const validValues = normalizeValuesData(getValuesData(formField, data)).map(v => v.value);
1657
1839
  return value.filter(v => validValues.includes(v));
1658
1840
  } catch (error) {
1659
1841
  // use default value in case of formatting error
@@ -1748,26 +1930,95 @@ Checklist.sanitizeValue = sanitizeMultiSelectValue;
1748
1930
  Checklist.group = 'selection';
1749
1931
 
1750
1932
  /**
1751
- * Check if condition is met with given variables.
1933
+ * Returns the conditionally filtered data of a form reactively.
1934
+ * Memoised to minimize re-renders
1935
+ *
1936
+ */
1937
+ function useFilteredFormData() {
1938
+ const {
1939
+ initialData,
1940
+ data
1941
+ } = useService('form')._getState();
1942
+ const conditionChecker = useService('conditionChecker', false);
1943
+ return useMemo(() => {
1944
+ const newData = conditionChecker ? conditionChecker.applyConditions(data, data) : data;
1945
+ return {
1946
+ ...initialData,
1947
+ ...newData
1948
+ };
1949
+ }, [conditionChecker, data, initialData]);
1950
+ }
1951
+
1952
+ /**
1953
+ * Evaluate if condition is met reactively based on the conditionChecker and form data.
1752
1954
  *
1753
1955
  * @param {string | undefined} condition
1754
- * @param {import('../../types').Data} data
1755
1956
  *
1756
1957
  * @returns {boolean} true if condition is met or no condition or condition checker exists
1757
1958
  */
1758
- function useCondition(condition, data) {
1759
- const initialData = useService('form')._getState().initialData;
1959
+ function useCondition(condition) {
1760
1960
  const conditionChecker = useService('conditionChecker', false);
1761
- if (!conditionChecker) {
1762
- return null;
1961
+ const filteredData = useFilteredFormData();
1962
+ return useMemo(() => {
1963
+ return conditionChecker ? conditionChecker.check(condition, filteredData) : null;
1964
+ }, [conditionChecker, condition, filteredData]);
1965
+ }
1966
+
1967
+ /**
1968
+ * Evaluate a string reactively based on the expressionLanguage and form data.
1969
+ * If the string is not an expression, it is returned as is.
1970
+ * Memoised to minimize re-renders.
1971
+ *
1972
+ * @param {string} value
1973
+ *
1974
+ */
1975
+ function useExpressionEvaluation(value) {
1976
+ const formData = useFilteredFormData();
1977
+ const expressionLanguage = useService('expressionLanguage');
1978
+ return useMemo(() => {
1979
+ if (expressionLanguage && expressionLanguage.isExpression(value)) {
1980
+ return expressionLanguage.evaluate(value, formData);
1981
+ }
1982
+ return value;
1983
+ }, [expressionLanguage, formData, value]);
1984
+ }
1985
+
1986
+ function useKeyDownAction(targetKey, action, listenerElement = window) {
1987
+ function downHandler({
1988
+ key
1989
+ }) {
1990
+ if (key === targetKey) {
1991
+ action();
1992
+ }
1763
1993
  }
1994
+ useEffect(() => {
1995
+ listenerElement.addEventListener('keydown', downHandler);
1996
+ return () => {
1997
+ listenerElement.removeEventListener('keydown', downHandler);
1998
+ };
1999
+ });
2000
+ }
1764
2001
 
1765
- // make sure we do not use data from hidden fields
1766
- const filteredData = {
1767
- ...initialData,
1768
- ...conditionChecker.applyConditions(data, data)
1769
- };
1770
- return conditionChecker.check(condition, filteredData);
2002
+ /**
2003
+ * Template a string reactively based on form data. If the string is not a template, it is returned as is.
2004
+ * Memoised to minimize re-renders
2005
+ *
2006
+ * @param {string} value
2007
+ * @param {Object} options
2008
+ * @param {boolean} [options.debug = false]
2009
+ * @param {boolean} [options.strict = false]
2010
+ * @param {Function} [options.buildDebugString]
2011
+ *
2012
+ */
2013
+ function useTemplateEvaluation(value, options) {
2014
+ const filteredData = useFilteredFormData();
2015
+ const templating = useService('templating');
2016
+ return useMemo(() => {
2017
+ if (templating.isTemplate(value)) {
2018
+ return templating.evaluate(value, filteredData, options);
2019
+ }
2020
+ return value;
2021
+ }, [filteredData, templating, value, options]);
1771
2022
  }
1772
2023
 
1773
2024
  const noop$1 = () => false;
@@ -1788,7 +2039,8 @@ function FormField(props) {
1788
2039
  } = form._getState();
1789
2040
  const {
1790
2041
  Element,
1791
- Empty
2042
+ Empty,
2043
+ Column
1792
2044
  } = useContext(FormRenderContext$1);
1793
2045
  const FormFieldComponent = formFields.get(field.type);
1794
2046
  if (!FormFieldComponent) {
@@ -1797,45 +2049,67 @@ function FormField(props) {
1797
2049
  const value = get(data, _path);
1798
2050
  const fieldErrors = findErrors(errors, _path);
1799
2051
  const disabled = properties.readOnly || field.disabled || false;
1800
- const hidden = useHideCondition(field, data);
2052
+ const hidden = useCondition(field.conditional && field.conditional.hide || null);
1801
2053
  if (hidden) {
1802
2054
  return jsx(Empty, {});
1803
2055
  }
1804
- return jsx(Element, {
2056
+ return jsx(Column, {
1805
2057
  field: field,
1806
- children: jsx(FormFieldComponent, {
1807
- ...props,
1808
- disabled: disabled,
1809
- errors: fieldErrors,
1810
- onChange: disabled ? noop$1 : onChange,
1811
- value: value
2058
+ class: gridColumnClasses(field),
2059
+ children: jsx(Element, {
2060
+ class: "fjs-element",
2061
+ field: field,
2062
+ children: jsx(FormFieldComponent, {
2063
+ ...props,
2064
+ disabled: disabled,
2065
+ errors: fieldErrors,
2066
+ onChange: disabled ? noop$1 : onChange,
2067
+ value: value
2068
+ })
1812
2069
  })
1813
2070
  });
1814
2071
  }
1815
- function useHideCondition(field, data) {
1816
- const hideCondition = field.conditional && field.conditional.hide;
1817
- return useCondition(hideCondition, data) === true;
1818
- }
1819
2072
 
1820
2073
  function Default(props) {
1821
2074
  const {
1822
2075
  Children,
1823
- Empty
2076
+ Empty,
2077
+ Row
1824
2078
  } = useContext(FormRenderContext$1);
1825
2079
  const {
1826
2080
  field
1827
2081
  } = props;
1828
2082
  const {
2083
+ id,
1829
2084
  components = []
1830
2085
  } = field;
2086
+ const formLayouter = useService('formLayouter');
2087
+ const formFieldRegistry = useService('formFieldRegistry');
2088
+ const rows = formLayouter.getRows(id);
1831
2089
  return jsxs(Children, {
1832
- class: "fjs-vertical-layout",
2090
+ class: "fjs-vertical-layout fjs-children cds--grid cds--grid--condensed",
1833
2091
  field: field,
1834
- children: [components.map(childField => {
1835
- return createElement(FormField, {
1836
- ...props,
1837
- key: childField.id,
1838
- field: childField
2092
+ children: [rows.map(row => {
2093
+ const {
2094
+ components = []
2095
+ } = row;
2096
+ if (!components.length) {
2097
+ return null;
2098
+ }
2099
+ return jsx(Row, {
2100
+ row: row,
2101
+ class: "fjs-layout-row cds--row",
2102
+ children: components.map(id => {
2103
+ const childField = formFieldRegistry.get(id);
2104
+ if (!childField) {
2105
+ return null;
2106
+ }
2107
+ return createElement(FormField, {
2108
+ ...props,
2109
+ key: childField.id,
2110
+ field: childField
2111
+ });
2112
+ })
1839
2113
  });
1840
2114
  }), components.length ? null : jsx(Empty, {})]
1841
2115
  });
@@ -2059,22 +2333,6 @@ var ClockIcon = (({
2059
2333
  d: "M6.222 25.64A14 14 0 1021.778 2.36 14 14 0 006.222 25.64zM7.333 4.023a12 12 0 1113.334 19.955A12 12 0 017.333 4.022z"
2060
2334
  })));
2061
2335
 
2062
- function useKeyDownAction(targetKey, action, listenerElement = window) {
2063
- function downHandler({
2064
- key
2065
- }) {
2066
- if (key === targetKey) {
2067
- action();
2068
- }
2069
- }
2070
- useEffect(() => {
2071
- listenerElement.addEventListener('keydown', downHandler);
2072
- return () => {
2073
- listenerElement.removeEventListener('keydown', downHandler);
2074
- };
2075
- });
2076
- }
2077
-
2078
2336
  const DEFAULT_LABEL_GETTER = value => value;
2079
2337
  const NOOP = () => {};
2080
2338
  function DropdownList(props) {
@@ -2621,46 +2879,131 @@ function FormComponent(props) {
2621
2879
  });
2622
2880
  }
2623
2881
 
2882
+ const NODE_TYPE_TEXT = 3,
2883
+ NODE_TYPE_ELEMENT = 1;
2884
+ const ALLOWED_NODES = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'em', 'a', 'p', 'div', 'ul', 'ol', 'li', 'hr', 'blockquote', 'img', 'pre', 'code', 'br', 'strong'];
2885
+ const ALLOWED_ATTRIBUTES = ['align', 'alt', 'class', 'href', 'id', 'name', 'rel', 'target', 'src'];
2886
+ const ALLOWED_URI_PATTERN = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
2887
+ const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape
2888
+ const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
2889
+
2890
+ const FORM_ELEMENT = document.createElement('form');
2891
+
2624
2892
  /**
2893
+ * Sanitize a HTML string and return the cleaned, safe version.
2625
2894
  *
2626
- * @param {string | undefined} expression
2627
- * @param {import('../../types').Data} data
2895
+ * @param {string} html
2896
+ * @return {string}
2628
2897
  */
2629
- function useEvaluation(expression, data) {
2630
- const initialData = useService('form')._getState().initialData;
2631
- const conditionChecker = useService('conditionChecker', false);
2632
- if (!conditionChecker) {
2633
- return null;
2898
+
2899
+ // see https://github.com/developit/snarkdown/issues/70
2900
+ function sanitizeHTML(html) {
2901
+ const doc = new DOMParser().parseFromString(`<!DOCTYPE html>\n<html><body><div>${html}`, 'text/html');
2902
+ doc.normalize();
2903
+ const element = doc.body.firstChild;
2904
+ if (element) {
2905
+ sanitizeNode( /** @type Element */element);
2906
+ return new XMLSerializer().serializeToString(element);
2907
+ } else {
2908
+ // handle the case that document parsing
2909
+ // does not work at all, due to HTML gibberish
2910
+ return '';
2634
2911
  }
2912
+ }
2635
2913
 
2636
- // make sure we do not use data from hidden fields
2637
- const filteredData = {
2638
- ...initialData,
2639
- ...conditionChecker.applyConditions(data, data)
2640
- };
2641
- return conditionChecker.evaluate(expression, filteredData);
2914
+ /**
2915
+ * Sanitizes an image source to ensure we only allow for data URI and links
2916
+ * that start with http(s).
2917
+ *
2918
+ * Note: Most browsers anyway do not support script execution in <img> elements.
2919
+ *
2920
+ * @param {string} src
2921
+ * @returns {string}
2922
+ */
2923
+ function sanitizeImageSource(src) {
2924
+ const valid = ALLOWED_IMAGE_SRC_PATTERN.test(src);
2925
+ return valid ? src : '';
2642
2926
  }
2643
2927
 
2644
2928
  /**
2929
+ * Recursively sanitize a HTML node, potentially
2930
+ * removing it, its children or attributes.
2645
2931
  *
2646
- * @param {string} value
2932
+ * Inspired by https://github.com/developit/snarkdown/issues/70
2933
+ * and https://github.com/cure53/DOMPurify. Simplified
2934
+ * for our use-case.
2935
+ *
2936
+ * @param {Element} node
2647
2937
  */
2648
- function useExpressionValue(value) {
2649
- const formData = useService('form')._getState().data;
2650
- if (!isExpression(value)) {
2651
- return value;
2938
+ function sanitizeNode(node) {
2939
+ // allow text nodes
2940
+ if (node.nodeType === NODE_TYPE_TEXT) {
2941
+ return;
2942
+ }
2943
+
2944
+ // disallow all other nodes but Element
2945
+ if (node.nodeType !== NODE_TYPE_ELEMENT) {
2946
+ return node.remove();
2947
+ }
2948
+ const lcTag = node.tagName.toLowerCase();
2949
+
2950
+ // disallow non-whitelisted tags
2951
+ if (!ALLOWED_NODES.includes(lcTag)) {
2952
+ return node.remove();
2953
+ }
2954
+ const attributes = node.attributes;
2955
+
2956
+ // clean attributes
2957
+ for (let i = attributes.length; i--;) {
2958
+ const attribute = attributes[i];
2959
+ const name = attribute.name;
2960
+ const lcName = name.toLowerCase();
2961
+
2962
+ // normalize node value
2963
+ const value = attribute.value.trim();
2964
+ node.removeAttribute(name);
2965
+ const valid = isValidAttribute(lcTag, lcName, value);
2966
+ if (valid) {
2967
+ node.setAttribute(name, value);
2968
+ }
2652
2969
  }
2653
2970
 
2654
- // We can ignore this hook rule as we do not use
2655
- // state or effects in our custom hooks
2656
- /* eslint-disable-next-line react-hooks/rules-of-hooks */
2657
- return useEvaluation(value, formData);
2971
+ // force noopener on target="_blank" links
2972
+ if (lcTag === 'a' && node.getAttribute('target') === '_blank' && node.getAttribute('rel') !== 'noopener') {
2973
+ node.setAttribute('rel', 'noopener');
2974
+ }
2975
+ for (let i = node.childNodes.length; i--;) {
2976
+ sanitizeNode( /** @type Element */node.childNodes[i]);
2977
+ }
2658
2978
  }
2659
2979
 
2660
- // helper ///////////////
2980
+ /**
2981
+ * Validates attributes for validity.
2982
+ *
2983
+ * @param {string} lcTag
2984
+ * @param {string} lcName
2985
+ * @param {string} value
2986
+ * @return {boolean}
2987
+ */
2988
+ function isValidAttribute(lcTag, lcName, value) {
2989
+ // disallow most attributes based on whitelist
2990
+ if (!ALLOWED_ATTRIBUTES.includes(lcName)) {
2991
+ return false;
2992
+ }
2993
+
2994
+ // disallow "DOM clobbering" / polution of document and wrapping form elements
2995
+ if ((lcName === 'id' || lcName === 'name') && (value in document || value in FORM_ELEMENT)) {
2996
+ return false;
2997
+ }
2998
+ if (lcName === 'target' && value !== '_blank') {
2999
+ return false;
3000
+ }
2661
3001
 
2662
- function isExpression(value) {
2663
- return isString(value) && value.startsWith('=');
3002
+ // allow valid url links only
3003
+ if (lcName === 'href' && !ALLOWED_URI_PATTERN.test(value.replace(ATTR_WHITESPACE_PATTERN, ''))) {
3004
+ return false;
3005
+ }
3006
+ return true;
2664
3007
  }
2665
3008
 
2666
3009
  function _extends$h() { _extends$h = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends$h.apply(this, arguments); }
@@ -2703,8 +3046,9 @@ function Image(props) {
2703
3046
  id,
2704
3047
  source
2705
3048
  } = field;
2706
- const safeSource = safeImageSource(useExpressionValue(source));
2707
- const altText = useExpressionValue(alt);
3049
+ const evaluatedImageSource = useExpressionEvaluation(source);
3050
+ const safeSource = useMemo(() => sanitizeImageSource(evaluatedImageSource), [evaluatedImageSource]);
3051
+ const altText = useExpressionEvaluation(alt);
2708
3052
  const {
2709
3053
  formId
2710
3054
  } = useContext(FormContext$1);
@@ -3596,16 +3940,29 @@ function Text(props) {
3596
3940
  disableLinks
3597
3941
  } = props;
3598
3942
  const {
3599
- text = ''
3943
+ text = '',
3944
+ strict = false
3600
3945
  } = field;
3601
- const textValue = useExpressionValue(text) || '';
3602
- const componentOverrides = disableLinks ? {
3946
+ const markdownRenderer = useService('markdownRenderer');
3947
+
3948
+ // feelers => pure markdown
3949
+ const markdown = useTemplateEvaluation(text, {
3950
+ debug: true,
3951
+ strict
3952
+ });
3953
+
3954
+ // markdown => safe HTML
3955
+ const safeHtml = useMemo(() => {
3956
+ const html = markdownRenderer.render(markdown);
3957
+ return sanitizeHTML(html);
3958
+ }, [markdownRenderer, markdown]);
3959
+ const componentOverrides = useMemo(() => disableLinks ? {
3603
3960
  'a': DisabledLink
3604
- } : {};
3961
+ } : {}, [disableLinks]);
3605
3962
  return jsx("div", {
3606
3963
  class: formFieldClasses(type$2),
3607
3964
  children: jsx(Markup, {
3608
- markup: safeMarkdown(textValue),
3965
+ markup: safeHtml,
3609
3966
  components: componentOverrides,
3610
3967
  trim: false
3611
3968
  })
@@ -4105,9 +4462,9 @@ var renderModule = {
4105
4462
 
4106
4463
  var core = {
4107
4464
  __depends__: [importModule, renderModule],
4108
- conditionChecker: ['type', ConditionChecker],
4109
4465
  eventBus: ['type', EventBus],
4110
4466
  formFieldRegistry: ['type', FormFieldRegistry],
4467
+ formLayouter: ['type', FormLayouter],
4111
4468
  validator: ['type', Validator]
4112
4469
  };
4113
4470
 
@@ -4369,7 +4726,7 @@ class Form {
4369
4726
  _createInjector(options, container) {
4370
4727
  const {
4371
4728
  additionalModules = [],
4372
- modules = []
4729
+ modules = this._getModules()
4373
4730
  } = options;
4374
4731
  const config = {
4375
4732
  renderer: {
@@ -4435,6 +4792,13 @@ class Form {
4435
4792
  this._emit('changed', this._getState());
4436
4793
  }
4437
4794
 
4795
+ /**
4796
+ * @internal
4797
+ */
4798
+ _getModules() {
4799
+ return [ExpressionLanguageModule, MarkdownModule];
4800
+ }
4801
+
4438
4802
  /**
4439
4803
  * @internal
4440
4804
  */
@@ -4477,7 +4841,7 @@ class Form {
4477
4841
  }
4478
4842
  }
4479
4843
 
4480
- const schemaVersion = 7;
4844
+ const schemaVersion = 8;
4481
4845
 
4482
4846
  /**
4483
4847
  * @typedef { import('./types').CreateFormOptions } CreateFormOptions
@@ -4502,5 +4866,5 @@ function createForm(options) {
4502
4866
  });
4503
4867
  }
4504
4868
 
4505
- export { Button, Checkbox, Checklist, DATETIME_SUBTYPES, DATETIME_SUBTYPES_LABELS, DATETIME_SUBTYPE_PATH, DATE_DISALLOW_PAST_PATH, DATE_LABEL_PATH, Datetime, Default, Form, FormComponent, FormContext$1 as FormContext, FormFieldRegistry, FormFields, FormRenderContext$1 as FormRenderContext, Image, MINUTES_IN_DAY, Numberfield, Radio, Select, TIME_INTERVAL_PATH, TIME_LABEL_PATH, TIME_SERIALISINGFORMAT_LABELS, TIME_SERIALISING_FORMATS, TIME_SERIALISING_FORMAT_PATH, TIME_USE24H_PATH, Taglist, Text, Textarea, Textfield, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS, VALUES_SOURCE_DEFAULT, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, getExpressionVariableNames, getSchemaVariables, getValuesSource, getVariableNames, iconsByType, isExpression$1 as isExpression, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
4869
+ export { Button, Checkbox, Checklist, ConditionChecker, DATETIME_SUBTYPES, DATETIME_SUBTYPES_LABELS, DATETIME_SUBTYPE_PATH, DATE_DISALLOW_PAST_PATH, DATE_LABEL_PATH, Datetime, Default, ExpressionLanguageModule, FeelExpressionLanguage, FeelersTemplating, Form, FormComponent, FormContext$1 as FormContext, FormFieldRegistry, FormFields, FormLayouter, FormRenderContext$1 as FormRenderContext, Image, MINUTES_IN_DAY, MarkdownModule, MarkdownRenderer, Numberfield, Radio, Select, TIME_INTERVAL_PATH, TIME_LABEL_PATH, TIME_SERIALISINGFORMAT_LABELS, TIME_SERIALISING_FORMATS, TIME_SERIALISING_FORMAT_PATH, TIME_USE24H_PATH, Taglist, Text, Textarea, Textfield, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS, VALUES_SOURCE_DEFAULT, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, getSchemaVariables, getValuesSource, iconsByType, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
4506
4870
  //# sourceMappingURL=index.es.js.map