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.
@@ -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
+ }
@@ -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,9 @@
1
+ class TextNode {
2
+ constructor(data) {
3
+ this.nodeName = '#text';
4
+ this.parent = null;
5
+ this.textContent = data;
6
+ }
7
+ get nodeValue() {return this.textContent}
8
+ get parentNode() {return this.parent}
9
+ }
@@ -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,5 @@
1
+ const VOID_TAGS = new Set([
2
+ "area", "base", "br", "col", "command", "embed", "hr", "img", "input",
3
+ "keygen", "link", "meta", "param", "source", "track", "wbr",
4
+ "!doctype",'?xml'
5
+ ]);
@@ -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
+ }