@oscarpalmer/toretto 0.24.0 → 0.25.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/html.js +33 -20
- package/dist/internal/sanitize.js +30 -0
- package/dist/sanitize.js +2 -26
- package/dist/toretto.full.js +68 -46
- package/package.json +9 -8
- package/src/html.ts +89 -49
- package/src/internal/sanitize.ts +78 -0
- package/src/sanitize.ts +6 -68
- package/types/html.d.ts +11 -5
- package/types/internal/sanitize.d.ts +13 -0
- package/types/sanitize.d.ts +2 -8
package/dist/html.js
CHANGED
|
@@ -1,38 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { sanitizeNodes } from "./internal/sanitize.js";
|
|
2
|
+
import { isPlainObject } from "@oscarpalmer/atoms/is";
|
|
3
|
+
function createTemplate(html$1, ignore) {
|
|
3
4
|
const template = document.createElement("template");
|
|
4
5
|
template.innerHTML = html$1;
|
|
5
|
-
templates[html$1] = template;
|
|
6
|
+
if (!ignore) templates[html$1] = template;
|
|
6
7
|
return template;
|
|
7
8
|
}
|
|
8
|
-
function
|
|
9
|
+
function getHtml(value, options) {
|
|
10
|
+
if (typeof value !== "string" && !(value instanceof HTMLTemplateElement)) return [];
|
|
11
|
+
const template = value instanceof HTMLTemplateElement ? value : getTemplate(value, options.ignoreCache);
|
|
12
|
+
if (template == null) return [];
|
|
13
|
+
const cloned = template.content.cloneNode(true);
|
|
14
|
+
const scripts = cloned.querySelectorAll("script");
|
|
15
|
+
for (const script of scripts) script.remove();
|
|
16
|
+
cloned.normalize();
|
|
17
|
+
return sanitizeNodes([...cloned.childNodes], options);
|
|
18
|
+
}
|
|
19
|
+
function getOptions(input) {
|
|
20
|
+
const options = isPlainObject(input) ? input : {};
|
|
21
|
+
options.ignoreCache = typeof options.ignoreCache === "boolean" ? options.ignoreCache : false;
|
|
22
|
+
options.sanitizeBooleanAttributes = typeof options.sanitizeBooleanAttributes === "boolean" ? options.sanitizeBooleanAttributes : true;
|
|
23
|
+
return options;
|
|
24
|
+
}
|
|
25
|
+
function getTemplate(value, ignore) {
|
|
9
26
|
if (typeof value !== "string" || value.trim().length === 0) return;
|
|
10
27
|
let template = templates[value];
|
|
11
28
|
if (template != null) return template;
|
|
12
29
|
const element = EXPRESSION_ID.test(value) ? document.querySelector(`#${value}`) : null;
|
|
13
|
-
template = element instanceof HTMLTemplateElement ? element : createTemplate(value);
|
|
14
|
-
templates[value] = template;
|
|
30
|
+
template = element instanceof HTMLTemplateElement ? element : createTemplate(value, ignore);
|
|
15
31
|
return template;
|
|
16
32
|
}
|
|
17
|
-
var html = ((value,
|
|
18
|
-
|
|
19
|
-
let options;
|
|
20
|
-
if (sanitization == null || sanitization === true) options = {};
|
|
21
|
-
else options = sanitization === false ? void 0 : sanitization;
|
|
22
|
-
const template = value instanceof HTMLTemplateElement ? value : getTemplate(value);
|
|
23
|
-
if (template == null) return [];
|
|
24
|
-
const cloned = template.content.cloneNode(true);
|
|
25
|
-
const scripts = cloned.querySelectorAll("script");
|
|
26
|
-
const { length } = scripts;
|
|
27
|
-
for (let index = 0; index < length; index += 1) scripts[index].remove();
|
|
28
|
-
cloned.normalize();
|
|
29
|
-
return options != null ? sanitize([...cloned.childNodes], options) : [...cloned.childNodes];
|
|
33
|
+
var html = ((value, options) => {
|
|
34
|
+
return getHtml(value, getOptions(options));
|
|
30
35
|
});
|
|
31
36
|
html.clear = () => {
|
|
32
37
|
templates = {};
|
|
33
38
|
};
|
|
34
39
|
html.remove = (template) => {
|
|
35
|
-
if (typeof template
|
|
40
|
+
if (typeof template !== "string" || templates[template] == null) return;
|
|
41
|
+
const keys = Object.keys(templates);
|
|
42
|
+
const { length } = keys;
|
|
43
|
+
const updated = {};
|
|
44
|
+
for (let index = 0; index < length; index += 1) {
|
|
45
|
+
const key = keys[index];
|
|
46
|
+
if (key !== template) updated[key] = templates[key];
|
|
47
|
+
}
|
|
48
|
+
templates = updated;
|
|
36
49
|
};
|
|
37
50
|
var EXPRESSION_ID = /^[a-z][\w-]*$/i;
|
|
38
51
|
var templates = {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isBadAttribute, isEmptyNonBooleanAttribute, isInvalidBooleanAttribute } from "../attribute.js";
|
|
2
|
+
import { isPlainObject } from "@oscarpalmer/atoms/is";
|
|
3
|
+
function getSanitizeOptions(input) {
|
|
4
|
+
const options = isPlainObject(input) ? input : {};
|
|
5
|
+
options.sanitizeBooleanAttributes = typeof options.sanitizeBooleanAttributes === "boolean" ? options.sanitizeBooleanAttributes : true;
|
|
6
|
+
return options;
|
|
7
|
+
}
|
|
8
|
+
function sanitizeAttributes(element, attributes, options) {
|
|
9
|
+
const { length } = attributes;
|
|
10
|
+
for (let index = 0; index < length; index += 1) {
|
|
11
|
+
const attribute = attributes[index];
|
|
12
|
+
if (isBadAttribute(attribute) || isEmptyNonBooleanAttribute(attribute)) element.removeAttribute(attribute.name);
|
|
13
|
+
else if (options.sanitizeBooleanAttributes && isInvalidBooleanAttribute(attribute)) element.setAttribute(attribute.name, "");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function sanitizeNodes(nodes, options) {
|
|
17
|
+
const actual = nodes.filter((node) => node instanceof Node);
|
|
18
|
+
const { length } = nodes;
|
|
19
|
+
for (let index = 0; index < length; index += 1) {
|
|
20
|
+
const node = actual[index];
|
|
21
|
+
if (node instanceof Element) {
|
|
22
|
+
const scripts = node.querySelectorAll("script");
|
|
23
|
+
for (const script of scripts) script.remove();
|
|
24
|
+
sanitizeAttributes(node, [...node.attributes], options);
|
|
25
|
+
}
|
|
26
|
+
if (node.hasChildNodes()) sanitizeNodes([...node.childNodes], options);
|
|
27
|
+
}
|
|
28
|
+
return nodes;
|
|
29
|
+
}
|
|
30
|
+
export { getSanitizeOptions, sanitizeAttributes, sanitizeNodes };
|
package/dist/sanitize.js
CHANGED
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPlainObject } from "@oscarpalmer/atoms/is";
|
|
3
|
-
function getOptions(input) {
|
|
4
|
-
const options = isPlainObject(input) ? input : {};
|
|
5
|
-
options.sanitizeBooleanAttributes = typeof options.sanitizeBooleanAttributes === "boolean" ? options.sanitizeBooleanAttributes : true;
|
|
6
|
-
return options;
|
|
7
|
-
}
|
|
1
|
+
import { getSanitizeOptions, sanitizeNodes } from "./internal/sanitize.js";
|
|
8
2
|
function sanitize(value, options) {
|
|
9
|
-
return sanitizeNodes(Array.isArray(value) ? value : [value],
|
|
10
|
-
}
|
|
11
|
-
function sanitizeAttributes(element, attributes, options) {
|
|
12
|
-
const { length } = attributes;
|
|
13
|
-
for (let index = 0; index < length; index += 1) {
|
|
14
|
-
const attribute = attributes[index];
|
|
15
|
-
if (isBadAttribute(attribute) || isEmptyNonBooleanAttribute(attribute)) element.removeAttribute(attribute.name);
|
|
16
|
-
else if (options.sanitizeBooleanAttributes && isInvalidBooleanAttribute(attribute)) element.setAttribute(attribute.name, "");
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function sanitizeNodes(nodes, options) {
|
|
20
|
-
const actual = nodes.filter((node) => node instanceof Node);
|
|
21
|
-
const { length } = nodes;
|
|
22
|
-
for (let index = 0; index < length; index += 1) {
|
|
23
|
-
const node = actual[index];
|
|
24
|
-
if (node instanceof Element) sanitizeAttributes(node, [...node.attributes], options);
|
|
25
|
-
if (node.hasChildNodes()) sanitizeNodes([...node.childNodes], options);
|
|
26
|
-
}
|
|
27
|
-
return nodes;
|
|
3
|
+
return sanitizeNodes(Array.isArray(value) ? value : [value], getSanitizeOptions(options));
|
|
28
4
|
}
|
|
29
5
|
export { sanitize };
|
package/dist/toretto.full.js
CHANGED
|
@@ -437,10 +437,8 @@ function calculate() {
|
|
|
437
437
|
function step(now) {
|
|
438
438
|
if (last != null) values.push(now - last);
|
|
439
439
|
last = now;
|
|
440
|
-
if (values.length >= CALCULATION_TOTAL)
|
|
441
|
-
|
|
442
|
-
resolve(median);
|
|
443
|
-
} else requestAnimationFrame(step);
|
|
440
|
+
if (values.length >= CALCULATION_TOTAL) resolve(values.sort().slice(2, -2).reduce((first, second) => first + second, 0) / (values.length - CALCULATION_TRIM));
|
|
441
|
+
else requestAnimationFrame(step);
|
|
444
442
|
}
|
|
445
443
|
requestAnimationFrame(step);
|
|
446
444
|
});
|
|
@@ -988,7 +986,8 @@ const TABINDEX_BASE = 0;
|
|
|
988
986
|
const TABINDEX_DEFAULT = -1;
|
|
989
987
|
const TYPE_RADIO = 'radio';
|
|
990
988
|
|
|
991
|
-
|
|
989
|
+
//
|
|
990
|
+
function getSanitizeOptions(input) {
|
|
992
991
|
const options = isPlainObject(input) ? input : {};
|
|
993
992
|
options.sanitizeBooleanAttributes =
|
|
994
993
|
typeof options.sanitizeBooleanAttributes === 'boolean'
|
|
@@ -996,15 +995,6 @@ function getOptions(input) {
|
|
|
996
995
|
: true;
|
|
997
996
|
return options;
|
|
998
997
|
}
|
|
999
|
-
/**
|
|
1000
|
-
* Sanitize one or more nodes, recursively
|
|
1001
|
-
* @param value Node or nodes to sanitize
|
|
1002
|
-
* @param options Sanitization options
|
|
1003
|
-
* @returns Sanitized nodes
|
|
1004
|
-
*/
|
|
1005
|
-
function sanitize(value, options) {
|
|
1006
|
-
return sanitizeNodes(Array.isArray(value) ? value : [value], getOptions(options));
|
|
1007
|
-
}
|
|
1008
998
|
function sanitizeAttributes(element, attributes, options) {
|
|
1009
999
|
const { length } = attributes;
|
|
1010
1000
|
for (let index = 0; index < length; index += 1) {
|
|
@@ -1024,6 +1014,10 @@ function sanitizeNodes(nodes, options) {
|
|
|
1024
1014
|
for (let index = 0; index < length; index += 1) {
|
|
1025
1015
|
const node = actual[index];
|
|
1026
1016
|
if (node instanceof Element) {
|
|
1017
|
+
const scripts = node.querySelectorAll('script');
|
|
1018
|
+
for (const script of scripts) {
|
|
1019
|
+
script.remove();
|
|
1020
|
+
}
|
|
1027
1021
|
sanitizeAttributes(node, [...node.attributes], options);
|
|
1028
1022
|
}
|
|
1029
1023
|
if (node.hasChildNodes()) {
|
|
@@ -1034,13 +1028,43 @@ function sanitizeNodes(nodes, options) {
|
|
|
1034
1028
|
}
|
|
1035
1029
|
|
|
1036
1030
|
//
|
|
1037
|
-
function createTemplate(html) {
|
|
1031
|
+
function createTemplate(html, ignore) {
|
|
1038
1032
|
const template = document.createElement('template');
|
|
1039
1033
|
template.innerHTML = html;
|
|
1040
|
-
|
|
1034
|
+
if (!ignore) {
|
|
1035
|
+
templates[html] = template;
|
|
1036
|
+
}
|
|
1041
1037
|
return template;
|
|
1042
1038
|
}
|
|
1043
|
-
function
|
|
1039
|
+
function getHtml(value, options) {
|
|
1040
|
+
if (typeof value !== 'string' && !(value instanceof HTMLTemplateElement)) {
|
|
1041
|
+
return [];
|
|
1042
|
+
}
|
|
1043
|
+
const template = value instanceof HTMLTemplateElement
|
|
1044
|
+
? value
|
|
1045
|
+
: getTemplate(value, options.ignoreCache);
|
|
1046
|
+
if (template == null) {
|
|
1047
|
+
return [];
|
|
1048
|
+
}
|
|
1049
|
+
const cloned = template.content.cloneNode(true);
|
|
1050
|
+
const scripts = cloned.querySelectorAll('script');
|
|
1051
|
+
for (const script of scripts) {
|
|
1052
|
+
script.remove();
|
|
1053
|
+
}
|
|
1054
|
+
cloned.normalize();
|
|
1055
|
+
return sanitizeNodes([...cloned.childNodes], options);
|
|
1056
|
+
}
|
|
1057
|
+
function getOptions(input) {
|
|
1058
|
+
const options = isPlainObject(input) ? input : {};
|
|
1059
|
+
options.ignoreCache =
|
|
1060
|
+
typeof options.ignoreCache === 'boolean' ? options.ignoreCache : false;
|
|
1061
|
+
options.sanitizeBooleanAttributes =
|
|
1062
|
+
typeof options.sanitizeBooleanAttributes === 'boolean'
|
|
1063
|
+
? options.sanitizeBooleanAttributes
|
|
1064
|
+
: true;
|
|
1065
|
+
return options;
|
|
1066
|
+
}
|
|
1067
|
+
function getTemplate(value, ignore) {
|
|
1044
1068
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
1045
1069
|
return;
|
|
1046
1070
|
}
|
|
@@ -1052,48 +1076,46 @@ function getTemplate(value) {
|
|
|
1052
1076
|
? document.querySelector(`#${value}`)
|
|
1053
1077
|
: null;
|
|
1054
1078
|
template =
|
|
1055
|
-
element instanceof HTMLTemplateElement
|
|
1056
|
-
|
|
1079
|
+
element instanceof HTMLTemplateElement
|
|
1080
|
+
? element
|
|
1081
|
+
: createTemplate(value, ignore);
|
|
1057
1082
|
return template;
|
|
1058
1083
|
}
|
|
1059
|
-
const html = ((value,
|
|
1060
|
-
|
|
1061
|
-
return [];
|
|
1062
|
-
}
|
|
1063
|
-
let options;
|
|
1064
|
-
if (sanitization == null || sanitization === true) {
|
|
1065
|
-
options = {};
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
options = sanitization === false ? undefined : sanitization;
|
|
1069
|
-
}
|
|
1070
|
-
const template = value instanceof HTMLTemplateElement ? value : getTemplate(value);
|
|
1071
|
-
if (template == null) {
|
|
1072
|
-
return [];
|
|
1073
|
-
}
|
|
1074
|
-
const cloned = template.content.cloneNode(true);
|
|
1075
|
-
const scripts = cloned.querySelectorAll('script');
|
|
1076
|
-
const { length } = scripts;
|
|
1077
|
-
for (let index = 0; index < length; index += 1) {
|
|
1078
|
-
scripts[index].remove();
|
|
1079
|
-
}
|
|
1080
|
-
cloned.normalize();
|
|
1081
|
-
return options != null
|
|
1082
|
-
? sanitize([...cloned.childNodes], options)
|
|
1083
|
-
: [...cloned.childNodes];
|
|
1084
|
+
const html = ((value, options) => {
|
|
1085
|
+
return getHtml(value, getOptions(options));
|
|
1084
1086
|
});
|
|
1085
1087
|
html.clear = () => {
|
|
1086
1088
|
templates = {};
|
|
1087
1089
|
};
|
|
1088
1090
|
html.remove = (template) => {
|
|
1089
|
-
if (typeof template
|
|
1090
|
-
|
|
1091
|
+
if (typeof template !== 'string' || templates[template] == null) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const keys = Object.keys(templates);
|
|
1095
|
+
const { length } = keys;
|
|
1096
|
+
const updated = {};
|
|
1097
|
+
for (let index = 0; index < length; index += 1) {
|
|
1098
|
+
const key = keys[index];
|
|
1099
|
+
if (key !== template) {
|
|
1100
|
+
updated[key] = templates[key];
|
|
1101
|
+
}
|
|
1091
1102
|
}
|
|
1103
|
+
templates = updated;
|
|
1092
1104
|
};
|
|
1093
1105
|
//
|
|
1094
1106
|
const EXPRESSION_ID = /^[a-z][\w-]*$/i;
|
|
1095
1107
|
let templates = {};
|
|
1096
1108
|
|
|
1109
|
+
/**
|
|
1110
|
+
* Sanitize one or more nodes, recursively
|
|
1111
|
+
* @param value Node or nodes to sanitize
|
|
1112
|
+
* @param options Sanitization options
|
|
1113
|
+
* @returns Sanitized nodes
|
|
1114
|
+
*/
|
|
1115
|
+
function sanitize(value, options) {
|
|
1116
|
+
return sanitizeNodes(Array.isArray(value) ? value : [value], getSanitizeOptions(options));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1097
1119
|
/**
|
|
1098
1120
|
* Get a style from an element
|
|
1099
1121
|
* @param element Element to get the style from
|
package/package.json
CHANGED
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
"url": "https://oscarpalmer.se"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@oscarpalmer/atoms": "^0.
|
|
7
|
+
"@oscarpalmer/atoms": "^0.114"
|
|
8
8
|
},
|
|
9
9
|
"description": "A collection of badass DOM utilities.",
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"@biomejs/biome": "^2.
|
|
11
|
+
"@biomejs/biome": "^2.3",
|
|
12
12
|
"@rollup/plugin-node-resolve": "^16",
|
|
13
|
-
"@rollup/plugin-typescript": "^12.
|
|
14
|
-
"@types/node": "^24.
|
|
15
|
-
"@vitest/coverage-istanbul": "^
|
|
16
|
-
"
|
|
13
|
+
"@rollup/plugin-typescript": "^12.3",
|
|
14
|
+
"@types/node": "^24.10",
|
|
15
|
+
"@vitest/coverage-istanbul": "^4",
|
|
16
|
+
"glob": "^11",
|
|
17
|
+
"jsdom": "^27.1",
|
|
17
18
|
"rollup": "^4.52",
|
|
18
19
|
"tslib": "^2.8",
|
|
19
20
|
"typescript": "^5.9",
|
|
20
21
|
"vite": "npm:rolldown-vite@latest",
|
|
21
|
-
"vitest": "^
|
|
22
|
+
"vitest": "^4"
|
|
22
23
|
},
|
|
23
24
|
"exports": {
|
|
24
25
|
"./package.json": "./package.json",
|
|
@@ -96,5 +97,5 @@
|
|
|
96
97
|
},
|
|
97
98
|
"type": "module",
|
|
98
99
|
"types": "types/index.d.ts",
|
|
99
|
-
"version": "0.
|
|
100
|
+
"version": "0.25.0"
|
|
100
101
|
}
|
package/src/html.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {type SanitizeOptions, sanitizeNodes} from './internal/sanitize';
|
|
2
3
|
|
|
3
4
|
//
|
|
4
5
|
|
|
@@ -6,21 +7,18 @@ type Html = {
|
|
|
6
7
|
/**
|
|
7
8
|
* Create nodes from an HTML string or a template element
|
|
8
9
|
* @param value HTML string or id for a template element
|
|
9
|
-
* @param
|
|
10
|
+
* @param options Options for creating nodes
|
|
10
11
|
* @returns Created nodes
|
|
11
12
|
*/
|
|
12
|
-
(value: string,
|
|
13
|
+
(value: string, options?: HtmlOptions): Node[];
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Create nodes from a template element
|
|
16
17
|
* @param template Template element
|
|
17
|
-
* @param
|
|
18
|
+
* @param options Options for creating nodes
|
|
18
19
|
* @returns Created nodes
|
|
19
20
|
*/
|
|
20
|
-
(
|
|
21
|
-
template: HTMLTemplateElement,
|
|
22
|
-
sanitization?: boolean | SanitizeOptions,
|
|
23
|
-
): Node[];
|
|
21
|
+
(template: HTMLTemplateElement, options?: HtmlOptions): Node[];
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* Clear cache of template elements
|
|
@@ -34,19 +32,77 @@ type Html = {
|
|
|
34
32
|
remove(template: string): void;
|
|
35
33
|
};
|
|
36
34
|
|
|
35
|
+
type HtmlOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* Ignore caching the template element for the HTML string? _(defaults to `false`)_
|
|
38
|
+
*/
|
|
39
|
+
ignoreCache?: boolean;
|
|
40
|
+
} & SanitizeOptions;
|
|
41
|
+
|
|
42
|
+
type Options = Required<HtmlOptions>;
|
|
43
|
+
|
|
37
44
|
//
|
|
38
45
|
|
|
39
|
-
function createTemplate(html: string): HTMLTemplateElement {
|
|
46
|
+
function createTemplate(html: string, ignore: boolean): HTMLTemplateElement {
|
|
40
47
|
const template = document.createElement('template');
|
|
41
48
|
|
|
42
49
|
template.innerHTML = html;
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
if (!ignore) {
|
|
52
|
+
templates[html] = template;
|
|
53
|
+
}
|
|
45
54
|
|
|
46
55
|
return template;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
function
|
|
58
|
+
function getHtml(
|
|
59
|
+
value: string | HTMLTemplateElement,
|
|
60
|
+
options: Options,
|
|
61
|
+
): Node[] {
|
|
62
|
+
if (typeof value !== 'string' && !(value instanceof HTMLTemplateElement)) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const template =
|
|
67
|
+
value instanceof HTMLTemplateElement
|
|
68
|
+
? value
|
|
69
|
+
: getTemplate(value, options.ignoreCache);
|
|
70
|
+
|
|
71
|
+
if (template == null) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cloned = template.content.cloneNode(true) as DocumentFragment;
|
|
76
|
+
|
|
77
|
+
const scripts = cloned.querySelectorAll('script');
|
|
78
|
+
|
|
79
|
+
for (const script of scripts) {
|
|
80
|
+
script.remove();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cloned.normalize();
|
|
84
|
+
|
|
85
|
+
return sanitizeNodes([...cloned.childNodes], options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getOptions(input?: HtmlOptions): Options {
|
|
89
|
+
const options = isPlainObject(input) ? input : {};
|
|
90
|
+
|
|
91
|
+
options.ignoreCache =
|
|
92
|
+
typeof options.ignoreCache === 'boolean' ? options.ignoreCache : false;
|
|
93
|
+
|
|
94
|
+
options.sanitizeBooleanAttributes =
|
|
95
|
+
typeof options.sanitizeBooleanAttributes === 'boolean'
|
|
96
|
+
? options.sanitizeBooleanAttributes
|
|
97
|
+
: true;
|
|
98
|
+
|
|
99
|
+
return options as Options;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getTemplate(
|
|
103
|
+
value: string,
|
|
104
|
+
ignore: boolean,
|
|
105
|
+
): HTMLTemplateElement | undefined {
|
|
50
106
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
51
107
|
return;
|
|
52
108
|
}
|
|
@@ -62,66 +118,50 @@ function getTemplate(value: string): HTMLTemplateElement | undefined {
|
|
|
62
118
|
: null;
|
|
63
119
|
|
|
64
120
|
template =
|
|
65
|
-
element instanceof HTMLTemplateElement
|
|
66
|
-
|
|
67
|
-
|
|
121
|
+
element instanceof HTMLTemplateElement
|
|
122
|
+
? element
|
|
123
|
+
: createTemplate(value, ignore);
|
|
68
124
|
|
|
69
125
|
return template;
|
|
70
126
|
}
|
|
71
127
|
|
|
72
128
|
const html = ((
|
|
73
129
|
value: string | HTMLTemplateElement,
|
|
74
|
-
|
|
130
|
+
options?: Options,
|
|
75
131
|
): Node[] => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
132
|
+
return getHtml(value, getOptions(options));
|
|
133
|
+
}) as Html;
|
|
79
134
|
|
|
80
|
-
|
|
135
|
+
html.clear = (): void => {
|
|
136
|
+
templates = {};
|
|
137
|
+
};
|
|
81
138
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
options = sanitization === false ? undefined : sanitization;
|
|
139
|
+
html.remove = (template: string): void => {
|
|
140
|
+
if (typeof template !== 'string' || templates[template] == null) {
|
|
141
|
+
return;
|
|
86
142
|
}
|
|
87
143
|
|
|
88
|
-
const
|
|
89
|
-
|
|
144
|
+
const keys = Object.keys(templates);
|
|
145
|
+
const {length} = keys;
|
|
90
146
|
|
|
91
|
-
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const cloned = template.content.cloneNode(true) as DocumentFragment;
|
|
96
|
-
const scripts = cloned.querySelectorAll('script');
|
|
97
|
-
const {length} = scripts;
|
|
147
|
+
const updated: Record<string, HTMLTemplateElement> = {};
|
|
98
148
|
|
|
99
149
|
for (let index = 0; index < length; index += 1) {
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
cloned.normalize();
|
|
104
|
-
|
|
105
|
-
return options != null
|
|
106
|
-
? sanitize([...cloned.childNodes], options)
|
|
107
|
-
: [...cloned.childNodes];
|
|
108
|
-
}) as Html;
|
|
150
|
+
const key = keys[index];
|
|
109
151
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
html.remove = (template: string): void => {
|
|
115
|
-
if (typeof template === 'string') {
|
|
116
|
-
templates[template] = undefined;
|
|
152
|
+
if (key !== template) {
|
|
153
|
+
updated[key] = templates[key];
|
|
154
|
+
}
|
|
117
155
|
}
|
|
156
|
+
|
|
157
|
+
templates = updated;
|
|
118
158
|
};
|
|
119
159
|
|
|
120
160
|
//
|
|
121
161
|
|
|
122
162
|
const EXPRESSION_ID = /^[a-z][\w-]*$/i;
|
|
123
163
|
|
|
124
|
-
let templates: Record<string, HTMLTemplateElement
|
|
164
|
+
let templates: Record<string, HTMLTemplateElement> = {};
|
|
125
165
|
|
|
126
166
|
//
|
|
127
167
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {
|
|
3
|
+
isBadAttribute,
|
|
4
|
+
isEmptyNonBooleanAttribute,
|
|
5
|
+
isInvalidBooleanAttribute,
|
|
6
|
+
} from '../attribute';
|
|
7
|
+
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
type Options = Required<SanitizeOptions>;
|
|
11
|
+
|
|
12
|
+
export type SanitizeOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* Sanitize boolean attributes? _(Defaults to `true`)_
|
|
15
|
+
*
|
|
16
|
+
* E.g. `checked="abc"` => `checked=""`
|
|
17
|
+
*/
|
|
18
|
+
sanitizeBooleanAttributes?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
//
|
|
22
|
+
|
|
23
|
+
export function getSanitizeOptions(input?: SanitizeOptions): Options {
|
|
24
|
+
const options = isPlainObject(input) ? input : {};
|
|
25
|
+
|
|
26
|
+
options.sanitizeBooleanAttributes =
|
|
27
|
+
typeof options.sanitizeBooleanAttributes === 'boolean'
|
|
28
|
+
? options.sanitizeBooleanAttributes
|
|
29
|
+
: true;
|
|
30
|
+
|
|
31
|
+
return options as Options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function sanitizeAttributes(
|
|
35
|
+
element: Element,
|
|
36
|
+
attributes: Attr[],
|
|
37
|
+
options: Options,
|
|
38
|
+
): void {
|
|
39
|
+
const {length} = attributes;
|
|
40
|
+
|
|
41
|
+
for (let index = 0; index < length; index += 1) {
|
|
42
|
+
const attribute = attributes[index];
|
|
43
|
+
|
|
44
|
+
if (isBadAttribute(attribute) || isEmptyNonBooleanAttribute(attribute)) {
|
|
45
|
+
element.removeAttribute(attribute.name);
|
|
46
|
+
} else if (
|
|
47
|
+
options.sanitizeBooleanAttributes &&
|
|
48
|
+
isInvalidBooleanAttribute(attribute)
|
|
49
|
+
) {
|
|
50
|
+
element.setAttribute(attribute.name, '');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function sanitizeNodes(nodes: Node[], options: Options): Node[] {
|
|
56
|
+
const actual = nodes.filter(node => node instanceof Node);
|
|
57
|
+
const {length} = nodes;
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < length; index += 1) {
|
|
60
|
+
const node = actual[index];
|
|
61
|
+
|
|
62
|
+
if (node instanceof Element) {
|
|
63
|
+
const scripts = node.querySelectorAll('script');
|
|
64
|
+
|
|
65
|
+
for (const script of scripts) {
|
|
66
|
+
script.remove();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
sanitizeAttributes(node, [...node.attributes], options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (node.hasChildNodes()) {
|
|
73
|
+
sanitizeNodes([...node.childNodes], options);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return nodes;
|
|
78
|
+
}
|
package/src/sanitize.ts
CHANGED
|
@@ -1,30 +1,8 @@
|
|
|
1
|
-
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from './
|
|
7
|
-
|
|
8
|
-
type Options = Required<SanitizeOptions>;
|
|
9
|
-
|
|
10
|
-
export type SanitizeOptions = {
|
|
11
|
-
/**
|
|
12
|
-
* - Sanitize boolean attributes? _(Defaults to `true`)_
|
|
13
|
-
* - E.g. `checked="abc"` => `checked=""`
|
|
14
|
-
*/
|
|
15
|
-
sanitizeBooleanAttributes?: boolean;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function getOptions(input?: SanitizeOptions): Options {
|
|
19
|
-
const options = isPlainObject(input) ? input : {};
|
|
20
|
-
|
|
21
|
-
options.sanitizeBooleanAttributes =
|
|
22
|
-
typeof options.sanitizeBooleanAttributes === 'boolean'
|
|
23
|
-
? options.sanitizeBooleanAttributes
|
|
24
|
-
: true;
|
|
25
|
-
|
|
26
|
-
return options as Options;
|
|
27
|
-
}
|
|
2
|
+
getSanitizeOptions,
|
|
3
|
+
type SanitizeOptions,
|
|
4
|
+
sanitizeNodes,
|
|
5
|
+
} from './internal/sanitize';
|
|
28
6
|
|
|
29
7
|
/**
|
|
30
8
|
* Sanitize one or more nodes, recursively
|
|
@@ -34,50 +12,10 @@ function getOptions(input?: SanitizeOptions): Options {
|
|
|
34
12
|
*/
|
|
35
13
|
export function sanitize(
|
|
36
14
|
value: Node | Node[],
|
|
37
|
-
options?:
|
|
15
|
+
options?: SanitizeOptions,
|
|
38
16
|
): Node[] {
|
|
39
17
|
return sanitizeNodes(
|
|
40
18
|
Array.isArray(value) ? value : [value],
|
|
41
|
-
|
|
19
|
+
getSanitizeOptions(options),
|
|
42
20
|
);
|
|
43
21
|
}
|
|
44
|
-
|
|
45
|
-
function sanitizeAttributes(
|
|
46
|
-
element: Element,
|
|
47
|
-
attributes: Attr[],
|
|
48
|
-
options: SanitizeOptions,
|
|
49
|
-
): void {
|
|
50
|
-
const {length} = attributes;
|
|
51
|
-
|
|
52
|
-
for (let index = 0; index < length; index += 1) {
|
|
53
|
-
const attribute = attributes[index];
|
|
54
|
-
|
|
55
|
-
if (isBadAttribute(attribute) || isEmptyNonBooleanAttribute(attribute)) {
|
|
56
|
-
element.removeAttribute(attribute.name);
|
|
57
|
-
} else if (
|
|
58
|
-
options.sanitizeBooleanAttributes &&
|
|
59
|
-
isInvalidBooleanAttribute(attribute)
|
|
60
|
-
) {
|
|
61
|
-
element.setAttribute(attribute.name, '');
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function sanitizeNodes(nodes: Node[], options: SanitizeOptions): Node[] {
|
|
67
|
-
const actual = nodes.filter(node => node instanceof Node);
|
|
68
|
-
const {length} = nodes;
|
|
69
|
-
|
|
70
|
-
for (let index = 0; index < length; index += 1) {
|
|
71
|
-
const node = actual[index];
|
|
72
|
-
|
|
73
|
-
if (node instanceof Element) {
|
|
74
|
-
sanitizeAttributes(node, [...node.attributes], options);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (node.hasChildNodes()) {
|
|
78
|
-
sanitizeNodes([...node.childNodes], options);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return nodes;
|
|
83
|
-
}
|
package/types/html.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { type SanitizeOptions } from './sanitize';
|
|
1
|
+
import { type SanitizeOptions } from './internal/sanitize';
|
|
2
2
|
type Html = {
|
|
3
3
|
/**
|
|
4
4
|
* Create nodes from an HTML string or a template element
|
|
5
5
|
* @param value HTML string or id for a template element
|
|
6
|
-
* @param
|
|
6
|
+
* @param options Options for creating nodes
|
|
7
7
|
* @returns Created nodes
|
|
8
8
|
*/
|
|
9
|
-
(value: string,
|
|
9
|
+
(value: string, options?: HtmlOptions): Node[];
|
|
10
10
|
/**
|
|
11
11
|
* Create nodes from a template element
|
|
12
12
|
* @param template Template element
|
|
13
|
-
* @param
|
|
13
|
+
* @param options Options for creating nodes
|
|
14
14
|
* @returns Created nodes
|
|
15
15
|
*/
|
|
16
|
-
(template: HTMLTemplateElement,
|
|
16
|
+
(template: HTMLTemplateElement, options?: HtmlOptions): Node[];
|
|
17
17
|
/**
|
|
18
18
|
* Clear cache of template elements
|
|
19
19
|
*/
|
|
@@ -24,5 +24,11 @@ type Html = {
|
|
|
24
24
|
*/
|
|
25
25
|
remove(template: string): void;
|
|
26
26
|
};
|
|
27
|
+
type HtmlOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* Ignore caching the template element for the HTML string? _(defaults to `false`)_
|
|
30
|
+
*/
|
|
31
|
+
ignoreCache?: boolean;
|
|
32
|
+
} & SanitizeOptions;
|
|
27
33
|
declare const html: Html;
|
|
28
34
|
export { html };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type Options = Required<SanitizeOptions>;
|
|
2
|
+
export type SanitizeOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Sanitize boolean attributes? _(Defaults to `true`)_
|
|
5
|
+
*
|
|
6
|
+
* E.g. `checked="abc"` => `checked=""`
|
|
7
|
+
*/
|
|
8
|
+
sanitizeBooleanAttributes?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function getSanitizeOptions(input?: SanitizeOptions): Options;
|
|
11
|
+
export declare function sanitizeAttributes(element: Element, attributes: Attr[], options: Options): void;
|
|
12
|
+
export declare function sanitizeNodes(nodes: Node[], options: Options): Node[];
|
|
13
|
+
export {};
|
package/types/sanitize.d.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* - Sanitize boolean attributes? _(Defaults to `true`)_
|
|
4
|
-
* - E.g. `checked="abc"` => `checked=""`
|
|
5
|
-
*/
|
|
6
|
-
sanitizeBooleanAttributes?: boolean;
|
|
7
|
-
};
|
|
1
|
+
import { type SanitizeOptions } from './internal/sanitize';
|
|
8
2
|
/**
|
|
9
3
|
* Sanitize one or more nodes, recursively
|
|
10
4
|
* @param value Node or nodes to sanitize
|
|
11
5
|
* @param options Sanitization options
|
|
12
6
|
* @returns Sanitized nodes
|
|
13
7
|
*/
|
|
14
|
-
export declare function sanitize(value: Node | Node[], options?:
|
|
8
|
+
export declare function sanitize(value: Node | Node[], options?: SanitizeOptions): Node[];
|