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.
- package/dist/API.js +19 -24
- package/dist/APIDeferred.js +5 -5
- package/dist/CONST.d.ts +3 -0
- package/dist/CONST.js +4 -1
- package/dist/Cookie.d.ts +5 -5
- package/dist/Cookie.js +6 -6
- package/dist/ExpenseRule.d.ts +10 -10
- package/dist/ExpenseRule.js +9 -9
- package/dist/ExpensiMark.d.ts +23 -51
- package/dist/ExpensiMark.js +311 -333
- package/dist/Func.js +3 -1
- package/dist/Log.js +1 -1
- package/dist/Logger.d.ts +2 -2
- package/dist/Logger.js +6 -4
- package/dist/Network.js +10 -10
- package/dist/Num.d.ts +1 -1
- package/dist/Num.js +9 -5
- package/dist/PageEvent.d.ts +2 -2
- package/dist/PageEvent.js +1 -1
- package/dist/PubSub.js +7 -7
- package/dist/ReportHistoryStore.d.ts +8 -71
- package/dist/ReportHistoryStore.js +106 -180
- package/dist/Templates.d.ts +13 -13
- package/dist/Templates.js +157 -183
- package/dist/components/StepProgressBar.d.ts +8 -4
- package/dist/components/StepProgressBar.js +4 -3
- package/dist/components/form/element/combobox.d.ts +1 -8
- package/dist/components/form/element/combobox.js +37 -37
- package/dist/components/form/element/switch.d.ts +2 -2
- package/dist/components/form/element/switch.js +7 -5
- package/dist/fastMerge.js +0 -2
- package/dist/index.js +0 -1
- package/dist/jquery.expensifyIframify.d.ts +1 -2
- package/dist/jquery.expensifyIframify.js +13 -15
- package/dist/md5.js +30 -29
- package/dist/mixins/PubSub.js +2 -2
- package/dist/str.js +19 -19
- package/dist/utils.d.ts +4 -4
- package/dist/utils.js +6 -6
- package/package.json +11 -10
package/dist/ExpensiMark.js
CHANGED
|
@@ -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
|
// will create styling issues so use  
|
|
120
335
|
replacement: (_extras, _match, _g1, _g2, textWithinFences) => {
|
|
121
|
-
const group = textWithinFences.
|
|
336
|
+
const group = textWithinFences.replaceAll(/(?:(?![\n\r])\s)/g, ' ');
|
|
122
337
|
return `<pre>${group}</pre>`;
|
|
123
338
|
},
|
|
124
339
|
rawInputReplacement: (_extras, _match, _g1, newLineCharacter, textWithinFences) => {
|
|
125
|
-
const group = textWithinFences.
|
|
340
|
+
const group = textWithinFences.replaceAll(/(?:(?![\n\r])\s)/g, ' ').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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 = /^(?:>|>)+ +(?! )(?![^<]*(?:<\/pre>|<\/code>|<\/video>))([^\v\n\r]*)/gm;
|
|
360
|
-
let replacedText =
|
|
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>') ||
|
|
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>') ||
|
|
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>') ||
|
|
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
|
-
.
|
|
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
|
-
|
|
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.
|
|
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
|
-
.
|
|
570
|
-
.
|
|
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
|
-
.
|
|
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.
|
|
587
|
-
modifiedText = modifiedText.
|
|
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 =
|
|
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 =
|
|
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
|
|
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', {
|
|
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
|
|
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', {
|
|
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
|
|
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', {
|
|
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
|
|
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', {
|
|
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: [] } :
|
|
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 =
|
|
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
|
|
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
|
|
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, {
|
|
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.
|
|
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.
|
|
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 =
|
|
1304
|
+
generatedMarkdown = ExpensiMark.unpackNestedQuotes(generatedMarkdown);
|
|
1120
1305
|
// Extract VictoryChart blocks before HTML stripping, then restore them.
|
|
1121
|
-
const { text: textWithPlaceholders, tags: victoryChartTags } =
|
|
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 =
|
|
1313
|
+
generatedMarkdown = replaceTextWithExtras(generatedMarkdown, rule.regex, extras, rule.replacement);
|
|
1129
1314
|
};
|
|
1130
|
-
this.htmlToMarkdownRules
|
|
1131
|
-
|
|
1132
|
-
|
|
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 =
|
|
1327
|
+
replacedText = replaceTextWithExtras(replacedText, rule.regex, extras, rule.replacement);
|
|
1141
1328
|
};
|
|
1142
|
-
this.htmlToTextRules
|
|
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 ' '
|
|
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.
|
|
1348
|
+
const textToReplace = text.replaceAll(/^(?:>|>)( )?/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,
|