als-document 1.4.2 → 1.5.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.
@@ -3,16 +3,30 @@ class Document extends Node {
3
3
  super('ROOT',{},null);
4
4
  this.isSingle = false
5
5
  this.URL = url
6
- if(html instanceof Document) this.childNodes = [...buildFromCache(cacheDoc(html)).childNodes]
6
+ if(html instanceof Document) {
7
+ this.childNodes = [...buildFromCache(cacheDoc(html)).childNodes]
8
+ this.childNodes.forEach(child => { if(child instanceof Node || child instanceof TextNode) child.parent = this }) // COMP-07: reparent cloned top-level nodes
9
+ }
7
10
  else if(typeof html === 'string') this.innerHTML = html
8
11
  else this.html // create innerHTML
9
12
  }
10
13
 
11
14
  get documentElement() {return this.html}
12
15
  get html() {
13
- const html = this.$('html')
14
- if(html === null) this.innerHTML = /*html*/`<html lang="en"><head><meta charset="UTF-8"><title></title></head><body></body></html>`
15
- return this.$('html')
16
+ let html = this.$('html')
17
+ if(html === null) {
18
+ const existing = this.childNodes // COMP-21: preserve fragment instead of wiping it
19
+ this.innerHTML = /*html*/`<html lang="en"><head><meta charset="UTF-8"><title></title></head><body></body></html>`
20
+ html = this.$('html')
21
+ if(existing.length) {
22
+ const body = this.$('body')
23
+ existing.forEach(child => {
24
+ body.childNodes.push(child)
25
+ if(child instanceof Node || child instanceof TextNode) child.parent = body
26
+ })
27
+ }
28
+ }
29
+ return html
16
30
  }
17
31
 
18
32
  get innerHTML() {
@@ -41,7 +55,10 @@ class Document extends Node {
41
55
 
42
56
  get charset() {return this.$('meta[charset]')}
43
57
 
44
- set title(title) {return this.head.$('title').innerText = title}
58
+ set title(title) {
59
+ if(this.head.$('title') === null) this.head.insert(1,'<title></title>') // COMP-18: create missing head/title before writing
60
+ this.head.$('title').innerText = title
61
+ }
45
62
 
46
- get clone() {return new Document(this)}
63
+ get clone() {return new Document(this, this.URL)}
47
64
  }
package/lib/node/node.js CHANGED
@@ -149,7 +149,10 @@ class Node {
149
149
 
150
150
  insertAdjacentHTML(position, html) {
151
151
  const newNode = parseHTML(html);
152
- newNode.childNodes.forEach(node => {
152
+ const nodes = [...newNode.childNodes];
153
+ const pos = position.toLowerCase();
154
+ if (pos === 'afterbegin' || pos === 'afterend') nodes.reverse();
155
+ nodes.forEach(node => {
153
156
  this.insertAdjacentElement(position, node);
154
157
  });
155
158
  return newNode
@@ -179,8 +182,12 @@ class Node {
179
182
  set outerHTML(html) {
180
183
  const parsed = parseHTML(html);
181
184
  if (!this.parent) throw new Error('element has no parent node')
182
- const index = this.childIndex
183
- if (index !== null) this.parent.childNodes.splice(index, 1, ...parsed.childNodes);
185
+ const index = this.childNodeIndex
186
+ if (index !== null) {
187
+ const nodes = parsed.childNodes
188
+ nodes.forEach(node => node.parent = this.parent)
189
+ this.parent.childNodes.splice(index, 1, ...nodes)
190
+ }
184
191
  }
185
192
 
186
193
  appendChild(newChild) {
package/lib/node/style.js CHANGED
@@ -1,26 +1,47 @@
1
- function buildStyle(attributes) {
2
- const styles = attributes.style || "";
3
-
4
- const camelToKebab = str => str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
5
- const kebabToCamel = str => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
6
-
7
- const baseStyleObj = styles.split(";").reduce((acc, style) => {
8
- const [key, value] = style.split(":").map(s => s.trim());
9
- if (key && value) acc[kebabToCamel(key)] = value;
10
- return acc;
11
- }, {});
12
-
13
- return new Proxy(baseStyleObj, {
14
- get: (obj, prop) => obj[camelToKebab(prop)] || obj[prop],
15
- set: (obj, prop, value) => {
16
- obj[camelToKebab(prop)] = value;
17
- attributes.style = Object.entries(obj).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
18
- return true;
19
- },
20
- deleteProperty: (obj, prop) => {
21
- delete obj[camelToKebab(prop)];
22
- attributes.style = Object.entries(obj).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
23
- return true;
24
- }
25
- });
26
- }
1
+ function buildStyle(attributes) {
2
+ const styles = attributes.style || "";
3
+
4
+ const camelToKebab = str => str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
5
+ const kebabToCamel = str => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
6
+
7
+ const splitDeclarations = str => {
8
+ const out = [];
9
+ let buf = "", depth = 0, quote = null;
10
+ for (let i = 0; i < str.length; i++) {
11
+ const c = str[i];
12
+ if (quote) { if (c === quote) quote = null; buf += c; }
13
+ else if (c === '"' || c === "'") { quote = c; buf += c; }
14
+ else if (c === '(') { depth++; buf += c; }
15
+ else if (c === ')') { if (depth > 0) depth--; buf += c; }
16
+ else if (c === ';' && depth === 0) { out.push(buf); buf = ""; }
17
+ else buf += c;
18
+ }
19
+ if (buf.trim()) out.push(buf);
20
+ return out;
21
+ };
22
+
23
+ const serialize = obj => Object.entries(obj).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
24
+
25
+ const baseStyleObj = splitDeclarations(styles).reduce((acc, decl) => {
26
+ const idx = decl.indexOf(":");
27
+ if (idx === -1) return acc;
28
+ const key = decl.slice(0, idx).trim();
29
+ const value = decl.slice(idx + 1).trim();
30
+ if (key && value) acc[kebabToCamel(key)] = value;
31
+ return acc;
32
+ }, {});
33
+
34
+ return new Proxy(baseStyleObj, {
35
+ get: (obj, prop) => typeof prop === 'string' ? obj[kebabToCamel(prop)] : obj[prop],
36
+ set: (obj, prop, value) => {
37
+ obj[kebabToCamel(prop)] = value;
38
+ attributes.style = serialize(obj);
39
+ return true;
40
+ },
41
+ deleteProperty: (obj, prop) => {
42
+ delete obj[kebabToCamel(prop)];
43
+ attributes.style = serialize(obj);
44
+ return true;
45
+ }
46
+ });
47
+ }
@@ -2,8 +2,12 @@ function buildFromCache(cached) {
2
2
  function buildNode(cache,parent=null) {
3
3
  if(typeof cache === 'string') return parent.childNodes.push(cache)
4
4
  const {isSingle,tagName,attributes,childNodes,textContent} = cache
5
- if(textContent) return parent.childNodes.push(new TextNode(textContent))
6
- if(isSingle && parent) return parent.childNodes.push(new SingleNode(tagName,attributes))
5
+ if(textContent !== undefined) { // COMP-19: empty string is a valid TextNode
6
+ const textNode = new TextNode(textContent)
7
+ textNode.parent = parent // COMP-08: restore parent link
8
+ return parent.childNodes.push(textNode)
9
+ }
10
+ if(isSingle && parent) return new SingleNode(tagName,attributes,parent) // COMP-08: constructor sets parent
7
11
  const newDoc = tagName === 'ROOT' ? new Root() : new Node(tagName,attributes,parent)
8
12
  childNodes.forEach(childNode => {
9
13
  buildNode(childNode,newDoc)
@@ -1,36 +1,36 @@
1
- function parseAttributes(str) {
2
- const attrs = {};
3
- let key = "";
4
- let value = "";
5
- let isKey = true;
6
- let quoteChar = null;
7
-
8
- for (let i = 0; i < str.length; i++) {
9
- const char = str[i];
10
-
11
- if (isKey && (char === '=' || char === ' ')) {
12
- if (char === '=') isKey = false;
13
- else if (key.trim()) { // We've found an attribute without a value
14
- attrs[key.trim()] = true; // Set the value to true for boolean attributes
15
- key = ""; // Reset the key
16
- }
17
- continue;
18
- }
19
-
20
- if (!quoteChar && (char === '"' || char === "'")) {
21
- quoteChar = char;
22
- continue; // we skip the quote itself
23
- } else if (quoteChar && char === quoteChar) {
24
- quoteChar = null;
25
- attrs[key.trim()] = value.trim();
26
- key = ""; value = ""; isKey = true;
27
- continue;
28
- }
29
-
30
- if (isKey) key += char;
31
- else value += char;
32
- }
33
-
34
- if (key.trim() && !value) attrs[key.trim()] = ''; // After the loop, check if there's a leftover key, which would be an attribute without a value
35
- return attrs;
36
- }
1
+ function parseAttributes(str) {
2
+ const attrs = {};
3
+ const len = str.length;
4
+ const isWS = c => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f';
5
+ let i = 0;
6
+
7
+ while (i < len) {
8
+ while (i < len && isWS(str[i])) i++; // skip leading whitespace
9
+ if (i >= len) break;
10
+
11
+ let name = "";
12
+ while (i < len && !isWS(str[i]) && str[i] !== '=') { name += str[i]; i++; }
13
+ if (!name) { i++; continue; } // stray '=' or delimiter, skip it
14
+
15
+ while (i < len && isWS(str[i])) i++; // allow whitespace before '='
16
+
17
+ if (str[i] === '=') {
18
+ i++; // consume '='
19
+ while (i < len && isWS(str[i])) i++; // allow whitespace after '='
20
+ let value = "";
21
+ const quote = str[i];
22
+ if (quote === '"' || quote === "'") {
23
+ i++; // consume opening quote
24
+ while (i < len && str[i] !== quote) { value += str[i]; i++; }
25
+ i++; // consume closing quote
26
+ } else {
27
+ while (i < len && !isWS(str[i])) { value += str[i]; i++; } // unquoted value
28
+ }
29
+ attrs[name] = value.trim();
30
+ } else {
31
+ attrs[name] = ''; // boolean attribute without a value
32
+ }
33
+ }
34
+
35
+ return attrs;
36
+ }
@@ -1,99 +1,134 @@
1
- function parseHTML(html) {
2
- const root = new Root();
3
- const stack = [root];
4
- let currentText = "", i = 0;
5
- let max = 0
6
-
7
- function parseScript() {
8
- if (!html.startsWith("<script", i)) return false;
9
- const openTagEnd = html.indexOf(">", i);
10
- if (openTagEnd === -1) return false;
11
-
12
- const attributesString = html.substring(i + 7, openTagEnd).trim(); // +7 чтобы пропустить "<script"
13
- const attributes = parseAttributes(attributesString); // Извлечь атрибуты
14
-
15
- let closeTagStart = html.indexOf("</script>", openTagEnd); // Находим закрывающий тег
16
- if (closeTagStart === -1) return false; // Если нет закрывающего тега, возвращаем ошибку
17
- const content = html.substring(openTagEnd + 1, closeTagStart); // Получить содержимое тега
18
-
19
- const scriptNode = new Node('script', attributes, stack[stack.length - 1]); // Создаем узел
20
- if(content.length > 0) scriptNode.childNodes.push(content);
21
-
22
- // Переместить указатель i к концу закрывающего тега
23
- i = closeTagStart + 9; // +9 чтобы пропустить "</script>"
24
- return true;
25
- }
26
-
27
- function parseSpecial(startStr, endStr, n1, n2, tag) {
28
- if (!html.startsWith(startStr, i)) return false
29
- if(currentText.length) stack[stack.length - 1].childNodes.push(new TextNode(currentText)); // new!
30
- const end = html.indexOf(endStr, i + n1);
31
- const strNode = new Node(tag, {}, stack[stack.length - 1]);
32
- strNode.childNodes.push(html.substring(i + n1, end));
33
- i = end + n2;
34
- return true
35
- }
36
-
37
- while (i < html.length) {
38
- if(i >= max) max = i;
39
- else break;
40
- if (parseScript()) continue
41
- if (parseSpecial("<!--", "-->", 4, 3, '#comment')) continue
42
- if (parseSpecial("<style", "</style>", 7, 8, 'style')) continue
43
-
44
- if (html.startsWith("<![CDATA[", i)) {
45
- const end = html.indexOf("]]>", i + 9);
46
- if (end === -1) break; // Убедитесь, что существует закрывающий тег
47
- const content = html.substring(i + 9, end); // это содержимое CDATA
48
- const cdataNode = new SingleNode("#cdata-section", {}, stack[stack.length - 1]);
49
- cdataNode.nodeValue = content;
50
- i = end + 3;
51
- continue;
52
- }
53
-
54
- if (html.startsWith("<", i)) {
55
- if (currentText && stack[stack.length - 1]) {
56
- const textNode = new TextNode(currentText)
57
- stack[stack.length - 1].childNodes.push(textNode);
58
- textNode.parent = stack[stack.length - 1]
59
- currentText = "";
60
- }
61
-
62
- let tagEnd = i + 1;
63
- let insideQuotes = false;
64
- let quoteChar = null;
65
-
66
- while (tagEnd < html.length) {
67
- const char = html[tagEnd];
68
- if (!insideQuotes && (char === '"' || char === "'")) {
69
- insideQuotes = true;
70
- quoteChar = char;
71
- } else if (insideQuotes && char === quoteChar) {
72
- insideQuotes = false;
73
- quoteChar = null;
74
- }
75
-
76
- if (!insideQuotes && char === '>') break;
77
- tagEnd++;
78
- }
79
-
80
- const tagContent = html.substring(i + 1, tagEnd);
81
- if (tagContent.startsWith("/")) stack.pop(); // It is close tag
82
- else { // It is open tag
83
- let isSelfClosing = tagContent.endsWith('/');
84
- const tagNameEnd = tagContent.search(/\s|>|\//); // add a check for "/"
85
- const tagName = tagContent.substring(0, tagNameEnd > 0 ? tagNameEnd : tagEnd - i - 1);
86
- const attributesString = tagContent.substring(tagName.length, isSelfClosing ? tagContent.length - 1 : tagContent.length).trim();
87
- const attributes = parseAttributes(attributesString);
88
- if (VOID_TAGS.has(tagName.toLowerCase()) || isSelfClosing) new SingleNode(tagName, attributes, stack[stack.length - 1])
89
- else stack.push(new Node(tagName, attributes, stack[stack.length - 1]));
90
- }
91
- i = tagEnd + 1;
92
- } else {
93
- currentText += html[i];
94
- i++;
95
- }
96
- }
97
- if (currentText.trim() && stack[stack.length - 1]) stack[stack.length - 1].childNodes.push(new TextNode(currentText));
98
- return root;
99
- }
1
+ function parseHTML(html) {
2
+ const root = new Root();
3
+ const stack = [root];
4
+ let currentText = "", i = 0;
5
+ let max = 0
6
+
7
+ const isWS = c => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f';
8
+
9
+ function flushText() {
10
+ if (currentText) {
11
+ const textNode = new TextNode(currentText)
12
+ stack[stack.length - 1].childNodes.push(textNode)
13
+ textNode.parent = stack[stack.length - 1]
14
+ currentText = ""
15
+ }
16
+ }
17
+
18
+ function startsWithCI(str, pos) {
19
+ return html.substr(pos, str.length).toLowerCase() === str;
20
+ }
21
+
22
+ function indexOfCI(needle, from) { // case-insensitive indexOf without allocating a lowercased copy
23
+ const nlen = needle.length
24
+ const limit = html.length - nlen
25
+ for (let j = from; j <= limit; j++) {
26
+ let ok = true
27
+ for (let k = 0; k < nlen; k++) {
28
+ let c = html[j + k]
29
+ if (c >= 'A' && c <= 'Z') c = c.toLowerCase()
30
+ if (c !== needle[k]) { ok = false; break }
31
+ }
32
+ if (ok) return j
33
+ }
34
+ return -1
35
+ }
36
+
37
+ function scanTagEnd(from) { // index of the '>' that closes the tag starting at 'from', respecting quotes
38
+ let j = from + 1
39
+ let inQuotes = false, q = null
40
+ while (j < html.length) {
41
+ const c = html[j]
42
+ if (!inQuotes && (c === '"' || c === "'")) { inQuotes = true; q = c }
43
+ else if (inQuotes && c === q) { inQuotes = false; q = null }
44
+ if (!inQuotes && c === '>') return j
45
+ j++
46
+ }
47
+ return -1
48
+ }
49
+
50
+ function parseRawText(tagName) { // <script>/<style>: raw text content, case-insensitive, quote-aware
51
+ if (html[i] !== '<') return false // fast path: avoid substring allocation on text characters
52
+ const open = "<" + tagName
53
+ if (!startsWithCI(open, i)) return false
54
+ const after = html[i + open.length]
55
+ if (after !== undefined && !isWS(after) && after !== '>' && after !== '/') return false
56
+ const openTagEnd = scanTagEnd(i)
57
+ if (openTagEnd === -1) return false
58
+ flushText()
59
+ let attributesString = html.substring(i + open.length, openTagEnd).trim()
60
+ if (attributesString.endsWith('/')) attributesString = attributesString.slice(0, -1).trim()
61
+ const attributes = parseAttributes(attributesString)
62
+ const closeTag = "</" + tagName + ">"
63
+ const closeStart = indexOfCI(closeTag, openTagEnd + 1)
64
+ const missing = closeStart === -1
65
+ const content = html.substring(openTagEnd + 1, missing ? html.length : closeStart)
66
+ const node = new Node(tagName, attributes, stack[stack.length - 1]);
67
+ if (content.length > 0) node.childNodes.push(content);
68
+ i = missing ? html.length : closeStart + closeTag.length
69
+ return true
70
+ }
71
+
72
+ function parseSpecial(startStr, endStr, n1, n2, tag) { // comments: <!-- ... -->
73
+ if (!html.startsWith(startStr, i)) return false
74
+ flushText()
75
+ let end = html.indexOf(endStr, i + n1);
76
+ const missing = end === -1
77
+ if (missing) end = html.length
78
+ const strNode = new Node(tag, {}, stack[stack.length - 1]);
79
+ strNode.childNodes.push(html.substring(i + n1, end));
80
+ i = missing ? end : end + n2;
81
+ return true
82
+ }
83
+
84
+ while (i < html.length) {
85
+ if (i >= max) max = i;
86
+ else break;
87
+ if (parseRawText('script')) continue
88
+ if (parseSpecial("<!--", "-->", 4, 3, '#comment')) continue
89
+ if (parseRawText('style')) continue
90
+
91
+ if (html.startsWith("<![CDATA[", i)) {
92
+ if (currentText.trim()) flushText() // 1.4.x drops whitespace-only lead before CDATA; keep real text (COMP-01)
93
+ let end = html.indexOf("]]>", i + 9);
94
+ const missing = end === -1
95
+ if (missing) end = html.length
96
+ const content = html.substring(i + 9, end);
97
+ const cdataNode = new SingleNode("#cdata-section", {}, stack[stack.length - 1]);
98
+ cdataNode.nodeValue = content;
99
+ i = missing ? end : end + 3;
100
+ continue;
101
+ }
102
+
103
+ if (html.startsWith("<", i)) {
104
+ flushText()
105
+
106
+ const tagEnd = scanTagEnd(i);
107
+ const realEnd = tagEnd === -1 ? html.length : tagEnd;
108
+
109
+ const tagContent = html.substring(i + 1, realEnd);
110
+ if (tagContent.startsWith("/")) { // closing tag
111
+ const closeName = tagContent.slice(1).trim().toLowerCase();
112
+ let idx = -1;
113
+ for (let s = stack.length - 1; s >= 1; s--) { // never pop the root at index 0
114
+ if (stack[s]._tagName === closeName) { idx = s; break }
115
+ }
116
+ if (idx !== -1) stack.length = idx; // close the matched element and any still-open descendants
117
+ } else { // opening tag
118
+ let isSelfClosing = tagContent.endsWith('/');
119
+ const tagNameEnd = tagContent.search(/\s|>|\//);
120
+ const tagName = tagContent.substring(0, tagNameEnd > 0 ? tagNameEnd : realEnd - i - 1);
121
+ const attributesString = tagContent.substring(tagName.length, isSelfClosing ? tagContent.length - 1 : tagContent.length).trim();
122
+ const attributes = parseAttributes(attributesString);
123
+ if (VOID_TAGS.has(tagName.toLowerCase()) || isSelfClosing) new SingleNode(tagName, attributes, stack[stack.length - 1])
124
+ else stack.push(new Node(tagName, attributes, stack[stack.length - 1]));
125
+ }
126
+ i = realEnd + 1;
127
+ } else {
128
+ currentText += html[i];
129
+ i++;
130
+ }
131
+ }
132
+ if (currentText.trim() && stack[stack.length - 1]) stack[stack.length - 1].childNodes.push(new TextNode(currentText));
133
+ return root;
134
+ }
@@ -20,7 +20,7 @@ class Query {
20
20
  this.stringValues.push(value)
21
21
  return `[${this.stringValues.length - 1}]`
22
22
  })
23
- let [element, ancestors] = this.splitAndCutLast(selector, ' ') // \s - ancestor
23
+ let [element, ancestors] = this.splitAndCutLast(selector.trim(), /\s+/) // any CSS whitespace = descendant combinator
24
24
  element = this.getFamily(element)
25
25
  if (ancestors.length > 0)
26
26
  element.ancestors = ancestors.map(ancestor => this.getFamily(ancestor))
@@ -84,7 +84,7 @@ class Query {
84
84
  classList.push($class.replace(/^\./, '')); return ''
85
85
  })
86
86
  element = element.replace(/(\w\:?-?)*/, $tag => {
87
- tag = $tag == '' ? null : $tag; return ''
87
+ tag = $tag == '' ? null : $tag.toLowerCase(); return ''
88
88
  })
89
89
  let attribs = this.getAttributes(element)
90
90
  element = { query }
@@ -105,7 +105,7 @@ class Query {
105
105
  let query = attrib
106
106
  attrib = attrib.replace('[', '').replace(']', '')
107
107
  let [name,...values] = attrib.split('=')
108
- const value = values.join('=').trim().replace(/^\"/,'').replace(/\"$/,'')
108
+ const value = values.join('=').trim().replace(/^['"]/,'').replace(/['"]$/,'')
109
109
  let sign
110
110
  attrib = {query,name}
111
111
  if(value) {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "als-document",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "A powerful HTML parser & DOM manipulation library for both backend and frontend.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
7
7
  "scripts": {
8
8
  "build": "node ./build.js",
9
9
  "watch": "node ./build.js --watch",
10
- "test": "node --test --experimental-test-coverage",
10
+ "test": "node --test --experimental-test-coverage \"tests/**/*.test.js\"",
11
11
  "report": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
12
12
  },
13
13
  "keywords": ["HTML", "DOM", "parser", "manipulation", "backend", "frontend", "als-document"],
package/readme.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # als-document: HTML Parser & DOM Manipulation Library
2
2
 
3
-
4
3
  ## Overview
5
4
 
6
5
  `als-document` is a powerful library for parsing HTML and XML, building and manipulating virtual DOM structure on backend and frontend. It provides a robust and intuitive API for querying and interacting with DOM elements using selectors, making it a valuable tool for web developers.
@@ -47,7 +46,26 @@ import { parseHTML, Node, Query, TextNode, SingleNode, Root, Document } from 'al
47
46
  * Document new getters and setters include clone
48
47
  * tagName - uppers, _tagName - lowers
49
48
 
50
-
49
+
50
+ ## Changelog
51
+
52
+ ### 1.5.0
53
+
54
+ #### Fixed
55
+
56
+ - Improved HTML parsing for malformed markup, raw-text elements, CDATA, comments, quoted and unquoted attributes.
57
+ - Preserved text and node order during parsing and `insertAdjacentHTML()` operations.
58
+ - Fixed `outerHTML` replacement when text or comment nodes precede an element.
59
+ - Restored correct parent references, document URLs, and empty text nodes when cloning or rebuilding from cache.
60
+ - Fixed CSS style updates, including camelCase properties and values containing colons.
61
+ - Improved selectors with case-insensitive tag names, single-quoted attributes, and CSS whitespace support.
62
+ - Fixed missing document structure creation without discarding existing content.
63
+
64
+ #### Added
65
+
66
+ - TypeScript declarations.
67
+ - Expanded regression test coverage.
68
+
51
69
  ## parseHTML
52
70
 
53
71
  `parseHTML` is a function that takes an HTML string and constructs a DOM tree representation from it. It recognizes various HTML elements, such as comments, scripts, styles, and CDATA, and organizes them into nodes that can be manipulated and queried.
@@ -87,7 +105,7 @@ const parsedScript = parseHTML('<script>console.log("Hello, world!");</script>')
87
105
 
88
106
  Remember, the actual tree structure will be more complex and detailed, but the provided examples give you a basic understanding of how to navigate through the parsed result.
89
107
 
90
-
108
+
91
109
  ## Node
92
110
 
93
111
  `Node` is a fundamental class that represents an element node in the DOM tree. It provides functionality similar to the native DOM API in browsers, but with its own implementation.
@@ -134,15 +152,15 @@ const foundP = div.querySelector('p');
134
152
  console.log(foundP.textContent); // Outputs: Hello, world!
135
153
  ```
136
154
 
137
-
155
+
138
156
  ### SingleNode
139
157
 
140
158
  `SingleNode` extends from the `Node` class and represents elements that don't have closing tags (self-closing tags) in HTML. Examples include `<img>`, `<br>`, and `<!DOCTYPE>`. This class has restricted methods and properties since these elements can't have child nodes.
141
-
159
+
142
160
  ### TextNode
143
161
 
144
162
  `TextNode` is a class that represents text content within the DOM. A TextNode holds raw text data and does not have child nodes.
145
-
163
+
146
164
  ### Document node (extends Node)
147
165
 
148
166
  Has additional getters and setters:
@@ -154,7 +172,7 @@ Has additional getters and setters:
154
172
  * get charset
155
173
  * set title
156
174
  * get clone - return cloned new instance of Document
157
-
175
+
158
176
  ## Query
159
177
 
160
178
  The `Query` class is designed to parse CSS selector strings and transform them into a structured object format, providing detailed insights into each selector and its components.
@@ -265,7 +283,7 @@ if attribute has value, attrib object will contain check function with one param
265
283
  let s = Query.get('[test^="some"]')[0]
266
284
  console.log(s.attribs[0].check('some value test')) // true
267
285
  ```
268
-
286
+
269
287
  ## buildFromCache and cacheDoc
270
288
 
271
289
  Building DOM from raw html, usually takes tens of milliseconds. But now, you can build DOM once and save it's cache as regular stringified JSON.