als-document 0.12.0 → 1.0.0-beta
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/document.js +41 -589
- package/index.js +40 -0
- package/index.mjs +40 -0
- package/package.json +9 -9
- package/readme.md +219 -150
- package/src/build.js +66 -0
- package/src/node/class-list.js +25 -0
- package/src/node/dataset.js +15 -0
- package/src/node/find.js +12 -0
- package/src/node/node.js +193 -0
- package/src/node/root.js +11 -0
- package/src/node/single-node.js +31 -0
- package/src/node/style.js +26 -0
- package/src/node/text-node.js +9 -0
- package/src/parse/cache.js +33 -0
- package/src/parse/parse-atts.js +36 -0
- package/src/parse/parser.js +98 -0
- package/src/parse/void-tags.js +5 -0
- package/src/query/check-element.js +83 -0
- package/src/query/query.js +142 -0
- package/tests/cache.js +19 -0
- package/tests/data/html1.js +2579 -0
- package/tests/data/html2.js +1124 -0
- package/tests/data/svg.js +66 -0
- package/tests/index.html +31 -0
- package/tests/node.js +196 -0
- package/tests/parse-real.js +53 -0
- package/tests/parser.js +351 -0
- package/tests/query.js +66 -0
- package/tests/test.js +169 -0
- package/tests/utils.js +37 -0
package/src/node/node.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
function insertBefore(arr, index, newItem) {
|
|
2
|
+
const existingIndex = arr.indexOf(newItem);
|
|
3
|
+
if (existingIndex !== -1) arr.splice(existingIndex, 1);
|
|
4
|
+
arr.splice(index, 0, newItem);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class Node {
|
|
8
|
+
constructor(tagName, attributes = {}, parent = null) {
|
|
9
|
+
this.isSingle = false;
|
|
10
|
+
this.tagName = tagName;
|
|
11
|
+
this.attributes = attributes;
|
|
12
|
+
this.childNodes = [];
|
|
13
|
+
if (parent !== null) parent.childNodes.push(this)
|
|
14
|
+
this.parent = parent;
|
|
15
|
+
this._classList = null; // Cache the classList instance
|
|
16
|
+
this.__style = null;
|
|
17
|
+
this._dataset = null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get id() { return this.attributes.id ? this.attributes.id : null; }
|
|
21
|
+
set id(newValue) { this.attributes.id = newValue; }
|
|
22
|
+
get className() {return this.attributes.class || null}
|
|
23
|
+
get parentNode() { return this.parent }
|
|
24
|
+
get ancestors() {
|
|
25
|
+
if(!this.parent) return []
|
|
26
|
+
const ancestors = []
|
|
27
|
+
let element = this.parent
|
|
28
|
+
while (element.tagName !== 'ROOT') {
|
|
29
|
+
ancestors.push(element)
|
|
30
|
+
element = element.parent
|
|
31
|
+
}
|
|
32
|
+
return ancestors.reverse()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get childNodeIndex() {
|
|
36
|
+
if(!this.parent) return null
|
|
37
|
+
return this.parent.childNodes ? this.parent.childNodes.indexOf(this) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get childIndex() {
|
|
41
|
+
if(!this.parent) return null
|
|
42
|
+
return this.parent.children ? this.parent.children.indexOf(this) : null
|
|
43
|
+
}
|
|
44
|
+
get previousElementSibling() { return this.prev }
|
|
45
|
+
get prev() {
|
|
46
|
+
if (!this.childIndex) return null // if no index or index == 0
|
|
47
|
+
return this.parent.children[this.childIndex - 1]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get nextElementSibling() { return this.next }
|
|
51
|
+
get next() {
|
|
52
|
+
if (!this.childIndex) return null
|
|
53
|
+
return this.parent.children[this.childIndex + 1] || null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get dataset() {
|
|
57
|
+
if (!this._dataset) this._dataset = getDataset(this);
|
|
58
|
+
return this._dataset;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get classList() {
|
|
62
|
+
if (!this._classList) this._classList = new NodeClassList(this);
|
|
63
|
+
return this._classList;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get style() {
|
|
67
|
+
if (!this.__style) this.__style = buildStyle(this.attributes)
|
|
68
|
+
return this.__style
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get outerHTML() {
|
|
72
|
+
const attrs = Object.entries(this.attributes).map(([key, val]) => val.length ? `${key}="${val}"` : key).join(" ");
|
|
73
|
+
return `<${this.tagName}${attrs ? ' '+attrs : ''}>${this.innerHTML}</${this.tagName}>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getAttribute(attrName) { return this.attributes[attrName] || null }
|
|
77
|
+
setAttribute(attrName, value) { this.attributes[attrName] = value }
|
|
78
|
+
removeAttribute(attrName) { delete this.attributes[attrName] }
|
|
79
|
+
|
|
80
|
+
remove() {
|
|
81
|
+
if (!this.parent) return
|
|
82
|
+
const index = this.childNodeIndex;
|
|
83
|
+
if (index !== null) this.parent.childNodes.splice(index, 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get innerHTML() {
|
|
87
|
+
return this.childNodes.map(child => {
|
|
88
|
+
if (child instanceof Node || child instanceof SingleNode) return child.outerHTML;
|
|
89
|
+
else if (child instanceof TextNode) return child.textContent; // Assuming child is a text node or something that can be stringified.
|
|
90
|
+
else return child
|
|
91
|
+
}).join("");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
$$(query) {return this.querySelectorAll(query)}
|
|
95
|
+
querySelectorAll(query) {
|
|
96
|
+
const selectors = Query.get(query)
|
|
97
|
+
return find(selectors, this, new Set())
|
|
98
|
+
}
|
|
99
|
+
$(query) {return this.querySelector(query)}
|
|
100
|
+
querySelector(query) {
|
|
101
|
+
const selectors = Query.get(query)
|
|
102
|
+
return find(selectors, this, new Set(), true)[0] || null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getElementsByClassName(query) { return this.querySelectorAll('.' + query) }
|
|
106
|
+
getElementsByTagName(query) { return this.querySelectorAll(query) }
|
|
107
|
+
getElementById(query) { return this.querySelector('#' + query) }
|
|
108
|
+
|
|
109
|
+
get children() {
|
|
110
|
+
return this.childNodes.filter(child => {
|
|
111
|
+
if (!(child instanceof Node)) return false
|
|
112
|
+
if (child.tagName === '#comment') return false
|
|
113
|
+
return true
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
insertAdjacentElement(position, newElement) {
|
|
118
|
+
if(newElement.tagName === 'ROOT' && newElement.childNodes.length > 0) newElement = newElement.childNodes[0]
|
|
119
|
+
const pos = position.toLowerCase();
|
|
120
|
+
if (pos === "afterbegin") this.childNodes.unshift(newElement);
|
|
121
|
+
else if (pos === "beforeend") this.childNodes.push(newElement);
|
|
122
|
+
newElement.parent = this
|
|
123
|
+
if (!this.parent) return newElement
|
|
124
|
+
if (pos === "beforebegin") insertBefore(this.parent.childNodes, this.childNodeIndex, newElement)
|
|
125
|
+
else if (pos === "afterend") this.parent.childNodes.splice(this.childNodeIndex + 1, 0, newElement);
|
|
126
|
+
newElement.parent = this.parent
|
|
127
|
+
return newElement
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
insertAdjacentHTML(position, html) {
|
|
131
|
+
const newNode = parseHTML(html);
|
|
132
|
+
newNode.childNodes.reverse().forEach(node => {
|
|
133
|
+
this.insertAdjacentElement(position, node);
|
|
134
|
+
});
|
|
135
|
+
return newNode
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
insertAdjacentText(position, text) {
|
|
139
|
+
return this.insertAdjacentElement(position, new TextNode(text));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
insert(position,element) {
|
|
143
|
+
const positions = ['beforebegin','afterbegin','beforeend','afterend']
|
|
144
|
+
if(positions[position]) position = positions[position]
|
|
145
|
+
if(typeof element === 'string') {
|
|
146
|
+
element = element.trim()
|
|
147
|
+
if(element.startsWith('<') && element.endsWith('>')) {
|
|
148
|
+
return this.insertAdjacentHTML(position,element)
|
|
149
|
+
}
|
|
150
|
+
return this.insertAdjacentText(position,element)
|
|
151
|
+
}
|
|
152
|
+
return this.insertAdjacentElement(position,element)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
set innerHTML(html) {
|
|
156
|
+
const parsed = parseHTML(html);
|
|
157
|
+
this.childNodes = parsed.childNodes;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
set outerHTML(html) {
|
|
161
|
+
const parsed = parseHTML(html);
|
|
162
|
+
if (!this.parent) return console.log('element has no parent node')
|
|
163
|
+
const index = this.childIndex
|
|
164
|
+
if (index !== null) this.parent.childNodes.splice(index, 1, ...parsed.childNodes);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
appendChild(newChild) {
|
|
168
|
+
if (newChild instanceof Node || newChild instanceof TextNode || newChild instanceof SingleNode) {
|
|
169
|
+
if (newChild.parent) newChild.parent.childNodes = newChild.parent.childNodes.filter(child => child !== newChild); // Если у newChild уже есть родительский узел, необходимо его удалить оттуда
|
|
170
|
+
} else if(typeof newChild === 'string') newChild = new TextNode(newChild)
|
|
171
|
+
else return newChild
|
|
172
|
+
this.childNodes.push(newChild);
|
|
173
|
+
newChild.parent = this;
|
|
174
|
+
return newChild; // возвращаем добавленный узел (по спецификации DOM)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get textContent() {
|
|
178
|
+
if (this.childNodes.length === 0) return this.nodeName === '#text' ? this.nodeValue : '';
|
|
179
|
+
return this.childNodes.map(child => { // Concatenate text content of this node and all descendants
|
|
180
|
+
if(child instanceof SingleNode) return ''
|
|
181
|
+
if(child instanceof TextNode) return child.nodeValue
|
|
182
|
+
if(child instanceof Node) return child.textContent;
|
|
183
|
+
else return child;
|
|
184
|
+
}).join(" ");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
set textContent(value) {
|
|
188
|
+
this.childNodes = []; // Clear the current child nodes
|
|
189
|
+
if (value !== null && value !== undefined) { // Add a new text node with the given value
|
|
190
|
+
this.childNodes.push(value.toString()); // In your code, text nodes are just strings.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
package/src/node/root.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class Root extends Node {
|
|
2
|
+
constructor() {
|
|
3
|
+
super('ROOT',{},null);
|
|
4
|
+
this.isSingle = false
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
get body() {return this.$('body')}
|
|
8
|
+
get head() {return this.$('head')}
|
|
9
|
+
get title() {return this.$('title')}
|
|
10
|
+
set title(title) {return this.$('title').innerHTML = title}
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class SingleNode extends Node {
|
|
2
|
+
constructor(tagName, attributes = {}, parent = null) {
|
|
3
|
+
if(attributes['?'] && tagName === '?xml') delete attributes['?']
|
|
4
|
+
super(tagName, attributes, parent);
|
|
5
|
+
this.isSingle = true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get outerHTML() { // outerHTML for single node
|
|
9
|
+
if (this.tagName === "#cdata-section") return `<![CDATA[${this.nodeValue}]]>`;
|
|
10
|
+
const attrs = Object.entries(this.attributes).map(([key, val]) => val.length ? `${key}="${val}"` : key).join(" ");
|
|
11
|
+
return `<${this.tagName} ${attrs}${this.tagName === '?xml' ? '?' : ''}>`;
|
|
12
|
+
}
|
|
13
|
+
// Remove getters,setters and methods which no make sence in single node
|
|
14
|
+
get innerHTML() { return ""; }
|
|
15
|
+
set innerHTML(_) { }
|
|
16
|
+
$(_) {return null}
|
|
17
|
+
$$(_) {return []}
|
|
18
|
+
querySelectorAll(_) { return []; }
|
|
19
|
+
querySelector(_) { return null; }
|
|
20
|
+
getElementsByClassName(_) { return []; }
|
|
21
|
+
getElementsByTagName(_) { return []; }
|
|
22
|
+
getElementById(_) { return null; }
|
|
23
|
+
get children() { return []; }
|
|
24
|
+
insertAdjacentElement(_, __) { }
|
|
25
|
+
insertAdjacentHTML(_, __) { }
|
|
26
|
+
insertAdjacentText(_, __) { }
|
|
27
|
+
appendChild(_) { }
|
|
28
|
+
insert(_,__) { }
|
|
29
|
+
get textContent() { return ""; }
|
|
30
|
+
set textContent(_) { }
|
|
31
|
+
}
|
|
@@ -0,0 +1,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 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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function buildFromCache(cached) {
|
|
2
|
+
function buildNode(cache,parent=null) {
|
|
3
|
+
if(typeof cache === 'string') return parent.childNodes.push(cache)
|
|
4
|
+
const {isSingle,tagName,attributes,childNodes,textContent} = cache
|
|
5
|
+
if(textContent) return parent.childNodes.push(new TextNode(textContent))
|
|
6
|
+
if(isSingle) return parent.childNodes.push(new SingleNode(tagName,attributes))
|
|
7
|
+
const newDoc = tagName === 'ROOT' ? new Root() : new Node(tagName,attributes,parent)
|
|
8
|
+
childNodes.forEach(childNode => {
|
|
9
|
+
buildNode(childNode,newDoc)
|
|
10
|
+
});
|
|
11
|
+
return newDoc
|
|
12
|
+
}
|
|
13
|
+
return buildNode(cached)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
function cacheDoc(doc) {
|
|
18
|
+
const props = ['isSingle','tagName','attributes']
|
|
19
|
+
function addToCache(element,cache={}) {
|
|
20
|
+
if(typeof element === 'string') return element
|
|
21
|
+
if(element.nodeName === '#text') return {textContent:element.textContent}
|
|
22
|
+
props.forEach(prop => {
|
|
23
|
+
if(element[prop]) cache[prop] = element[prop]
|
|
24
|
+
});
|
|
25
|
+
if(!element.childNodes) return cache
|
|
26
|
+
cache.childNodes = []
|
|
27
|
+
element.childNodes.forEach(childNode => {
|
|
28
|
+
cache.childNodes.push(addToCache(childNode))
|
|
29
|
+
});
|
|
30
|
+
return cache
|
|
31
|
+
}
|
|
32
|
+
return addToCache(doc)
|
|
33
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
const end = html.indexOf(endStr, i + n1);
|
|
30
|
+
const strNode = new Node(tag, {}, stack[stack.length - 1]);
|
|
31
|
+
strNode.childNodes.push(html.substring(i + n1, end));
|
|
32
|
+
i = end + n2;
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
while (i < html.length) {
|
|
37
|
+
if(i >= max) max = i;
|
|
38
|
+
else break;
|
|
39
|
+
if (parseScript()) continue
|
|
40
|
+
if (parseSpecial("<!--", "-->", 4, 3, '#comment')) continue
|
|
41
|
+
if (parseSpecial("<style", "</style>", 7, 8, 'style')) continue
|
|
42
|
+
|
|
43
|
+
if (html.startsWith("<![CDATA[", i)) {
|
|
44
|
+
const end = html.indexOf("]]>", i + 9);
|
|
45
|
+
if (end === -1) break; // Убедитесь, что существует закрывающий тег
|
|
46
|
+
const content = html.substring(i + 9, end); // это содержимое CDATA
|
|
47
|
+
const cdataNode = new SingleNode("#cdata-section", {}, stack[stack.length - 1]);
|
|
48
|
+
cdataNode.nodeValue = content;
|
|
49
|
+
i = end + 3;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (html.startsWith("<", i)) {
|
|
54
|
+
if (currentText && stack[stack.length - 1]) {
|
|
55
|
+
const textNode = new TextNode(currentText)
|
|
56
|
+
stack[stack.length - 1].childNodes.push(textNode);
|
|
57
|
+
textNode.parent = stack[stack.length - 1]
|
|
58
|
+
currentText = "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let tagEnd = i + 1;
|
|
62
|
+
let insideQuotes = false;
|
|
63
|
+
let quoteChar = null;
|
|
64
|
+
|
|
65
|
+
while (tagEnd < html.length) {
|
|
66
|
+
const char = html[tagEnd];
|
|
67
|
+
if (!insideQuotes && (char === '"' || char === "'")) {
|
|
68
|
+
insideQuotes = true;
|
|
69
|
+
quoteChar = char;
|
|
70
|
+
} else if (insideQuotes && char === quoteChar) {
|
|
71
|
+
insideQuotes = false;
|
|
72
|
+
quoteChar = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!insideQuotes && char === '>') break;
|
|
76
|
+
tagEnd++;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const tagContent = html.substring(i + 1, tagEnd);
|
|
80
|
+
if (tagContent.startsWith("/")) stack.pop(); // It is close tag
|
|
81
|
+
else { // It is open tag
|
|
82
|
+
let isSelfClosing = tagContent.endsWith('/');
|
|
83
|
+
const tagNameEnd = tagContent.search(/\s|>|\//); // add a check for "/"
|
|
84
|
+
const tagName = tagContent.substring(0, tagNameEnd > 0 ? tagNameEnd : tagEnd - i - 1);
|
|
85
|
+
const attributesString = tagContent.substring(tagName.length, isSelfClosing ? tagContent.length - 1 : tagContent.length).trim();
|
|
86
|
+
const attributes = parseAttributes(attributesString);
|
|
87
|
+
if (VOID_TAGS.has(tagName.toLowerCase()) || isSelfClosing) new SingleNode(tagName, attributes, stack[stack.length - 1])
|
|
88
|
+
else stack.push(new Node(tagName, attributes, stack[stack.length - 1]));
|
|
89
|
+
}
|
|
90
|
+
i = tagEnd + 1;
|
|
91
|
+
} else {
|
|
92
|
+
currentText += html[i];
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (currentText.trim() && stack[stack.length - 1]) stack[stack.length - 1].childNodes.push(new TextNode(currentText));
|
|
97
|
+
return root;
|
|
98
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function checkElement(el,selector) {
|
|
2
|
+
if(selector == undefined) return true
|
|
3
|
+
if(el == null) return false
|
|
4
|
+
let {tag,classList,attribs:attributes,id,prev,ancestors,parents,prevAny} = selector
|
|
5
|
+
if(typeof el === 'string') return false
|
|
6
|
+
if(id !== undefined && el.id === null) return false
|
|
7
|
+
if(id && id !== el.id) return false
|
|
8
|
+
if(tag && el.tagName === undefined) return false
|
|
9
|
+
else if(tag && tag !== el.tagName) return false
|
|
10
|
+
const clas = el.attributes.class
|
|
11
|
+
if(classList !== undefined && (clas === undefined || clas === '')) return false
|
|
12
|
+
else if(classList !== undefined) {
|
|
13
|
+
if(classList.every(e => el.classList.contains(e)) === false) return false
|
|
14
|
+
}
|
|
15
|
+
if(checkattributes(attributes,el) === false) return false
|
|
16
|
+
if(checkElement(el.prev,prev) === false) return false
|
|
17
|
+
if(checkAncestors(el.ancestors,ancestors) === false) return false
|
|
18
|
+
if(checkParents(el.ancestors,parents) === false) return false
|
|
19
|
+
if(el.parent) {
|
|
20
|
+
if(checkPrevAny(el.parent.children,el.childIndex,prevAny) == false) return false
|
|
21
|
+
}
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function checkattributes(attributes=[],el) {
|
|
26
|
+
let elattributes = el.attributes
|
|
27
|
+
let names = Object.keys(elattributes)
|
|
28
|
+
let passedTests = 0
|
|
29
|
+
if(attributes) for(let i=0; i<attributes.length; i++) {
|
|
30
|
+
let {name,value,check} = attributes[i]
|
|
31
|
+
if(name == 'inner' && value !== undefined && check && el.inner) {
|
|
32
|
+
if(check(el.inner)) passedTests++
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if(!names.includes(name)) continue
|
|
36
|
+
else if(value == undefined) passedTests++
|
|
37
|
+
else if(value && elattributes[name]) {
|
|
38
|
+
if(check(elattributes[name]) == false) continue
|
|
39
|
+
else passedTests++
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if(passedTests == attributes.length) return true
|
|
43
|
+
else return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkPrevAny(children=[],index,prevAny) {
|
|
47
|
+
let size = children.length
|
|
48
|
+
if((size == 0 || index == 0) && prevAny) return false
|
|
49
|
+
for(let i=index; i>=0; i--) {
|
|
50
|
+
if(checkElement(children[i],prevAny)) return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkAncestors(ancestors=[],selectorAncestors=[]) {
|
|
56
|
+
let count = 0
|
|
57
|
+
if(selectorAncestors.length == 0) return true
|
|
58
|
+
let endIndex = ancestors.length-1
|
|
59
|
+
let selectorIndex = selectorAncestors.length-1
|
|
60
|
+
while(selectorIndex>=0) {
|
|
61
|
+
for(let i=endIndex; i>=0; i--) {
|
|
62
|
+
endIndex=i-1
|
|
63
|
+
if(checkElement(ancestors[i],selectorAncestors[selectorIndex]) == true) {
|
|
64
|
+
count++
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
selectorIndex--
|
|
69
|
+
}
|
|
70
|
+
if(count == selectorAncestors.length) return true
|
|
71
|
+
else return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkParents(ancestors=[],selectorParents=[]) {
|
|
75
|
+
if(selectorParents.length === 0) return true
|
|
76
|
+
if(ancestors.length < selectorParents.length) return false
|
|
77
|
+
let index = ancestors.length-1
|
|
78
|
+
for(let i=selectorParents.length-1; i>=0; i--) {
|
|
79
|
+
if(checkElement(ancestors[index],selectorParents[i]) === false) return false
|
|
80
|
+
index--
|
|
81
|
+
}
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
class Query {
|
|
2
|
+
static get(query) {
|
|
3
|
+
let q = new Query(query)
|
|
4
|
+
return q.selectors
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
constructor(query) {
|
|
8
|
+
this.query = query
|
|
9
|
+
this.selectors = []
|
|
10
|
+
this.stringValues = [];
|
|
11
|
+
this.parseSelectors(query.split(','))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parseSelectors(selectors) {
|
|
15
|
+
selectors.forEach(selector => {
|
|
16
|
+
let originalSelector = selector.trim()
|
|
17
|
+
selector = this.removeSpaces(selector)
|
|
18
|
+
this.stringValues = []
|
|
19
|
+
selector = selector.replace(/\[.*?\]/g, (value) => {
|
|
20
|
+
this.stringValues.push(value)
|
|
21
|
+
return `[${this.stringValues.length - 1}]`
|
|
22
|
+
})
|
|
23
|
+
let [element, ancestors] = this.splitAndCutLast(selector, ' ') // \s - ancestor
|
|
24
|
+
element = this.getFamily(element)
|
|
25
|
+
if (ancestors.length > 0)
|
|
26
|
+
element.ancestors = ancestors.map(ancestor => this.getFamily(ancestor))
|
|
27
|
+
element.group = originalSelector
|
|
28
|
+
this.selectors.push(element)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
splitAndCutLast(string, splitBy) {
|
|
33
|
+
const array = string.split(splitBy);
|
|
34
|
+
const last = array.pop(); // pop() удаляет и возвращает последний элемент массива
|
|
35
|
+
return [last, array];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getFamily(group, element, prev, prevAny, sign) {
|
|
39
|
+
if (group.match(/\~|\+/) !== null) {
|
|
40
|
+
let [last, prevBrothers] = this.splitAndCutLast(group, /\~|\+/)
|
|
41
|
+
let signs = group.replace(last, '')
|
|
42
|
+
prevBrothers.forEach(el => signs = signs.replace(el, ''))
|
|
43
|
+
signs = signs.match(/\~|\+/g)
|
|
44
|
+
if (signs.length == 1) {
|
|
45
|
+
sign = signs[0]
|
|
46
|
+
} else if (signs.length > 1) {
|
|
47
|
+
sign = signs.splice(signs.length - 1, signs.length - 1)[0]
|
|
48
|
+
prevBrothers[0] = prevBrothers.map((b, i) => {
|
|
49
|
+
if (i < prevBrothers.length - 1) b += signs[i]
|
|
50
|
+
return b
|
|
51
|
+
}).join('')
|
|
52
|
+
prevBrothers[0] = this.getFamily(prevBrothers[0])
|
|
53
|
+
}
|
|
54
|
+
if (sign == '~') prevAny = prevBrothers[0] // ~ - any prev brother
|
|
55
|
+
else if (sign == '+') prev = prevBrothers[0] // + - prev brother
|
|
56
|
+
element = last
|
|
57
|
+
} else element = group
|
|
58
|
+
let family
|
|
59
|
+
if (prev || prevAny) {
|
|
60
|
+
family = this.getParents(element)
|
|
61
|
+
if (prev) family.prev = this.getParents(prev)
|
|
62
|
+
if (prevAny) family.prevAny = this.getParents(prevAny)
|
|
63
|
+
} else family = this.getParents(element)
|
|
64
|
+
if (family.query !== group) family.group = group
|
|
65
|
+
return family
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getParents(selector) {
|
|
69
|
+
if (typeof selector == 'string') {
|
|
70
|
+
let [element, parents] = this.splitAndCutLast(selector, '>')
|
|
71
|
+
element = this.buildElement(element)
|
|
72
|
+
parents = parents.map(parent => this.buildElement(parent))
|
|
73
|
+
if (parents.length > 0) element.parents = parents
|
|
74
|
+
return element
|
|
75
|
+
} else return selector
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildElement(element, id = null, tag = null, classList = []) {
|
|
79
|
+
let query = element
|
|
80
|
+
element = element.replace(/\#(\w-?)*/, $id => {
|
|
81
|
+
id = $id.replace(/^\#/, ''); return ''
|
|
82
|
+
})
|
|
83
|
+
element = element.replace(/\.(\w-?)*/, $class => {
|
|
84
|
+
classList.push($class.replace(/^\./, '')); return ''
|
|
85
|
+
})
|
|
86
|
+
element = element.replace(/(\w\:?-?)*/, $tag => {
|
|
87
|
+
tag = $tag == '' ? null : $tag; return ''
|
|
88
|
+
})
|
|
89
|
+
let attribs = this.getAttributes(element)
|
|
90
|
+
element = { query }
|
|
91
|
+
if (id) element.id = id
|
|
92
|
+
if (tag) element.tag = tag
|
|
93
|
+
if (classList.length > 0) element.classList = classList
|
|
94
|
+
if (attribs.length > 0) element.attribs = attribs
|
|
95
|
+
return element
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getAttributes(element) {
|
|
99
|
+
let attribs = this.stringValues.filter((value, index) => {
|
|
100
|
+
let searchValue = `[${index}]`
|
|
101
|
+
if (element.match(searchValue)) return true
|
|
102
|
+
else return false
|
|
103
|
+
})
|
|
104
|
+
attribs = attribs.map(attrib => {
|
|
105
|
+
let query = attrib
|
|
106
|
+
attrib = attrib.replace('[', '').replace(']', '')
|
|
107
|
+
let [name, value] = attrib.split(/[\~\|\^\$\*]?\=/)
|
|
108
|
+
let sign = attrib.replace(name, '').replace(value, '')
|
|
109
|
+
attrib = { query }
|
|
110
|
+
if (name) attrib.name = name
|
|
111
|
+
if (value) attrib.value = value.trim().replace(/^\"/, '').replace(/\"$/, '')
|
|
112
|
+
if (sign) {
|
|
113
|
+
attrib.sign = sign
|
|
114
|
+
attrib.check = this.getAttribFn(sign).bind(attrib)
|
|
115
|
+
}
|
|
116
|
+
return attrib
|
|
117
|
+
});
|
|
118
|
+
return attribs
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getAttribFn(sign) {
|
|
122
|
+
if (sign == '=') return function (value) { return value === this.value }
|
|
123
|
+
if (sign == '*=') return function (value) { return value.includes(this.value) }
|
|
124
|
+
if (sign == '^=') return function (value) { return value.startsWith(this.value) }
|
|
125
|
+
if (sign == '$=') return function (value) { return value.endsWith(this.value) }
|
|
126
|
+
if (sign == '|=') return function (value) {
|
|
127
|
+
return value.trim().split(' ').length == 1
|
|
128
|
+
&& (value.startsWith(this.value) || value.startsWith(this.value + '-'))
|
|
129
|
+
? true : false
|
|
130
|
+
}
|
|
131
|
+
if (sign == '~=') return function (value) { // includes whole word only
|
|
132
|
+
return this.value.trim().split(' ').length == 1 && value.includes(this.value) ? true : false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
removeSpaces(selector) {
|
|
137
|
+
selector = selector.replace(/\s{2}/g, ' ') // remove double spaces
|
|
138
|
+
selector = selector.replace(/\s?\^?\$?\|?\~?\*?\=\s*/g, (m) => m.trim()) // remove spaces inside []
|
|
139
|
+
selector = selector.replace(/\s?(\+|\~|\>)\s?/g, (m) => m.trim()) // remove spaces around +|~|>
|
|
140
|
+
return selector
|
|
141
|
+
}
|
|
142
|
+
}
|