@slickgrid-universal/utils 4.0.2 → 4.1.0
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/cjs/domUtils.js.map +1 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/nodeExtend.js +125 -0
- package/dist/cjs/nodeExtend.js.map +1 -0
- package/dist/esm/domUtils.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/nodeExtend.js +121 -0
- package/dist/esm/nodeExtend.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/domUtils.d.ts +1 -1
- package/dist/types/domUtils.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/nodeExtend.d.ts +13 -0
- package/dist/types/nodeExtend.d.ts.map +1 -0
- package/package.json +4 -3
- package/src/domUtils.ts +249 -0
- package/src/index.ts +5 -0
- package/src/nodeExtend.ts +131 -0
- package/src/stripTagsUtil.ts +193 -0
- package/src/types/htmlElementPosition.interface.ts +6 -0
- package/src/types/index.ts +2 -0
- package/src/types/infer.type.ts +6 -0
- package/src/utils.ts +399 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slickgrid-universal/utils",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Common set of small utilities",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"/dist"
|
|
28
|
+
"/dist",
|
|
29
|
+
"/src"
|
|
29
30
|
],
|
|
30
31
|
"license": "MIT",
|
|
31
32
|
"author": "Ghislain B.",
|
|
@@ -43,5 +44,5 @@
|
|
|
43
44
|
"> 1%",
|
|
44
45
|
"not dead"
|
|
45
46
|
],
|
|
46
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "1cfc2658f5d70e66c096e5ea77d1827dd44e0292"
|
|
47
48
|
}
|
package/src/domUtils.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { HtmlElementPosition, InferDOMType } from './types/index';
|
|
2
|
+
|
|
3
|
+
/** calculate available space for each side of the DOM element */
|
|
4
|
+
export function calculateAvailableSpace(element: HTMLElement): { top: number; bottom: number; left: number; right: number; } {
|
|
5
|
+
let bottom = 0;
|
|
6
|
+
let top = 0;
|
|
7
|
+
let left = 0;
|
|
8
|
+
let right = 0;
|
|
9
|
+
|
|
10
|
+
const windowHeight = window.innerHeight ?? 0;
|
|
11
|
+
const windowWidth = window.innerWidth ?? 0;
|
|
12
|
+
const scrollPosition = windowScrollPosition();
|
|
13
|
+
const pageScrollTop = scrollPosition.top;
|
|
14
|
+
const pageScrollLeft = scrollPosition.left;
|
|
15
|
+
const elmOffset = getOffset(element);
|
|
16
|
+
|
|
17
|
+
if (elmOffset) {
|
|
18
|
+
const elementOffsetTop = elmOffset.top ?? 0;
|
|
19
|
+
const elementOffsetLeft = elmOffset.left ?? 0;
|
|
20
|
+
top = elementOffsetTop - pageScrollTop;
|
|
21
|
+
bottom = windowHeight - (elementOffsetTop - pageScrollTop);
|
|
22
|
+
left = elementOffsetLeft - pageScrollLeft;
|
|
23
|
+
right = windowWidth - (elementOffsetLeft - pageScrollLeft);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { top, bottom, left, right };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a DOM Element with any optional attributes or properties.
|
|
31
|
+
* It will only accept valid DOM element properties that `createElement` would accept.
|
|
32
|
+
* For example: `createDomElement('div', { className: 'my-css-class' })`,
|
|
33
|
+
* for style or dataset you need to use nested object `{ style: { display: 'none' }}
|
|
34
|
+
* The last argument is to optionally append the created element to a parent container element.
|
|
35
|
+
* @param {String} tagName - html tag
|
|
36
|
+
* @param {Object} options - element properties
|
|
37
|
+
* @param {[Element]} appendToParent - parent element to append to
|
|
38
|
+
*/
|
|
39
|
+
export function createDomElement<T extends keyof HTMLElementTagNameMap, K extends keyof HTMLElementTagNameMap[T]>(
|
|
40
|
+
tagName: T,
|
|
41
|
+
elementOptions?: null | { [P in K]: InferDOMType<HTMLElementTagNameMap[T][P]> },
|
|
42
|
+
appendToParent?: Element
|
|
43
|
+
): HTMLElementTagNameMap[T] {
|
|
44
|
+
const elm = document.createElement<T>(tagName);
|
|
45
|
+
|
|
46
|
+
if (elementOptions) {
|
|
47
|
+
Object.keys(elementOptions).forEach((elmOptionKey) => {
|
|
48
|
+
if (elmOptionKey === 'innerHTML') {
|
|
49
|
+
console.warn(`[Slickgrid-Universal] For better CSP (Content Security Policy) support, do not use "innerHTML" directly in "createDomElement('${tagName}', { innerHTML: 'some html'})", ` +
|
|
50
|
+
`it is better as separate assignment: "const elm = createDomElement('span'); elm.innerHTML = 'some html';"`);
|
|
51
|
+
}
|
|
52
|
+
const elmValue = elementOptions[elmOptionKey as keyof typeof elementOptions];
|
|
53
|
+
if (typeof elmValue === 'object') {
|
|
54
|
+
Object.assign(elm[elmOptionKey as K] as object, elmValue);
|
|
55
|
+
} else {
|
|
56
|
+
elm[elmOptionKey as K] = (elementOptions as any)[elmOptionKey as keyof typeof elementOptions];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (appendToParent?.appendChild) {
|
|
61
|
+
appendToParent.appendChild(elm);
|
|
62
|
+
}
|
|
63
|
+
return elm;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Loop through all properties of an object and nullify any properties that are instanceof HTMLElement,
|
|
68
|
+
* if we detect an array then use recursion to go inside it and apply same logic
|
|
69
|
+
* @param obj - object containing 1 or more properties with DOM Elements
|
|
70
|
+
*/
|
|
71
|
+
export function destroyAllElementProps(obj: any) {
|
|
72
|
+
if (obj) {
|
|
73
|
+
for (const key of Object.keys(obj)) {
|
|
74
|
+
if (Array.isArray(obj[key])) {
|
|
75
|
+
destroyAllElementProps(obj[key]);
|
|
76
|
+
}
|
|
77
|
+
if (obj[key] instanceof HTMLElement) {
|
|
78
|
+
obj[key] = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Empty a DOM element by removing all of its DOM element children leaving with an empty element (basically an empty shell)
|
|
86
|
+
* @return {object} element - updated element
|
|
87
|
+
*/
|
|
88
|
+
export function emptyElement<T extends Element = Element>(element?: T | null): T | undefined | null {
|
|
89
|
+
while (element?.firstChild) {
|
|
90
|
+
element.removeChild(element.firstChild);
|
|
91
|
+
}
|
|
92
|
+
return element;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* From a DocumentFragment, get the innerHTML or outerHTML of all child elements.
|
|
97
|
+
* We can get the HTML by looping through all fragment `childNodes`
|
|
98
|
+
*/
|
|
99
|
+
export function getHTMLFromFragment(input: DocumentFragment, type: 'innerHTML' | 'outerHTML' = 'innerHTML'): string {
|
|
100
|
+
if (input instanceof DocumentFragment) {
|
|
101
|
+
return [].map.call(input.childNodes, (x: HTMLElement) => x[type]).join('') || input.textContent || '';
|
|
102
|
+
}
|
|
103
|
+
return input;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Get offset of HTML element relative to a parent element */
|
|
107
|
+
export function getOffsetRelativeToParent(parentElm: HTMLElement | null, childElm: HTMLElement | null) {
|
|
108
|
+
if (!parentElm || !childElm) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const parentPos = parentElm.getBoundingClientRect();
|
|
112
|
+
const childPos = childElm.getBoundingClientRect();
|
|
113
|
+
return {
|
|
114
|
+
top: childPos.top - parentPos.top,
|
|
115
|
+
right: childPos.right - parentPos.right,
|
|
116
|
+
bottom: childPos.bottom - parentPos.bottom,
|
|
117
|
+
left: childPos.left - parentPos.left,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Get HTML element offset with pure JS */
|
|
122
|
+
export function getOffset(elm?: HTMLElement | null): HtmlElementPosition | undefined {
|
|
123
|
+
if (!elm || !elm.getBoundingClientRect) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const box = elm.getBoundingClientRect();
|
|
127
|
+
const docElem = document.documentElement;
|
|
128
|
+
|
|
129
|
+
let top = 0;
|
|
130
|
+
let left = 0;
|
|
131
|
+
let bottom = 0;
|
|
132
|
+
let right = 0;
|
|
133
|
+
|
|
134
|
+
if (box?.top !== undefined && box.left !== undefined) {
|
|
135
|
+
top = box.top + window.pageYOffset - docElem.clientTop;
|
|
136
|
+
left = box.left + window.pageXOffset - docElem.clientLeft;
|
|
137
|
+
right = box.right;
|
|
138
|
+
bottom = box.bottom;
|
|
139
|
+
}
|
|
140
|
+
return { top, left, bottom, right };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getInnerSize(elm: HTMLElement, type: 'height' | 'width') {
|
|
144
|
+
let size = 0;
|
|
145
|
+
|
|
146
|
+
if (elm) {
|
|
147
|
+
const clientSize = type === 'height' ? 'clientHeight' : 'clientWidth';
|
|
148
|
+
const sides = type === 'height' ? ['top', 'bottom'] : ['left', 'right'];
|
|
149
|
+
size = elm[clientSize];
|
|
150
|
+
for (const side of sides) {
|
|
151
|
+
const sideSize = (parseFloat(getStyleProp(elm, `padding-${side}`) || '') || 0);
|
|
152
|
+
size -= sideSize;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return size;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Get a DOM element style property value by calling getComputedStyle() on the element */
|
|
159
|
+
export function getStyleProp(elm: HTMLElement, property: string) {
|
|
160
|
+
if (elm) {
|
|
161
|
+
return window.getComputedStyle(elm).getPropertyValue(property);
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function findFirstAttribute(inputElm: Element | null | undefined, attributes: string[]): string | null {
|
|
167
|
+
if (inputElm) {
|
|
168
|
+
for (const attribute of attributes) {
|
|
169
|
+
const attrData = inputElm.getAttribute(attribute);
|
|
170
|
+
if (attrData) {
|
|
171
|
+
return attrData;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Provide a width as a number or a string and find associated value in valid css style format or use default value when provided (or "auto" otherwise).
|
|
180
|
+
* @param {Number|String} inputWidth - input width, could be a string or number
|
|
181
|
+
* @param {Number | String} defaultValue [defaultValue=auto] - optional default value or use "auto" when nothing is provided
|
|
182
|
+
* @returns {String} string output
|
|
183
|
+
*/
|
|
184
|
+
export function findWidthOrDefault(inputWidth?: number | string | null, defaultValue = 'auto'): string {
|
|
185
|
+
return (/^[0-9]+$/i.test(`${inputWidth}`) ? `${+(inputWidth as number)}px` : inputWidth as string) || defaultValue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* HTML encode using a plain <div>
|
|
190
|
+
* Create a in-memory div, set it's inner text(which a div can encode)
|
|
191
|
+
* then grab the encoded contents back out. The div never exists on the page.
|
|
192
|
+
* @param {String} inputValue - input value to be encoded
|
|
193
|
+
* @return {String}
|
|
194
|
+
*/
|
|
195
|
+
export function htmlEncode(inputValue: string): string {
|
|
196
|
+
const val = typeof inputValue === 'string' ? inputValue : String(inputValue);
|
|
197
|
+
const entityMap: { [char: string]: string; } = {
|
|
198
|
+
'&': '&',
|
|
199
|
+
'<': '<',
|
|
200
|
+
'>': '>',
|
|
201
|
+
'"': '"',
|
|
202
|
+
'\'': ''',
|
|
203
|
+
};
|
|
204
|
+
return (val || '').toString().replace(/[&<>"']/g, (s) => entityMap[s as keyof { [char: string]: string; }]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Decode text into html entity
|
|
209
|
+
* @param string text: input text
|
|
210
|
+
* @param string text: output text
|
|
211
|
+
*/
|
|
212
|
+
export function htmlEntityDecode(input: string): string {
|
|
213
|
+
return input.replace(/&#(\d+);/g, (_match, dec) => {
|
|
214
|
+
return String.fromCharCode(dec);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Encode string to html special char and add html space padding defined
|
|
220
|
+
* @param {string} inputStr - input string
|
|
221
|
+
* @param {number} paddingLength - padding to add
|
|
222
|
+
*/
|
|
223
|
+
export function htmlEncodeWithPadding(inputStr: string, paddingLength: number): string {
|
|
224
|
+
const inputStrLn = inputStr.length;
|
|
225
|
+
let outputStr = htmlEncode(inputStr);
|
|
226
|
+
|
|
227
|
+
if (inputStrLn < paddingLength) {
|
|
228
|
+
for (let i = inputStrLn; i < paddingLength; i++) {
|
|
229
|
+
outputStr += ` `;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return outputStr;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** insert an HTML Element after a target Element in the DOM */
|
|
236
|
+
export function insertAfterElement(referenceNode: HTMLElement, newNode: HTMLElement) {
|
|
237
|
+
referenceNode.parentNode?.insertBefore(newNode, referenceNode.nextSibling);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get the Window Scroll top/left Position
|
|
242
|
+
* @returns
|
|
243
|
+
*/
|
|
244
|
+
export function windowScrollPosition(): { left: number; top: number; } {
|
|
245
|
+
return {
|
|
246
|
+
left: window.pageXOffset || document.documentElement.scrollLeft || 0,
|
|
247
|
+
top: window.pageYOffset || document.documentElement.scrollTop || 0,
|
|
248
|
+
};
|
|
249
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* eslint-disable guard-for-in */
|
|
2
|
+
/**
|
|
3
|
+
* This extend function is a reimplementation of the npm package `extend` (also named `node-extend`).
|
|
4
|
+
* The reason for the reimplementation was mostly because the original project is not ESM compatible
|
|
5
|
+
* and written with old ES6 IIFE syntax, the goal was to reimplement and fix these old syntax and build problems.
|
|
6
|
+
* e.g. it used `var` everywhere, it used `arguments` to get function arguments, ...
|
|
7
|
+
*
|
|
8
|
+
* The previous lib can be found here at this Github link:
|
|
9
|
+
* https://github.com/justmoon/node-extend
|
|
10
|
+
* With an MIT licence that and can be found at
|
|
11
|
+
* https://github.com/justmoon/node-extend/blob/main/LICENSE
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
15
|
+
const toStr = Object.prototype.toString;
|
|
16
|
+
const defineProperty = Object.defineProperty;
|
|
17
|
+
const gOPD = Object.getOwnPropertyDescriptor;
|
|
18
|
+
|
|
19
|
+
const isArray = function isArray(arr: any) {
|
|
20
|
+
if (typeof Array.isArray === 'function') {
|
|
21
|
+
return Array.isArray(arr);
|
|
22
|
+
}
|
|
23
|
+
/* istanbul ignore next */
|
|
24
|
+
return toStr.call(arr) === '[object Array]';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isPlainObject = function isPlainObject(obj: any) {
|
|
28
|
+
if (!obj || toStr.call(obj) !== '[object Object]') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hasOwnConstructor = hasOwn.call(obj, 'constructor');
|
|
33
|
+
const hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
|
|
34
|
+
// Not own constructor property must be Object
|
|
35
|
+
if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Own properties are enumerated firstly, so to speed up, if last one is own, then all properties are own.
|
|
40
|
+
let key;
|
|
41
|
+
for (key in obj) { /**/ }
|
|
42
|
+
|
|
43
|
+
return typeof key === 'undefined' || hasOwn.call(obj, key);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target
|
|
47
|
+
const setProperty = function setProperty(target: any, options: any) {
|
|
48
|
+
if (defineProperty && options.name === '__proto__') {
|
|
49
|
+
defineProperty(target, options.name, {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
value: options.newValue,
|
|
53
|
+
writable: true
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
target[options.name] = options.newValue;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Return undefined instead of __proto__ if '__proto__' is not an own property
|
|
61
|
+
const getProperty = function getProperty(obj: any, name: any) {
|
|
62
|
+
if (name === '__proto__') {
|
|
63
|
+
if (!hasOwn.call(obj, name)) {
|
|
64
|
+
return void 0;
|
|
65
|
+
} else if (gOPD) {
|
|
66
|
+
// In early versions of node, obj['__proto__'] is buggy when obj has __proto__ as an own property. Object.getOwnPropertyDescriptor() works.
|
|
67
|
+
return gOPD(obj, name)!.value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return obj[name];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function extend<T = any>(...args: any[]): T {
|
|
75
|
+
let options;
|
|
76
|
+
let name;
|
|
77
|
+
let src;
|
|
78
|
+
let copy;
|
|
79
|
+
let copyIsArray;
|
|
80
|
+
let clone;
|
|
81
|
+
let target = args[0];
|
|
82
|
+
let i = 1;
|
|
83
|
+
const length = args.length;
|
|
84
|
+
let deep = false;
|
|
85
|
+
|
|
86
|
+
// Handle a deep copy situation
|
|
87
|
+
if (typeof target === 'boolean') {
|
|
88
|
+
deep = target;
|
|
89
|
+
target = args[1] || {};
|
|
90
|
+
// skip the boolean and the target
|
|
91
|
+
i = 2;
|
|
92
|
+
}
|
|
93
|
+
if (target === null || target === undefined || (typeof target !== 'object' && typeof target !== 'function')) {
|
|
94
|
+
target = {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (; i < length; ++i) {
|
|
98
|
+
options = args[i];
|
|
99
|
+
// Only deal with non-null/undefined values
|
|
100
|
+
if (options !== null && options !== undefined) {
|
|
101
|
+
// Extend the base object
|
|
102
|
+
for (name in options) {
|
|
103
|
+
src = getProperty(target, name);
|
|
104
|
+
copy = getProperty(options, name);
|
|
105
|
+
|
|
106
|
+
// Prevent never-ending loop
|
|
107
|
+
if (target !== copy) {
|
|
108
|
+
// Recurse if we're merging plain objects or arrays
|
|
109
|
+
if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
|
|
110
|
+
if (copyIsArray) {
|
|
111
|
+
copyIsArray = false;
|
|
112
|
+
clone = src && isArray(src) ? src : [];
|
|
113
|
+
} else {
|
|
114
|
+
clone = src && isPlainObject(src) ? src : {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Never move original objects, clone them
|
|
118
|
+
setProperty(target, { name, newValue: extend(deep, clone, copy) });
|
|
119
|
+
|
|
120
|
+
// Don't bring in undefined values
|
|
121
|
+
} else if (typeof copy !== 'undefined') {
|
|
122
|
+
setProperty(target, { name, newValue: copy });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Return the modified object
|
|
130
|
+
return target;
|
|
131
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This stripTags function is a lib that already existed
|
|
3
|
+
* but was not TypeScript and ESM friendly.
|
|
4
|
+
* So I ported the code into the project and removed any old code like IE11 that we don't need for our project.
|
|
5
|
+
* I also accept more input types without throwing, the code below accepts `string | number | boolean | HTMLElement` while original code only accepted string.
|
|
6
|
+
*
|
|
7
|
+
* The previous lib can be found here at this Github link:
|
|
8
|
+
* https://github.com/ericnorris/striptags/
|
|
9
|
+
* With an MIT licence that and can be found at
|
|
10
|
+
* https://github.com/ericnorris/striptags/blob/main/LICENSE
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isNumber } from './utils';
|
|
14
|
+
|
|
15
|
+
const STATE_PLAINTEXT = Symbol('plaintext');
|
|
16
|
+
const STATE_HTML = Symbol('html');
|
|
17
|
+
const STATE_COMMENT = Symbol('comment');
|
|
18
|
+
const ALLOWED_TAGS_REGEX = /<(\w*)>/g;
|
|
19
|
+
const NORMALIZE_TAG_REGEX = /<\/?([^\s\/>]+)/;
|
|
20
|
+
|
|
21
|
+
interface Context {
|
|
22
|
+
allowable_tags: Set<string | null>;
|
|
23
|
+
tag_replacement: string;
|
|
24
|
+
state: symbol;
|
|
25
|
+
tag_buffer: string;
|
|
26
|
+
depth: number;
|
|
27
|
+
in_quote_char: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function stripTags(htmlText: string | number | boolean | HTMLElement, allowableTags?: string | string[], tagReplacement?: string) {
|
|
31
|
+
|
|
32
|
+
/** main init function that will be executed when calling the global function */
|
|
33
|
+
function init(html: string | number | boolean | HTMLElement, allowable_tags?: string | string[], tag_replacement?: string) {
|
|
34
|
+
// number/boolean should be accepted but converted to string and returned on the spot
|
|
35
|
+
// since there's no html tags to be found but we still expect a string output
|
|
36
|
+
if (typeof html !== 'string' && (isNumber(html) || typeof html === 'boolean')) {
|
|
37
|
+
return String(html);
|
|
38
|
+
}
|
|
39
|
+
if (html instanceof HTMLElement) {
|
|
40
|
+
html = html.innerHTML;
|
|
41
|
+
}
|
|
42
|
+
if (typeof html !== 'string' && html !== undefined && html !== null) {
|
|
43
|
+
throw new TypeError(`'html' parameter must be a string`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return striptags_internal(
|
|
47
|
+
html || '',
|
|
48
|
+
init_context(allowable_tags || '', tag_replacement || '')
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function init_context(allowable_tags: string | string[], tag_replacement: string): Context {
|
|
53
|
+
return {
|
|
54
|
+
allowable_tags: parse_allowable_tags(allowable_tags),
|
|
55
|
+
tag_replacement,
|
|
56
|
+
state: STATE_PLAINTEXT,
|
|
57
|
+
tag_buffer: '',
|
|
58
|
+
depth: 0,
|
|
59
|
+
in_quote_char: ''
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function striptags_internal(html: string, context: Context): string {
|
|
64
|
+
const allowable_tags = context.allowable_tags;
|
|
65
|
+
const tag_replacement = context.tag_replacement;
|
|
66
|
+
|
|
67
|
+
let state = context.state;
|
|
68
|
+
let tag_buffer = context.tag_buffer;
|
|
69
|
+
let depth = context.depth;
|
|
70
|
+
let in_quote_char = context.in_quote_char;
|
|
71
|
+
let output = '';
|
|
72
|
+
|
|
73
|
+
for (let idx = 0, length = html.length; idx < length; idx++) {
|
|
74
|
+
const char = html[idx];
|
|
75
|
+
|
|
76
|
+
if (state === STATE_PLAINTEXT) {
|
|
77
|
+
switch (char) {
|
|
78
|
+
case '<':
|
|
79
|
+
state = STATE_HTML;
|
|
80
|
+
tag_buffer += char;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
output += char;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
} else if (state === STATE_HTML) {
|
|
87
|
+
switch (char) {
|
|
88
|
+
case '<':
|
|
89
|
+
// ignore '<' if inside a quote
|
|
90
|
+
if (in_quote_char) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
// we're seeing a nested '<'
|
|
94
|
+
depth++;
|
|
95
|
+
break;
|
|
96
|
+
case '>':
|
|
97
|
+
// ignore '>' if inside a quote
|
|
98
|
+
if (in_quote_char) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
// something like this is happening: '<<>>'
|
|
102
|
+
if (depth) {
|
|
103
|
+
depth--;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
// this is closing the tag in tag_buffer
|
|
107
|
+
in_quote_char = '';
|
|
108
|
+
state = STATE_PLAINTEXT;
|
|
109
|
+
tag_buffer += '>';
|
|
110
|
+
|
|
111
|
+
if (allowable_tags.has(normalize_tag(tag_buffer))) {
|
|
112
|
+
output += tag_buffer;
|
|
113
|
+
} else {
|
|
114
|
+
output += tag_replacement;
|
|
115
|
+
}
|
|
116
|
+
tag_buffer = '';
|
|
117
|
+
break;
|
|
118
|
+
case '"':
|
|
119
|
+
case '\'':
|
|
120
|
+
// catch both single and double quotes
|
|
121
|
+
if (char === in_quote_char) {
|
|
122
|
+
in_quote_char = '';
|
|
123
|
+
} else {
|
|
124
|
+
in_quote_char = in_quote_char || char;
|
|
125
|
+
}
|
|
126
|
+
tag_buffer += char;
|
|
127
|
+
break;
|
|
128
|
+
case '-':
|
|
129
|
+
if (tag_buffer === '<!-') {
|
|
130
|
+
state = STATE_COMMENT;
|
|
131
|
+
}
|
|
132
|
+
tag_buffer += char;
|
|
133
|
+
break;
|
|
134
|
+
case ' ':
|
|
135
|
+
case '\n':
|
|
136
|
+
if (tag_buffer === '<') {
|
|
137
|
+
state = STATE_PLAINTEXT;
|
|
138
|
+
output += '< ';
|
|
139
|
+
tag_buffer = '';
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
tag_buffer += char;
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
tag_buffer += char;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
} else if (state === STATE_COMMENT) {
|
|
149
|
+
switch (char) {
|
|
150
|
+
case '>':
|
|
151
|
+
if (tag_buffer.slice(-2) === '--') {
|
|
152
|
+
// close the comment
|
|
153
|
+
state = STATE_PLAINTEXT;
|
|
154
|
+
}
|
|
155
|
+
tag_buffer = '';
|
|
156
|
+
break;
|
|
157
|
+
default:
|
|
158
|
+
tag_buffer += char;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// save the context for future iterations
|
|
165
|
+
context.state = state;
|
|
166
|
+
context.tag_buffer = tag_buffer;
|
|
167
|
+
context.depth = depth;
|
|
168
|
+
context.in_quote_char = in_quote_char;
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parse_allowable_tags(allowable_tags: string | string[]): Set<string | null> {
|
|
173
|
+
let tag_set = new Set<string>();
|
|
174
|
+
|
|
175
|
+
if (typeof allowable_tags === 'string') {
|
|
176
|
+
let match;
|
|
177
|
+
while ((match = ALLOWED_TAGS_REGEX.exec(allowable_tags))) {
|
|
178
|
+
tag_set.add(match[1]);
|
|
179
|
+
}
|
|
180
|
+
} else if (typeof allowable_tags[Symbol.iterator] === 'function') {
|
|
181
|
+
tag_set = new Set(allowable_tags);
|
|
182
|
+
}
|
|
183
|
+
return tag_set;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalize_tag(tag_buffer: string) {
|
|
187
|
+
const match = NORMALIZE_TAG_REGEX.exec(tag_buffer);
|
|
188
|
+
return match ? match[1].toLowerCase() : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// init
|
|
192
|
+
return init(htmlText, allowableTags, tagReplacement);
|
|
193
|
+
}
|