expensify-common 2.0.42 → 2.0.44
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/CONST.d.ts +0 -6
- package/dist/CONST.js +0 -7
- package/dist/ExpensiMark.d.ts +27 -0
- package/dist/ExpensiMark.js +170 -4
- package/package.json +1 -1
package/dist/CONST.d.ts
CHANGED
|
@@ -325,12 +325,6 @@ declare const CONST: {
|
|
|
325
325
|
* @type String
|
|
326
326
|
*/
|
|
327
327
|
readonly MARKDOWN_EMAIL: "(?=((?=[\\w'#%+-]+(?:\\.[\\w'#%+-]+)*@)[\\w\\.'#%+-]{1,64}@(?:(?=[a-z\\d]+(?:-+[a-z\\d]+)*\\.)(?:[a-z\\d-]{1,63}\\.)+[a-z]{2,63})(?= |_|\\b))(?<end>.*))\\S{3,254}(?=\\k<end>$)";
|
|
328
|
-
/**
|
|
329
|
-
* Regex matching an text containing an Emoji
|
|
330
|
-
*
|
|
331
|
-
* @type RegExp
|
|
332
|
-
*/
|
|
333
|
-
readonly EMOJIS: RegExp;
|
|
334
328
|
/**
|
|
335
329
|
* Regex matching an text containing an Emoji that can be a single emoji or made up by some different emojis
|
|
336
330
|
*
|
package/dist/CONST.js
CHANGED
|
@@ -333,13 +333,6 @@ const CONST = {
|
|
|
333
333
|
* @type String
|
|
334
334
|
*/
|
|
335
335
|
MARKDOWN_EMAIL: EMAIL_BASE_REGEX,
|
|
336
|
-
/**
|
|
337
|
-
* Regex matching an text containing an Emoji
|
|
338
|
-
*
|
|
339
|
-
* @type RegExp
|
|
340
|
-
*/
|
|
341
|
-
// eslint-disable-next-line no-misleading-character-class
|
|
342
|
-
EMOJIS: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu,
|
|
343
336
|
/**
|
|
344
337
|
* Regex matching an text containing an Emoji that can be a single emoji or made up by some different emojis
|
|
345
338
|
*
|
package/dist/ExpensiMark.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ type ReplaceOptions = {
|
|
|
29
29
|
shouldEscapeText?: boolean;
|
|
30
30
|
shouldKeepRawInput?: boolean;
|
|
31
31
|
};
|
|
32
|
+
type TruncateOptions = {
|
|
33
|
+
ellipsis?: string;
|
|
34
|
+
truncateLastWord?: boolean;
|
|
35
|
+
slop?: number;
|
|
36
|
+
removeImageTag?: boolean;
|
|
37
|
+
};
|
|
32
38
|
export default class ExpensiMark {
|
|
33
39
|
static Log: Logger;
|
|
34
40
|
/**
|
|
@@ -138,6 +144,27 @@ export default class ExpensiMark {
|
|
|
138
144
|
* @returns original MD content escaped for use in HTML attribute value
|
|
139
145
|
*/
|
|
140
146
|
escapeAttributeContent(content: string): string;
|
|
147
|
+
/**
|
|
148
|
+
* Determines the end position to truncate the HTML content while considering word boundaries.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} content - The HTML content to be truncated.
|
|
151
|
+
* @param {number} tailPosition - The position up to which the content should be considered.
|
|
152
|
+
* @param {number} maxLength - The maximum length of the truncated content.
|
|
153
|
+
* @param {number} totalLength - The length of the content processed so far.
|
|
154
|
+
* @param {Object} opts - Options to customize the truncation.
|
|
155
|
+
* @returns {number} The calculated position to truncate the content.
|
|
156
|
+
*/
|
|
157
|
+
getEndPosition(content: string, tailPosition: number | undefined, maxLength: number, totalLength: number, opts: TruncateOptions): number;
|
|
158
|
+
/**
|
|
159
|
+
* Truncate HTML string and keep tag safe.
|
|
160
|
+
* pulled from https://github.com/huang47/nodejs-html-truncate/blob/master/lib/truncate.js
|
|
161
|
+
*
|
|
162
|
+
* @param {string} html - The string that needs to be truncated
|
|
163
|
+
* @param {number} maxLength - Length of truncated string
|
|
164
|
+
* @param {Object} [options] - Optional configuration options
|
|
165
|
+
* @returns {string} The truncated string
|
|
166
|
+
*/
|
|
167
|
+
truncateHTML(html: string, maxLength: number, options?: TruncateOptions): string;
|
|
141
168
|
/**
|
|
142
169
|
* Replaces text with a replacement based on a regex
|
|
143
170
|
* @param text - The text to replace
|
package/dist/ExpensiMark.js
CHANGED
|
@@ -107,7 +107,7 @@ class ExpensiMark {
|
|
|
107
107
|
return this.modifyTextForEmailLinks(regex, textToProcess, replacement, shouldKeepRawInput);
|
|
108
108
|
},
|
|
109
109
|
replacement: (_extras, match, g1, g2) => {
|
|
110
|
-
if (
|
|
110
|
+
if (!g1.trim()) {
|
|
111
111
|
return match;
|
|
112
112
|
}
|
|
113
113
|
const label = g1.trim();
|
|
@@ -116,7 +116,7 @@ class ExpensiMark {
|
|
|
116
116
|
return `<a href="${href}">${formattedLabel}</a>`;
|
|
117
117
|
},
|
|
118
118
|
rawInputReplacement: (_extras, match, g1, g2, g3) => {
|
|
119
|
-
if (
|
|
119
|
+
if (!g1.trim()) {
|
|
120
120
|
return match;
|
|
121
121
|
}
|
|
122
122
|
const dataRawHref = g2 ? g2 + g3 : g3;
|
|
@@ -177,13 +177,13 @@ class ExpensiMark {
|
|
|
177
177
|
name: 'link',
|
|
178
178
|
process: (textToProcess, replacement) => this.modifyTextForUrlLinks(MARKDOWN_LINK_REGEX, textToProcess, replacement),
|
|
179
179
|
replacement: (_extras, match, g1, g2) => {
|
|
180
|
-
if (
|
|
180
|
+
if (!g1.trim()) {
|
|
181
181
|
return match;
|
|
182
182
|
}
|
|
183
183
|
return `<a href="${str_1.default.sanitizeURL(g2)}" target="_blank" rel="noreferrer noopener">${g1.trim()}</a>`;
|
|
184
184
|
},
|
|
185
185
|
rawInputReplacement: (_extras, match, g1, g2) => {
|
|
186
|
-
if (
|
|
186
|
+
if (!g1.trim()) {
|
|
187
187
|
return match;
|
|
188
188
|
}
|
|
189
189
|
return `<a href="${str_1.default.sanitizeURL(g2)}" data-raw-href="${g2}" data-link-variant="labeled" target="_blank" rel="noreferrer noopener">${g1.trim()}</a>`;
|
|
@@ -1093,6 +1093,172 @@ class ExpensiMark {
|
|
|
1093
1093
|
originalContent = str_1.default.replaceAll(originalContent, '\n', '');
|
|
1094
1094
|
return Utils.escape(originalContent);
|
|
1095
1095
|
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Determines the end position to truncate the HTML content while considering word boundaries.
|
|
1098
|
+
*
|
|
1099
|
+
* @param {string} content - The HTML content to be truncated.
|
|
1100
|
+
* @param {number} tailPosition - The position up to which the content should be considered.
|
|
1101
|
+
* @param {number} maxLength - The maximum length of the truncated content.
|
|
1102
|
+
* @param {number} totalLength - The length of the content processed so far.
|
|
1103
|
+
* @param {Object} opts - Options to customize the truncation.
|
|
1104
|
+
* @returns {number} The calculated position to truncate the content.
|
|
1105
|
+
*/
|
|
1106
|
+
getEndPosition(content, tailPosition, maxLength, totalLength, opts) {
|
|
1107
|
+
const WORD_BREAK_REGEX = /\W+/g;
|
|
1108
|
+
// Calculate the default position to truncate based on the maximum length and the length of the content processed so far
|
|
1109
|
+
const defaultPosition = maxLength - totalLength;
|
|
1110
|
+
// Define the slop value, which determines the tolerance for cutting off content near the maximum length
|
|
1111
|
+
const slop = opts.slop;
|
|
1112
|
+
if (!slop)
|
|
1113
|
+
return defaultPosition;
|
|
1114
|
+
// Initialize the position to the default position
|
|
1115
|
+
let position = defaultPosition;
|
|
1116
|
+
// Determine if the default position is considered "short" based on the slop value
|
|
1117
|
+
const isShort = defaultPosition < slop;
|
|
1118
|
+
// Calculate the position within the slop range
|
|
1119
|
+
const slopPos = isShort ? defaultPosition : slop - 1;
|
|
1120
|
+
// Extract the substring to analyze for word boundaries, considering the slop and tail position
|
|
1121
|
+
const substr = content.slice(isShort ? 0 : defaultPosition - slop, tailPosition !== undefined ? tailPosition : defaultPosition + slop);
|
|
1122
|
+
// Find the first word boundary within the substring
|
|
1123
|
+
const wordBreakMatch = WORD_BREAK_REGEX.exec(substr);
|
|
1124
|
+
// Adjust the position to avoid truncating in the middle of a word if the option is enabled
|
|
1125
|
+
if (!opts.truncateLastWord) {
|
|
1126
|
+
if (tailPosition && substr.length <= tailPosition) {
|
|
1127
|
+
// If tail position is defined and the substring length is within the tail position, set position to the substring length
|
|
1128
|
+
position = substr.length;
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
// Iterate through word boundary matches to adjust the position
|
|
1132
|
+
while (wordBreakMatch !== null) {
|
|
1133
|
+
if (wordBreakMatch.index < slopPos) {
|
|
1134
|
+
// If the word boundary is before the slop position, adjust position backward
|
|
1135
|
+
position = defaultPosition - (slopPos - wordBreakMatch.index);
|
|
1136
|
+
if (wordBreakMatch.index === 0 && defaultPosition <= 1) {
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
else if (wordBreakMatch.index === slopPos) {
|
|
1141
|
+
// If the word boundary is at the slop position, set position to the default position
|
|
1142
|
+
position = defaultPosition;
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
// If the word boundary is after the slop position, adjust position forward
|
|
1147
|
+
position = defaultPosition + (wordBreakMatch.index - slopPos);
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// If the character at the determined position is a whitespace, adjust position backward
|
|
1153
|
+
if (content.charAt(position - 1).match(/\s$/)) {
|
|
1154
|
+
position--;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// Return the calculated position to truncate the content
|
|
1158
|
+
return position;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Truncate HTML string and keep tag safe.
|
|
1162
|
+
* pulled from https://github.com/huang47/nodejs-html-truncate/blob/master/lib/truncate.js
|
|
1163
|
+
*
|
|
1164
|
+
* @param {string} html - The string that needs to be truncated
|
|
1165
|
+
* @param {number} maxLength - Length of truncated string
|
|
1166
|
+
* @param {Object} [options] - Optional configuration options
|
|
1167
|
+
* @returns {string} The truncated string
|
|
1168
|
+
*/
|
|
1169
|
+
truncateHTML(html, maxLength, options) {
|
|
1170
|
+
const EMPTY_STRING = '';
|
|
1171
|
+
const DEFAULT_TRUNCATE_SYMBOL = '...';
|
|
1172
|
+
const DEFAULT_SLOP = Math.min(10, maxLength);
|
|
1173
|
+
const tagsStack = [];
|
|
1174
|
+
const KEY_VALUE_REGEX = '((?:\\s+(?:\\w+|-)+(?:\\s*=\\s*(?:"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|[^\'">\\s]+))?)*)';
|
|
1175
|
+
const IS_CLOSE_REGEX = '\\s*\\/?\\s*';
|
|
1176
|
+
const CLOSE_REGEX = '\\s*\\/\\s*';
|
|
1177
|
+
const SELF_CLOSE_REGEX = new RegExp(`<\\/?(\\w+)${KEY_VALUE_REGEX}${CLOSE_REGEX}>`);
|
|
1178
|
+
const HTML_TAG_REGEX = new RegExp(`<\\/?(\\w+)${KEY_VALUE_REGEX}${IS_CLOSE_REGEX}>`);
|
|
1179
|
+
const URL_REGEX = /(((ftp|https?):\/\/)[\\-\w@:%_\\+.~#?,&\\/\\/=]+)|((mailto:)?[_.\w\\-]+@([\w][\w\\-]+\.)+[a-zA-Z]{2,3})/g;
|
|
1180
|
+
const IMAGE_TAG_REGEX = new RegExp(`<img\\s*${KEY_VALUE_REGEX}${CLOSE_REGEX}>`);
|
|
1181
|
+
let truncatedContent = EMPTY_STRING;
|
|
1182
|
+
let totalLength = 0;
|
|
1183
|
+
let matches = HTML_TAG_REGEX.exec(html);
|
|
1184
|
+
let endResult;
|
|
1185
|
+
let index;
|
|
1186
|
+
let tag;
|
|
1187
|
+
let selfClose = null;
|
|
1188
|
+
let htmlString = html;
|
|
1189
|
+
const opts = Object.assign({ ellipsis: DEFAULT_TRUNCATE_SYMBOL, truncateLastWord: true, slop: DEFAULT_SLOP }, options);
|
|
1190
|
+
function removeImageTag(content) {
|
|
1191
|
+
const match = IMAGE_TAG_REGEX.exec(content);
|
|
1192
|
+
if (!match) {
|
|
1193
|
+
return content;
|
|
1194
|
+
}
|
|
1195
|
+
const matchIndex = match.index;
|
|
1196
|
+
const matchLength = match[0].length;
|
|
1197
|
+
return content.substring(0, matchIndex) + content.substring(matchIndex + matchLength);
|
|
1198
|
+
}
|
|
1199
|
+
function closeTags(tags) {
|
|
1200
|
+
return tags
|
|
1201
|
+
.reverse()
|
|
1202
|
+
.map((mappedTag) => {
|
|
1203
|
+
return `</${mappedTag}>`;
|
|
1204
|
+
})
|
|
1205
|
+
.join('');
|
|
1206
|
+
}
|
|
1207
|
+
while (matches) {
|
|
1208
|
+
matches = HTML_TAG_REGEX.exec(htmlString);
|
|
1209
|
+
if (!matches) {
|
|
1210
|
+
if (totalLength >= maxLength) {
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
matches = URL_REGEX.exec(htmlString);
|
|
1214
|
+
if (!matches || matches.index >= maxLength) {
|
|
1215
|
+
truncatedContent += htmlString.substring(0, this.getEndPosition(htmlString, undefined, maxLength, totalLength, opts));
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
while (matches) {
|
|
1219
|
+
endResult = matches[0];
|
|
1220
|
+
if (endResult !== null) {
|
|
1221
|
+
index = matches.index;
|
|
1222
|
+
truncatedContent += htmlString.substring(0, index + endResult.length - totalLength);
|
|
1223
|
+
htmlString = htmlString.substring(index + endResult.length);
|
|
1224
|
+
matches = URL_REGEX.exec(htmlString);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
endResult = matches[0];
|
|
1230
|
+
index = matches.index;
|
|
1231
|
+
if (totalLength + index > maxLength) {
|
|
1232
|
+
truncatedContent += htmlString.substring(0, this.getEndPosition(htmlString, index, maxLength, totalLength, opts));
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
totalLength += index;
|
|
1237
|
+
truncatedContent += htmlString.substring(0, index);
|
|
1238
|
+
}
|
|
1239
|
+
if (endResult[1] === '/') {
|
|
1240
|
+
tagsStack.pop();
|
|
1241
|
+
selfClose = null;
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
selfClose = SELF_CLOSE_REGEX.exec(endResult);
|
|
1245
|
+
if (!selfClose) {
|
|
1246
|
+
tag = matches[1];
|
|
1247
|
+
tagsStack.push(tag);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
truncatedContent += selfClose ? selfClose[0] : endResult;
|
|
1251
|
+
htmlString = htmlString.substring(index + endResult.length); // Update htmlString
|
|
1252
|
+
}
|
|
1253
|
+
if (htmlString.length > maxLength - totalLength && opts.ellipsis) {
|
|
1254
|
+
truncatedContent += opts.ellipsis ? '...' : '';
|
|
1255
|
+
}
|
|
1256
|
+
truncatedContent += closeTags(tagsStack);
|
|
1257
|
+
if (opts.removeImageTag) {
|
|
1258
|
+
truncatedContent = removeImageTag(truncatedContent);
|
|
1259
|
+
}
|
|
1260
|
+
return truncatedContent;
|
|
1261
|
+
}
|
|
1096
1262
|
/**
|
|
1097
1263
|
* Replaces text with a replacement based on a regex
|
|
1098
1264
|
* @param text - The text to replace
|