expensify-common 2.0.43 → 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.
@@ -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
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expensify-common",
3
- "version": "2.0.43",
3
+ "version": "2.0.44",
4
4
  "author": "Expensify, Inc.",
5
5
  "description": "Expensify libraries and components shared across different repos",
6
6
  "homepage": "https://expensify.com",