@bpmn-io/form-js-viewer 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,37 @@ form.on('changed', 500, (event) => {
48
48
  });
49
49
  ```
50
50
 
51
+ ### Customize document preview requests
52
+
53
+ If you use the `documentPreview` field and need custom authentication for file downloads/previews,
54
+ you can provide a `documentEndpointBuilder` service via dependency injection.
55
+
56
+ ```javascript
57
+ import { Form } from '@bpmn-io/form-js-viewer';
58
+
59
+ const DocumentPreviewRequestModule = {
60
+ documentEndpointBuilder: [
61
+ 'value',
62
+ {
63
+ buildUrl: (document) => document.endpoint,
64
+ buildRequestInit: (document) => ({
65
+ headers: {
66
+ 'x-document-id': document.documentId,
67
+ },
68
+ credentials: 'include',
69
+ }),
70
+ },
71
+ ],
72
+ };
73
+
74
+ const form = new Form({
75
+ container: document.querySelector('#form'),
76
+ additionalModules: [DocumentPreviewRequestModule],
77
+ });
78
+ ```
79
+
80
+ `buildRequestInit` is optional, used to [configure the fetch request](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). If omitted, document requests use the default `fetch(url)` behavior.
81
+
51
82
  Check out [a full example](https://github.com/bpmn-io/form-js-examples).
52
83
 
53
84
  ## Styling
package/dist/index.cjs CHANGED
@@ -280,16 +280,38 @@ class FeelExpressionLanguage {
280
280
  * @returns {any}
281
281
  */
282
282
  evaluate(expression, data = {}) {
283
- if (!expression) {
283
+ if (!this.isExpression(expression)) {
284
+ return null;
285
+ }
286
+ try {
287
+ const {
288
+ value: result
289
+ } = feelin.evaluate(expression.slice(1), data);
290
+ return result;
291
+ } catch (error) {
292
+ this._eventBus.fire('error', {
293
+ error
294
+ });
284
295
  return null;
285
296
  }
286
- if (!minDash.isString(expression) || !expression.startsWith('=')) {
297
+ }
298
+
299
+ /**
300
+ * Evaluate a unary test expression. Returns null for invalid/missing expressions.
301
+ *
302
+ * @param {string} expression
303
+ * @param {import('../../types').Data} [data]
304
+ *
305
+ * @returns {boolean|null}
306
+ */
307
+ evaluateUnaryTest(expression, data = {}) {
308
+ if (!this.isExpression(expression)) {
287
309
  return null;
288
310
  }
289
311
  try {
290
312
  const {
291
313
  value: result
292
- } = feelin.evaluate(expression.slice(1), data);
314
+ } = feelin.unaryTest(expression.slice(1), data);
293
315
  return result;
294
316
  } catch (error) {
295
317
  this._eventBus.fire('error', {
@@ -766,31 +788,53 @@ function buildExpressionContext(context) {
766
788
  /**
767
789
  * If the value is a valid expression, it is evaluated and returned. Otherwise, it is returned as-is.
768
790
  *
769
- * @param {any} expressionLanguage - The expression language to use.
791
+ * @param {import('../types').ExpressionLanguage} expressionLanguage - The expression language to use.
770
792
  * @param {any} value - The static value or expression to evaluate.
771
793
  * @param {Object} expressionContextInfo - The context information to use.
772
794
  * @returns {any} - Evaluated value or the original value if not an expression.
773
795
  */
774
796
  function runExpressionEvaluation(expressionLanguage, value, expressionContextInfo) {
775
- if (expressionLanguage && expressionLanguage.isExpression(value)) {
797
+ if (expressionLanguage.isExpression(value)) {
776
798
  return expressionLanguage.evaluate(value, buildExpressionContext(expressionContextInfo));
777
799
  }
778
800
  return value;
779
801
  }
780
802
 
781
803
  /**
782
- * Evaluate if condition is met reactively based on the conditionChecker and form data.
804
+ * Evaluate a value as a unary test expression. Returns null for invalid/missing expressions or
805
+ * if the expression language is not available.
806
+ *
807
+ * @param {import('../types').ExpressionLanguage} expressionLanguage - The expression language to use.
808
+ * @param {string} value - The unary test expression to evaluate.
809
+ * @param {Object} expressionContextInfo - The context information to use.
810
+ * @returns {boolean | null} - Evaluated result, or null if expression is invalid/missing.
811
+ */
812
+ function runUnaryTestEvaluation(expressionLanguage, value, expressionContextInfo) {
813
+ return expressionLanguage.evaluateUnaryTest(value, buildExpressionContext(expressionContextInfo));
814
+ }
815
+
816
+ /**
817
+ * Evaluate a unary test expression reactively. Returns null for invalid/missing expressions.
818
+ * The function is memoized to minimize re-renders.
819
+ *
820
+ * @param {string | undefined} value - A unary test expression to evaluate.
821
+ * @returns {boolean | null} - Evaluated result, or null if expression is invalid/missing.
822
+ */
823
+ function useUnaryTestEvaluation(value) {
824
+ const expressionLanguage = useService('expressionLanguage');
825
+ const expressionContextInfo = hooks.useContext(LocalExpressionContext);
826
+ return hooks.useMemo(() => runUnaryTestEvaluation(expressionLanguage, value, expressionContextInfo), [expressionLanguage, expressionContextInfo, value]);
827
+ }
828
+
829
+ /**
830
+ * Evaluate if condition is met reactively based on the expression language and form data.
783
831
  *
784
832
  * @param {string | undefined} condition
785
833
  *
786
- * @returns {boolean} true if condition is met or no condition or condition checker exists
834
+ * @returns {boolean | null} true if condition is met, false if not, null if no condition or expression language
787
835
  */
788
836
  function useCondition(condition) {
789
- const conditionChecker = useService('conditionChecker', false);
790
- const expressionContextInfo = hooks.useContext(LocalExpressionContext);
791
- return hooks.useMemo(() => {
792
- return conditionChecker ? conditionChecker.check(condition, buildExpressionContext(expressionContextInfo)) : null;
793
- }, [conditionChecker, condition, expressionContextInfo]);
837
+ return useUnaryTestEvaluation(condition);
794
838
  }
795
839
 
796
840
  /**
@@ -1278,16 +1322,16 @@ function useKeyDownAction(targetKey, action, listenerElement = window) {
1278
1322
  */
1279
1323
  function useReadonly(formField, properties = {}) {
1280
1324
  const expressionLanguage = useService('expressionLanguage');
1281
- const conditionChecker = useService('conditionChecker', false);
1282
- const expressionContextInfo = hooks.useContext(LocalExpressionContext);
1283
1325
  const {
1284
1326
  readonly
1285
1327
  } = formField;
1328
+ const isExpression = expressionLanguage && expressionLanguage.isExpression(readonly);
1329
+ const evaluatedReadonly = useUnaryTestEvaluation(isExpression ? readonly : undefined);
1286
1330
  if (properties.readOnly) {
1287
1331
  return true;
1288
1332
  }
1289
- if (expressionLanguage && expressionLanguage.isExpression(readonly)) {
1290
- return conditionChecker ? conditionChecker.check(readonly, buildExpressionContext(expressionContextInfo)) : false;
1333
+ if (isExpression) {
1334
+ return evaluatedReadonly === true;
1291
1335
  }
1292
1336
  return readonly || false;
1293
1337
  }
@@ -5952,7 +5996,8 @@ const type = 'documentPreview';
5952
5996
 
5953
5997
  /**
5954
5998
  * @typedef DocumentEndpointBuilder
5955
- * @property {(document: DocumentMetadata) => string} buildUrl
5999
+ * @property {(document: DocumentMetadata) => string} [buildUrl]
6000
+ * @property {(document: DocumentMetadata) => RequestInit|undefined} [buildRequestInit]
5956
6001
  */
5957
6002
 
5958
6003
  /**
@@ -6003,13 +6048,18 @@ function DocumentPreview(props) {
6003
6048
  class: `fjs-${type}-document-container`,
6004
6049
  id: domId,
6005
6050
  children: data.map((document, index) => {
6006
- const finalEndpoint = tryCatch(() => documentEndpointBuilder?.buildUrl(document)) ?? document.endpoint;
6007
- return isValidDocumentEndpoint(finalEndpoint) ? jsxRuntime.jsx(DocumentRenderer, {
6051
+ const finalEndpoint = tryCatch(() => documentEndpointBuilder?.buildUrl?.(document)) ?? document.endpoint;
6052
+ if (!isValidDocumentEndpoint(finalEndpoint)) {
6053
+ return null;
6054
+ }
6055
+ const requestInit = getDocumentRequestInit(documentEndpointBuilder, document);
6056
+ return jsxRuntime.jsx(DocumentRenderer, {
6008
6057
  documentMetadata: document,
6009
6058
  endpoint: finalEndpoint,
6059
+ requestInit: requestInit,
6010
6060
  maxHeight: maxHeight,
6011
6061
  domId: `${domId}-${index}`
6012
- }, document.documentId) : null;
6062
+ }, document.documentId);
6013
6063
  })
6014
6064
  }), jsxRuntime.jsx(Errors, {
6015
6065
  id: errorMessageId,
@@ -6085,13 +6135,15 @@ function useValidDocumentData(dataSource) {
6085
6135
  * @param {string} props.fileName
6086
6136
  * @param {Function} props.onError
6087
6137
  * @param {string} props.errorMessageId
6138
+ * @param {RequestInit|undefined} props.requestInit
6088
6139
  * @returns {import("preact").JSX.Element}
6089
6140
  */
6090
6141
  function PdfRenderer(props) {
6091
6142
  const {
6092
6143
  url,
6093
6144
  onError,
6094
- errorMessageId
6145
+ errorMessageId,
6146
+ requestInit
6095
6147
  } = props;
6096
6148
  /** @type {ReturnType<typeof import("preact/hooks").useState<null | string>>} */
6097
6149
  const [pdfObjectUrl, setPdfObjectUrl] = hooks.useState(null);
@@ -6101,7 +6153,7 @@ function PdfRenderer(props) {
6101
6153
  let objectUrl = null;
6102
6154
  const fetchPdf = async () => {
6103
6155
  try {
6104
- const response = await fetch(url);
6156
+ const response = await fetch(url, requestInit);
6105
6157
  if (!response.ok) {
6106
6158
  setHasError(true);
6107
6159
  onError();
@@ -6121,7 +6173,7 @@ function PdfRenderer(props) {
6121
6173
  URL.revokeObjectURL(objectUrl);
6122
6174
  }
6123
6175
  };
6124
- }, [url, onError]);
6176
+ }, [url, onError, requestInit]);
6125
6177
  return jsxRuntime.jsxs(jsxRuntime.Fragment, {
6126
6178
  children: [pdfObjectUrl !== null ? jsxRuntime.jsx("embed", {
6127
6179
  src: pdfObjectUrl,
@@ -6134,12 +6186,61 @@ function PdfRenderer(props) {
6134
6186
  });
6135
6187
  }
6136
6188
 
6189
+ /**
6190
+ * @param {Object} props
6191
+ * @param {string} props.url
6192
+ * @param {string} props.alt
6193
+ * @param {Function} props.onError
6194
+ * @param {RequestInit|undefined} props.requestInit
6195
+ * @returns {import("preact").JSX.Element}
6196
+ */
6197
+ function ImageRenderer(props) {
6198
+ const {
6199
+ url,
6200
+ alt,
6201
+ onError,
6202
+ requestInit
6203
+ } = props;
6204
+ /** @type {ReturnType<typeof import("preact/hooks").useState<null | string>>} */
6205
+ const [imageObjectUrl, setImageObjectUrl] = hooks.useState(null);
6206
+ hooks.useEffect(() => {
6207
+ /** @type {null | string} */
6208
+ let objectUrl = null;
6209
+ const fetchImage = async () => {
6210
+ try {
6211
+ const response = await fetch(url, requestInit);
6212
+ if (!response.ok) {
6213
+ onError();
6214
+ return;
6215
+ }
6216
+ const blob = await response.blob();
6217
+ objectUrl = URL.createObjectURL(blob);
6218
+ setImageObjectUrl(objectUrl);
6219
+ } catch {
6220
+ onError();
6221
+ }
6222
+ };
6223
+ fetchImage();
6224
+ return () => {
6225
+ if (objectUrl) {
6226
+ URL.revokeObjectURL(objectUrl);
6227
+ }
6228
+ };
6229
+ }, [url, onError, requestInit]);
6230
+ return imageObjectUrl !== null ? jsxRuntime.jsx("img", {
6231
+ src: imageObjectUrl,
6232
+ alt: alt,
6233
+ class: `fjs-${type}-image`
6234
+ }) : null;
6235
+ }
6236
+
6137
6237
  /**
6138
6238
  *
6139
6239
  * @param {Object} props
6140
6240
  * @param {DocumentMetadata} props.documentMetadata
6141
6241
  * @param {string} props.endpoint
6142
6242
  * @param {string} props.domId
6243
+ * @param {RequestInit|undefined} props.requestInit
6143
6244
  * @param {number|undefined} props.maxHeight
6144
6245
  *
6145
6246
  * @returns {import("preact").JSX.Element}
@@ -6149,7 +6250,8 @@ function DocumentRenderer(props) {
6149
6250
  documentMetadata,
6150
6251
  endpoint,
6151
6252
  maxHeight,
6152
- domId
6253
+ domId,
6254
+ requestInit
6153
6255
  } = props;
6154
6256
  const {
6155
6257
  metadata
@@ -6168,13 +6270,15 @@ function DocumentRenderer(props) {
6168
6270
  maxHeight
6169
6271
  },
6170
6272
  "aria-describedby": hasError ? errorMessageId : undefined,
6171
- children: [jsxRuntime.jsx("img", {
6172
- src: endpoint,
6273
+ children: [jsxRuntime.jsx(ImageRenderer, {
6274
+ url: endpoint,
6173
6275
  alt: metadata.fileName,
6174
- class: `fjs-${type}-image`
6276
+ requestInit: requestInit,
6277
+ onError: () => setHasError(true)
6175
6278
  }), jsxRuntime.jsx(DownloadButton, {
6176
6279
  endpoint: endpoint,
6177
6280
  fileName: metadata.fileName,
6281
+ requestInit: requestInit,
6178
6282
  onDownloadError: () => {
6179
6283
  setHasError(true);
6180
6284
  }
@@ -6194,6 +6298,7 @@ function DocumentRenderer(props) {
6194
6298
  children: jsxRuntime.jsx(PdfRenderer, {
6195
6299
  url: endpoint,
6196
6300
  fileName: metadata.fileName,
6301
+ requestInit: requestInit,
6197
6302
  onError: () => setHasError(true),
6198
6303
  errorMessageId: errorMessageId
6199
6304
  })
@@ -6214,6 +6319,7 @@ function DocumentRenderer(props) {
6214
6319
  }), jsxRuntime.jsx(DownloadButton, {
6215
6320
  endpoint: endpoint,
6216
6321
  fileName: metadata.fileName,
6322
+ requestInit: requestInit,
6217
6323
  onDownloadError: () => {
6218
6324
  setHasError(true);
6219
6325
  }
@@ -6226,6 +6332,7 @@ function DocumentRenderer(props) {
6226
6332
  * @param {string} props.endpoint
6227
6333
  * @param {string} props.fileName
6228
6334
  * @param {Function} props.onDownloadError
6335
+ * @param {RequestInit|undefined} props.requestInit
6229
6336
  *
6230
6337
  * @returns {import("preact").JSX.Element}
6231
6338
  */
@@ -6233,11 +6340,12 @@ function DownloadButton(props) {
6233
6340
  const {
6234
6341
  endpoint,
6235
6342
  fileName,
6236
- onDownloadError
6343
+ onDownloadError,
6344
+ requestInit
6237
6345
  } = props;
6238
6346
  const handleDownload = async () => {
6239
6347
  try {
6240
- const response = await fetch(endpoint);
6348
+ const response = await fetch(endpoint, requestInit);
6241
6349
  if (!response.ok) {
6242
6350
  onDownloadError();
6243
6351
  return;
@@ -6291,6 +6399,16 @@ function useInViewport(ref) {
6291
6399
  return isInViewport;
6292
6400
  }
6293
6401
 
6402
+ /**
6403
+ * @param {DocumentEndpointBuilder | null} documentEndpointBuilder
6404
+ * @param {DocumentMetadata} document
6405
+ * @returns {RequestInit|undefined}
6406
+ */
6407
+ function getDocumentRequestInit(documentEndpointBuilder, document) {
6408
+ const requestInit = tryCatch(() => documentEndpointBuilder?.buildRequestInit?.(document));
6409
+ return requestInit !== null && typeof requestInit === 'object' ? requestInit : undefined;
6410
+ }
6411
+
6294
6412
  /**
6295
6413
  * @template T
6296
6414
  * @param {() => T} fn - Function to execute
@@ -6467,6 +6585,8 @@ const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suff
6467
6585
 
6468
6586
  /**
6469
6587
  * @typedef { import('../types').Schema } Schema
6588
+ * @typedef { import('../types').ExpressionLanguage } ExpressionLanguage
6589
+ * @typedef { import('../types').Templating } Templating
6470
6590
  */
6471
6591
 
6472
6592
  /**
@@ -6489,8 +6609,8 @@ const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suff
6489
6609
  *
6490
6610
  * @param {Schema} schema
6491
6611
  * @param {object} [options]
6492
- * @param {any} [options.expressionLanguage]
6493
- * @param {any} [options.templating]
6612
+ * @param {ExpressionLanguage} [options.expressionLanguage]
6613
+ * @param {Templating} [options.templating]
6494
6614
  * @param {any} [options.formFields]
6495
6615
  * @param {boolean} [options.inputs=true]
6496
6616
  * @param {boolean} [options.outputs=true]
@@ -6589,15 +6709,26 @@ const getAncestryList = (formFieldId, formFieldRegistry) => {
6589
6709
  return ids;
6590
6710
  };
6591
6711
 
6712
+ /**
6713
+ * @typedef { import('../../types').ExpressionLanguage } ExpressionLanguage
6714
+ */
6715
+
6592
6716
  /**
6593
6717
  * @typedef {object} Condition
6594
6718
  * @property {string} [hide]
6595
6719
  */
6596
6720
 
6597
6721
  class ConditionChecker {
6598
- constructor(formFieldRegistry, pathRegistry, eventBus) {
6722
+ /**
6723
+ * @param {Object} formFieldRegistry
6724
+ * @param {Object} pathRegistry
6725
+ * @param {ExpressionLanguage} expressionLanguage
6726
+ * @param {Object} eventBus
6727
+ */
6728
+ constructor(formFieldRegistry, pathRegistry, expressionLanguage, eventBus) {
6599
6729
  this._formFieldRegistry = formFieldRegistry;
6600
6730
  this._pathRegistry = pathRegistry;
6731
+ this._expressionLanguage = expressionLanguage;
6601
6732
  this._eventBus = eventBus;
6602
6733
  }
6603
6734
 
@@ -6720,24 +6851,7 @@ class ConditionChecker {
6720
6851
  * @returns {boolean|null}
6721
6852
  */
6722
6853
  check(condition, data = {}) {
6723
- if (!condition) {
6724
- return null;
6725
- }
6726
- if (!minDash.isString(condition) || !condition.startsWith('=')) {
6727
- return null;
6728
- }
6729
- try {
6730
- // cut off initial '='
6731
- const {
6732
- value: result
6733
- } = feelin.unaryTest(condition.slice(1), data);
6734
- return result;
6735
- } catch (error) {
6736
- this._eventBus.fire('error', {
6737
- error
6738
- });
6739
- return null;
6740
- }
6854
+ return this._expressionLanguage.evaluateUnaryTest(condition, data);
6741
6855
  }
6742
6856
 
6743
6857
  /**
@@ -6771,7 +6885,7 @@ class ConditionChecker {
6771
6885
  return Array.isArray(parentObject) && (!parentObject.length || parentObject.every(item => item === undefined));
6772
6886
  }
6773
6887
  }
6774
- ConditionChecker.$inject = ['formFieldRegistry', 'pathRegistry', 'eventBus'];
6888
+ ConditionChecker.$inject = ['formFieldRegistry', 'pathRegistry', 'expressionLanguage', 'eventBus'];
6775
6889
 
6776
6890
  const ExpressionLanguageModule = {
6777
6891
  __init__: ['expressionLanguage', 'templating', 'conditionChecker'],
@@ -8281,6 +8395,10 @@ function invokeFunction(fn, args) {
8281
8395
  return fn.apply(null, args);
8282
8396
  }
8283
8397
 
8398
+ /**
8399
+ * @typedef { import('../types').ExpressionLanguage } ExpressionLanguage
8400
+ */
8401
+
8284
8402
  const EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
8285
8403
  const PHONE_PATTERN = /(\+|00)(297|93|244|1264|358|355|376|971|54|374|1684|1268|61|43|994|257|32|229|226|880|359|973|1242|387|590|375|501|1441|591|55|1246|673|975|267|236|1|61|41|56|86|225|237|243|242|682|57|269|238|506|53|5999|61|1345|357|420|49|253|1767|45|1809|1829|1849|213|593|20|291|212|34|372|251|358|679|500|33|298|691|241|44|995|44|233|350|224|590|220|245|240|30|1473|299|502|594|1671|592|852|504|385|509|36|62|44|91|246|353|98|964|354|972|39|1876|44|962|81|76|77|254|996|855|686|1869|82|383|965|856|961|231|218|1758|423|94|266|370|352|371|853|590|212|377|373|261|960|52|692|389|223|356|95|382|976|1670|258|222|1664|596|230|265|60|262|264|687|227|672|234|505|683|31|47|977|674|64|968|92|507|64|51|63|680|675|48|1787|1939|850|351|595|970|689|974|262|40|7|250|966|249|221|65|500|4779|677|232|503|378|252|508|381|211|239|597|421|386|46|268|1721|248|963|1649|235|228|66|992|690|993|670|676|1868|216|90|688|886|255|256|380|598|1|998|3906698|379|1784|58|1284|1340|84|678|681|685|967|27|260|263)(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{4,20}$/;
8286
8404
  const VALIDATE_FEEL_PROPERTIES = ['min', 'max', 'minLength', 'maxLength'];
@@ -8409,6 +8527,12 @@ function runPresetValidation(field, validation, value) {
8409
8527
  }
8410
8528
  return errors;
8411
8529
  }
8530
+
8531
+ /**
8532
+ * @param {Object} validate
8533
+ * @param {ExpressionLanguage} expressionLanguage
8534
+ * @param {Object} expressionContextInfo
8535
+ */
8412
8536
  function evaluateFEELValues(validate, expressionLanguage, expressionContextInfo) {
8413
8537
  const evaluatedValidate = {
8414
8538
  ...validate
@@ -8421,6 +8545,13 @@ function evaluateFEELValues(validate, expressionLanguage, expressionContextInfo)
8421
8545
  });
8422
8546
  return evaluatedValidate;
8423
8547
  }
8548
+
8549
+ /**
8550
+ * @param {Object} validate
8551
+ * @param {ExpressionLanguage} expressionLanguage
8552
+ * @param {Object} conditionChecker
8553
+ * @param {Object} form
8554
+ */
8424
8555
  function oldEvaluateFEELValues(validate, expressionLanguage, conditionChecker, form) {
8425
8556
  const evaluatedValidate = {
8426
8557
  ...validate
@@ -9974,6 +10105,7 @@ exports.pathParse = pathParse;
9974
10105
  exports.pathsEqual = pathsEqual;
9975
10106
  exports.runExpressionEvaluation = runExpressionEvaluation;
9976
10107
  exports.runRecursively = runRecursively;
10108
+ exports.runUnaryTestEvaluation = runUnaryTestEvaluation;
9977
10109
  exports.sanitizeDateTimePickerValue = sanitizeDateTimePickerValue;
9978
10110
  exports.sanitizeHTML = sanitizeHTML;
9979
10111
  exports.sanitizeIFrameSource = sanitizeIFrameSource;