happy-dom 9.15.0 → 9.17.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.

Potentially problematic release.


This version of happy-dom might be problematic. Click here for more details.

Files changed (39) hide show
  1. package/lib/nodes/character-data/CharacterDataUtility.d.ts +0 -7
  2. package/lib/nodes/character-data/CharacterDataUtility.d.ts.map +1 -1
  3. package/lib/nodes/character-data/CharacterDataUtility.js +0 -35
  4. package/lib/nodes/character-data/CharacterDataUtility.js.map +1 -1
  5. package/lib/nodes/element/Element.d.ts.map +1 -1
  6. package/lib/nodes/element/Element.js +7 -5
  7. package/lib/nodes/element/Element.js.map +1 -1
  8. package/lib/nodes/html-template-element/HTMLTemplateElement.d.ts.map +1 -1
  9. package/lib/nodes/html-template-element/HTMLTemplateElement.js +5 -2
  10. package/lib/nodes/html-template-element/HTMLTemplateElement.js.map +1 -1
  11. package/lib/nodes/shadow-root/ShadowRoot.d.ts.map +1 -1
  12. package/lib/nodes/shadow-root/ShadowRoot.js +3 -1
  13. package/lib/nodes/shadow-root/ShadowRoot.js.map +1 -1
  14. package/lib/query-selector/ISelectorAttribute.d.ts +2 -0
  15. package/lib/query-selector/ISelectorAttribute.d.ts.map +1 -1
  16. package/lib/query-selector/SelectorItem.d.ts.map +1 -1
  17. package/lib/query-selector/SelectorItem.js +5 -42
  18. package/lib/query-selector/SelectorItem.js.map +1 -1
  19. package/lib/query-selector/SelectorParser.d.ts +10 -0
  20. package/lib/query-selector/SelectorParser.d.ts.map +1 -1
  21. package/lib/query-selector/SelectorParser.js +65 -20
  22. package/lib/query-selector/SelectorParser.js.map +1 -1
  23. package/lib/xml-parser/XMLParser.d.ts.map +1 -1
  24. package/lib/xml-parser/XMLParser.js +33 -8
  25. package/lib/xml-parser/XMLParser.js.map +1 -1
  26. package/lib/xml-serializer/XMLSerializer.d.ts +16 -5
  27. package/lib/xml-serializer/XMLSerializer.d.ts.map +1 -1
  28. package/lib/xml-serializer/XMLSerializer.js +58 -11
  29. package/lib/xml-serializer/XMLSerializer.js.map +1 -1
  30. package/package.json +2 -3
  31. package/src/nodes/character-data/CharacterDataUtility.ts +0 -37
  32. package/src/nodes/element/Element.ts +7 -5
  33. package/src/nodes/html-template-element/HTMLTemplateElement.ts +5 -2
  34. package/src/nodes/shadow-root/ShadowRoot.ts +3 -1
  35. package/src/query-selector/ISelectorAttribute.ts +2 -0
  36. package/src/query-selector/SelectorItem.ts +7 -43
  37. package/src/query-selector/SelectorParser.ts +72 -20
  38. package/src/xml-parser/XMLParser.ts +18 -8
  39. package/src/xml-serializer/XMLSerializer.ts +38 -11
@@ -14,16 +14,17 @@ import ISelectorPseudo from './ISelectorPseudo';
14
14
  * Group 6: Attribute name when there is a value using apostrophe (e.g. "attr1")
15
15
  * Group 7: Attribute operator when using apostrophe (e.g. "~")
16
16
  * Group 8: Attribute value when using apostrophe (e.g. "value1")
17
- * Group 9: Attribute name when threre is a value not using apostrophe (e.g. "attr1")
18
- * Group 10: Attribute operator when not using apostrophe (e.g. "~")
19
- * Group 11: Attribute value when notusing apostrophe (e.g. "value1")
20
- * Group 12: Pseudo name when arguments (e.g. "nth-child")
21
- * Group 13: Arguments of pseudo (e.g. "2n + 1")
22
- * Group 14: Pseudo name when no arguments (e.g. "empty")
23
- * Group 15: Combinator.
17
+ * Group 9: Attribute modifier when using apostrophe (e.g. "i" or "s")
18
+ * Group 10: Attribute name when threre is a value not using apostrophe (e.g. "attr1")
19
+ * Group 11: Attribute operator when not using apostrophe (e.g. "~")
20
+ * Group 12: Attribute value when notusing apostrophe (e.g. "value1")
21
+ * Group 13: Pseudo name when arguments (e.g. "nth-child")
22
+ * Group 14: Arguments of pseudo (e.g. "2n + 1")
23
+ * Group 15: Pseudo name when no arguments (e.g. "empty")
24
+ * Group 16: Combinator.
24
25
  */
25
26
  const SELECTOR_REGEXP =
26
- /(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_]|\\.)+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_]+)\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1}) *= *["']{1}([^"']*)["']{1}\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1}) *= *([^\]]*)\]|:([a-zA-Z-]+) *\(([^)]+)\)|:([a-zA-Z-]+)|([ ,+>]*)/g;
27
+ /(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_]|\\.)+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_]+)\]|\[([a-zA-Z0-9-_]+) *([~|^$*]{0,1}) *= *["']{1}([^"']*)["']{1} *(s|i){0,1}\]|\[([a-zA-Z0-9-_]+) *([~|^$*]{0,1}) *= *([^\]]*)\]|:([a-zA-Z-]+) *\(([^)]+)\)|:([a-zA-Z-]+)|([ ,+>]*)/g;
27
28
 
28
29
  /**
29
30
  * Escaped Character RegExp.
@@ -117,30 +118,40 @@ export default class SelectorParser {
117
118
  currentSelectorItem.attributes.push({
118
119
  name: match[5].toLowerCase(),
119
120
  operator: null,
120
- value: null
121
+ value: null,
122
+ modifier: null,
123
+ regExp: null
121
124
  });
122
125
  } else if (match[6] && match[8] !== undefined) {
123
126
  currentSelectorItem.attributes = currentSelectorItem.attributes || [];
124
127
  currentSelectorItem.attributes.push({
125
128
  name: match[6].toLowerCase(),
126
129
  operator: match[7] || null,
127
- value: match[8]
130
+ value: match[8],
131
+ modifier: match[9] || null,
132
+ regExp: this.getAttributeRegExp({
133
+ operator: match[7],
134
+ value: match[8],
135
+ modifier: match[9]
136
+ })
128
137
  });
129
- } else if (match[9] && match[11] !== undefined) {
138
+ } else if (match[10] && match[12] !== undefined) {
130
139
  currentSelectorItem.attributes = currentSelectorItem.attributes || [];
131
140
  currentSelectorItem.attributes.push({
132
- name: match[9].toLowerCase(),
133
- operator: match[10] || null,
134
- value: match[11]
141
+ name: match[10].toLowerCase(),
142
+ operator: match[11] || null,
143
+ value: match[12],
144
+ modifier: null,
145
+ regExp: this.getAttributeRegExp({ operator: match[7], value: match[8] })
135
146
  });
136
- } else if (match[12] && match[13]) {
147
+ } else if (match[13] && match[14]) {
137
148
  currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
138
- currentSelectorItem.pseudos.push(this.getPseudo(match[12], match[13]));
139
- } else if (match[14]) {
140
- currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
141
- currentSelectorItem.pseudos.push(this.getPseudo(match[14]));
149
+ currentSelectorItem.pseudos.push(this.getPseudo(match[13], match[14]));
142
150
  } else if (match[15]) {
143
- switch (match[15].trim()) {
151
+ currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
152
+ currentSelectorItem.pseudos.push(this.getPseudo(match[15]));
153
+ } else if (match[16]) {
154
+ switch (match[16].trim()) {
144
155
  case ',':
145
156
  currentSelectorItem = new SelectorItem({
146
157
  combinator: SelectorCombinatorEnum.descendant
@@ -178,6 +189,47 @@ export default class SelectorParser {
178
189
  return groups;
179
190
  }
180
191
 
192
+ /**
193
+ * Returns attribute RegExp.
194
+ *
195
+ * @param attribute Attribute.
196
+ * @param attribute.value Attribute value.
197
+ * @param attribute.operator Attribute operator.
198
+ * @param attribute.modifier Attribute modifier.
199
+ * @returns Attribute RegExp.
200
+ */
201
+ private static getAttributeRegExp(attribute: {
202
+ value?: string;
203
+ operator?: string;
204
+ modifier?: string;
205
+ }): RegExp | null {
206
+ const modifier = attribute.modifier === 'i' ? 'i' : '';
207
+
208
+ if (!attribute.operator || !attribute.value) {
209
+ return null;
210
+ }
211
+
212
+ switch (attribute.operator) {
213
+ // [attribute~="value"] - Contains a specified word.
214
+ case '~':
215
+ return new RegExp(`[- ]${attribute.value}|${attribute.value}[- ]`, modifier);
216
+ // [attribute|="value"] - Starts with the specified word.
217
+ case '|':
218
+ return new RegExp(`^${attribute.value}[- ]`, modifier);
219
+ // [attribute^="value"] - Begins with a specified value.
220
+ case '^':
221
+ return new RegExp(`^${attribute.value}`, modifier);
222
+ // [attribute$="value"] - Ends with a specified value.
223
+ case '$':
224
+ return new RegExp(`${attribute.value}$`, modifier);
225
+ // [attribute*="value"] - Contains a specified value.
226
+ case '*':
227
+ return new RegExp(`${attribute.value}`, modifier);
228
+ default:
229
+ return null;
230
+ }
231
+ }
232
+
181
233
  /**
182
234
  * Returns pseudo.
183
235
  *
@@ -9,7 +9,7 @@ import PlainTextElements from '../config/PlainTextElements';
9
9
  import IDocumentType from '../nodes/document-type/IDocumentType';
10
10
  import INode from '../nodes/node/INode';
11
11
  import IDocumentFragment from '../nodes/document-fragment/IDocumentFragment';
12
- import { decode } from 'he';
12
+ import * as Entities from 'entities';
13
13
 
14
14
  /**
15
15
  * Markup RegExp.
@@ -99,7 +99,7 @@ export default class XMLParser {
99
99
  // Plain text between tags.
100
100
 
101
101
  currentNode.appendChild(
102
- document.createTextNode(xml.substring(lastIndex, match.index))
102
+ document.createTextNode(Entities.decodeHTML(xml.substring(lastIndex, match.index)))
103
103
  );
104
104
  }
105
105
 
@@ -161,13 +161,17 @@ export default class XMLParser {
161
161
  // Comment.
162
162
 
163
163
  currentNode.appendChild(
164
- document.createComment((match[6] ? '?' : '') + (match[3] || match[4] || match[6]))
164
+ document.createComment(
165
+ Entities.decodeHTML((match[6] ? '?' : '') + (match[3] || match[4] || match[6]))
166
+ )
165
167
  );
166
168
  } else if (match[5]) {
167
169
  // Exclamation mark comment (usually <!DOCTYPE>).
168
170
 
171
+ const exclamationComment = Entities.decodeHTML(match[5]);
169
172
  currentNode.appendChild(
170
- this.getDocumentTypeNode(document, match[5]) || document.createComment(match[5])
173
+ this.getDocumentTypeNode(document, exclamationComment) ||
174
+ document.createComment(exclamationComment)
171
175
  );
172
176
  } else if (match[6]) {
173
177
  // Processing instruction (not supported by HTML).
@@ -176,7 +180,9 @@ export default class XMLParser {
176
180
  // Plain text between tags, including the match as it is not a valid start or end tag.
177
181
 
178
182
  currentNode.appendChild(
179
- document.createTextNode(xml.substring(lastIndex, markupRegexp.lastIndex))
183
+ document.createTextNode(
184
+ Entities.decodeHTML(xml.substring(lastIndex, markupRegexp.lastIndex))
185
+ )
180
186
  );
181
187
  }
182
188
 
@@ -205,7 +211,7 @@ export default class XMLParser {
205
211
 
206
212
  const name = attributeMatch[1] || attributeMatch[5] || attributeMatch[9] || '';
207
213
  const rawValue = attributeMatch[3] || attributeMatch[7] || '';
208
- const value = rawValue ? decode(rawValue) : '';
214
+ const value = rawValue ? Entities.decodeHTMLAttribute(rawValue) : '';
209
215
  const namespaceURI =
210
216
  (<IElement>currentNode).tagName === 'SVG' && name === 'xmlns' ? value : null;
211
217
 
@@ -271,7 +277,9 @@ export default class XMLParser {
271
277
 
272
278
  // Plain text elements such as <script> and <style> should only contain text.
273
279
  currentNode.appendChild(
274
- document.createTextNode(xml.substring(startTagIndex, match.index))
280
+ document.createTextNode(
281
+ Entities.decodeHTML(xml.substring(startTagIndex, match.index))
282
+ )
275
283
  );
276
284
 
277
285
  stack.pop();
@@ -289,7 +297,9 @@ export default class XMLParser {
289
297
  if (lastIndex !== xml.length) {
290
298
  // Plain text after tags.
291
299
 
292
- currentNode.appendChild(document.createTextNode(xml.substring(lastIndex)));
300
+ currentNode.appendChild(
301
+ document.createTextNode(Entities.decodeHTML(xml.substring(lastIndex)))
302
+ );
293
303
  }
294
304
  }
295
305
 
@@ -2,26 +2,48 @@ import Element from '../nodes/element/Element';
2
2
  import Node from '../nodes/node/Node';
3
3
  import VoidElements from '../config/VoidElements';
4
4
  import DocumentType from '../nodes/document-type/DocumentType';
5
- import { escape } from 'he';
6
5
  import INode from '../nodes/node/INode';
7
6
  import IElement from '../nodes/element/IElement';
8
7
  import IHTMLTemplateElement from '../nodes/html-template-element/IHTMLTemplateElement';
9
8
  import NodeTypeEnum from '../nodes/node/NodeTypeEnum';
10
9
  import IProcessingInstruction from '../nodes/processing-instruction/IProcessingInstruction';
10
+ import * as Entities from 'entities';
11
11
 
12
12
  /**
13
13
  * Utility for converting an element to string.
14
14
  */
15
15
  export default class XMLSerializer {
16
+ public _options = {
17
+ includeShadowRoots: false,
18
+ escapeEntities: true
19
+ };
20
+
21
+ /**
22
+ * Constructor.
23
+ *
24
+ * @param [options] Options.
25
+ * @param [options.includeShadowRoots] Include shadow roots.
26
+ * @param [options.escapeEntities] Escape text.
27
+ */
28
+ constructor(options?: { includeShadowRoots?: boolean; escapeEntities?: boolean }) {
29
+ if (options) {
30
+ if (options.includeShadowRoots !== undefined) {
31
+ this._options.includeShadowRoots = options.includeShadowRoots;
32
+ }
33
+
34
+ if (options.escapeEntities !== undefined) {
35
+ this._options.escapeEntities = options.escapeEntities;
36
+ }
37
+ }
38
+ }
39
+
16
40
  /**
17
41
  * Renders an element as HTML.
18
42
  *
19
43
  * @param root Root element.
20
- * @param [options] Options.
21
- * @param [options.includeShadowRoots] Set to "true" to include shadow roots.
22
44
  * @returns Result.
23
45
  */
24
- public serializeToString(root: INode, options?: { includeShadowRoots?: boolean }): string {
46
+ public serializeToString(root: INode): string {
25
47
  switch (root.nodeType) {
26
48
  case NodeTypeEnum.elementNode:
27
49
  const element = <Element>root;
@@ -38,14 +60,14 @@ export default class XMLSerializer {
38
60
  let innerHTML = '';
39
61
 
40
62
  for (const node of childNodes) {
41
- innerHTML += this.serializeToString(node, options);
63
+ innerHTML += this.serializeToString(node);
42
64
  }
43
65
 
44
- if (options?.includeShadowRoots && element.shadowRoot) {
66
+ if (this._options.includeShadowRoots && element.shadowRoot) {
45
67
  innerHTML += `<template shadowrootmode="${element.shadowRoot.mode}">`;
46
68
 
47
69
  for (const node of element.shadowRoot.childNodes) {
48
- innerHTML += this.serializeToString(node, options);
70
+ innerHTML += this.serializeToString(node);
49
71
  }
50
72
 
51
73
  innerHTML += '</template>';
@@ -56,7 +78,7 @@ export default class XMLSerializer {
56
78
  case Node.DOCUMENT_NODE:
57
79
  let html = '';
58
80
  for (const node of root.childNodes) {
59
- html += this.serializeToString(node, options);
81
+ html += this.serializeToString(node);
60
82
  }
61
83
  return html;
62
84
  case NodeTypeEnum.commentNode:
@@ -65,7 +87,9 @@ export default class XMLSerializer {
65
87
  // TODO: Add support for processing instructions.
66
88
  return `<!--?${(<IProcessingInstruction>root).target} ${root.textContent}?-->`;
67
89
  case NodeTypeEnum.textNode:
68
- return root.textContent;
90
+ return this._options.escapeEntities
91
+ ? Entities.escapeText(root.textContent)
92
+ : root.textContent;
69
93
  case NodeTypeEnum.documentTypeNode:
70
94
  const doctype = <DocumentType>root;
71
95
  const identifier = doctype.publicId ? ' PUBLIC' : doctype.systemId ? ' SYSTEM' : '';
@@ -87,12 +111,15 @@ export default class XMLSerializer {
87
111
  let attributeString = '';
88
112
 
89
113
  if (!(<Element>element)._attributes.is && (<Element>element)._isValue) {
90
- attributeString += ' is="' + escape((<Element>element)._isValue) + '"';
114
+ attributeString += ' is="' + (<Element>element)._isValue + '"';
91
115
  }
92
116
 
93
117
  for (const attribute of Object.values((<Element>element)._attributes)) {
94
118
  if (attribute.value !== null) {
95
- attributeString += ' ' + attribute.name + '="' + escape(attribute.value) + '"';
119
+ const escapedValue = this._options.escapeEntities
120
+ ? Entities.escapeText(attribute.value)
121
+ : attribute.value;
122
+ attributeString += ' ' + attribute.name + '="' + escapedValue + '"';
96
123
  }
97
124
  }
98
125
  return attributeString;