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