@shgysk8zer0/polyfills 0.0.7 → 0.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @copyright 2022-2023 Chris Zuber <admin@kernvalley.us>
3
3
  */
4
- import { nativeSupport, getSantizerUtils, sanitize, sanitizeFor, trustPolicies } from './sanitizerUtils.js';
4
+ import { nativeSupport, getSantizerUtils, sanitize, trustPolicies } from './sanitizerUtils.js';
5
5
  import { SanitizerConfig as defaultConfig } from './SanitizerConfigW3C.js';
6
6
 
7
7
  const protectedData = new WeakMap();
@@ -43,11 +43,13 @@ export class Sanitizer {
43
43
  }
44
44
 
45
45
  sanitize(input) {
46
- return sanitize(input, { config: this.getConfiguration() });
46
+ return sanitize(input, this.getConfiguration());
47
47
  }
48
48
 
49
49
  sanitizeFor(tag, content) {
50
- return sanitizeFor(tag, content, { config: this.getConfiguration() });
50
+ const el = document.createElement(tag);
51
+ el.setHTML(content, this.getConfiguration());
52
+ return el;
51
53
  }
52
54
 
53
55
  static getDefaultConfiguration() {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * This **REQUIRES** a CSP with `style-src blob:`
3
+ */
4
+ export const adoptedStyleSheets = {
5
+ get() {
6
+ return Array.from(this.styleSheets)
7
+ .filter(sheet => sheet.ownerNode.classList.contains('_adopted'));
8
+ },
9
+ set(sheets) {
10
+ if (! Array.isArray(sheets)) {
11
+ throw new TypeError('Must be an array');
12
+ } else {
13
+ const current = new Set(this.adoptedStyleSheets);
14
+
15
+ current.difference(sheets).forEach(sheet => sheet.ownerNode.remove());
16
+
17
+ const links = [...new Set(sheets).difference(current)].map(sheet => {
18
+ const link = document.createElement('link');
19
+ const controller = new AbortController();
20
+ const signal = controller.signal;
21
+ const url = URL.createObjectURL(new File([], 'adopted.css', { type: 'text/css' }));
22
+ link.rel = 'stylesheet';
23
+ link.href = url;
24
+ link.classList.add('_adopted');
25
+ link.disabled = sheet.disabled;
26
+
27
+
28
+ link.addEventListener('load', ({ target }) => {
29
+ controller.abort();
30
+ link.media = sheet.media.mediaText;
31
+ [...sheet.cssRules].forEach((rule, index) => {
32
+ target.sheet.insertRule(rule.cssText, index);
33
+ });
34
+
35
+ setTimeout(() => URL.revokeObjectURL(target.href), 100);
36
+ }, { once: true, signal });
37
+
38
+ link.addEventListener('error', ({ target }) => {
39
+ controller.abort();
40
+ URL.revokeObjectURL(target.href);
41
+ }, { once: true, signal });
42
+
43
+ return link;
44
+ });
45
+
46
+ if (this instanceof Document) {
47
+ this.head.append(...links);
48
+ } else {
49
+ this.append(...links);
50
+ }
51
+ }
52
+ }
53
+ };
@@ -0,0 +1,6 @@
1
+ export const HTML = 'http://www.w3.org/1999/xhtml';
2
+ export const SVG = 'http://www.w3.org/2000/svg';
3
+ export const MathML = 'http://www.w3.org/1998/Math/MathML';
4
+ export const XML = 'http://www.w3.org/XML/1998/namespace';
5
+ export const XMLNS = 'http://www.w3.org/2000/xmlns/';
6
+ export const XSLT = 'http://www.w3.org/1999/XSL/Transform';
@@ -9,6 +9,11 @@ import { urls } from './attributes.js';
9
9
  export const supported = () => 'Sanitizer' in globalThis;
10
10
  export const nativeSupport = supported();
11
11
 
12
+ export const setHTML = function setHTML(el, input, opts = defaultConfig) {
13
+ const doc = safeParseHTML(input, opts);
14
+ el.append(documentToFragment(doc));
15
+ };
16
+
12
17
  const allowProtocols = ['https:'];
13
18
 
14
19
  if (! allowProtocols.includes(location.protocol)) {
@@ -18,166 +23,190 @@ const policyName = 'sanitizer-raw#html';
18
23
  const getPolicy = callOnce(() => createPolicy(policyName, { createHTML: input => input }));
19
24
  const createHTML = input => getPolicy().createHTML(input);
20
25
 
21
- function documentToFragment(doc) {
26
+ export function documentToFragment(doc) {
22
27
  const frag = document.createDocumentFragment();
23
28
  const clone = doc.cloneNode(true);
24
29
  frag.append(...clone.head.childNodes, ...clone.body.childNodes);
25
30
  return frag;
26
31
  }
27
32
 
28
- export function sanitize(input, { config = defaultConfig } = {}) {
33
+ /**
34
+ * Helper function to adapt to changes in spec
35
+ */
36
+ export function convertSanitizerConfig({
37
+ allowAttributes, allowComments, allowElements, allowCustomElements,
38
+ blockElements, dropAttributes, dropElements, allowUnknownMarkup, sanitizer,
39
+ } = {}, context) {
40
+ if (sanitizer instanceof Sanitizer) {
41
+ return convertSanitizerConfig(sanitizer.getConfiguration(), context);
42
+ } else {
43
+ switch (context) {
44
+ default:
45
+ if (typeof allowAttributes === 'undefined' && typeof dropAttributes === 'undefined') {
46
+ allowAttributes = defaultConfig.allowAttributes;
47
+ }
48
+
49
+ if (typeof allowElements === 'undefined' && typeof dropElements === 'undefined') {
50
+ allowElements = defaultConfig.allowElements;
51
+ }
52
+ return {
53
+ allowAttributes, allowComments, allowElements, allowCustomElements,
54
+ blockElements, dropAttributes, dropElements, allowUnknownMarkup,
55
+ };
56
+ }
57
+ }
58
+ }
59
+
60
+ export function convertToSanitizerConfig({
61
+ allowAttributes, allowComments, allowElements, allowCustomElements,
62
+ blockElements, dropAttributes, dropElements, allowUnknownMarkup,
63
+ } = {}) {
64
+ if (typeof allowAttributes === 'undefined' && typeof dropAttributes === 'undefined') {
65
+ allowAttributes = defaultConfig.allowAttributes;
66
+ }
67
+
68
+ if (typeof allowElements === 'undefined' && typeof dropElements === 'undefined') {
69
+ allowElements = defaultConfig.allowElements;
70
+ }
71
+ return {
72
+ allowAttributes, allowComments, allowElements, allowCustomElements,
73
+ blockElements, dropAttributes, dropElements, allowUnknownMarkup,
74
+ };
75
+ }
76
+
77
+ export function safeParseHTML(input, opts = defaultConfig) {
78
+ const doc = new DOMParser().parseFromString(createHTML(input), 'text/html');
79
+ // Not sure if this will be in spec, but it is necessary
80
+ if (Array.isArray(opts.allowElements) && ! opts.allowElements.includes('html') ) {
81
+ opts.allowElements = [...new Set([...opts.allowElements, 'html' ,'head', 'body'])];
82
+ }
83
+ return sanitizeNode(doc, opts);
84
+ }
85
+
86
+ export function sanitize(input, opts = defaultConfig) {
29
87
  if (! (input instanceof Node)) {
30
88
  throw new TypeError('sanitize requires a Document or DocumentFragment');
31
89
  } else if (input.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
32
- return sanitizeNode(input, config);
90
+ return sanitizeNode(input, opts);
33
91
  } else if (input.nodeType === Node.DOCUMENT_NODE) {
34
- return sanitizeNode(documentToFragment(input), { config });
92
+ return sanitizeNode(documentToFragment(input), opts);
35
93
  } else {
36
94
  throw new TypeError('sanitize requires a Document or DocumentFragment');
37
95
  }
38
96
  }
39
97
 
40
- export function sanitizeFor(tag, content, { config = defaultConfig } = {}) {
98
+ export function sanitizeFor(tag, content, opts = defaultConfig) {
41
99
  const el = document.createElement(tag);
42
100
  const temp = document.createElement('template');
43
101
  temp.innerHTML = createHTML(content);
44
- el.append(sanitize(temp.content, { config }));
102
+ el.append(sanitize(temp.content, opts));
45
103
  return el;
46
104
  }
47
105
 
48
- export function sanitizeNode(node, { config = defaultConfig } = {}) {
106
+ export function sanitizeNode(root, opts = defaultConfig) {
49
107
  try {
50
- if (! (node instanceof Node)) {
51
- throw new TypeError(`Expected a Node but got a ${getType(node)}.`);
52
- } else if (! isObject(config)) {
53
- throw new TypeError(`Expected config to be an object but got ${getType(config)}.`);
108
+ if (! (root instanceof Node)) {
109
+ throw new TypeError(`Expected a Node but got a ${getType(root)}.`);
110
+ } else if (! isObject(opts)) {
111
+ throw new TypeError(`Expected config to be an object but got ${getType(opts)}.`);
54
112
  }
55
113
 
56
114
  const {
57
115
  allowElements, allowComments, allowAttributes, allowCustomElements,
58
116
  blockElements, dropAttributes, dropElements, allowUnknownMarkup,
59
- } = config;
60
-
61
- switch(node.nodeType) {
62
- case Node.TEXT_NODE:
63
- break;
64
-
65
- case Node.ELEMENT_NODE: {
66
- if (
67
- ! allowUnknownMarkup
68
- && ( ! (node instanceof HTMLElement) || node instanceof HTMLUnknownElement)
69
- ) {
70
- node.remove();
71
- break;
72
- }
73
-
74
- const tag = node.tagName.toLowerCase();
117
+ } = convertSanitizerConfig(opts);
75
118
 
76
- if (Array.isArray(dropElements) && dropElements.includes(tag)) {
77
- node.remove();
78
- } else if (Array.isArray(blockElements) && blockElements.includes(tag)) {
79
- if (node.hasChildNodes()) {
80
- [...node.childNodes].forEach(node => sanitizeNode(node, config));
81
- node.replaceWith(...node.childNodes);
82
- } else {
83
- node.remove();
84
- }
85
- } else if (tag.includes('-') && ! allowCustomElements) {
86
- node.remove();
87
- } else if (Array.isArray(allowElements) && ! allowElements.includes(tag)) {
88
- node.remove();
89
- } else if (tag === 'template') {
90
- sanitizeNode(node.content, config);
91
- } else {
92
- if (node.hasAttributes()) {
93
- node.getAttributeNames()
94
- .forEach(attr => sanitizeNode(node.getAttributeNode(attr), config));
95
- }
96
-
97
- if (node.hasChildNodes()) {
98
- [...node.childNodes].forEach(node => sanitizeNode(node, config));
99
- }
100
- }
119
+ const iter = document.createNodeIterator(
120
+ root,
121
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
122
+ );
101
123
 
102
- break;
103
- }
124
+ let node = iter.root.nodeType === Node.ELEMENT_NODE
125
+ ? iter.root
126
+ : iter.nextNode();
104
127
 
105
- case Node.ATTRIBUTE_NODE: {
106
- const { value, ownerElement } = node;
107
- const name = node.name.toLowerCase();
108
- const tag = ownerElement.tagName.toLowerCase();
109
-
110
- if (
111
- urls.includes(name)
112
- && ! allowProtocols.includes(new URL(value, document.baseURI).protocol)
113
- ) {
114
- ownerElement.removeAttributeNode(node);
115
- } else if (isObject(dropAttributes)) {
128
+ while (node instanceof Node) {
129
+ switch(node.nodeType) {
130
+ case Node.ELEMENT_NODE: {
116
131
  if (
117
- name in dropAttributes
118
- && ['*', tag].some(sel => dropAttributes[name].includes(sel))
132
+ ! allowUnknownMarkup
133
+ && ( ! (node instanceof HTMLElement) || node instanceof HTMLUnknownElement)
119
134
  ) {
120
- ownerElement.removeAttributeNode(node);
121
-
122
- if (name.startsWith('on')) {
123
- delete ownerElement[name];
124
- }
135
+ node.remove();
136
+ break;
125
137
  }
126
- } else if (isObject(allowAttributes)) {
127
- if (
128
- ! name.startsWith('data-')
129
- && ! (name in allowAttributes
130
- && ['*', tag].some(sel => allowAttributes[name].includes(sel)))
131
- ) {
132
- ownerElement.removeAttributeNode(node);
133
138
 
134
- if (name.startsWith('on')) {
135
- delete ownerElement[name];
139
+ const tag = node.tagName.toLowerCase();
140
+
141
+ if (Array.isArray(dropElements) && dropElements.includes(tag)) {
142
+ node.remove();
143
+ } else if (Array.isArray(blockElements) && blockElements.includes(tag)) {
144
+ if (node.hasChildNodes()) {
145
+ node.replaceWith(...node.childNodes);
146
+ } else {
147
+ node.remove();
148
+ }
149
+ } else if (tag.includes('-') && ! allowCustomElements) {
150
+ node.remove();
151
+ } else if (Array.isArray(allowElements) && ! allowElements.includes(tag)) {
152
+ node.remove();
153
+ } else {
154
+ if (node.hasAttributes()) {
155
+ node.getAttributeNames().forEach(name => {
156
+ const attr = node.getAttributeNode(name);
157
+ const { value } = attr;
158
+
159
+ if (
160
+ urls.includes(name)
161
+ && ! allowProtocols.includes(new URL(value, document.baseURI).protocol)
162
+ ) {
163
+ node.removeAttributeNode(attr);
164
+ } else if (isObject(dropAttributes)) {
165
+ if (
166
+ name in dropAttributes
167
+ && ['*', tag].some(sel => dropAttributes[name].includes(sel))
168
+ ) {
169
+ node.removeAttributeNode(attr);
170
+ }
171
+ } else if (isObject(allowAttributes)) {
172
+ if (
173
+ ! (name in allowAttributes
174
+ && ['*', tag].some(sel => allowAttributes[name].includes(sel)))
175
+ ) {
176
+ node.removeAttributeNode(attr);
177
+ }
178
+ }
179
+ });
136
180
  }
137
181
  }
138
- }
139
-
140
- break;
141
- }
142
182
 
143
- case Node.COMMENT_NODE: {
144
- if (! allowComments) {
145
- node.remove();
183
+ break;
146
184
  }
147
185
 
148
- break;
149
- }
186
+ case Node.COMMENT_NODE: {
187
+ if (! allowComments) {
188
+ node.remove();
189
+ }
150
190
 
151
- case Node.DOCUMENT_NODE:
152
- case Node.DOCUMENT_FRAGMENT_NODE: {
153
- if (node.hasChildNodes()) {
154
- [...node.childNodes].forEach(node => sanitizeNode(node, config));
191
+ break;
155
192
  }
156
-
157
- break;
158
193
  }
159
194
 
160
- case Node.CDATA_SECTION_NODE:
161
- case Node.PROCESSING_INSTRUCTION_NODE:
162
- case Node.DOCUMENT_TYPE_NODE:
163
- default: {
164
- node.parentElement.removeChild(node);
195
+ if (node.localName === 'template') {
196
+ sanitizeNode(node.content, opts);
165
197
  }
198
+
199
+ node = iter.nextNode();
166
200
  }
201
+
202
+ return root;
167
203
  } catch(err) {
168
- node.parentElement.removeChild(node);
169
204
  console.error(err);
205
+ root.parentElement.removeChild(root);
170
206
  }
171
-
172
- return node;
173
207
  }
174
208
 
175
209
  export function getSantizerUtils(Sanitizer, defaultConfig) {
176
- const setHTML = function setHTML(el, input, { sanitizer = new Sanitizer() } = {}) {
177
- const div = sanitizer.sanitizeFor('div', input);
178
- el.replaceChildren(...div.children);
179
- };
180
-
181
210
  const polyfill = function polyfill() {
182
211
  let polyfilled = false;
183
212
 
@@ -225,37 +254,24 @@ export function getSantizerUtils(Sanitizer, defaultConfig) {
225
254
  } else if (! [Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(input.nodeType)) {
226
255
  throw new TypeError('Expected a Document or DocumentFragment in `Sanitizer.sanitize()`.');
227
256
  } else {
228
- return sanitize(input, { config: this.getConfiguration() });
257
+ return sanitize(input, this.getConfiguration());
229
258
  }
230
259
  };
231
260
  polyfilled = true;
232
261
  }
233
262
 
234
- if (
235
- ! (globalThis.Sanitizer.prototype.sanitizeFor instanceof Function)
236
- && Element.prototype.setHTML instanceof Function
237
- ) {
238
- globalThis.Sanitizer.prototype.sanitizeFor = function(element, input) {
239
- const el = document.createElement(element);
240
- el.setHTML(input, { sanitizer: this });
241
- return el;
242
- };
243
- polyfilled = true;
244
- } else if (! (globalThis.Sanitizer.prototype.sanitizeFor instanceof Function)) {
263
+ if (! (globalThis.Sanitizer.prototype.sanitizeFor instanceof Function)) {
245
264
  globalThis.Sanitizer.prototype.sanitizeFor = function(element, input) {
246
265
  const el = document.createElement(element);
247
- const tmp = document.createElement('template');
248
- tmp.innerHTML = createHTML(input);
249
- el.append(this.sanitize(tmp.content));
266
+ setHTML(el, input,this.getConfiguration());
250
267
  return el;
251
268
  };
252
269
  polyfilled = true;
253
270
  }
254
271
 
255
272
  if (! (Element.prototype.setHTML instanceof Function)) {
256
- Element.prototype.setHTML = function(input, { sanitizer = new globalThis.Sanitizer() } = {}) {
257
- const el = sanitizer.sanitizeFor('div', input);
258
- this.replaceChildren(...el.children);
273
+ Element.prototype.setHTML = function(input, opts = defaultConfig) {
274
+ setHTML(this, input, opts);
259
275
  };
260
276
  polyfilled = true;
261
277
  }
@@ -0,0 +1,3 @@
1
+ import { polyfill, trustPolicies } from '../assets/Sanitizer.js';
2
+ export const polyfilled = polyfill();
3
+ export { trustPolicies };
package/element.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { aria } from './aom.js';
2
+ import { overwriteMethod } from './utils.js';
3
+ import { SanitizerConfig as defaultConfig } from './assets/SanitizerConfigW3C.js';
4
+ import { setHTML as safeSetHTML, convertToSanitizerConfig } from './assets/sanitizerUtils.js';
2
5
 
3
6
  if (! (HTMLScriptElement.supports instanceof Function)) {
4
7
  HTMLScriptElement.supports = function supports(type) {
@@ -123,22 +126,21 @@ if (! (HTMLImageElement.prototype.decode instanceof Function)) {
123
126
  };
124
127
  }
125
128
 
126
- if (
127
- ! (Element.prototype.setHTML instanceof Function)
128
- && 'Sanitizer' in globalThis
129
- && globalThis.Sanitizer.prototype.sanitizeFor instanceof Function
130
- ) {
131
- Element.prototype.setHTML = function setHTML(input, { sanitizer = new globalThis.Sanitizer() } = {}) {
132
- if (
133
- ('Sanitizer' in globalThis && sanitizer instanceof globalThis.Sanitizer)
134
- || (typeof sanitizer !== 'undefined' && sanitizer.sanitizeFor instanceof Function)
135
- ) {
136
- const el = sanitizer.sanitizeFor(this.tagName.toLowerCase(), input);
137
- this.replaceChildren(...el.children);
138
- } else {
139
- throw new TypeError('`sanitizer` is not a valid Sanitizer');
140
- }
129
+ if (! (Element.prototype.setHTML instanceof Function)) {
130
+ Element.prototype.setHTML = function setHTML(input, opts = defaultConfig) {
131
+ safeSetHTML(this, input, opts);
141
132
  };
133
+ } else {
134
+ overwriteMethod(Element.prototype, 'setHTML', function(orig) {
135
+ return function setHTML(input, opts = {}) {
136
+ if (! (opts.sanitizer instanceof Sanitizer)) {
137
+ const sanitizer = new Sanitizer(convertToSanitizerConfig(opts));
138
+ orig.call(this, input, { sanitizer });
139
+ } else {
140
+ orig.call(this, input, opts);
141
+ }
142
+ };
143
+ });
142
144
  }
143
145
 
144
146
  if (! HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) {
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@shgysk8zer0/polyfills",
3
- "version": "0.0.7",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
+ "type": "module",
5
6
  "description": "A collection of JavaScript polyfills",
6
7
  "main": "all.min.js",
7
8
  "module": "all.js",
@@ -49,10 +50,8 @@
49
50
  },
50
51
  "homepage": "https://github.com/shgysk8zer0/polyfills#readme",
51
52
  "devDependencies": {
52
- "@rollup/plugin-terser": "^0.4.1",
53
- "eslint": "^8.40.0",
53
+ "@shgysk8zer0/js-utils": "^1.0.0",
54
54
  "htmlhint": "^1.1.4",
55
- "http-server": "^14.1.1",
56
- "rollup": "^2.79.1"
55
+ "http-server": "^14.1.1"
57
56
  }
58
57
  }
package/rollup.config.js CHANGED
@@ -1,12 +1,8 @@
1
1
  /* eslint-env node */
2
- import terser from '@rollup/plugin-terser';
2
+ import { getConfig } from '@shgysk8zer0/js-utils/rollup';
3
3
 
4
- export default {
5
- input: 'all.js',
6
- output: {
7
- file: 'all.min.js',
8
- format: 'iife',
9
- sourcemap: true,
10
- },
11
- plugins: [terser()],
12
- };
4
+ export default getConfig('./all.js', {
5
+ sourcemap: true,
6
+ minify: true,
7
+ format: 'iife',
8
+ });
package/url.js ADDED
@@ -0,0 +1,10 @@
1
+ import { polyfillMethod } from './utils.js';
2
+
3
+ polyfillMethod(URL, 'canParse', (str, base) => {
4
+ try {
5
+ new URL(str, base);
6
+ return true;
7
+ } catch {
8
+ return false;
9
+ }
10
+ });
package/utils.js ADDED
@@ -0,0 +1,31 @@
1
+ export function polyfillMethod(parent, name, value, {
2
+ writable = true,
3
+ enumerable = true,
4
+ configurable = true,
5
+ } = {}) {
6
+ if (! (parent[name] instanceof Function)) {
7
+ Object.defineProperty(parent, name, { value, writable, enumerable, configurable });
8
+ }
9
+ }
10
+
11
+ export function polyfillGetterSetter(parent, name, {
12
+ get,
13
+ set,
14
+ enumerable = true,
15
+ configurable = true,
16
+ } = {}) {
17
+ if (! parent.hasOwnProperty(name)) {
18
+ Object.defineProperty(parent, name, { get, set, enumerable, configurable });
19
+ }
20
+ }
21
+
22
+ export function overwriteMethod(parent, name, func) {
23
+ const { value, enumerable, configurable, writable } = Object.getOwnPropertyDescriptor(parent, name);
24
+ const newMethod = func(value);
25
+
26
+ if (! (newMethod instanceof Function)) {
27
+ throw new TypeError(`Error overwriting ${name}. The func MUST be a function that accepts the original as an argument and return a function.`);
28
+ } else {
29
+ Object.defineProperty(parent, name, { value: newMethod, enumerable, configurable, writable });
30
+ }
31
+ }
package/sanitizer.js DELETED
@@ -1,3 +0,0 @@
1
- import { polyfill, trustPolicies } from './assets/Sanitizer.js';
2
- export const polyfilled = polyfill();
3
- export { trustPolicies };