html-truncate-ts
Version:
TypeScript library for truncating HTML strings while preserving HTML tags and structure
162 lines (156 loc) • 6.28 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.HtmlTruncate = {}));
})(this, (function (exports) { 'use strict';
const EMPTY_OBJECT = {};
const EMPTY_STRING = '';
const DEFAULT_TRUNCATE_SYMBOL = '...';
const EXCLUDE_TAGS = ['img', 'br'];
const KEY_VALUE_REGEX = '([\\w|-]+\\s*=\\s*"[^"]*"\\s*)*';
const IS_CLOSE_REGEX = '\\s*\\/?\\s*';
const CLOSE_REGEX = '\\s*\\/\\s*';
const SELF_CLOSE_REGEX = new RegExp('<\\/?\\w+\\s*' + KEY_VALUE_REGEX + CLOSE_REGEX + '>');
const HTML_TAG_REGEX = new RegExp('<\\/?\\w+\\s*' + KEY_VALUE_REGEX + IS_CLOSE_REGEX + '>');
const URL_REGEX = /(((ftp|https?):\/\/)[-\w@:%_+.~#?,&//=]+)|((mailto:)?[_.\w-]+@([\w][\w-]+\.)+[a-zA-Z]{2,3})/g;
const IMAGE_TAG_REGEX = new RegExp('<img\\s*' + KEY_VALUE_REGEX + IS_CLOSE_REGEX + '>');
const WORD_BREAK_REGEX = new RegExp('\\W+', 'g');
const removeImageTag = (string) => {
const match = IMAGE_TAG_REGEX.exec(string);
let index, len;
if (!match) {
return string;
}
index = match.index;
len = match[0].length;
return string.substring(0, index) + string.substring(index + len);
};
const dumpCloseTag = (tags) => {
let html = '';
tags.reverse().forEach((tag) => {
if (-1 === EXCLUDE_TAGS.indexOf(tag)) {
html += '</' + tag + '>';
}
});
return html;
};
const getTag = (string) => {
let tail = string.indexOf(' ');
if (-1 === tail) {
tail = string.indexOf('>');
if (-1 === tail) {
throw new Error('HTML tag is not well-formed : ' + string);
}
}
return string.substring(1, tail);
};
const getEndPosition = (string, maxLength, total, tailPos, options) => {
const DEFAULT_SLOP = 10 > maxLength ? maxLength : 10;
const { truncateLastWord, slop } = options || {};
const defaultPos = maxLength - total;
const isShort = defaultPos < (slop || DEFAULT_SLOP);
const slopPos = isShort ? defaultPos : (slop || DEFAULT_SLOP) - 1;
const startSlice = isShort ? 0 : defaultPos - (slop || DEFAULT_SLOP);
const endSlice = tailPos || defaultPos + (slop || DEFAULT_SLOP);
let position = defaultPos, substr, result;
if (!truncateLastWord) {
substr = string.slice(startSlice, endSlice);
if (tailPos && substr.length <= tailPos) {
position = substr.length;
}
else {
while ((result = WORD_BREAK_REGEX.exec(substr)) !== null) {
if (result.index < slopPos) {
position = defaultPos - (slopPos - result.index);
if (result.index === 0 && defaultPos <= 1)
break;
}
else if (result.index === slopPos) {
position = defaultPos;
break;
}
else {
position = defaultPos + (result.index - slopPos);
break;
}
}
}
if (string.charAt(position - 1).match(/\s$/))
position--;
}
return position;
};
const truncate = (string, maxLength, options) => {
const DEFAULT_SLOP = 10 > maxLength ? maxLength : 10;
const items = [];
let total = 0;
let content = EMPTY_STRING;
let matches;
let result;
let index;
let tag;
let selfClose;
options = options || EMPTY_OBJECT;
options.ellipsis =
undefined !== options.ellipsis ? options.ellipsis : DEFAULT_TRUNCATE_SYMBOL;
options.truncateLastWord =
undefined !== options.truncateLastWord ? options.truncateLastWord : true;
options.slop = undefined !== options.slop ? options.slop : DEFAULT_SLOP;
while ((matches = HTML_TAG_REGEX.exec(string))) {
result = matches[0];
index = matches.index;
if (total + index > maxLength) {
content += string.substring(0, getEndPosition(string, maxLength, total, index, options));
break;
}
else {
total += index;
content += string.substring(0, index);
}
if ('/' === result[1]) {
items.pop();
selfClose = null;
}
else {
selfClose = SELF_CLOSE_REGEX.exec(result);
if (!selfClose) {
tag = getTag(result);
items.push(tag);
}
}
if (selfClose) {
content += selfClose[0];
}
else {
content += result;
}
string = string.substring(index + result.length);
}
if (total >= maxLength) ;
else {
matches = URL_REGEX.exec(string);
if (!matches || matches.index >= maxLength) {
content += string.substring(0, getEndPosition(string, maxLength, total, undefined, options));
}
else {
while (matches) {
result = matches[0];
index = matches.index;
content += string.substring(0, index + result.length - total);
string = string.substring(index + result.length);
matches = URL_REGEX.exec(string);
}
}
}
if (string.length > maxLength - total && options.ellipsis) {
content += options.ellipsis;
}
content += dumpCloseTag(items);
if (!options.keepImageTag) {
content = removeImageTag(content);
}
return content;
};
exports.truncate = truncate;
}));
//# sourceMappingURL=html-truncate.js.map