als-document 1.0.7-alpha → 1.1.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/document.js +13 -6
- package/index.js +13 -6
- package/index.mjs +13 -6
- package/package.json +2 -5
- package/readme.md +32 -3
- package/src/build.js +3 -3
- package/src/node/node.js +38 -23
- package/src/node/root.js +11 -0
- package/src/node/single-node.js +5 -4
- package/src/parse/cache.js +33 -0
- package/src/parse/parse-atts.js +1 -1
- package/src/parse/parser.js +1 -1
- package/src/query/query.js +13 -6
- package/tests/cache.js +19 -0
- package/tests/index.html +3 -2
- package/tests/parser.js +1 -1
- package/tests/test.js +169 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "als-document",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -10,8 +10,5 @@
|
|
|
10
10
|
},
|
|
11
11
|
"keywords": ["HTML", "DOM", "parser", "manipulation", "backend", "frontend", "als-document"],
|
|
12
12
|
"author": "Alex Sorkin <alexsorkin1980@gmail.com>",
|
|
13
|
-
"license": "ISC"
|
|
14
|
-
"devDependencies": {
|
|
15
|
-
"als-simple-test": "^0.3.5"
|
|
16
|
-
}
|
|
13
|
+
"license": "ISC"
|
|
17
14
|
}
|
package/readme.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# als-document: HTML Parser & DOM Manipulation Library
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
## Overview
|
|
4
5
|
|
|
5
6
|
`als-document` is a powerful library for parsing HTML and manipulating the 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.
|
|
6
7
|
|
|
8
|
+
## Release notes
|
|
9
|
+
* als-document is still on alpha testing. All tested features works fine, but through the use, discovering some bugs or things that should work different. For example in this release, changed the way for storing attributes with empty value.
|
|
10
|
+
* Also, this release, has additional very powefull feature which is building cache for storing DOM tree as json and building back DOM from cache.
|
|
7
11
|
|
|
8
12
|
## Installation
|
|
9
13
|
|
|
@@ -21,13 +25,13 @@ The library provides three different files to cater to different module systems:
|
|
|
21
25
|
1. **index.js**: This file uses the CommonJS module system. It's suitable for projects using Node.js or bundlers like Browserify or Webpack. The entry point in `package.json` for this file is "main".
|
|
22
26
|
|
|
23
27
|
```javascript
|
|
24
|
-
const { parseHTML, Node, Query, TextNode, SingleNode } = require('als-document');
|
|
28
|
+
const { parseHTML, Node, Query, TextNode, SingleNode,Root } = require('als-document');
|
|
25
29
|
```
|
|
26
30
|
|
|
27
31
|
2. **index.mjs**: This file uses the ES Modules (ESM) system. It's suitable for modern JavaScript environments that support ESM. The entry point in `package.json` for this file is "module".
|
|
28
32
|
|
|
29
33
|
```js
|
|
30
|
-
import { parseHTML, Node, Query, TextNode, SingleNode } from 'als-document';
|
|
34
|
+
import { parseHTML, Node, Query, TextNode, SingleNode, Root } from 'als-document';
|
|
31
35
|
```
|
|
32
36
|
|
|
33
37
|
3. **document.js**: By including this file, a constant variable named `alsDocument` is created, which wraps all the exports.
|
|
@@ -35,7 +39,7 @@ import { parseHTML, Node, Query, TextNode, SingleNode } from 'als-document';
|
|
|
35
39
|
```html
|
|
36
40
|
<script src="/node_modules/als-document/document.js"></script>
|
|
37
41
|
<script>
|
|
38
|
-
const { parseHTML, Node, Query, TextNode, SingleNode } = alsDocument
|
|
42
|
+
const { parseHTML, Node, Query, TextNode, SingleNode, buildFromCache, cacheDoc, Root } = alsDocument
|
|
39
43
|
</script>
|
|
40
44
|
```
|
|
41
45
|
|
|
@@ -101,6 +105,7 @@ Remember, the actual tree structure will be more complex and detailed, but the p
|
|
|
101
105
|
- **getElementsByClassName, getElementsByTagName, getElementById**: Get elements by class, tag, or id respectively.
|
|
102
106
|
- **insertAdjacentElement, insertAdjacentHTML, insertAdjacentText**: Insert content relative to the element.
|
|
103
107
|
- **appendChild**: Add a child node to the element.
|
|
108
|
+
- **insert(place,element)**: place (0-3) or beforebegin,afterbegin,... eleemnt - raw html or element
|
|
104
109
|
|
|
105
110
|
|
|
106
111
|
### SingleNode
|
|
@@ -112,6 +117,15 @@ Remember, the actual tree structure will be more complex and detailed, but the p
|
|
|
112
117
|
`TextNode` is a class that represents text content within the DOM. A TextNode holds raw text data and does not have child nodes.
|
|
113
118
|
|
|
114
119
|
|
|
120
|
+
### Root node (extends Node)
|
|
121
|
+
|
|
122
|
+
Has additional getters and setters:
|
|
123
|
+
* getter root.title
|
|
124
|
+
* setter root.title
|
|
125
|
+
* getter root.body
|
|
126
|
+
* getter root.head
|
|
127
|
+
|
|
128
|
+
|
|
115
129
|
|
|
116
130
|
### Examples:
|
|
117
131
|
|
|
@@ -242,3 +256,18 @@ if attribute has value, attrib object will contain check function with one param
|
|
|
242
256
|
let s = Query.get('[test^="some"]')[0]
|
|
243
257
|
console.log(s.attribs[0].check('some value test')) // true
|
|
244
258
|
```
|
|
259
|
+
|
|
260
|
+
## buildFromCache and cacheDoc
|
|
261
|
+
|
|
262
|
+
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.
|
|
263
|
+
The caching process and building from cache takes less then 5ms for each and require realy low resources.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
How it works?
|
|
267
|
+
```js
|
|
268
|
+
const html = `` // some real html 255KB
|
|
269
|
+
const root = parseHTML(html); // 31.9ms
|
|
270
|
+
const cache = cacheDoc(root); // 2.4ms
|
|
271
|
+
const root1 = buildFromCache(cache); // 1.2ms
|
|
272
|
+
console.log(root.inneHTML === root1.innerHTML) // true
|
|
273
|
+
```
|
package/src/build.js
CHANGED
|
@@ -22,9 +22,9 @@ const files = {
|
|
|
22
22
|
'query': ['query','check-element'],
|
|
23
23
|
'node':[
|
|
24
24
|
'dataset','find','text-node',
|
|
25
|
-
'style','class-list','node','single-node'
|
|
25
|
+
'style','class-list','node','single-node','root'
|
|
26
26
|
],
|
|
27
|
-
'parse':['parse-atts','void-tags','parser'],
|
|
27
|
+
'parse':['parse-atts','void-tags','parser','cache'],
|
|
28
28
|
}
|
|
29
29
|
const fileList = []
|
|
30
30
|
function buildFileList() {
|
|
@@ -40,7 +40,7 @@ buildFileList()
|
|
|
40
40
|
function build() {
|
|
41
41
|
let content = fileList.map(filePath => readFileSync(filePath, 'utf-8')).join('\n');
|
|
42
42
|
|
|
43
|
-
const toReturn = '{ parseHTML, Node, Query, TextNode, SingleNode }'
|
|
43
|
+
const toReturn = '{ parseHTML, Node, Query, TextNode, SingleNode, buildFromCache, cacheDoc, Root }'
|
|
44
44
|
content = optimizeCode(content)
|
|
45
45
|
writeFileSync(join(root, 'document.js'), `const alsDocument = (function(){\n${content}\nreturn ${toReturn}\n})()`)
|
|
46
46
|
writeFileSync(join(root, 'index.js'), content + '\n' + `module.exports = ${toReturn}`)
|
package/src/node/node.js
CHANGED
|
@@ -19,10 +19,10 @@ class Node {
|
|
|
19
19
|
|
|
20
20
|
get id() { return this.attributes.id ? this.attributes.id : null; }
|
|
21
21
|
set id(newValue) { this.attributes.id = newValue; }
|
|
22
|
-
get className() {return this.attributes.class || null}
|
|
22
|
+
get className() { return this.attributes.class || null }
|
|
23
23
|
get parentNode() { return this.parent }
|
|
24
24
|
get ancestors() {
|
|
25
|
-
if(!this.parent) return []
|
|
25
|
+
if (!this.parent) return []
|
|
26
26
|
const ancestors = []
|
|
27
27
|
let element = this.parent
|
|
28
28
|
while (element.tagName !== 'ROOT') {
|
|
@@ -33,13 +33,13 @@ class Node {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
get childNodeIndex() {
|
|
36
|
-
if(!this.parent) return null
|
|
37
|
-
return this.parent.childNodes ? this.parent.childNodes.indexOf(this) : null
|
|
36
|
+
if (!this.parent) return null
|
|
37
|
+
return this.parent.childNodes ? this.parent.childNodes.indexOf(this) : null
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
get childIndex() {
|
|
41
|
-
if(!this.parent) return null
|
|
42
|
-
return this.parent.children ? this.parent.children.indexOf(this) : null
|
|
40
|
+
get childIndex() {
|
|
41
|
+
if (!this.parent) return null
|
|
42
|
+
return this.parent.children ? this.parent.children.indexOf(this) : null
|
|
43
43
|
}
|
|
44
44
|
get previousElementSibling() { return this.prev }
|
|
45
45
|
get prev() {
|
|
@@ -69,8 +69,8 @@ class Node {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
get outerHTML() {
|
|
72
|
-
const attrs = Object.entries(this.attributes).map(([key, val]) => `${key}="${val}"`).join(" ");
|
|
73
|
-
return `<${this.tagName}${attrs ? ' '+attrs : ''}>${this.innerHTML}</${this.tagName}>`;
|
|
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
74
|
}
|
|
75
75
|
|
|
76
76
|
getAttribute(attrName) { return this.attributes[attrName] || null }
|
|
@@ -91,12 +91,12 @@ class Node {
|
|
|
91
91
|
}).join("");
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
$$(query) {return this.querySelectorAll(query)}
|
|
94
|
+
$$(query) { return this.querySelectorAll(query) }
|
|
95
95
|
querySelectorAll(query) {
|
|
96
96
|
const selectors = Query.get(query)
|
|
97
97
|
return find(selectors, this, new Set())
|
|
98
98
|
}
|
|
99
|
-
$(query) {return this.querySelector(query)}
|
|
99
|
+
$(query) { return this.querySelector(query) }
|
|
100
100
|
querySelector(query) {
|
|
101
101
|
const selectors = Query.get(query)
|
|
102
102
|
return find(selectors, this, new Set(), true)[0] || null
|
|
@@ -115,12 +115,15 @@ class Node {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
insertAdjacentElement(position, newElement) {
|
|
118
|
-
if(newElement.tagName === 'ROOT' && newElement.childNodes.length > 0) newElement = newElement.childNodes[0]
|
|
118
|
+
if (newElement.tagName === 'ROOT' && newElement.childNodes.length > 0) newElement = newElement.childNodes[0]
|
|
119
119
|
const pos = position.toLowerCase();
|
|
120
|
-
if (pos ===
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
if (pos === 'afterbegin' || pos === 'beforeend') {
|
|
121
|
+
if (pos === "afterbegin") this.childNodes.unshift(newElement);
|
|
122
|
+
else if (pos === "beforeend") this.childNodes.push(newElement);
|
|
123
|
+
newElement.parent = this
|
|
124
|
+
return newElement
|
|
125
|
+
}
|
|
126
|
+
if (!this.parent) throw new Error("Can't insert element to element without parent")
|
|
124
127
|
if (pos === "beforebegin") insertBefore(this.parent.childNodes, this.childNodeIndex, newElement)
|
|
125
128
|
else if (pos === "afterend") this.parent.childNodes.splice(this.childNodeIndex + 1, 0, newElement);
|
|
126
129
|
newElement.parent = this.parent
|
|
@@ -129,7 +132,7 @@ class Node {
|
|
|
129
132
|
|
|
130
133
|
insertAdjacentHTML(position, html) {
|
|
131
134
|
const newNode = parseHTML(html);
|
|
132
|
-
newNode.childNodes.
|
|
135
|
+
newNode.childNodes.forEach(node => {
|
|
133
136
|
this.insertAdjacentElement(position, node);
|
|
134
137
|
});
|
|
135
138
|
return newNode
|
|
@@ -139,9 +142,21 @@ class Node {
|
|
|
139
142
|
return this.insertAdjacentElement(position, new TextNode(text));
|
|
140
143
|
}
|
|
141
144
|
|
|
145
|
+
insert(position, element) {
|
|
146
|
+
const positions = ['beforebegin', 'afterbegin', 'beforeend', 'afterend']
|
|
147
|
+
if (positions[position]) position = positions[position]
|
|
148
|
+
if (typeof element === 'string') {
|
|
149
|
+
element = element.trim()
|
|
150
|
+
if (element.startsWith('<') && element.endsWith('>')) {
|
|
151
|
+
return this.insertAdjacentHTML(position, element)
|
|
152
|
+
}
|
|
153
|
+
return this.insertAdjacentText(position, element)
|
|
154
|
+
}
|
|
155
|
+
return this.insertAdjacentElement(position, element)
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
set innerHTML(html) {
|
|
143
|
-
|
|
144
|
-
this.childNodes = parsed.childNodes;
|
|
159
|
+
this.childNodes = html.trim().startsWith('<') ? parseHTML(html).childNodes : [html]
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
set outerHTML(html) {
|
|
@@ -154,7 +169,7 @@ class Node {
|
|
|
154
169
|
appendChild(newChild) {
|
|
155
170
|
if (newChild instanceof Node || newChild instanceof TextNode || newChild instanceof SingleNode) {
|
|
156
171
|
if (newChild.parent) newChild.parent.childNodes = newChild.parent.childNodes.filter(child => child !== newChild); // Если у newChild уже есть родительский узел, необходимо его удалить оттуда
|
|
157
|
-
} else if(typeof newChild === 'string') newChild = new TextNode(newChild)
|
|
172
|
+
} else if (typeof newChild === 'string') newChild = new TextNode(newChild)
|
|
158
173
|
else return newChild
|
|
159
174
|
this.childNodes.push(newChild);
|
|
160
175
|
newChild.parent = this;
|
|
@@ -164,9 +179,9 @@ class Node {
|
|
|
164
179
|
get textContent() {
|
|
165
180
|
if (this.childNodes.length === 0) return this.nodeName === '#text' ? this.nodeValue : '';
|
|
166
181
|
return this.childNodes.map(child => { // Concatenate text content of this node and all descendants
|
|
167
|
-
if(child instanceof SingleNode) return ''
|
|
168
|
-
if(child instanceof TextNode) return child.nodeValue
|
|
169
|
-
if(child instanceof Node) return child.textContent;
|
|
182
|
+
if (child instanceof SingleNode) return ''
|
|
183
|
+
if (child instanceof TextNode) return child.nodeValue
|
|
184
|
+
if (child instanceof Node) return child.textContent;
|
|
170
185
|
else return child;
|
|
171
186
|
}).join(" ");
|
|
172
187
|
}
|
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
|
+
}
|
package/src/node/single-node.js
CHANGED
|
@@ -5,12 +5,12 @@ class SingleNode extends Node {
|
|
|
5
5
|
this.isSingle = true
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
get outerHTML() { //
|
|
8
|
+
get outerHTML() { // outerHTML for single node
|
|
9
9
|
if (this.tagName === "#cdata-section") return `<![CDATA[${this.nodeValue}]]>`;
|
|
10
|
-
const attrs = Object.entries(this.attributes).map(([key, val]) => `${key}="${val}"`).join(" ");
|
|
10
|
+
const attrs = Object.entries(this.attributes).map(([key, val]) => val.length ? `${key}="${val}"` : key).join(" ");
|
|
11
11
|
return `<${this.tagName} ${attrs}${this.tagName === '?xml' ? '?' : ''}>`;
|
|
12
12
|
}
|
|
13
|
-
//
|
|
13
|
+
// Remove getters,setters and methods which no make sence in single node
|
|
14
14
|
get innerHTML() { return ""; }
|
|
15
15
|
set innerHTML(_) { }
|
|
16
16
|
$(_) {return null}
|
|
@@ -25,6 +25,7 @@ class SingleNode extends Node {
|
|
|
25
25
|
insertAdjacentHTML(_, __) { }
|
|
26
26
|
insertAdjacentText(_, __) { }
|
|
27
27
|
appendChild(_) { }
|
|
28
|
+
insert(_,__) { }
|
|
28
29
|
get textContent() { return ""; }
|
|
29
30
|
set textContent(_) { }
|
|
30
|
-
}
|
|
31
|
+
}
|
|
@@ -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
|
+
}
|
package/src/parse/parse-atts.js
CHANGED
|
@@ -31,6 +31,6 @@ function parseAttributes(str) {
|
|
|
31
31
|
else value += char;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
if (key.trim() && !value) attrs[key.trim()] =
|
|
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
35
|
return attrs;
|
|
36
36
|
}
|
package/src/parse/parser.js
CHANGED
package/src/query/query.js
CHANGED
|
@@ -31,7 +31,7 @@ class Query {
|
|
|
31
31
|
|
|
32
32
|
splitAndCutLast(string, splitBy) {
|
|
33
33
|
const array = string.split(splitBy);
|
|
34
|
-
const last = array.pop();
|
|
34
|
+
const last = array.pop();
|
|
35
35
|
return [last, array];
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -104,11 +104,18 @@ class Query {
|
|
|
104
104
|
attribs = attribs.map(attrib => {
|
|
105
105
|
let query = attrib
|
|
106
106
|
attrib = attrib.replace('[', '').replace(']', '')
|
|
107
|
-
let [name
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if
|
|
107
|
+
let [name,...values] = attrib.split('=')
|
|
108
|
+
const value = values.join('=').trim().replace(/^\"/,'').replace(/\"$/,'')
|
|
109
|
+
let sign
|
|
110
|
+
attrib = {query,name}
|
|
111
|
+
if(value) {
|
|
112
|
+
sign = '='
|
|
113
|
+
attrib.name = attrib.name.replace(/[\~\|\^\$\*]$/,(match => {
|
|
114
|
+
sign = match+sign
|
|
115
|
+
return ''
|
|
116
|
+
}))
|
|
117
|
+
attrib.value = value
|
|
118
|
+
}
|
|
112
119
|
if (sign) {
|
|
113
120
|
attrib.sign = sign
|
|
114
121
|
attrib.check = this.getAttribFn(sign).bind(attrib)
|
package/tests/cache.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function mesureTime(fn) {
|
|
2
|
+
let time = performance.now()
|
|
3
|
+
const result = fn()
|
|
4
|
+
time = performance.now() - time
|
|
5
|
+
return {result,time}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('Cache and build from cache', () => {
|
|
9
|
+
const {result:root,time:rootTime} = mesureTime(() => parseHTML(html1))
|
|
10
|
+
const {result:cache,time:cacheTime} = mesureTime(() => cacheDoc(root))
|
|
11
|
+
const {result:root1,time:root1Time} = mesureTime(() => buildFromCache(cache))
|
|
12
|
+
|
|
13
|
+
it('HTML from cache and HTML from not cached same',() => {
|
|
14
|
+
console.log({rootTime,cacheTime,root1Time})
|
|
15
|
+
assert(cacheTime < 5,'Build cache takes less then 5ms')
|
|
16
|
+
assert(root1Time < 5,'Build DOM from cache takes less then 5ms')
|
|
17
|
+
assert(root.innerHTML === root1.innerHTML)
|
|
18
|
+
})
|
|
19
|
+
})
|
package/tests/index.html
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Document</title>
|
|
7
|
-
<script src="
|
|
7
|
+
<script src="test.js"></script>
|
|
8
8
|
<script src="../document.js"></script>
|
|
9
9
|
<!-- <script src="./data/html1.js"></script> -->
|
|
10
10
|
<script src="./data/html2.js"></script>
|
|
11
11
|
<script src="./data/svg.js"></script>
|
|
12
12
|
<script>
|
|
13
|
-
const { parseHTML, Node, Query, TextNode, SingleNode } = alsDocument
|
|
13
|
+
const { parseHTML, Node, Query, TextNode, SingleNode, buildFromCache, cacheDoc,Root } = alsDocument
|
|
14
14
|
let {describe,it,beforeEach,runTests,expect,delay,assert,beforeAll} = SimpleTest
|
|
15
15
|
SimpleTest.showFullError = true
|
|
16
16
|
</script>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
<script src="./query.js"></script>
|
|
19
19
|
<script src="parser.js"></script>
|
|
20
20
|
<script src="node.js"></script>
|
|
21
|
+
<script src="./cache.js"></script>
|
|
21
22
|
</head>
|
|
22
23
|
<body>
|
|
23
24
|
<script src="parse-real.js"></script>
|
package/tests/parser.js
CHANGED
|
@@ -70,7 +70,7 @@ describe('Advanced tests', () => {
|
|
|
70
70
|
let time = Date.now() - now
|
|
71
71
|
// console.log(memoryAfter - memoryBefore)
|
|
72
72
|
assert(time < 20, `Big html (${(deepHTML.length / 1024).toFixed(2)}KB) in less then 20ms (${time}ms)`)
|
|
73
|
-
expect(result).instanceof(
|
|
73
|
+
expect(result).instanceof(Root); // or any other validation you see fit
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
it('handles incorrectly closed tags', () => {
|
package/tests/test.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
class SimpleTest {
|
|
2
|
+
static tests = [];
|
|
3
|
+
static beforeEachCallback;
|
|
4
|
+
static afterEachCallback;
|
|
5
|
+
static nestingLevel = 0;
|
|
6
|
+
static currentParent = [];
|
|
7
|
+
static space = ''
|
|
8
|
+
static results = {}
|
|
9
|
+
static testTitle = ''
|
|
10
|
+
static showFullError = false
|
|
11
|
+
static colors = {
|
|
12
|
+
bold:'\x1b[1m',
|
|
13
|
+
cblue:'\x1b[34m',
|
|
14
|
+
cred:'\x1b[31m',
|
|
15
|
+
cgreen:'\x1b[32m',
|
|
16
|
+
cgray:'\x1b[37m',
|
|
17
|
+
reset:'\x1b[0m'
|
|
18
|
+
}
|
|
19
|
+
static delay = (ms,toResolve) => new Promise((resolve) => {
|
|
20
|
+
setTimeout(() => {resolve(toResolve)}, ms);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
static describe(title, callback,pause=false) {
|
|
24
|
+
if(pause) return
|
|
25
|
+
let { nestingLevel, currentParent } = SimpleTest
|
|
26
|
+
nestingLevel++;
|
|
27
|
+
currentParent.push(title);
|
|
28
|
+
callback();
|
|
29
|
+
|
|
30
|
+
currentParent.pop();
|
|
31
|
+
nestingLevel--;
|
|
32
|
+
if (nestingLevel === 0) {
|
|
33
|
+
SimpleTest.beforeEachCallback = null; // Clear beforeEachCallback after finishing the outermost describe block
|
|
34
|
+
SimpleTest.afterEachCallback = null; // Clear beforeEachCallback after finishing the outermost describe block
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static beforeEach(callback) {SimpleTest.beforeEachCallback = callback;}
|
|
39
|
+
static afterEach(callback) {SimpleTest.afterEachCallback = callback;}
|
|
40
|
+
static beforeAll(callback) {SimpleTest.beforAllCallback = callback;}
|
|
41
|
+
static afterAll(callback) {SimpleTest.afterAllCallback = callback;}
|
|
42
|
+
|
|
43
|
+
static it(title, callback) {
|
|
44
|
+
SimpleTest.tests.push({
|
|
45
|
+
titles: [...SimpleTest.currentParent, title],
|
|
46
|
+
before: SimpleTest.beforeEachCallback,
|
|
47
|
+
after:SimpleTest.afterEachCallback,
|
|
48
|
+
test: callback,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static async runTests(consoleLog=true) {
|
|
53
|
+
const {bold,cblue,cred,reset} = SimpleTest.colors
|
|
54
|
+
SimpleTest.consoleLog = consoleLog
|
|
55
|
+
let { tests,results,showFullError,beforAllCallback,afterAllCallback } = SimpleTest;
|
|
56
|
+
let previousTitles = [];
|
|
57
|
+
if(typeof beforAllCallback == 'function') await beforAllCallback()
|
|
58
|
+
for(const test of tests) {
|
|
59
|
+
let curentResult = results
|
|
60
|
+
SimpleTest.space = test.titles.map(t => ' ').join('');
|
|
61
|
+
test.titles.forEach((title, index) => {
|
|
62
|
+
if(curentResult[title] == undefined) {
|
|
63
|
+
if(index == test.titles.length-1) curentResult[title] = []
|
|
64
|
+
else curentResult[title] = {}
|
|
65
|
+
}
|
|
66
|
+
curentResult = curentResult[title]
|
|
67
|
+
if(previousTitles[index] !== title) {
|
|
68
|
+
const indentation = ' '.repeat(index);
|
|
69
|
+
console.log(bold+cblue+'%s'+reset,`${indentation} ${title}`);
|
|
70
|
+
previousTitles[index] = title;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
SimpleTest.curentResult = curentResult
|
|
74
|
+
try {
|
|
75
|
+
if (test.before) await test.before();
|
|
76
|
+
await test.test();
|
|
77
|
+
if (test.after) await test.after();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if(showFullError) console.log(error)
|
|
80
|
+
else console.log(`${SimpleTest.space}${cred}Error:${reset} ${error.message}`);
|
|
81
|
+
curentResult.push({error})
|
|
82
|
+
}
|
|
83
|
+
console.log('\n');
|
|
84
|
+
}
|
|
85
|
+
if(typeof afterAllCallback == 'function') await afterAllCallback()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static assert(value,testTitle) {
|
|
89
|
+
if(testTitle) SimpleTest.testTitle = testTitle
|
|
90
|
+
SimpleTest.logResult(value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static expect(value1) {
|
|
94
|
+
let { logResult, isEqual } = SimpleTest
|
|
95
|
+
const methods = {
|
|
96
|
+
is: (text='') => {
|
|
97
|
+
SimpleTest.testTitle = text;
|
|
98
|
+
SimpleTest.not = false
|
|
99
|
+
return methods;
|
|
100
|
+
},
|
|
101
|
+
isNot: (text='') => {
|
|
102
|
+
SimpleTest.testTitle = text;
|
|
103
|
+
SimpleTest.not = true
|
|
104
|
+
return methods;
|
|
105
|
+
},
|
|
106
|
+
equalTo: (value2) => {logResult(value1 === value2);},
|
|
107
|
+
between(value2,value3) {logResult(value1 >= value2 && value1 <= value3);},
|
|
108
|
+
below(value2) {logResult(value1 < value2);},
|
|
109
|
+
above(value2) {logResult(value1 > value2);},
|
|
110
|
+
atLeast(value2) {logResult(value1 >= value2);},
|
|
111
|
+
atMost(value2) {logResult(value1 <= value2);},
|
|
112
|
+
sameAs: (value2) => {logResult(isEqual(value1, value2));},
|
|
113
|
+
defined: () => {logResult(value1 !== undefined);},
|
|
114
|
+
matchTo(pattern) {logResult(pattern.test(new RegExp(value1)));},
|
|
115
|
+
includes(value2) {logResult(value1.includes(value2));},
|
|
116
|
+
error: () => {
|
|
117
|
+
if(typeof value1 !== 'function') return console.log('Expected value has to be function')
|
|
118
|
+
try {
|
|
119
|
+
let result = value1()
|
|
120
|
+
if(result instanceof Error) logResult(true)
|
|
121
|
+
else logResult(false);
|
|
122
|
+
} catch (error) {logResult(true)}
|
|
123
|
+
},
|
|
124
|
+
hasProperty: (property) => {logResult(Object.prototype.hasOwnProperty.call(value1, property));},
|
|
125
|
+
instanceof: (classType) => {logResult(value1.constructor.name == classType.name)},
|
|
126
|
+
closeTo(value2, decimalPlaces = 2) {
|
|
127
|
+
const multiplier = 10 ** decimalPlaces;
|
|
128
|
+
const roundedActual = Math.round(value1 * multiplier);
|
|
129
|
+
const roundedExpected = Math.round(value2 * multiplier);
|
|
130
|
+
const diff = Math.abs(roundedActual - roundedExpected) / multiplier;
|
|
131
|
+
logResult(diff < 1 / multiplier);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return methods;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static logResult(result) {
|
|
138
|
+
let {curentResult,testTitle,not,space,colors} = SimpleTest
|
|
139
|
+
let {cgreen,cred,cgray,reset} = colors
|
|
140
|
+
if(not) result = !result
|
|
141
|
+
curentResult.push({result:result ? true : false,testTitle,error:null})
|
|
142
|
+
const status = result ? 'Success' : 'Failed';
|
|
143
|
+
const color = result ? cgreen : cred;
|
|
144
|
+
if (testTitle == '') console.log(`${color}${space}${status}${reset}`);
|
|
145
|
+
else console.log(`${space}${color}${status}: ${testTitle ? `${cgray}${testTitle}` : ''}`+reset,);
|
|
146
|
+
SimpleTest.not = false
|
|
147
|
+
SimpleTest.testTitle = ''
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static isEqual(obj1, obj2, visited = new WeakMap()) {
|
|
151
|
+
if (obj1 === obj2) return true;
|
|
152
|
+
if (visited.has(obj1) && visited.get(obj1) === obj2) return true;
|
|
153
|
+
const type1 = typeof obj1;
|
|
154
|
+
const type2 = typeof obj2;
|
|
155
|
+
|
|
156
|
+
if (type1 !== type2 || type1 !== 'object' || obj1 === null || obj2 === null) return false;
|
|
157
|
+
visited.set(obj1, obj2);
|
|
158
|
+
|
|
159
|
+
const keys1 = Object.keys(obj1);
|
|
160
|
+
const keys2 = Object.keys(obj2);
|
|
161
|
+
if(keys1.length !== keys2.length) return false;
|
|
162
|
+
|
|
163
|
+
for (let key of keys1) {
|
|
164
|
+
if (!keys2.includes(key) || !SimpleTest.isEqual(obj1[key], obj2[key], visited)) return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
try {module.exports = SimpleTest} catch (error) {}
|