@limetech/lime-elements 40.5.1 → 40.5.4

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 (97) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/{_baseEach-Bygi-un6.js → _baseForOwn-8jMPwf3X.js} +1 -43
  3. package/dist/cjs/{_baseIsEqual-CQZKiKbe.js → _baseIsEqual-zzV9DKMv.js} +0 -1
  4. package/dist/cjs/{_baseIteratee-DDklwtn9.js → _baseIteratee-BIMRm4nq.js} +1 -3
  5. package/dist/cjs/_baseSet-DykMx7uF.js +92 -0
  6. package/dist/cjs/_getPrototype-CCLHkBHj.js +8 -0
  7. package/dist/cjs/{cloneDeep-hE40Q8aw.js → cloneDeep-D2immmwT.js} +16 -160
  8. package/dist/cjs/{difference-B543MGsY.js → isArrayLikeObject-U2Ddbw4n.js} +5 -93
  9. package/dist/cjs/{isEqual-Cd9J8a5-.js → isEqual-D3eFkhqd.js} +1 -1
  10. package/dist/cjs/limel-action-bar_3.cjs.entry.js +1 -1
  11. package/dist/cjs/limel-chip-set.cjs.entry.js +2 -2
  12. package/dist/cjs/limel-dialog.cjs.entry.js +2 -2
  13. package/dist/cjs/limel-file-dropzone_2.cjs.entry.js +45 -6
  14. package/dist/cjs/limel-file-viewer.cjs.entry.js +510 -70
  15. package/dist/cjs/limel-flatpickr-adapter.cjs.entry.js +73 -11
  16. package/dist/cjs/limel-form.cjs.entry.js +5985 -3415
  17. package/dist/cjs/limel-popover_2.cjs.entry.js +1 -1
  18. package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +326 -262
  19. package/dist/cjs/limel-tab-bar.cjs.entry.js +83 -4
  20. package/dist/cjs/limel-table.cjs.entry.js +598 -199
  21. package/dist/collection/components/action-bar/action-bar-item/action-bar-item.css +1 -1
  22. package/dist/collection/components/form/fields/schema-field.js +8 -0
  23. package/dist/collection/components/form/form.js +1 -0
  24. package/dist/collection/components/form/form.test-schemas.js +47 -0
  25. package/dist/collection/components/form/templates/array-field-collapsible-item.js +38 -11
  26. package/dist/collection/components/popover-surface/popover-surface.css +1 -0
  27. package/dist/collection/components/table/table.css +1 -2
  28. package/dist/esm/{_baseEach-lvj-g1Zj.js → _baseForOwn-D5j7fHlv.js} +2 -42
  29. package/dist/esm/{_baseIsEqual-B6bIeexY.js → _baseIsEqual-CyF4P8uM.js} +2 -2
  30. package/dist/esm/{_baseIteratee-CEZRuq23.js → _baseIteratee-CpbczHY1.js} +3 -3
  31. package/dist/esm/_baseSet-Cw2IE6ZF.js +89 -0
  32. package/dist/esm/_getPrototype-B0kMNv1F.js +6 -0
  33. package/dist/esm/{_getTag-CMJL-6hk.js → _getTag-BiAaLWvH.js} +1 -1
  34. package/dist/esm/{cloneDeep-CL7UD3mR.js → cloneDeep-DAbOtbs8.js} +18 -154
  35. package/dist/esm/{difference-DND5tWiA.js → isArrayLikeObject-CJB8HbtH.js} +7 -91
  36. package/dist/esm/{isEmpty-DHgukT5k.js → isEmpty-BGVZ12Ea.js} +1 -1
  37. package/dist/esm/{isEqual-4tbMaaf_.js → isEqual-CE83yS8T.js} +1 -1
  38. package/dist/esm/limel-action-bar_3.entry.js +1 -1
  39. package/dist/esm/limel-chip-set.entry.js +3 -3
  40. package/dist/esm/limel-chip_2.entry.js +2 -2
  41. package/dist/esm/limel-dialog.entry.js +3 -3
  42. package/dist/esm/limel-file-dropzone_2.entry.js +45 -6
  43. package/dist/esm/limel-file-viewer.entry.js +510 -70
  44. package/dist/esm/limel-flatpickr-adapter.entry.js +69 -7
  45. package/dist/esm/limel-form.entry.js +5982 -3412
  46. package/dist/esm/limel-popover_2.entry.js +1 -1
  47. package/dist/esm/limel-prosemirror-adapter.entry.js +327 -263
  48. package/dist/esm/limel-tab-bar.entry.js +83 -4
  49. package/dist/esm/limel-table.entry.js +595 -196
  50. package/dist/lime-elements/lime-elements.esm.js +1 -1
  51. package/dist/lime-elements/p-05afb316.entry.js +1 -0
  52. package/dist/lime-elements/p-0fba64eb.entry.js +16 -0
  53. package/dist/lime-elements/p-10594468.entry.js +1 -0
  54. package/dist/lime-elements/p-19344984.entry.js +13 -0
  55. package/dist/lime-elements/p-26c5c6b9.entry.js +1 -0
  56. package/dist/lime-elements/{p-11a96a8c.entry.js → p-36ce4fbc.entry.js} +1 -1
  57. package/dist/lime-elements/{p-955292eb.entry.js → p-4aae63bd.entry.js} +1 -1
  58. package/dist/lime-elements/{p-b8c76487.entry.js → p-5d974afa.entry.js} +1 -1
  59. package/dist/lime-elements/p-B6-3WXAv.js +1 -0
  60. package/dist/lime-elements/p-BQeIv4aA.js +1 -0
  61. package/dist/lime-elements/p-BY5NjPj8.js +1 -0
  62. package/dist/lime-elements/p-COdirXg1.js +1 -0
  63. package/dist/lime-elements/p-CW2-jhzP.js +1 -0
  64. package/dist/lime-elements/p-D2VFSWDU.js +1 -0
  65. package/dist/lime-elements/{p-D4RdItCq.js → p-DK0pjX-Y.js} +1 -1
  66. package/dist/lime-elements/p-ORUNaKm-.js +1 -0
  67. package/dist/lime-elements/{p-5b371d6b.entry.js → p-a71f6e87.entry.js} +4 -4
  68. package/dist/lime-elements/{p-1dd86ad4.entry.js → p-bd7b3f8e.entry.js} +1 -1
  69. package/dist/lime-elements/p-cef66e53.entry.js +1 -0
  70. package/dist/lime-elements/{p-02812dc8.entry.js → p-d3bd4b80.entry.js} +1 -1
  71. package/dist/lime-elements/p-nVemxylR.js +1 -0
  72. package/dist/lime-elements/p-pk5KIdKH.js +1 -0
  73. package/dist/types/components/form/form.test-schemas.d.ts +3 -0
  74. package/dist/types/components/form/templates/array-field-collapsible-item.d.ts +0 -1
  75. package/package.json +16 -16
  76. package/dist/cjs/_getAllKeysIn-BJsgUTAt.js +0 -119
  77. package/dist/cjs/_isIterateeCall-BUGt9SrH.js +0 -73
  78. package/dist/cjs/pickBy-D-X6S5kg.js +0 -201
  79. package/dist/esm/_getAllKeysIn-kT3KizYN.js +0 -114
  80. package/dist/esm/_isIterateeCall-Bb4gUZLK.js +0 -70
  81. package/dist/esm/pickBy-C4C3CM4m.js +0 -195
  82. package/dist/lime-elements/p-055b11f7.entry.js +0 -13
  83. package/dist/lime-elements/p-56bkn1D-.js +0 -1
  84. package/dist/lime-elements/p-67f02b69.entry.js +0 -1
  85. package/dist/lime-elements/p-867681aa.entry.js +0 -1
  86. package/dist/lime-elements/p-9d3437ac.entry.js +0 -16
  87. package/dist/lime-elements/p-B8yl2MCT.js +0 -1
  88. package/dist/lime-elements/p-BAR6am-7.js +0 -1
  89. package/dist/lime-elements/p-BE1Efiuj.js +0 -1
  90. package/dist/lime-elements/p-BsX7ma3n.js +0 -1
  91. package/dist/lime-elements/p-CR7u-m0b.js +0 -1
  92. package/dist/lime-elements/p-CVzgpz9P.js +0 -1
  93. package/dist/lime-elements/p-D8H5afYg.js +0 -1
  94. package/dist/lime-elements/p-DtrvybFR.js +0 -1
  95. package/dist/lime-elements/p-f309ceb8.entry.js +0 -1
  96. package/dist/lime-elements/p-f3c108c9.entry.js +0 -1
  97. package/dist/lime-elements/p-mmkVLOjW.js +0 -1
@@ -236,12 +236,129 @@ function normalizeCharset(charset) {
236
236
  return charset.replace(/^(?:x-ms-|x-|cs)/, '').replace(/[\s._-]+/g, '');
237
237
  }
238
238
 
239
+ // Every label of the WHATWG Encoding Standard, https://encoding.spec.whatwg.org/encodings.json
240
+ //
241
+ // TextDecoder accepts these and nothing else, so a label outside the list is turned away
242
+ // without constructing a decoder for it. Letting the constructor throw instead costs
243
+ // several times the decode itself, and a message controls how often it happens: once
244
+ // per encoded word, twice when the label also fails after normalization.
245
+ const ENCODING_LABELS = new Set(
246
+ [
247
+ // UTF-8
248
+ 'unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8',
249
+ // IBM866
250
+ '866 cp866 csibm866 ibm866',
251
+ // ISO-8859-2
252
+ 'csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2',
253
+ // ISO-8859-3
254
+ 'csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3',
255
+ // ISO-8859-4
256
+ 'csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4',
257
+ // ISO-8859-5
258
+ 'csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988',
259
+ // ISO-8859-6
260
+ 'arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e',
261
+ 'iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987',
262
+ // ISO-8859-7
263
+ 'csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7',
264
+ 'iso_8859-7:1987 sun_eu_greek',
265
+ // ISO-8859-8
266
+ 'csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8',
267
+ 'iso_8859-8:1988 visual',
268
+ // ISO-8859-8-I
269
+ 'csiso88598i iso-8859-8-i logical',
270
+ // ISO-8859-10
271
+ 'csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6',
272
+ // ISO-8859-13
273
+ 'iso-8859-13 iso8859-13 iso885913',
274
+ // ISO-8859-14
275
+ 'iso-8859-14 iso8859-14 iso885914',
276
+ // ISO-8859-15
277
+ 'csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9',
278
+ // ISO-8859-16
279
+ 'iso-8859-16',
280
+ // KOI8-R
281
+ 'cskoi8r koi koi8 koi8-r koi8_r',
282
+ // KOI8-U
283
+ 'koi8-ru koi8-u',
284
+ // macintosh
285
+ 'csmacintosh mac macintosh x-mac-roman',
286
+ // windows-874
287
+ 'dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874',
288
+ // windows-1250
289
+ 'cp1250 windows-1250 x-cp1250',
290
+ // windows-1251
291
+ 'cp1251 windows-1251 x-cp1251',
292
+ // windows-1252
293
+ 'ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591',
294
+ 'iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252',
295
+ // windows-1253
296
+ 'cp1253 windows-1253 x-cp1253',
297
+ // windows-1254
298
+ 'cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5',
299
+ 'windows-1254 x-cp1254',
300
+ // windows-1255
301
+ 'cp1255 windows-1255 x-cp1255',
302
+ // windows-1256
303
+ 'cp1256 windows-1256 x-cp1256',
304
+ // windows-1257
305
+ 'cp1257 windows-1257 x-cp1257',
306
+ // windows-1258
307
+ 'cp1258 windows-1258 x-cp1258',
308
+ // x-mac-cyrillic
309
+ 'x-mac-cyrillic x-mac-ukrainian',
310
+ // GBK
311
+ 'chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk',
312
+ // gb18030
313
+ 'gb18030',
314
+ // Big5
315
+ 'big5 big5-hkscs cn-big5 csbig5 x-x-big5',
316
+ // EUC-JP
317
+ 'cseucpkdfmtjapanese euc-jp x-euc-jp',
318
+ // ISO-2022-JP
319
+ 'csiso2022jp iso-2022-jp',
320
+ // Shift_JIS
321
+ 'csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis',
322
+ // EUC-KR
323
+ 'cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601',
324
+ 'windows-949',
325
+ // replacement
326
+ 'csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement',
327
+ // UTF-16BE
328
+ 'unicodefffe utf-16be',
329
+ // UTF-16LE
330
+ 'csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le',
331
+ // x-user-defined
332
+ 'x-user-defined'
333
+ ]
334
+ .join(' ')
335
+ .split(' ')
336
+ );
337
+
338
+ // Decoders by label, or null where the runtime refuses the label (the replacement
339
+ // encoding, or an encoding a runtime was built without). Only labels from the list above
340
+ // are stored, so the map can not grow past it however many labels a message makes up.
341
+ //
342
+ // Sharing a decoder is safe because nothing decodes with `stream: true`, so every
343
+ // decode() call starts from a fresh state.
344
+ const decoders = new Map();
345
+
239
346
  function tryDecoder(charset) {
240
- try {
241
- return new TextDecoder(charset);
242
- } catch (err) {
347
+ if (!ENCODING_LABELS.has(charset)) {
243
348
  return null;
244
349
  }
350
+
351
+ if (!decoders.has(charset)) {
352
+ let decoder = null;
353
+ try {
354
+ decoder = new TextDecoder(charset);
355
+ } catch (err) {
356
+ // not supported by this runtime
357
+ }
358
+ decoders.set(charset, decoder);
359
+ }
360
+
361
+ return decoders.get(charset);
245
362
  }
246
363
 
247
364
  function getDecoder(charset) {
@@ -261,7 +378,7 @@ function getDecoder(charset) {
261
378
  // differed from a supported label only by a prefix or a separator, eg. x-big5.
262
379
  const alias = (codePage && codePageAliases.get(codePage[1])) || charsetAliases.get(normalized) || normalized;
263
380
 
264
- return tryDecoder(alias) || new TextDecoder('windows-1252');
381
+ return tryDecoder(alias) || tryDecoder('windows-1252');
265
382
  }
266
383
 
267
384
  /**
@@ -765,7 +882,22 @@ const headerDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
765
882
  // `from` header, and since the first occurrence of a header wins it outranked the real
766
883
  // sender. Leaving the character in the key keeps the line visible without letting it
767
884
  // collide with a genuine header.
768
- const trimWsp = str => str.replace(/^[ \t]+|[ \t]+$/g, '');
885
+ //
886
+ // An index scan rather than `/^[ \t]+|[ \t]+$/g`, which retries the trailing branch at
887
+ // every position of a blank run that is followed by other text, so a single header with
888
+ // a long run of spaces in the middle took seconds to trim.
889
+ const isWsp = c => c === 0x20 || c === 0x09;
890
+ const trimWsp = str => {
891
+ let start = 0;
892
+ let end = str.length;
893
+ while (start < end && isWsp(str.charCodeAt(start))) {
894
+ start++;
895
+ }
896
+ while (end > start && isWsp(str.charCodeAt(end - 1))) {
897
+ end--;
898
+ }
899
+ return str.slice(start, end);
900
+ };
769
901
 
770
902
  // Headers that decide how this part's body is read, see processHeaders
771
903
  const CONTENT_HEADERS = new Set([
@@ -892,17 +1024,26 @@ class MimeNode {
892
1024
  // A parameter value starts at `=` and ends at the `;` that begins the next one
893
1025
  let inParameterValue = false;
894
1026
 
1027
+ // Whether result ends in SP or HTAB. Kept up to date on every append, because
1028
+ // testing `/[ \t]$/` against result flattens the whole string on each `(` and is
1029
+ // quadratic in the length of the header.
1030
+ let endsWithWsp = false;
1031
+ const append = c => {
1032
+ result += c;
1033
+ endsWithWsp = c === ' ' || c === '\t';
1034
+ };
1035
+
895
1036
  // A comment may only appear where linear whitespace is allowed, so inside a
896
1037
  // parameter value the parenthesis has to follow whitespace to open one. Outside
897
1038
  // one, eg. after the type itself, anything goes.
898
- const opensComment = () => !inParameterValue || !result.length || /[ \t]$/.test(result);
1039
+ const opensComment = () => !inParameterValue || !result.length || endsWithWsp;
899
1040
 
900
1041
  for (let i = 0; i < str.length; i++) {
901
1042
  const chr = str.charAt(i);
902
1043
 
903
1044
  if (escaped) {
904
1045
  if (depth === 0) {
905
- result += chr;
1046
+ append(chr);
906
1047
  }
907
1048
  escaped = false;
908
1049
  continue;
@@ -911,14 +1052,14 @@ class MimeNode {
911
1052
  if (chr === '\\') {
912
1053
  escaped = true;
913
1054
  if (depth === 0) {
914
- result += chr;
1055
+ append(chr);
915
1056
  }
916
1057
  continue;
917
1058
  }
918
1059
 
919
1060
  if (chr === '"' && depth === 0) {
920
1061
  inQuote = !inQuote;
921
- result += chr;
1062
+ append(chr);
922
1063
  continue;
923
1064
  }
924
1065
 
@@ -944,7 +1085,7 @@ class MimeNode {
944
1085
  }
945
1086
 
946
1087
  if (depth === 0) {
947
- result += chr;
1088
+ append(chr);
948
1089
  }
949
1090
  }
950
1091
 
@@ -1101,30 +1242,61 @@ class MimeNode {
1101
1242
  }
1102
1243
 
1103
1244
  decodeFlowedText(str, delSp) {
1104
- return (
1105
- str
1106
- .split(/\r?\n/)
1107
- // remove whitespace stuffing before anything else
1108
- // http://tools.ietf.org/html/rfc3676#section-4.4
1109
- // doing it after the join leaves the stuffed space of a continuation line
1110
- // sitting in the middle of the joined paragraph
1111
- .map(line => (line.charAt(0) === ' ' ? line.slice(1) : line))
1112
- // remove soft linebreaks
1113
- // soft linebreaks are added after space symbols
1114
- .reduce((previousValue, currentValue) => {
1115
- if (previousValue.endsWith(' ') && previousValue !== '-- ' && !previousValue.endsWith('\n-- ')) {
1116
- if (delSp) {
1117
- // delsp adds space to text to be able to fold it
1118
- // these spaces can be removed once the text is unfolded
1119
- return previousValue.slice(0, -1) + currentValue;
1245
+ // Pieces of the result, joined once at the end. Growing a single string and
1246
+ // calling endsWith() on it for every line flattens the whole paragraph again on
1247
+ // each line, which is quadratic in the length of a paragraph.
1248
+ // Empty pieces are never stored, so the last piece always holds the last
1249
+ // character of the result.
1250
+ const parts = [];
1251
+ // The unfolded line being built, ie. everything after the last hard line break,
1252
+ // starts at this index of parts and is this many characters long
1253
+ let lineStart = 0;
1254
+ let lineLength = 0;
1255
+
1256
+ const lines = str.split(/\r?\n/);
1257
+ for (let i = 0; i < lines.length; i++) {
1258
+ let line = lines[i];
1259
+
1260
+ // remove whitespace stuffing before anything else
1261
+ // http://tools.ietf.org/html/rfc3676#section-4.4
1262
+ // doing it after the join leaves the stuffed space of a continuation line
1263
+ // sitting in the middle of the joined paragraph
1264
+ if (line.charAt(0) === ' ') {
1265
+ line = line.slice(1);
1266
+ }
1267
+
1268
+ if (i) {
1269
+ const last = parts.length ? parts[parts.length - 1] : '';
1270
+
1271
+ // soft linebreaks are added after space symbols, except for the signature
1272
+ // separator which is a line of its own
1273
+ const isSignature = lineLength === 3 && parts.slice(lineStart).join('') === '-- ';
1274
+
1275
+ if (last.endsWith(' ') && !isSignature) {
1276
+ if (delSp) {
1277
+ // delsp adds space to text to be able to fold it
1278
+ // these spaces can be removed once the text is unfolded
1279
+ if (last.length > 1) {
1280
+ parts[parts.length - 1] = last.slice(0, -1);
1120
1281
  } else {
1121
- return previousValue + currentValue;
1282
+ parts.pop();
1122
1283
  }
1123
- } else {
1124
- return previousValue + '\n' + currentValue;
1284
+ lineLength--;
1125
1285
  }
1126
- })
1127
- );
1286
+ } else {
1287
+ parts.push('\n');
1288
+ lineStart = parts.length;
1289
+ lineLength = 0;
1290
+ }
1291
+ }
1292
+
1293
+ if (line) {
1294
+ parts.push(line);
1295
+ lineLength += line.length;
1296
+ }
1297
+ }
1298
+
1299
+ return parts.join('');
1128
1300
  }
1129
1301
 
1130
1302
  getTextContent() {
@@ -3565,31 +3737,251 @@ function textToHtml(str) {
3565
3737
  return '<div>' + html + '</div>';
3566
3738
  }
3567
3739
 
3740
+ // htmlToText strips tags with regexes like `<br\b[^>]*>` and `<!--.*?-->`. Handed to
3741
+ // String#replace as they are, the engine tries every candidate in turn and a candidate
3742
+ // that does not close scans on to the next `>`, the end of the line or the end of the
3743
+ // input before giving up, so an html part holding many unclosed `<`, `<a ` or `<!--`
3744
+ // took seconds to minutes to convert. The helpers below give the same results in linear
3745
+ // time.
3746
+
3747
+ // Characters that `.` does not match, so a `.*` can not run across them
3748
+ const LINE_TERMINATOR = /[\n\r\u2028\u2029]/g;
3749
+
3750
+ function lastLineTerminator(str) {
3751
+ return Math.max(str.lastIndexOf('\n'), str.lastIndexOf('\r'), str.lastIndexOf('\u2028'), str.lastIndexOf('\u2029'));
3752
+ }
3753
+
3754
+ /**
3755
+ * Returns a function that finds the next match of a global regex at or after a position.
3756
+ *
3757
+ * Callers only ever ask about positions that move forward, so a match stays the answer
3758
+ * until a position passes it, and no match stays the answer for good. That way every part
3759
+ * of the string is scanned once, however many times it is asked about.
3760
+ *
3761
+ * @param {String} str String to search
3762
+ * @param {RegExp} regex Global regex to look for
3763
+ * @return {Function} `pos => match` of the next match, or null if there is none
3764
+ */
3765
+ function createFinder(str, regex) {
3766
+ let searchedFrom = Infinity;
3767
+ let found = null;
3768
+
3769
+ return pos => {
3770
+ if (pos < searchedFrom || (found && pos > found.index)) {
3771
+ regex.lastIndex = pos;
3772
+ found = regex.exec(str);
3773
+ searchedFrom = pos;
3774
+ }
3775
+ return found;
3776
+ };
3777
+ }
3778
+
3779
+ /**
3780
+ * `str.replace(pattern, replacement)` for a pattern shaped like `<tag\b[^>]*>`.
3781
+ *
3782
+ * Such a pattern only fails at a candidate that has no `>` after it, so the text after
3783
+ * the last `>` can never match and is where every rescan happened. Leaving it out keeps
3784
+ * the native replace, and its speed, for everything else.
3785
+ *
3786
+ * @param {String} str String to process
3787
+ * @param {RegExp} pattern Global regex that ends on the first `>` after its start
3788
+ * @param {String} replacement Replacement text
3789
+ * @return {String} Processed string
3790
+ */
3791
+ function replaceClosedTags(str, pattern, replacement) {
3792
+ const end = str.lastIndexOf('>') + 1;
3793
+ return str.slice(0, end).replace(pattern, replacement) + str.slice(end);
3794
+ }
3795
+
3796
+ /**
3797
+ * `str.replace(pattern, replacement)` for a tag pattern that may fail even when a `>`
3798
+ * follows, like `<a\b[^>]*href...>`, or that runs on past its `>`.
3799
+ *
3800
+ * A candidate with no `>` after it ends the search, since no later candidate can close
3801
+ * either. A candidate that does not match skips every other candidate before the same
3802
+ * `>`, since those see the same tag body and fail the same way.
3803
+ *
3804
+ * @param {String} str String to process
3805
+ * @param {RegExp} prefix Global regex for the start of a candidate, which can not contain `>`
3806
+ * @param {RegExp} pattern The whole pattern as a sticky regex
3807
+ * @param {Function} replacement Function given the match and its groups
3808
+ * @return {String} Processed string
3809
+ */
3810
+ function replaceTags(str, prefix, pattern, replacement) {
3811
+ const parts = [];
3812
+ // everything before this index is already in parts
3813
+ let copied = 0;
3814
+ let searchFrom = 0;
3815
+
3816
+ while (true) {
3817
+ prefix.lastIndex = searchFrom;
3818
+ const candidate = prefix.exec(str);
3819
+ if (!candidate) {
3820
+ break;
3821
+ }
3822
+
3823
+ const start = candidate.index;
3824
+ pattern.lastIndex = start;
3825
+ const match = pattern.exec(str);
3826
+ if (!match) {
3827
+ const gt = str.indexOf('>', start);
3828
+ if (gt < 0) {
3829
+ break;
3830
+ }
3831
+ searchFrom = gt + 1;
3832
+ continue;
3833
+ }
3834
+
3835
+ parts.push(str.slice(copied, start), replacement(...match));
3836
+ copied = searchFrom = start + match[0].length;
3837
+ }
3838
+
3839
+ parts.push(str.slice(copied));
3840
+ return parts.join('');
3841
+ }
3842
+
3843
+ /**
3844
+ * `str.replace(pattern, replacement)` for a pattern like `<!--.*?-->` or
3845
+ * `<script\b[^>]*>.*?<\/script\b[^>]*>`, ie. an opener and a closer, either of which may
3846
+ * continue with `[^>]*>`, and a lazy `.*?` between them. The engine rescans to the end of
3847
+ * the line for every opener that is not closed.
3848
+ *
3849
+ * @param {String} str String to process
3850
+ * @param {RegExp} open Global regex for the opener
3851
+ * @param {Boolean} openToGt Whether the opener continues with `[^>]*>`
3852
+ * @param {RegExp} close Global regex for the closer
3853
+ * @param {Boolean} closeToGt Whether the closer continues with `[^>]*>`
3854
+ * @param {String} replacement Replacement text
3855
+ * @return {String} Processed string
3856
+ */
3857
+ function replaceBlocks(str, open, openToGt, close, closeToGt, replacement) {
3858
+ const nextGt = createFinder(str, />/g);
3859
+ const nextClose = createFinder(str, close);
3860
+ const nextLineEnd = createFinder(str, LINE_TERMINATOR);
3861
+ const parts = [];
3862
+ let copied = 0;
3863
+ let searchFrom = 0;
3864
+
3865
+ while (true) {
3866
+ open.lastIndex = searchFrom;
3867
+ const opener = open.exec(str);
3868
+ if (!opener) {
3869
+ break;
3870
+ }
3871
+
3872
+ const start = opener.index;
3873
+ searchFrom = start + 1;
3874
+
3875
+ // where the lazy `.*?` starts
3876
+ let from = start + opener[0].length;
3877
+ if (openToGt) {
3878
+ const gt = nextGt(from);
3879
+ if (!gt) {
3880
+ // neither this opener nor any later one can close
3881
+ break;
3882
+ }
3883
+ from = gt.index + 1;
3884
+ }
3885
+
3886
+ const closer = nextClose(from);
3887
+ if (!closer) {
3888
+ // no closer follows this opener, so none follows a later one either
3889
+ break;
3890
+ }
3891
+
3892
+ // `.*?` can not reach the closer across a line terminator
3893
+ const lineEnd = nextLineEnd(from);
3894
+ if (lineEnd && lineEnd.index < closer.index) {
3895
+ continue;
3896
+ }
3897
+
3898
+ let end = closer.index + closer[0].length;
3899
+ if (closeToGt) {
3900
+ const gt = nextGt(end);
3901
+ if (!gt) {
3902
+ // this is the first closer after the opener, and any other one comes
3903
+ // later, so none of them is followed by `>`
3904
+ break;
3905
+ }
3906
+ end = gt.index + 1;
3907
+ }
3908
+
3909
+ parts.push(str.slice(copied, start), replacement);
3910
+ copied = searchFrom = end;
3911
+ }
3912
+
3913
+ parts.push(str.slice(copied));
3914
+ return parts.join('');
3915
+ }
3916
+
3917
+ /**
3918
+ * `str.replace(pattern, '')` for `^.*<tag\b[^>]*>`, ie. drops everything up to and
3919
+ * including the last such tag on the first line.
3920
+ *
3921
+ * @param {String} str String to process
3922
+ * @param {RegExp} prefix Global regex for the start of the tag, which can not contain `>`
3923
+ * @return {String} Processed string
3924
+ */
3925
+ function stripThroughLastTag(str, prefix) {
3926
+ const lineEnd = str.search(LINE_TERMINATOR);
3927
+ // `.*` stops at the first line terminator, and `[^>]*>` needs a `>` after the tag
3928
+ const limit = Math.min(lineEnd < 0 ? str.length : lineEnd, str.lastIndexOf('>'));
3929
+
3930
+ let last = -1;
3931
+ prefix.lastIndex = 0;
3932
+ let match;
3933
+ while ((match = prefix.exec(str)) && match.index < limit) {
3934
+ last = match.index;
3935
+ }
3936
+
3937
+ return last < 0 ? str : str.slice(str.indexOf('>', last) + 1);
3938
+ }
3939
+
3940
+ /**
3941
+ * `str.replace(pattern, '')` for `<tag\b[^>]*>.*$`, ie. drops the first such tag whose
3942
+ * `>` is on the last line, and everything after it.
3943
+ *
3944
+ * @param {String} str String to process
3945
+ * @param {RegExp} prefix Global regex for the start of the tag, which can not contain `>`
3946
+ * @return {String} Processed string
3947
+ */
3948
+ function stripFromFirstTag(str, prefix) {
3949
+ // `.*$` has to reach the end of the string without crossing a line terminator, so the
3950
+ // tag has to start after the last `>` that comes before the last line terminator
3951
+ const lineEnd = lastLineTerminator(str);
3952
+ prefix.lastIndex = lineEnd < 0 ? 0 : str.lastIndexOf('>', lineEnd) + 1;
3953
+
3954
+ const match = prefix.exec(str);
3955
+ return match && match.index < str.lastIndexOf('>') ? str.slice(0, match.index) : str;
3956
+ }
3957
+
3568
3958
  function htmlToText(str) {
3569
- str = str
3570
- // we can't process tags on multiple lines so remove newlines first
3571
- .replace(/\r?\n/g, '\u0001')
3572
- .replace(/<\!\-\-.*?\-\->/gi, ' ')
3959
+ // we can't process tags on multiple lines so remove newlines first
3960
+ str = str.replace(/\r?\n/g, '\u0001');
3573
3961
 
3574
- .replace(/<br\b[^>]*>/gi, '\n')
3575
- .replace(/<\/?(p|div|table|tr|td|th)\b[^>]*>/gi, '\n\n')
3576
- .replace(/<script\b[^>]*>.*?<\/script\b[^>]*>/gi, ' ')
3577
- .replace(/^.*<body\b[^>]*>/i, '')
3578
- .replace(/^.*<\/head\b[^>]*>/i, '')
3579
- .replace(/^.*<\!doctype\b[^>]*>/i, '')
3580
- .replace(/<\/body\b[^>]*>.*$/i, '')
3581
- .replace(/<\/html\b[^>]*>.*$/i, '')
3962
+ str = replaceBlocks(str, /<!--/g, false, /-->/g, false, ' ');
3582
3963
 
3583
- .replace(/<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/gi, ' ($1) ')
3964
+ str = replaceClosedTags(str, /<br\b[^>]*>/gi, '\n');
3965
+ str = replaceClosedTags(str, /<\/?(p|div|table|tr|td|th)\b[^>]*>/gi, '\n\n');
3966
+ str = replaceBlocks(str, /<script\b/gi, true, /<\/script\b/gi, true, ' ');
3967
+ str = stripThroughLastTag(str, /<body\b/gi);
3968
+ str = stripThroughLastTag(str, /<\/head\b/gi);
3969
+ str = stripThroughLastTag(str, /<!doctype\b/gi);
3970
+ str = stripFromFirstTag(str, /<\/body\b/gi);
3971
+ str = stripFromFirstTag(str, /<\/html\b/gi);
3584
3972
 
3585
- .replace(/<\/?(span|em|i|strong|b|u|a)\b[^>]*>/gi, '')
3973
+ str = replaceTags(str, /<a\b/gi, /<a\b[^>]*href\s*=\s*["']?([^\s"']+)[^>]*>/iy, (match, href) => ` (${href}) `);
3586
3974
 
3587
- .replace(/<li\b[^>]*>[\n\u0001\s]*/gi, '* ')
3975
+ str = replaceClosedTags(str, /<\/?(span|em|i|strong|b|u|a)\b[^>]*>/gi, '');
3588
3976
 
3589
- .replace(/<hr\b[^>]*>/g, '\n-------------\n')
3977
+ // the whitespace after the tag may run on past the last `>`
3978
+ str = replaceTags(str, /<li\b/gi, /<li\b[^>]*>[\n\u0001\s]*/iy, () => '* ');
3590
3979
 
3591
- .replace(/<[^>]*>/g, ' ')
3980
+ str = replaceClosedTags(str, /<hr\b[^>]*>/g, '\n-------------\n');
3592
3981
 
3982
+ str = replaceClosedTags(str, /<[^>]*>/g, ' ');
3983
+
3984
+ str = str
3593
3985
  // convert linebreak placeholders back to newlines
3594
3986
  .replace(/\u0001/g, '\n')
3595
3987
 
@@ -3909,6 +4301,41 @@ function findAddressInText(text) {
3909
4301
  return null;
3910
4302
  }
3911
4303
 
4304
+ /**
4305
+ * Checks whether text holds an angle bracket address, ie. `<` and `>` around a run
4306
+ * without other angle brackets that has an '@' with at least one character on each side.
4307
+ *
4308
+ * Same answer as `/<[^<>]+@[^<>]+>/.test(text)`, which backtracks quadratically on a `<`
4309
+ * followed by a long run of '@' and no closing `>`.
4310
+ *
4311
+ * @param {String} text Text to check
4312
+ * @return {Boolean} true if an angle bracket address is present
4313
+ */
4314
+ function hasAngleAddress(text) {
4315
+ // position right after the `<` that opened the current bracket, or -1
4316
+ let start = -1;
4317
+ // first '@' inside the bracket that has a character before it, or -1
4318
+ let at = -1;
4319
+
4320
+ for (let i = 0; i < text.length; i++) {
4321
+ const chr = text.charAt(i);
4322
+ if (chr === '<') {
4323
+ start = i + 1;
4324
+ at = -1;
4325
+ } else if (chr === '>') {
4326
+ if (at >= 0 && at < i - 1) {
4327
+ return true;
4328
+ }
4329
+ start = -1;
4330
+ at = -1;
4331
+ } else if (chr === '@' && start >= 0 && at < 0 && i > start) {
4332
+ at = i;
4333
+ }
4334
+ }
4335
+
4336
+ return false;
4337
+ }
4338
+
3912
4339
  /**
3913
4340
  * Converts tokens for a single address into an address object
3914
4341
  *
@@ -4002,7 +4429,9 @@ function _handleAddress(tokens, depth) {
4002
4429
  parsedGroup.forEach(member => {
4003
4430
  if (member.group) {
4004
4431
  // Nested group detected - flatten it by adding its members directly
4005
- groupMembers = groupMembers.concat(member.group);
4432
+ for (const nestedMember of member.group) {
4433
+ groupMembers.push(nestedMember);
4434
+ }
4006
4435
  } else {
4007
4436
  groupMembers.push(member);
4008
4437
  }
@@ -4036,11 +4465,13 @@ function _handleAddress(tokens, depth) {
4036
4465
  const found = findAddressInText(data.text[i]);
4037
4466
  if (found) {
4038
4467
  data.address = [found.value];
4039
- // the address and the whitespace around it collapse to one space
4468
+ // the address and the whitespace around it collapse to one space.
4469
+ // trimEnd rather than `/\s+$/`, which retries at every position of
4470
+ // a whitespace run that is not at the end and is quadratic in it.
4040
4471
  data.text[i] = (
4041
- data.text[i].substring(0, found.index).replace(/\s+$/, '') +
4472
+ data.text[i].substring(0, found.index).trimEnd() +
4042
4473
  ' ' +
4043
- data.text[i].substring(found.index + found.length).replace(/^\s+/, '')
4474
+ data.text[i].substring(found.index + found.length).trimStart()
4044
4475
  ).trim();
4045
4476
  break;
4046
4477
  }
@@ -4074,7 +4505,7 @@ function _handleAddress(tokens, depth) {
4074
4505
  // Security: only re-parse if decoded text contains angle-bracket addresses.
4075
4506
  // Without this, a bare encoded email (e.g. =?utf-8?B?dGVzdEBldmlsLmNv?=)
4076
4507
  // would be fabricated into an address from attacker-controlled input.
4077
- if (/<[^<>]+@[^<>]+>/.test(decodedText)) {
4508
+ if (hasAngleAddress(decodedText)) {
4078
4509
  const parsedSubAddresses = addressParser(decodedText);
4079
4510
  if (parsedSubAddresses && parsedSubAddresses.length) {
4080
4511
  return parsedSubAddresses;
@@ -4258,7 +4689,7 @@ function addressParser(str, options) {
4258
4689
 
4259
4690
  let addresses = [];
4260
4691
  let address = [];
4261
- let parsedAddresses = [];
4692
+ const parsedAddresses = [];
4262
4693
 
4263
4694
  tokens.forEach(token => {
4264
4695
  if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
@@ -4275,12 +4706,13 @@ function addressParser(str, options) {
4275
4706
  addresses.push(address);
4276
4707
  }
4277
4708
 
4278
- addresses.forEach(address => {
4279
- address = _handleAddress(address, depth);
4280
- if (address.length) {
4281
- parsedAddresses = parsedAddresses.concat(address);
4709
+ // Appended in place. Rebuilding the list with concat() for every address copies
4710
+ // everything parsed so far and is quadratic in the number of addresses.
4711
+ for (const tokens of addresses) {
4712
+ for (const address of _handleAddress(tokens, depth)) {
4713
+ parsedAddresses.push(address);
4282
4714
  }
4283
- });
4715
+ }
4284
4716
 
4285
4717
  if (options.flatten) {
4286
4718
  let addresses = [];
@@ -4684,9 +5116,15 @@ class PostalMime {
4684
5116
  .trim();
4685
5117
  }
4686
5118
 
4687
- // Enforce into unicode
4688
- const decodedText = node.getTextContent().replace(/\r?\n/g, '\n').replace(/\n*$/, '\n');
4689
- attachment.content = textEncoder.encode(decodedText);
5119
+ // Enforce into unicode, ending in exactly one newline. The trailing
5120
+ // newlines are counted rather than replaced with `/\n*$/`, which
5121
+ // retries at every newline of a run that does not end the text.
5122
+ const decodedText = node.getTextContent().replace(/\r?\n/g, '\n');
5123
+ let end = decodedText.length;
5124
+ while (end > 0 && decodedText.charCodeAt(end - 1) === 0x0a) {
5125
+ end--;
5126
+ }
5127
+ attachment.content = textEncoder.encode(decodedText.slice(0, end) + '\n');
4690
5128
  break;
4691
5129
  }
4692
5130
 
@@ -4937,15 +5375,17 @@ class PostalMime {
4937
5375
  }
4938
5376
 
4939
5377
  for (const key of ['to', 'cc', 'bcc', 'reply-to']) {
4940
- const addressHeaders = this.root.headers.filter(line => line.key === key);
4941
- let addresses = [];
4942
-
4943
- addressHeaders
4944
- .filter(entry => entry && entry.value)
4945
- .map(entry => addressParser(entry.value))
4946
- .forEach(parsed => (addresses = addresses.concat(parsed || [])));
5378
+ // Appended in place, concat() copies the whole list for every header
5379
+ const addresses = [];
5380
+ for (const entry of this.root.headers) {
5381
+ if (entry.key === key && entry.value) {
5382
+ for (const address of addressParser(entry.value)) {
5383
+ addresses.push(address);
5384
+ }
5385
+ }
5386
+ }
4947
5387
 
4948
- if (addresses && addresses.length) {
5388
+ if (addresses.length) {
4949
5389
  const camelKey = toCamelCase(key);
4950
5390
  message[camelKey] = addresses;
4951
5391
  }