expensify-common 2.0.181 → 2.0.183

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.
@@ -54,6 +54,242 @@ const VICTORY_CHART_REGEX = /<VictoryChart\b[^>]*\/>|<VictoryChart\b[^>]*>[\s\S]
54
54
  const VICTORY_CHART_PLACEHOLDER_DELIMITER = String.fromCharCode(0);
55
55
  const VICTORY_CHART_PLACEHOLDER_PATTERN = new RegExp(`${VICTORY_CHART_PLACEHOLDER_DELIMITER}(\\d+)${VICTORY_CHART_PLACEHOLDER_DELIMITER}`, 'g');
56
56
  const createVictoryChartPlaceholder = (index) => `${VICTORY_CHART_PLACEHOLDER_DELIMITER}${index}${VICTORY_CHART_PLACEHOLDER_DELIMITER}`;
57
+ function extractVictoryChartTags(text) {
58
+ const tags = [];
59
+ const out = text.replaceAll(VICTORY_CHART_REGEX, (match) => {
60
+ const placeholder = createVictoryChartPlaceholder(tags.length);
61
+ tags.push(match);
62
+ return placeholder;
63
+ });
64
+ return { text: out, tags };
65
+ }
66
+ function restoreVictoryChartTags(text, tags) {
67
+ if (tags.length === 0) {
68
+ return text;
69
+ }
70
+ return text.replaceAll(VICTORY_CHART_PLACEHOLDER_PATTERN, (match, idx) => { var _a; return (_a = tags[Number(idx)]) !== null && _a !== void 0 ? _a : match; });
71
+ }
72
+ function resolveAttributeCache(extras) {
73
+ var _a, _b;
74
+ if (!extras) {
75
+ return { attrCachingFn: undefined, attrCache: undefined };
76
+ }
77
+ return {
78
+ attrCachingFn: (_a = extras.mediaAttributeCachingFn) !== null && _a !== void 0 ? _a : extras.cacheVideoAttributes,
79
+ attrCache: (_b = extras.mediaAttributeCache) !== null && _b !== void 0 ? _b : extras.videoAttributeCache,
80
+ };
81
+ }
82
+ function replaceTextWithExtras(text, regexp, extras, replacement) {
83
+ if (typeof replacement === 'function') {
84
+ return text.replace(regexp, (...args) => replacement(extras, ...args));
85
+ }
86
+ return text.replace(regexp, replacement);
87
+ }
88
+ /**
89
+ * replace block element with '\n' if :
90
+ * 1. We have text within the element.
91
+ * 2. The text does not end with a new line.
92
+ * 3. It's not the last element in the string.
93
+ */
94
+ function replaceBlockElementWithNewLine(htmlString) {
95
+ let splitText = htmlString
96
+ // Lines starting with quote mark '>' will have '\n' added to them so to avoid adding extra '\n', remove the block element right next to it
97
+ .replaceAll(/<blockquote>> (<div.*?>|<\/div>|<comment.*?>|\n<\/comment>|<\/comment>|<h1>|<\/h1>|<h2>|<\/h2>|<h3>|<\/h3>|<h4>|<\/h4>|<h5>|<\/h5>|<h6>|<\/h6>|<p>|<\/p>|<li>|<\/li>)/gi, '<blockquote>> ')
98
+ .split(/<div.*?>|<\/div>|<comment.*?>|\n<\/comment>|<\/comment>|<h1>|<\/h1>|<h2>|<\/h2>|<h3>|<\/h3>|<h4>|<\/h4>|<h5>|<\/h5>|<h6>|<\/h6>|<p>|<\/p>|<li>|<\/li>|<blockquote>|<\/blockquote>/);
99
+ const stripHTML = (text) => str_1.default.stripHTML(text);
100
+ splitText = splitText.map(stripHTML);
101
+ let joinedText = '';
102
+ // Delete whitespace at the end
103
+ while (splitText.length) {
104
+ if (splitText[splitText.length - 1].trim().length > 0 || splitText[splitText.length - 1].match(/\n/)) {
105
+ break;
106
+ }
107
+ splitText.pop();
108
+ }
109
+ const processText = (text, index) => {
110
+ if (text.trim().length === 0 && !text.match(/\n/)) {
111
+ return;
112
+ }
113
+ // Insert '\n' unless it ends with '\n' or it's the last element, or if it's a header ('# ') with a space.
114
+ if (text.match(/\n[\s]?$/) || index === splitText.length - 1 || text === '# ') {
115
+ joinedText += text;
116
+ }
117
+ else {
118
+ joinedText += `${text}\n`;
119
+ }
120
+ };
121
+ for (const [index, text] of splitText.entries()) {
122
+ processText(text, index);
123
+ }
124
+ return joinedText;
125
+ }
126
+ /** Check if the input text includes only the open or the close tag of an element. */
127
+ function containsNonPairTag(textToCheck) {
128
+ // Create a regular expression to match HTML tags
129
+ const tagRegExp = /<([a-z][a-z0-9-]*)\b[^>]*>|<\/([a-z][a-z0-9-]*)\s*>/gi;
130
+ // Use a stack to keep track of opening tags
131
+ const tagStack = [];
132
+ // Match all HTML tags in the string
133
+ let match = tagRegExp.exec(textToCheck);
134
+ while (match) {
135
+ const openingTag = match[1];
136
+ const closingTag = match[2];
137
+ if (openingTag && openingTag !== 'br') {
138
+ // If it's an opening tag, push it onto the stack
139
+ tagStack.push(openingTag);
140
+ }
141
+ else if (closingTag) {
142
+ // If it's a closing tag, pop the top of the stack
143
+ const expectedTag = tagStack.pop();
144
+ // If the closing tag doesn't match the expected opening tag, return false
145
+ if (closingTag !== expectedTag) {
146
+ return true;
147
+ }
148
+ }
149
+ match = tagRegExp.exec(textToCheck);
150
+ }
151
+ // If there are any tags left in the stack, they're unclosed
152
+ return tagStack.length !== 0;
153
+ }
154
+ /**
155
+ * Determines the end position to truncate the HTML content while considering word boundaries.
156
+ */
157
+ function getEndPosition(content, tailPosition, maxLength, totalLength, opts) {
158
+ const WORD_BREAK_REGEX = /\W+/g;
159
+ const defaultPosition = maxLength - totalLength;
160
+ const slop = opts.slop;
161
+ if (!slop) {
162
+ return defaultPosition;
163
+ }
164
+ let position = defaultPosition;
165
+ const isShort = defaultPosition < slop;
166
+ const slopPos = isShort ? defaultPosition : slop - 1;
167
+ const substr = content.slice(isShort ? 0 : defaultPosition - slop, tailPosition !== undefined ? tailPosition : defaultPosition + slop);
168
+ const wordBreakMatch = WORD_BREAK_REGEX.exec(substr);
169
+ if (!opts.truncateLastWord) {
170
+ if (tailPosition && substr.length <= tailPosition) {
171
+ position = substr.length;
172
+ }
173
+ else {
174
+ while (wordBreakMatch !== null) {
175
+ if (wordBreakMatch.index < slopPos) {
176
+ position = defaultPosition - (slopPos - wordBreakMatch.index);
177
+ if (wordBreakMatch.index === 0 && defaultPosition <= 1) {
178
+ break;
179
+ }
180
+ }
181
+ else if (wordBreakMatch.index === slopPos) {
182
+ position = defaultPosition;
183
+ break;
184
+ }
185
+ else {
186
+ position = defaultPosition + (wordBreakMatch.index - slopPos);
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ if (content.charAt(position - 1).match(/\s$/)) {
192
+ position--;
193
+ }
194
+ }
195
+ return position;
196
+ }
197
+ /**
198
+ * Truncate HTML string and keep tag safe.
199
+ * pulled from https://github.com/huang47/nodejs-html-truncate/blob/master/lib/truncate.js
200
+ */
201
+ function truncateHTML(html, maxLength, options) {
202
+ const EMPTY_STRING = '';
203
+ const DEFAULT_TRUNCATE_SYMBOL = '...';
204
+ const DEFAULT_SLOP = Math.min(10, maxLength);
205
+ const tagsStack = [];
206
+ const KEY_VALUE_REGEX = '((?:\\s+(?:\\w+|-)+(?:\\s*=\\s*(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|[^\'">\\s]+))?)*)';
207
+ const IS_CLOSE_REGEX = '\\s*\\/?\\s*';
208
+ const CLOSE_REGEX = '\\s*\\/\\s*';
209
+ const SELF_CLOSE_REGEX = new RegExp(`<\\/?(\\w+)${KEY_VALUE_REGEX}${CLOSE_REGEX}>`);
210
+ const HTML_TAG_REGEX = new RegExp(`<\\/?(\\w+)${KEY_VALUE_REGEX}${IS_CLOSE_REGEX}>`);
211
+ const URL_REGEX = /(((ftp|https?):\/\/)[\\-\w@:%_\\+.~#?,&\\/\\/=]+)|((mailto:)?[_.\w\\-]+@([\w][\w\\-]+\.)+[a-zA-Z]{2,3})/g;
212
+ const IMAGE_TAG_REGEX = new RegExp(`<img\\s*${KEY_VALUE_REGEX}${CLOSE_REGEX}>`);
213
+ let truncatedContent = EMPTY_STRING;
214
+ let totalLength = 0;
215
+ let matches = HTML_TAG_REGEX.exec(html);
216
+ let endResult;
217
+ let index;
218
+ let tag;
219
+ let selfClose = null;
220
+ let htmlString = html;
221
+ const opts = Object.assign({ ellipsis: DEFAULT_TRUNCATE_SYMBOL, truncateLastWord: true, slop: DEFAULT_SLOP }, options);
222
+ function removeImageTag(content) {
223
+ const match = IMAGE_TAG_REGEX.exec(content);
224
+ if (!match) {
225
+ return content;
226
+ }
227
+ const matchIndex = match.index;
228
+ const matchLength = match[0].length;
229
+ return content.substring(0, matchIndex) + content.substring(matchIndex + matchLength);
230
+ }
231
+ function closeTags(tags) {
232
+ return tags
233
+ .reverse()
234
+ .map((mappedTag) => `</${mappedTag}>`)
235
+ .join('');
236
+ }
237
+ while (matches) {
238
+ matches = HTML_TAG_REGEX.exec(htmlString);
239
+ if (!matches) {
240
+ if (totalLength >= maxLength) {
241
+ break;
242
+ }
243
+ matches = URL_REGEX.exec(htmlString);
244
+ if (!matches || matches.index >= maxLength) {
245
+ truncatedContent += htmlString.substring(0, getEndPosition(htmlString, undefined, maxLength, totalLength, opts));
246
+ break;
247
+ }
248
+ while (matches) {
249
+ endResult = matches[0];
250
+ if (endResult !== null) {
251
+ index = matches.index;
252
+ const truncateEnd = index + endResult.length;
253
+ truncatedContent += htmlString.substring(0, truncateEnd - totalLength);
254
+ htmlString = htmlString.substring(index + endResult.length);
255
+ matches = URL_REGEX.exec(htmlString);
256
+ }
257
+ }
258
+ break;
259
+ }
260
+ endResult = matches[0];
261
+ index = matches.index;
262
+ if (totalLength + index > maxLength) {
263
+ truncatedContent += htmlString.substring(0, getEndPosition(htmlString, index, maxLength, totalLength, opts));
264
+ break;
265
+ }
266
+ else {
267
+ totalLength += index;
268
+ truncatedContent += htmlString.substring(0, index);
269
+ }
270
+ if (endResult[1] === '/') {
271
+ tagsStack.pop();
272
+ selfClose = null;
273
+ }
274
+ else {
275
+ selfClose = SELF_CLOSE_REGEX.exec(endResult);
276
+ if (!selfClose) {
277
+ tag = matches[1];
278
+ tagsStack.push(tag);
279
+ }
280
+ }
281
+ truncatedContent += selfClose ? selfClose[0] : endResult;
282
+ htmlString = htmlString.substring(index + endResult.length);
283
+ }
284
+ if (htmlString.length > maxLength - totalLength && opts.ellipsis) {
285
+ truncatedContent += opts.ellipsis ? '...' : '';
286
+ }
287
+ truncatedContent += closeTags(tagsStack);
288
+ if (opts.removeImageTag) {
289
+ truncatedContent = removeImageTag(truncatedContent);
290
+ }
291
+ return truncatedContent;
292
+ }
57
293
  class ExpensiMark {
58
294
  /**
59
295
  * Set the logger to use for logging inside of the ExpensiMark class
@@ -63,35 +299,14 @@ class ExpensiMark {
63
299
  ExpensiMark.Log = logger;
64
300
  }
65
301
  constructor() {
66
- this.extractVictoryChartTags = (text) => {
67
- const tags = [];
68
- const out = text.replace(VICTORY_CHART_REGEX, (match) => {
69
- const placeholder = createVictoryChartPlaceholder(tags.length);
70
- tags.push(match);
71
- return placeholder;
72
- });
73
- return { text: out, tags };
74
- };
75
- this.restoreVictoryChartTags = (text, tags) => {
76
- if (tags.length === 0) {
77
- return text;
78
- }
79
- return text.replace(VICTORY_CHART_PLACEHOLDER_PATTERN, (match, idx) => { var _a; return (_a = tags[Number(idx)]) !== null && _a !== void 0 ? _a : match; });
80
- };
81
- this.getAttributeCache = (extras) => {
82
- var _a, _b;
83
- if (!extras) {
84
- return { attrCachingFn: undefined, attrCache: undefined };
85
- }
86
- return {
87
- attrCachingFn: (_a = extras.mediaAttributeCachingFn) !== null && _a !== void 0 ? _a : extras.cacheVideoAttributes,
88
- attrCache: (_b = extras.mediaAttributeCache) !== null && _b !== void 0 ? _b : extras.videoAttributeCache,
89
- };
90
- };
91
302
  /**
92
303
  * The list of rules that we have to exclude in shouldKeepWhitespaceRules list.
93
304
  */
94
305
  this.whitespaceRulesToDisable = ['newline', 'replacepre', 'replacebr', 'replaceh1br'];
306
+ this.getAttributeCache = resolveAttributeCache;
307
+ this.replaceBlockElementWithNewLine = replaceBlockElementWithNewLine;
308
+ this.containsNonPairTag = containsNonPairTag;
309
+ this.truncateHTML = truncateHTML;
95
310
  /**
96
311
  * The list of regex replacements to do on a comment. Check the link regex is first so links are processed
97
312
  * before other delimiters
@@ -118,11 +333,11 @@ class ExpensiMark {
118
333
  // want to do this anywhere else since that would break HTML.
119
334
  // &nbsp; will create styling issues so use &#32;
120
335
  replacement: (_extras, _match, _g1, _g2, textWithinFences) => {
121
- const group = textWithinFences.replace(/(?:(?![\n\r])\s)/g, '&#32;');
336
+ const group = textWithinFences.replaceAll(/(?:(?![\n\r])\s)/g, '&#32;');
122
337
  return `<pre>${group}</pre>`;
123
338
  },
124
339
  rawInputReplacement: (_extras, _match, _g1, newLineCharacter, textWithinFences) => {
125
- const group = textWithinFences.replace(/(?:(?![\n\r])\s)/g, '&#32;').replace(/<emoji>|<\/emoji>/g, '');
340
+ const group = textWithinFences.replaceAll(/(?:(?![\n\r])\s)/g, '&#32;').replaceAll(/<emoji>|<\/emoji>/g, '');
126
341
  return `<pre>${newLineCharacter}${group}</pre>`;
127
342
  },
128
343
  },
@@ -141,12 +356,12 @@ class ExpensiMark {
141
356
  * @return Returns the HTML video tag
142
357
  */
143
358
  replacement: (extras, _match, videoName, videoSource) => {
144
- const attrCache = this.getAttributeCache(extras).attrCache;
359
+ const attrCache = resolveAttributeCache(extras).attrCache;
145
360
  const extraAttrs = attrCache && attrCache[videoSource];
146
361
  return `<video data-expensify-source="${str_1.default.sanitizeURL(videoSource)}" ${extraAttrs || ''}>${videoName ? `${videoName}` : ''}</video>`;
147
362
  },
148
363
  rawInputReplacement: (extras, _match, videoName, videoSource) => {
149
- const attrCache = this.getAttributeCache(extras).attrCache;
364
+ const attrCache = resolveAttributeCache(extras).attrCache;
150
365
  const extraAttrs = attrCache && attrCache[videoSource];
151
366
  return `<video data-expensify-source="${str_1.default.sanitizeURL(videoSource)}" data-raw-href="${videoSource}" data-link-variant="${typeof videoName === 'string' ? 'labeled' : 'auto'}" ${extraAttrs || ''}>${videoName ? `${videoName}` : ''}</video>`;
152
367
  },
@@ -210,7 +425,7 @@ class ExpensiMark {
210
425
  name: 'heading1',
211
426
  process: (textToProcess, replacement, shouldKeepRawInput = false) => {
212
427
  const regexp = shouldKeepRawInput ? /^# ( *(?! )(?:(?!<pre>|<video>|\n|\r\n).)+)/gm : /^# +(?! )((?:(?!<pre>|<video>|\n|\r\n).)+)/gm;
213
- return this.replaceTextWithExtras(textToProcess, regexp, EXTRAS_DEFAULT, replacement);
428
+ return replaceTextWithExtras(textToProcess, regexp, EXTRAS_DEFAULT, replacement);
214
429
  },
215
430
  replacement: '<h1>$1</h1>',
216
431
  },
@@ -226,18 +441,16 @@ class ExpensiMark {
226
441
  name: 'image',
227
442
  regex: MARKDOWN_IMAGE_REGEX,
228
443
  replacement: (extras, _match, imgAlt, imgSource) => {
229
- const attrCache = this.getAttributeCache(extras).attrCache;
444
+ const attrCache = resolveAttributeCache(extras).attrCache;
230
445
  const extraAttrs = attrCache && attrCache[imgSource];
231
446
  return `<img src="${str_1.default.sanitizeURL(imgSource)}"${imgAlt ? ` alt="${this.escapeAttributeContent(imgAlt)}"` : ''} ${extraAttrs || ''}/>`;
232
447
  },
233
448
  rawInputReplacement: (extras, _match, imgAlt, imgSource) => {
234
- const attrCache = this.getAttributeCache(extras).attrCache;
449
+ const attrCache = resolveAttributeCache(extras).attrCache;
235
450
  const extraAttrs = attrCache && attrCache[imgSource];
236
451
  return `<img src="${str_1.default.sanitizeURL(imgSource)}"${imgAlt ? ` alt="${this.escapeAttributeContent(imgAlt)}"` : ''} data-raw-href="${imgSource}" data-link-variant="${typeof imgAlt === 'string' ? 'labeled' : 'auto'}" ${extraAttrs || ''}/>`;
237
452
  },
238
- shouldSkipProcessing: (textToCheck) => {
239
- return !textToCheck.includes('!') || !textToCheck.includes('(') || !textToCheck.includes(')');
240
- },
453
+ shouldSkipProcessing: (textToCheck) => !textToCheck.includes('!') || !textToCheck.includes('(') || !textToCheck.includes(')'),
241
454
  },
242
455
  /**
243
456
  * Converts markdown style links to anchor tags e.g. [Expensify](https://www.expensify.com)
@@ -259,9 +472,7 @@ class ExpensiMark {
259
472
  }
260
473
  return `<a href="${str_1.default.sanitizeURL(g2)}" data-raw-href="${g2}" data-link-variant="labeled" target="_blank" rel="noreferrer noopener">${g1}</a>`;
261
474
  },
262
- shouldSkipProcessing: (textToCheck) => {
263
- return !textToCheck.includes('.');
264
- },
475
+ shouldSkipProcessing: (textToCheck) => !textToCheck.includes('.'),
265
476
  },
266
477
  /**
267
478
  * Apply the hereMention first because the string @here is still a valid mention for the userMention regex.
@@ -346,9 +557,7 @@ class ExpensiMark {
346
557
  const href = str_1.default.sanitizeURL(g2);
347
558
  return `${g1}<a href="${href}" data-raw-href="${g2}" data-link-variant="auto" target="_blank" rel="noreferrer noopener">${g2}</a>${g1}`;
348
559
  },
349
- shouldSkipProcessing: (textToCheck) => {
350
- return !textToCheck.includes('.');
351
- },
560
+ shouldSkipProcessing: (textToCheck) => !textToCheck.includes('.'),
352
561
  },
353
562
  {
354
563
  name: 'quote',
@@ -357,7 +566,7 @@ class ExpensiMark {
357
566
  // inline code blocks. A single prepending space should be stripped if it exists
358
567
  process: (textToProcess, replacement, shouldKeepRawInput = false) => {
359
568
  const regex = /^(?:&gt;|>)+ +(?! )(?![^<]*(?:<\/pre>|<\/code>|<\/video>))([^\v\n\r]*)/gm;
360
- let replacedText = this.replaceTextWithExtras(textToProcess, regex, EXTRAS_DEFAULT, replacement);
569
+ let replacedText = replaceTextWithExtras(textToProcess, regex, EXTRAS_DEFAULT, replacement);
361
570
  if (shouldKeepRawInput) {
362
571
  return replacedText;
363
572
  }
@@ -390,7 +599,7 @@ class ExpensiMark {
390
599
  return html;
391
600
  }
392
601
  // If any tags are included inside underscores, ignore it. ie. _abc <pre>pre tag</pre> abc_
393
- if (textWithinUnderscores.includes('</pre>') || this.containsNonPairTag(textWithinUnderscores)) {
602
+ if (textWithinUnderscores.includes('</pre>') || containsNonPairTag(textWithinUnderscores)) {
394
603
  return match;
395
604
  }
396
605
  if (String(textWithinUnderscores).match(`^${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}`)) {
@@ -449,13 +658,13 @@ class ExpensiMark {
449
658
  if (g1.includes('_')) {
450
659
  return `${g1}<strong>${g2}</strong>`;
451
660
  }
452
- return g2.includes('</pre>') || this.containsNonPairTag(g2) ? match : `<strong>${g2}</strong>`;
661
+ return g2.includes('</pre>') || containsNonPairTag(g2) ? match : `<strong>${g2}</strong>`;
453
662
  },
454
663
  },
455
664
  {
456
665
  name: 'strikethrough',
457
666
  regex: /(?<!<[^>]*)\B~((?![\s~])[\s\S]*?[^\s~](?<!\s))~\B(?![^<]*>)(?![^<]*(<\/pre>|<\/code>|<\/a>|<\/video>))/g,
458
- replacement: (_extras, match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<del>${g1}</del>`),
667
+ replacement: (_extras, match, g1) => (g1.includes('</pre>') || containsNonPairTag(g1) ? match : `<del>${g1}</del>`),
459
668
  },
460
669
  {
461
670
  name: 'newline',
@@ -503,7 +712,7 @@ class ExpensiMark {
503
712
  pre: (inputString) => inputString
504
713
  .replace('<br></br>', '<br/>')
505
714
  .replace('<br><br/>', '<br/>')
506
- .replace(/(<tr.*?<\/tr>)/g, '$1<br/>')
715
+ .replaceAll(/(<tr.*?<\/tr>)/g, '$1<br/>')
507
716
  .replace('<br/></tbody>', '')
508
717
  .replace(SLACK_SPAN_NEW_LINE_TAG + SLACK_SPAN_NEW_LINE_TAG, '<br/><br/><br/>')
509
718
  .replace(SLACK_SPAN_NEW_LINE_TAG, '<br/><br/>'),
@@ -533,7 +742,11 @@ class ExpensiMark {
533
742
  replacement: (extras, match, tagName, innerContent) => {
534
743
  // To check if style attribute contains bold font-weight
535
744
  const isBoldFromStyle = (style) => {
536
- return style ? style.replace(/\s/g, '').includes('font-weight:bold;') || style.replace(/\s/g, '').includes('font-weight:700;') : false;
745
+ if (!style) {
746
+ return false;
747
+ }
748
+ const normalizedStyle = style.replaceAll(/\s/g, '');
749
+ return normalizedStyle.includes('font-weight:bold;') || normalizedStyle.includes('font-weight:700;');
537
750
  };
538
751
  const updateSpacesAndWrapWithAsterisksIfBold = (content, isBold) => {
539
752
  const trimmedContent = content.trim();
@@ -547,7 +760,7 @@ class ExpensiMark {
547
760
  const isFontWeightBold = isBoldFromStyle(styleAttributeMatch ? styleAttributeMatch[1] : null);
548
761
  const isBold = styleAttributeMatch ? isFontWeightBold : tagName === 'b' || tagName === 'strong';
549
762
  // Process nested spans with potential bold style
550
- const processedInnerContent = innerContent.replace(/<span(?:"[^"]*"|'[^']*'|[^'">])*>([\s\S]*?)<\/span>/gi, (nestedMatch, nestedContent) => {
763
+ const processedInnerContent = innerContent.replaceAll(/<span(?:"[^"]*"|'[^']*'|[^'">])*>([\s\S]*?)<\/span>/gi, (nestedMatch, nestedContent) => {
551
764
  const nestedStyleMatch = nestedMatch.match(fontWeightRegex);
552
765
  const isNestedBold = isBoldFromStyle(nestedStyleMatch ? nestedStyleMatch[1] : null);
553
766
  return updateSpacesAndWrapWithAsterisksIfBold(nestedContent, isNestedBold);
@@ -566,15 +779,13 @@ class ExpensiMark {
566
779
  replacement: (_extras, _match, _g1, g2) => {
567
780
  // We remove the line break before heading inside quote to avoid adding extra line
568
781
  let resultString = g2
569
- .replace(/\n?(<h1># )/g, '$1')
570
- .replace(/(<h1>|<\/h1>)+/g, '\n')
782
+ .replaceAll(/\n?(<h1># )/g, '$1')
783
+ .replaceAll(/(<h1>|<\/h1>)+/g, '\n')
571
784
  // Replace trim() with manually removing line breaks at the beginning and end of the string to avoid adding extra lines
572
- .replace(/^(\n)+|(\n)+$/g, '')
785
+ .replaceAll(/^(\n)+|(\n)+$/g, '')
573
786
  .split('\n');
574
787
  // Wrap each string in the array with <blockquote> and </blockquote>
575
- resultString = resultString.map((line) => {
576
- return `<blockquote>${line}</blockquote>`;
577
- });
788
+ resultString = resultString.map((line) => `<blockquote>${line}</blockquote>`);
578
789
  resultString = resultString
579
790
  .map((text) => {
580
791
  let modifiedText = text;
@@ -583,8 +794,8 @@ class ExpensiMark {
583
794
  depth = (modifiedText.match(/<blockquote>/gi) || []).length;
584
795
  // Need (\s)? because the server usually sends a space character after <blockquote> so we need to consume it,
585
796
  // avoid being redundant because it is added in the return part
586
- modifiedText = modifiedText.replace(/<blockquote>(\s)?/gi, '');
587
- modifiedText = modifiedText.replace(/<\/blockquote>/gi, '');
797
+ modifiedText = modifiedText.replaceAll(/<blockquote>(\s)?/gi, '');
798
+ modifiedText = modifiedText.replaceAll(/<\/blockquote>/gi, '');
588
799
  } while (/<blockquote>/i.test(modifiedText));
589
800
  return `${'>'.repeat(depth)} ${modifiedText}`;
590
801
  })
@@ -630,7 +841,7 @@ class ExpensiMark {
630
841
  let altText = '';
631
842
  const altRegex = /alt\s*=\s*(['"])(.*?)\1/i;
632
843
  const altMatch = imgAttrs.match(altRegex);
633
- const attrCachingFn = this.getAttributeCache(extras).attrCachingFn;
844
+ const attrCachingFn = resolveAttributeCache(extras).attrCachingFn;
634
845
  let attributes = imgAttrs;
635
846
  if (altMatch) {
636
847
  altText = altMatch[2];
@@ -663,7 +874,7 @@ class ExpensiMark {
663
874
  * @returns The markdown video tag
664
875
  */
665
876
  replacement: (extras, _match, _g1, videoSource, videoAttrs, videoName) => {
666
- const attrCachingFn = this.getAttributeCache(extras).attrCachingFn;
877
+ const attrCachingFn = resolveAttributeCache(extras).attrCachingFn;
667
878
  if (videoAttrs && attrCachingFn && typeof attrCachingFn === 'function') {
668
879
  attrCachingFn(videoSource, videoAttrs);
669
880
  }
@@ -676,10 +887,12 @@ class ExpensiMark {
676
887
  {
677
888
  name: 'reportMentions',
678
889
  regex: /<mention-report reportID="?(\d+)"?(?: *\/>|><\/mention-report>)/gi,
679
- replacement: (extras, _match, g1, _offset, _string) => {
890
+ replacement: (extras, _match, g1) => {
680
891
  const reportToNameMap = extras.reportIDToName;
681
892
  if (!reportToNameMap || !reportToNameMap[g1]) {
682
- ExpensiMark.Log.alert('[ExpensiMark] Missing report name', { reportID: g1 });
893
+ ExpensiMark.Log.alert('[ExpensiMark] Missing report name', {
894
+ reportID: g1,
895
+ });
683
896
  return '#Hidden';
684
897
  }
685
898
  return reportToNameMap[g1];
@@ -688,12 +901,14 @@ class ExpensiMark {
688
901
  {
689
902
  name: 'userMention',
690
903
  regex: /(?:<mention-user accountID="?(\d+)"?(?: *\/>|><\/mention-user>))|(?:<mention-user>(.*?)<\/mention-user>)/gi,
691
- replacement: (extras, _match, g1, g2, _offset, _string) => {
904
+ replacement: (extras, _match, g1, g2) => {
692
905
  var _a, _b;
693
906
  if (g1) {
694
907
  const accountToNameMap = extras.accountIDToName;
695
908
  if (!accountToNameMap || !accountToNameMap[g1]) {
696
- ExpensiMark.Log.alert('[ExpensiMark] Missing account name', { accountID: g1 });
909
+ ExpensiMark.Log.alert('[ExpensiMark] Missing account name', {
910
+ accountID: g1,
911
+ });
697
912
  return '@Hidden';
698
913
  }
699
914
  return `@${str_1.default.removeSMSDomain((_b = (_a = extras.accountIDToName) === null || _a === void 0 ? void 0 : _a[g1]) !== null && _b !== void 0 ? _b : '')}`;
@@ -760,10 +975,12 @@ class ExpensiMark {
760
975
  {
761
976
  name: 'reportMentions',
762
977
  regex: /<mention-report reportID="?(\d+)"?(?: *\/>|><\/mention-report>)/gi,
763
- replacement: (extras, _match, g1, _offset, _string) => {
978
+ replacement: (extras, _match, g1) => {
764
979
  const reportToNameMap = extras.reportIDToName;
765
980
  if (!reportToNameMap || !reportToNameMap[g1]) {
766
- ExpensiMark.Log.alert('[ExpensiMark] Missing report name', { reportID: g1 });
981
+ ExpensiMark.Log.alert('[ExpensiMark] Missing report name', {
982
+ reportID: g1,
983
+ });
767
984
  return '#Hidden';
768
985
  }
769
986
  return reportToNameMap[g1];
@@ -772,12 +989,14 @@ class ExpensiMark {
772
989
  {
773
990
  name: 'userMention',
774
991
  regex: /(?:<mention-user accountID="?(\d+)"?(?: *\/>|><\/mention-user>))|(?:<mention-user>(.*?)<\/mention-user>)/gi,
775
- replacement: (extras, _match, g1, g2, _offset, _string) => {
992
+ replacement: (extras, _match, g1, g2) => {
776
993
  var _a, _b;
777
994
  if (g1) {
778
995
  const accountToNameMap = extras.accountIDToName;
779
996
  if (!accountToNameMap || !accountToNameMap[g1]) {
780
- ExpensiMark.Log.alert('[ExpensiMark] Missing account name', { accountID: g1 });
997
+ ExpensiMark.Log.alert('[ExpensiMark] Missing account name', {
998
+ accountID: g1,
999
+ });
781
1000
  return '@Hidden';
782
1001
  }
783
1002
  return `@${str_1.default.removeSMSDomain((_b = (_a = extras.accountIDToName) === null || _a === void 0 ? void 0 : _a[g1]) !== null && _b !== void 0 ? _b : '')}`;
@@ -853,7 +1072,7 @@ class ExpensiMark {
853
1072
  }
854
1073
  // Extract VictoryChart blocks to preserve their markup during processing.
855
1074
  // Only safe for trusted server input - user input must be escaped to prevent XSS.
856
- const { text: textWithPlaceholders, tags: victoryChartTags } = shouldEscapeText ? { text, tags: [] } : this.extractVictoryChartTags(text);
1075
+ const { text: textWithPlaceholders, tags: victoryChartTags } = shouldEscapeText ? { text, tags: [] } : extractVictoryChartTags(text);
857
1076
  // This ensures that any html the user puts into the comment field shows as raw html
858
1077
  let replacedText = shouldEscapeText ? Utils.escapeText(textWithPlaceholders) : textWithPlaceholders;
859
1078
  const rules = this.getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput);
@@ -870,7 +1089,7 @@ class ExpensiMark {
870
1089
  replacedText = rule.process(replacedText, replacement, shouldKeepRawInput);
871
1090
  }
872
1091
  else {
873
- replacedText = this.replaceTextWithExtras(replacedText, rule.regex, extras, replacement);
1092
+ replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, replacement);
874
1093
  }
875
1094
  // Post-process text after applying regex
876
1095
  if (rule.post) {
@@ -878,14 +1097,16 @@ class ExpensiMark {
878
1097
  }
879
1098
  };
880
1099
  try {
881
- rules.forEach(processRule);
1100
+ for (const rule of rules) {
1101
+ processRule(rule);
1102
+ }
882
1103
  }
883
1104
  catch (e) {
884
1105
  ExpensiMark.Log.alert('Error replacing text with html in ExpensiMark.replace', { error: e });
885
1106
  // We want to return text without applying rules if exception occurs during replacing
886
1107
  return shouldEscapeText ? Utils.escapeText(text) : text;
887
1108
  }
888
- return this.restoreVictoryChartTags(replacedText, victoryChartTags);
1109
+ return restoreVictoryChartTags(replacedText, victoryChartTags);
889
1110
  }
890
1111
  /**
891
1112
  * Checks matched URLs for validity and replace valid links with html elements
@@ -1003,7 +1224,10 @@ class ExpensiMark {
1003
1224
  startIndex = match.index + match[0].length;
1004
1225
  // Line breaks (`\n`) followed by empty contents are already removed
1005
1226
  // but line breaks inside contents should be parsed to <br/> to skip `autoEmail` rule
1006
- replacedText = this.replace(replacedText, { filterRules: ['newline'], shouldEscapeText: false });
1227
+ replacedText = this.replace(replacedText, {
1228
+ filterRules: ['newline'],
1229
+ shouldEscapeText: false,
1230
+ });
1007
1231
  // Now we move to the next match that the js regex found in the text
1008
1232
  match = regex.exec(textToCheck);
1009
1233
  }
@@ -1012,43 +1236,6 @@ class ExpensiMark {
1012
1236
  }
1013
1237
  return replacedText;
1014
1238
  }
1015
- /**
1016
- * replace block element with '\n' if :
1017
- * 1. We have text within the element.
1018
- * 2. The text does not end with a new line.
1019
- * 3. It's not the last element in the string.
1020
- */
1021
- replaceBlockElementWithNewLine(htmlString) {
1022
- // eslint-disable-next-line max-len
1023
- let splitText = htmlString
1024
- // Lines starting with quote mark '>' will have '\n' added to them so to avoid adding extra '\n', remove the block element right next to it
1025
- .replaceAll(/<blockquote>> (<div.*?>|<\/div>|<comment.*?>|\n<\/comment>|<\/comment>|<h1>|<\/h1>|<h2>|<\/h2>|<h3>|<\/h3>|<h4>|<\/h4>|<h5>|<\/h5>|<h6>|<\/h6>|<p>|<\/p>|<li>|<\/li>)/gi, '<blockquote>> ')
1026
- .split(/<div.*?>|<\/div>|<comment.*?>|\n<\/comment>|<\/comment>|<h1>|<\/h1>|<h2>|<\/h2>|<h3>|<\/h3>|<h4>|<\/h4>|<h5>|<\/h5>|<h6>|<\/h6>|<p>|<\/p>|<li>|<\/li>|<blockquote>|<\/blockquote>/);
1027
- const stripHTML = (text) => str_1.default.stripHTML(text);
1028
- splitText = splitText.map(stripHTML);
1029
- let joinedText = '';
1030
- // Delete whitespace at the end
1031
- while (splitText.length) {
1032
- if (splitText[splitText.length - 1].trim().length > 0 || splitText[splitText.length - 1].match(/\n/)) {
1033
- break;
1034
- }
1035
- splitText.pop();
1036
- }
1037
- const processText = (text, index) => {
1038
- if (text.trim().length === 0 && !text.match(/\n/)) {
1039
- return;
1040
- }
1041
- // Insert '\n' unless it ends with '\n' or it's the last element, or if it's a header ('# ') with a space.
1042
- if (text.match(/\n[\s]?$/) || index === splitText.length - 1 || text === '# ') {
1043
- joinedText += text;
1044
- }
1045
- else {
1046
- joinedText += `${text}\n`;
1047
- }
1048
- };
1049
- splitText.forEach(processText);
1050
- return joinedText;
1051
- }
1052
1239
  /**
1053
1240
  * Unpacks nested quote HTML tags that have been packed by the 'quote' rule in this.rules for shouldKeepRawInput = false
1054
1241
  *
@@ -1069,10 +1256,8 @@ class ExpensiMark {
1069
1256
  * Note that there will always be only a single closing tag, even if multiple opening tags exist.
1070
1257
  * Only one closing tag is needed to detect if a nested quote has ended.
1071
1258
  */
1072
- unpackNestedQuotes(text) {
1073
- let parsedText = text.replace(/((<\/blockquote>)+(<br \/>)?)|(<br \/>)/g, (match) => {
1074
- return `${match}</split>`;
1075
- });
1259
+ static unpackNestedQuotes(text) {
1260
+ let parsedText = text.replaceAll(/((<\/blockquote>)+(<br \/>)?)|(<br \/>)/g, (match) => `${match}</split>`);
1076
1261
  const splittedText = parsedText.split('</split>');
1077
1262
  if (splittedText.length > 0 && splittedText[splittedText.length - 1] === '') {
1078
1263
  splittedText.pop();
@@ -1084,7 +1269,7 @@ class ExpensiMark {
1084
1269
  if (line === '' && count === 0) {
1085
1270
  return '';
1086
1271
  }
1087
- const textLine = line.replace(/(<br \/>)$/g, '');
1272
+ const textLine = line.replaceAll(/(<br \/>)$/g, '');
1088
1273
  if (textLine.startsWith('<blockquote>')) {
1089
1274
  count += (textLine.match(/<blockquote>/g) || []).length;
1090
1275
  }
@@ -1116,20 +1301,22 @@ class ExpensiMark {
1116
1301
  if (parseBodyTag) {
1117
1302
  generatedMarkdown = parseBodyTag[2];
1118
1303
  }
1119
- generatedMarkdown = this.unpackNestedQuotes(generatedMarkdown);
1304
+ generatedMarkdown = ExpensiMark.unpackNestedQuotes(generatedMarkdown);
1120
1305
  // Extract VictoryChart blocks before HTML stripping, then restore them.
1121
- const { text: textWithPlaceholders, tags: victoryChartTags } = this.extractVictoryChartTags(generatedMarkdown);
1306
+ const { text: textWithPlaceholders, tags: victoryChartTags } = extractVictoryChartTags(generatedMarkdown);
1122
1307
  generatedMarkdown = textWithPlaceholders;
1123
1308
  const processRule = (rule) => {
1124
1309
  // Pre-processes input HTML before applying regex
1125
1310
  if (rule.pre) {
1126
1311
  generatedMarkdown = rule.pre(generatedMarkdown);
1127
1312
  }
1128
- generatedMarkdown = this.replaceTextWithExtras(generatedMarkdown, rule.regex, extras, rule.replacement);
1313
+ generatedMarkdown = replaceTextWithExtras(generatedMarkdown, rule.regex, extras, rule.replacement);
1129
1314
  };
1130
- this.htmlToMarkdownRules.forEach(processRule);
1131
- const decoded = str_1.default.htmlDecode(this.replaceBlockElementWithNewLine(generatedMarkdown));
1132
- return this.restoreVictoryChartTags(decoded, victoryChartTags);
1315
+ for (const rule of this.htmlToMarkdownRules) {
1316
+ processRule(rule);
1317
+ }
1318
+ const decoded = str_1.default.htmlDecode(replaceBlockElementWithNewLine(generatedMarkdown));
1319
+ return restoreVictoryChartTags(decoded, victoryChartTags);
1133
1320
  }
1134
1321
  /**
1135
1322
  * Convert HTML to text
@@ -1137,9 +1324,11 @@ class ExpensiMark {
1137
1324
  htmlToText(htmlString, extras = EXTRAS_DEFAULT) {
1138
1325
  let replacedText = htmlString;
1139
1326
  const processRule = (rule) => {
1140
- replacedText = this.replaceTextWithExtras(replacedText, rule.regex, extras, rule.replacement);
1327
+ replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, rule.replacement);
1141
1328
  };
1142
- this.htmlToTextRules.forEach(processRule);
1329
+ for (const rule of this.htmlToTextRules) {
1330
+ processRule(rule);
1331
+ }
1143
1332
  // Unescaping because the text is escaped in 'replace' function
1144
1333
  // We use 'htmlDecode' instead of 'unescape' to replace entities like '&#32;'
1145
1334
  replacedText = str_1.default.htmlDecode(replacedText);
@@ -1156,7 +1345,7 @@ class ExpensiMark {
1156
1345
  isStartingWithSpace = !!g2;
1157
1346
  return '';
1158
1347
  };
1159
- const textToReplace = text.replace(/^(?:&gt;|>)( )?/gm, handleMatch);
1348
+ const textToReplace = text.replaceAll(/^(?:&gt;|>)( )?/gm, handleMatch);
1160
1349
  const filterRules = ['heading1'];
1161
1350
  // If we don't reach the max quote depth, we allow the recursive call to process other possible quotes
1162
1351
  if (this.currentQuoteDepth < this.maxQuoteDepth - 1 && !isStartingWithSpace) {
@@ -1171,36 +1360,6 @@ class ExpensiMark {
1171
1360
  this.currentQuoteDepth = 0;
1172
1361
  return { replacedText, shouldAddSpace: isStartingWithSpace };
1173
1362
  }
1174
- /**
1175
- * Check if the input text includes only the open or the close tag of an element.
1176
- */
1177
- containsNonPairTag(textToCheck) {
1178
- // Create a regular expression to match HTML tags
1179
- const tagRegExp = /<([a-z][a-z0-9-]*)\b[^>]*>|<\/([a-z][a-z0-9-]*)\s*>/gi;
1180
- // Use a stack to keep track of opening tags
1181
- const tagStack = [];
1182
- // Match all HTML tags in the string
1183
- let match = tagRegExp.exec(textToCheck);
1184
- while (match) {
1185
- const openingTag = match[1];
1186
- const closingTag = match[2];
1187
- if (openingTag && openingTag !== 'br') {
1188
- // If it's an opening tag, push it onto the stack
1189
- tagStack.push(openingTag);
1190
- }
1191
- else if (closingTag) {
1192
- // If it's a closing tag, pop the top of the stack
1193
- const expectedTag = tagStack.pop();
1194
- // If the closing tag doesn't match the expected opening tag, return false
1195
- if (closingTag !== expectedTag) {
1196
- return true;
1197
- }
1198
- }
1199
- match = tagRegExp.exec(textToCheck);
1200
- }
1201
- // If there are any tags left in the stack, they're unclosed
1202
- return tagStack.length !== 0;
1203
- }
1204
1363
  /**
1205
1364
  * @returns array or undefined if exception occurs when executing regex matching
1206
1365
  */
@@ -1243,187 +1402,6 @@ class ExpensiMark {
1243
1402
  originalContent = str_1.default.replaceAll(originalContent, '\n', '');
1244
1403
  return Utils.escapeText(originalContent);
1245
1404
  }
1246
- /**
1247
- * Determines the end position to truncate the HTML content while considering word boundaries.
1248
- *
1249
- * @param {string} content - The HTML content to be truncated.
1250
- * @param {number} tailPosition - The position up to which the content should be considered.
1251
- * @param {number} maxLength - The maximum length of the truncated content.
1252
- * @param {number} totalLength - The length of the content processed so far.
1253
- * @param {Object} opts - Options to customize the truncation.
1254
- * @returns {number} The calculated position to truncate the content.
1255
- */
1256
- getEndPosition(content, tailPosition, maxLength, totalLength, opts) {
1257
- const WORD_BREAK_REGEX = /\W+/g;
1258
- // Calculate the default position to truncate based on the maximum length and the length of the content processed so far
1259
- const defaultPosition = maxLength - totalLength;
1260
- // Define the slop value, which determines the tolerance for cutting off content near the maximum length
1261
- const slop = opts.slop;
1262
- if (!slop)
1263
- return defaultPosition;
1264
- // Initialize the position to the default position
1265
- let position = defaultPosition;
1266
- // Determine if the default position is considered "short" based on the slop value
1267
- const isShort = defaultPosition < slop;
1268
- // Calculate the position within the slop range
1269
- const slopPos = isShort ? defaultPosition : slop - 1;
1270
- // Extract the substring to analyze for word boundaries, considering the slop and tail position
1271
- const substr = content.slice(isShort ? 0 : defaultPosition - slop, tailPosition !== undefined ? tailPosition : defaultPosition + slop);
1272
- // Find the first word boundary within the substring
1273
- const wordBreakMatch = WORD_BREAK_REGEX.exec(substr);
1274
- // Adjust the position to avoid truncating in the middle of a word if the option is enabled
1275
- if (!opts.truncateLastWord) {
1276
- if (tailPosition && substr.length <= tailPosition) {
1277
- // If tail position is defined and the substring length is within the tail position, set position to the substring length
1278
- position = substr.length;
1279
- }
1280
- else {
1281
- // Iterate through word boundary matches to adjust the position
1282
- while (wordBreakMatch !== null) {
1283
- if (wordBreakMatch.index < slopPos) {
1284
- // If the word boundary is before the slop position, adjust position backward
1285
- position = defaultPosition - (slopPos - wordBreakMatch.index);
1286
- if (wordBreakMatch.index === 0 && defaultPosition <= 1) {
1287
- break;
1288
- }
1289
- }
1290
- else if (wordBreakMatch.index === slopPos) {
1291
- // If the word boundary is at the slop position, set position to the default position
1292
- position = defaultPosition;
1293
- break;
1294
- }
1295
- else {
1296
- // If the word boundary is after the slop position, adjust position forward
1297
- position = defaultPosition + (wordBreakMatch.index - slopPos);
1298
- break;
1299
- }
1300
- }
1301
- }
1302
- // If the character at the determined position is a whitespace, adjust position backward
1303
- if (content.charAt(position - 1).match(/\s$/)) {
1304
- position--;
1305
- }
1306
- }
1307
- // Return the calculated position to truncate the content
1308
- return position;
1309
- }
1310
- /**
1311
- * Truncate HTML string and keep tag safe.
1312
- * pulled from https://github.com/huang47/nodejs-html-truncate/blob/master/lib/truncate.js
1313
- *
1314
- * @param {string} html - The string that needs to be truncated
1315
- * @param {number} maxLength - Length of truncated string
1316
- * @param {Object} [options] - Optional configuration options
1317
- * @returns {string} The truncated string
1318
- */
1319
- truncateHTML(html, maxLength, options) {
1320
- const EMPTY_STRING = '';
1321
- const DEFAULT_TRUNCATE_SYMBOL = '...';
1322
- const DEFAULT_SLOP = Math.min(10, maxLength);
1323
- const tagsStack = [];
1324
- const KEY_VALUE_REGEX = '((?:\\s+(?:\\w+|-)+(?:\\s*=\\s*(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|[^\'">\\s]+))?)*)';
1325
- const IS_CLOSE_REGEX = '\\s*\\/?\\s*';
1326
- const CLOSE_REGEX = '\\s*\\/\\s*';
1327
- const SELF_CLOSE_REGEX = new RegExp(`<\\/?(\\w+)${KEY_VALUE_REGEX}${CLOSE_REGEX}>`);
1328
- const HTML_TAG_REGEX = new RegExp(`<\\/?(\\w+)${KEY_VALUE_REGEX}${IS_CLOSE_REGEX}>`);
1329
- const URL_REGEX = /(((ftp|https?):\/\/)[\\-\w@:%_\\+.~#?,&\\/\\/=]+)|((mailto:)?[_.\w\\-]+@([\w][\w\\-]+\.)+[a-zA-Z]{2,3})/g;
1330
- const IMAGE_TAG_REGEX = new RegExp(`<img\\s*${KEY_VALUE_REGEX}${CLOSE_REGEX}>`);
1331
- let truncatedContent = EMPTY_STRING;
1332
- let totalLength = 0;
1333
- let matches = HTML_TAG_REGEX.exec(html);
1334
- let endResult;
1335
- let index;
1336
- let tag;
1337
- let selfClose = null;
1338
- let htmlString = html;
1339
- const opts = Object.assign({ ellipsis: DEFAULT_TRUNCATE_SYMBOL, truncateLastWord: true, slop: DEFAULT_SLOP }, options);
1340
- function removeImageTag(content) {
1341
- const match = IMAGE_TAG_REGEX.exec(content);
1342
- if (!match) {
1343
- return content;
1344
- }
1345
- const matchIndex = match.index;
1346
- const matchLength = match[0].length;
1347
- return content.substring(0, matchIndex) + content.substring(matchIndex + matchLength);
1348
- }
1349
- function closeTags(tags) {
1350
- return tags
1351
- .reverse()
1352
- .map((mappedTag) => {
1353
- return `</${mappedTag}>`;
1354
- })
1355
- .join('');
1356
- }
1357
- while (matches) {
1358
- matches = HTML_TAG_REGEX.exec(htmlString);
1359
- if (!matches) {
1360
- if (totalLength >= maxLength) {
1361
- break;
1362
- }
1363
- matches = URL_REGEX.exec(htmlString);
1364
- if (!matches || matches.index >= maxLength) {
1365
- truncatedContent += htmlString.substring(0, this.getEndPosition(htmlString, undefined, maxLength, totalLength, opts));
1366
- break;
1367
- }
1368
- while (matches) {
1369
- endResult = matches[0];
1370
- if (endResult !== null) {
1371
- index = matches.index;
1372
- truncatedContent += htmlString.substring(0, index + endResult.length - totalLength);
1373
- htmlString = htmlString.substring(index + endResult.length);
1374
- matches = URL_REGEX.exec(htmlString);
1375
- }
1376
- }
1377
- break;
1378
- }
1379
- endResult = matches[0];
1380
- index = matches.index;
1381
- if (totalLength + index > maxLength) {
1382
- truncatedContent += htmlString.substring(0, this.getEndPosition(htmlString, index, maxLength, totalLength, opts));
1383
- break;
1384
- }
1385
- else {
1386
- totalLength += index;
1387
- truncatedContent += htmlString.substring(0, index);
1388
- }
1389
- if (endResult[1] === '/') {
1390
- tagsStack.pop();
1391
- selfClose = null;
1392
- }
1393
- else {
1394
- selfClose = SELF_CLOSE_REGEX.exec(endResult);
1395
- if (!selfClose) {
1396
- tag = matches[1];
1397
- tagsStack.push(tag);
1398
- }
1399
- }
1400
- truncatedContent += selfClose ? selfClose[0] : endResult;
1401
- htmlString = htmlString.substring(index + endResult.length); // Update htmlString
1402
- }
1403
- if (htmlString.length > maxLength - totalLength && opts.ellipsis) {
1404
- truncatedContent += opts.ellipsis ? '...' : '';
1405
- }
1406
- truncatedContent += closeTags(tagsStack);
1407
- if (opts.removeImageTag) {
1408
- truncatedContent = removeImageTag(truncatedContent);
1409
- }
1410
- return truncatedContent;
1411
- }
1412
- /**
1413
- * Replaces text with a replacement based on a regex
1414
- * @param text - The text to replace
1415
- * @param regexp - The regex to match
1416
- * @param extras - The extras object
1417
- * @param replacement - The replacement string or function
1418
- * @returns The replaced text
1419
- */
1420
- replaceTextWithExtras(text, regexp, extras, replacement) {
1421
- if (typeof replacement === 'function') {
1422
- // if the replacement is a function, we pass the extras object to it
1423
- return text.replace(regexp, (...args) => replacement(extras, ...args));
1424
- }
1425
- return text.replace(regexp, replacement);
1426
- }
1427
1405
  }
1428
1406
  ExpensiMark.Log = new Logger_1.default({
1429
1407
  serverLoggingCallback: () => undefined,