als-document 0.12.0 → 1.0.0-alpha
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 +33 -589
- package/index.js +32 -0
- package/index.mjs +32 -0
- package/package.json +13 -10
- package/readme.md +196 -156
- package/src/build.js +65 -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 +159 -0
- package/src/node/single-node.js +30 -0
- package/src/node/style.js +26 -0
- package/src/node/text-node.js +9 -0
- package/src/parse/parse-atts.js +36 -0
- package/src/parse/parser.js +74 -0
- package/src/parse/void-tags.js +5 -0
- package/src/query/check-element.js +84 -0
- package/src/query/query.js +142 -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 +30 -0
- package/tests/node.js +196 -0
- package/tests/parse-real.js +52 -0
- package/tests/parser.js +351 -0
- package/tests/query.js +66 -0
- package/tests/utils.js +37 -0
|
@@ -0,0 +1,30 @@
|
|
|
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 для одиночного узла
|
|
9
|
+
if (this.tagName === "#cdata-section") return `<![CDATA[${this.textContent}]]>`;
|
|
10
|
+
const attrs = Object.entries(this.attributes).map(([key, val]) => `${key}="${val}"`).join(" ");
|
|
11
|
+
return `<${this.tagName} ${attrs}${this.tagName === '?xml' ? '?' : ''}>`;
|
|
12
|
+
}
|
|
13
|
+
// Удаляем методы и свойства, которые не имеют смысла для одиночных узлов
|
|
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
|
+
get textContent() { return ""; }
|
|
29
|
+
set textContent(_) { }
|
|
30
|
+
}
|
|
@@ -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,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()] = true; // 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,74 @@
|
|
|
1
|
+
function parseHTML(html) {
|
|
2
|
+
const root = new Node("ROOT");
|
|
3
|
+
const stack = [root];
|
|
4
|
+
let currentText = "", i = 0;
|
|
5
|
+
|
|
6
|
+
function parseSpecial(startStr, endStr, n1, n2, tag) {
|
|
7
|
+
if (!html.startsWith(startStr, i)) return false
|
|
8
|
+
const end = html.indexOf(endStr, i + n1);
|
|
9
|
+
const strNode = new Node(tag, {}, stack[stack.length - 1]);
|
|
10
|
+
strNode.childNodes.push(html.substring(i + n1, end));
|
|
11
|
+
i = end + n2;
|
|
12
|
+
return true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
while (i < html.length) {
|
|
16
|
+
if (parseSpecial("<!--", "-->", 4, 3, '#comment')) continue
|
|
17
|
+
if (parseSpecial("<script", "</script>", 8, 9, 'script')) continue
|
|
18
|
+
if (parseSpecial("<style", "</style>", 7, 8, 'style')) continue
|
|
19
|
+
|
|
20
|
+
if (html.startsWith("<![CDATA[", i)) {
|
|
21
|
+
const end = html.indexOf("]]>", i + 9);
|
|
22
|
+
if (end === -1) break; // Убедитесь, что существует закрывающий тег
|
|
23
|
+
const content = html.substring(i + 9, end); // это содержимое CDATA
|
|
24
|
+
const cdataNode = new SingleNode("#cdata-section", {}, stack[stack.length - 1]);
|
|
25
|
+
cdataNode.nodeValue = content;
|
|
26
|
+
i = end + 3;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (html.startsWith("<", i)) {
|
|
31
|
+
if (currentText.trim()) {
|
|
32
|
+
stack[stack.length - 1].childNodes.push(new TextNode(currentText.trim()));
|
|
33
|
+
currentText = "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let tagEnd = i + 1;
|
|
37
|
+
let insideQuotes = false;
|
|
38
|
+
let quoteChar = null;
|
|
39
|
+
|
|
40
|
+
while (tagEnd < html.length) {
|
|
41
|
+
const char = html[tagEnd];
|
|
42
|
+
if (!insideQuotes && (char === '"' || char === "'")) {
|
|
43
|
+
insideQuotes = true;
|
|
44
|
+
quoteChar = char;
|
|
45
|
+
} else if (insideQuotes && char === quoteChar) {
|
|
46
|
+
insideQuotes = false;
|
|
47
|
+
quoteChar = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!insideQuotes && char === '>') break;
|
|
51
|
+
tagEnd++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tagContent = html.substring(i + 1, tagEnd);
|
|
55
|
+
|
|
56
|
+
if (tagContent.startsWith("/")) stack.pop(); // It is close tag
|
|
57
|
+
else { // It is open tag
|
|
58
|
+
let isSelfClosing = tagContent.endsWith('/');
|
|
59
|
+
const tagNameEnd = tagContent.search(/\s|>|\//); // add a check for "/"
|
|
60
|
+
const tagName = tagContent.substring(0, tagNameEnd > 0 ? tagNameEnd : tagEnd - i - 1);
|
|
61
|
+
const attributesString = tagContent.substring(tagName.length, isSelfClosing ? tagContent.length - 1 : tagContent.length).trim();
|
|
62
|
+
const attributes = parseAttributes(attributesString);
|
|
63
|
+
if (VOID_TAGS.has(tagName.toLowerCase()) || isSelfClosing) new SingleNode(tagName, attributes, stack[stack.length - 1])
|
|
64
|
+
else stack.push(new Node(tagName, attributes, stack[stack.length - 1]));
|
|
65
|
+
}
|
|
66
|
+
i = tagEnd + 1;
|
|
67
|
+
} else {
|
|
68
|
+
currentText += html[i];
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (currentText.trim()) stack[stack.length - 1].childNodes.push(new TextNode(currentText.trim()));
|
|
73
|
+
return root;
|
|
74
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function checkElement(el,selector) {
|
|
2
|
+
if(selector == undefined) return true
|
|
3
|
+
if(el == null) return false
|
|
4
|
+
let {tag,classList,attributes,id,prev,ancestors,parents,prevAny} = selector
|
|
5
|
+
if(typeof el === 'string') return false
|
|
6
|
+
if(el.isSpecial) return false
|
|
7
|
+
if(id !== undefined && el.id === null) return false
|
|
8
|
+
if(id && id !== el.id) return false
|
|
9
|
+
if(tag && el.tagName === undefined) return false
|
|
10
|
+
else if(tag && tag !== el.tagName) return false
|
|
11
|
+
const clas = el.attributes.class
|
|
12
|
+
if(classList !== undefined && (clas === undefined || clas === '')) return false
|
|
13
|
+
else if(classList !== undefined) {
|
|
14
|
+
if(classList.every(e => el.classList.contains(e)) === false) return false
|
|
15
|
+
}
|
|
16
|
+
if(checkattributes(attributes,el) === false) return false
|
|
17
|
+
if(checkElement(el.prev,prev) === false) return false
|
|
18
|
+
if(checkAncestors(el.ancestors,ancestors) === false) return false
|
|
19
|
+
if(checkParents(el.ancestors,parents) === false) return false
|
|
20
|
+
if(el.parent) {
|
|
21
|
+
if(checkPrevAny(el.parent.children,el.childIndex,prevAny) == false) return false
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function checkattributes(attributes=[],el) {
|
|
27
|
+
let elattributes = el.attributes
|
|
28
|
+
let names = Object.keys(elattributes)
|
|
29
|
+
let passedTests = 0
|
|
30
|
+
if(attributes) for(let i=0; i<attributes.length; i++) {
|
|
31
|
+
let {name,value,check} = attributes[i]
|
|
32
|
+
if(name == 'inner' && value !== undefined && check && el.inner) {
|
|
33
|
+
if(check(el.inner)) passedTests++
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if(!names.includes(name)) continue
|
|
37
|
+
else if(value == undefined) passedTests++
|
|
38
|
+
else if(value && elattributes[name]) {
|
|
39
|
+
if(check(elattributes[name]) == false) continue
|
|
40
|
+
else passedTests++
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if(passedTests == attributes.length) return true
|
|
44
|
+
else return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkPrevAny(children=[],index,prevAny) {
|
|
48
|
+
let size = children.length
|
|
49
|
+
if((size == 0 || index == 0) && prevAny) return false
|
|
50
|
+
for(let i=index; i>=0; i--) {
|
|
51
|
+
if(checkElement(children[i],prevAny)) return true
|
|
52
|
+
}
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkAncestors(ancestors=[],selectorAncestors=[]) {
|
|
57
|
+
let count = 0
|
|
58
|
+
if(selectorAncestors.length == 0) return true
|
|
59
|
+
let endIndex = ancestors.length-1
|
|
60
|
+
let selectorIndex = selectorAncestors.length-1
|
|
61
|
+
while(selectorIndex>=0) {
|
|
62
|
+
for(let i=endIndex; i>=0; i--) {
|
|
63
|
+
endIndex=i-1
|
|
64
|
+
if(checkElement(ancestors[i],selectorAncestors[selectorIndex]) == true) {
|
|
65
|
+
count++
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
selectorIndex--
|
|
70
|
+
}
|
|
71
|
+
if(count == selectorAncestors.length) return true
|
|
72
|
+
else return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checkParents(ancestors=[],selectorParents=[]) {
|
|
76
|
+
if(selectorParents.length === 0) return true
|
|
77
|
+
if(ancestors.length < selectorParents.length) return false
|
|
78
|
+
let index = ancestors.length-1
|
|
79
|
+
for(let i=selectorParents.length-1; i>=0; i--) {
|
|
80
|
+
if(checkElement(ancestors[index],selectorParents[i]) === false) return false
|
|
81
|
+
index--
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
@@ -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
|
+
}
|