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.
- package/browser-tests/index.html +6 -8
- package/browser-tests/node.js +18 -18
- package/browser-tests/parse-real.js +8 -8
- package/browser-tests/parser.js +36 -36
- package/browser-tests/query.js +36 -39
- package/build-readme.js +29 -7
- package/build.js +14 -14
- package/docs/changelog.md +18 -0
- package/document.js +198 -9
- package/index.js +198 -9
- package/index.mjs +199 -9
- package/lib/node/document.js +23 -6
- package/lib/node/node.js +10 -3
- package/lib/node/style.js +47 -26
- package/lib/parse/cache.js +6 -2
- package/lib/parse/parse-atts.js +36 -36
- package/lib/parse/parser.js +134 -99
- package/lib/query/query.js +3 -3
- package/package.json +2 -2
- package/readme.md +26 -8
- package/tests/cache.test.js +68 -3
- package/tests/document.test.js +98 -2
- package/tests/node.test.js +129 -3
- package/tests/parser.test.js +242 -6
- package/tests/query.test.js +61 -3
- package/browser-tests/simple-test.js +0 -169
package/lib/node/document.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
14
|
-
if(html === null)
|
|
15
|
-
|
|
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) {
|
|
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
|
|
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.
|
|
183
|
-
if (index !== null)
|
|
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
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/lib/parse/cache.js
CHANGED
|
@@ -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)
|
|
6
|
-
|
|
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)
|
package/lib/parse/parse-atts.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
function parseAttributes(str) {
|
|
2
|
-
const attrs = {};
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
let
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
package/lib/parse/parser.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
package/lib/query/query.js
CHANGED
|
@@ -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,
|
|
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(
|
|
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.
|
|
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.
|