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