als-document 1.0.2-alpha → 1.0.2
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 +15 -7
- package/index.js +15 -7
- package/index.mjs +15 -7
- package/package.json +2 -5
- package/readme.md +32 -3
- package/src/build.js +5 -3
- package/src/node/node.js +50 -13
- 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 +30 -6
- package/src/query/check-element.js +1 -1
- package/src/query/query.js +13 -6
- package/tests/cache.js +19 -0
- package/tests/index.html +6 -5
- package/tests/node.js +1 -1
- package/tests/parse-real.js +3 -2
- package/tests/parser.js +6 -6
- 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.2
|
|
3
|
+
"version": "1.0.2",
|
|
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
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
`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
6
|
|
|
7
|
+
## Release notes
|
|
8
|
+
* 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.
|
|
9
|
+
* Also, this release, has additional very powefull feature which is building cache for storing DOM tree as json and building back DOM from cache.
|
|
10
|
+
|
|
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
|
@@ -2,7 +2,9 @@ const { readFileSync, writeFileSync, watchFile, watch } = require('fs')
|
|
|
2
2
|
const { join,basename } = require('path')
|
|
3
3
|
|
|
4
4
|
function optimizeCode(content) {
|
|
5
|
+
// content = content.replace(/(?<!\\)\/\/.*$/gm, '') // remove comments
|
|
5
6
|
content = content.replace(/^(?<!\\)\/\/.*$|(?<=\s)(?<!\\)\/\/.*$/gm, '') // remove comments
|
|
7
|
+
// content = content.replace(/\;\s*?$/gm,'') // remove ; at end of line
|
|
6
8
|
content = content.replace(/\[\s*?\n\s*/gm, '[') //
|
|
7
9
|
content = content.replace(/\s*?\]/gm, ']') //
|
|
8
10
|
content = content.replace(/\s*?$/gm, '') // remove space at end of line
|
|
@@ -20,9 +22,9 @@ const files = {
|
|
|
20
22
|
'query': ['query','check-element'],
|
|
21
23
|
'node':[
|
|
22
24
|
'dataset','find','text-node',
|
|
23
|
-
'style','class-list','node','single-node'
|
|
25
|
+
'style','class-list','node','single-node','root'
|
|
24
26
|
],
|
|
25
|
-
'parse':['parse-atts','void-tags','parser'],
|
|
27
|
+
'parse':['parse-atts','void-tags','parser','cache'],
|
|
26
28
|
}
|
|
27
29
|
const fileList = []
|
|
28
30
|
function buildFileList() {
|
|
@@ -38,7 +40,7 @@ buildFileList()
|
|
|
38
40
|
function build() {
|
|
39
41
|
let content = fileList.map(filePath => readFileSync(filePath, 'utf-8')).join('\n');
|
|
40
42
|
|
|
41
|
-
const toReturn = '{ parseHTML, Node, Query, TextNode, SingleNode }'
|
|
43
|
+
const toReturn = '{ parseHTML, Node, Query, TextNode, SingleNode, buildFromCache, cacheDoc, Root }'
|
|
42
44
|
content = optimizeCode(content)
|
|
43
45
|
writeFileSync(join(root, 'document.js'), `const alsDocument = (function(){\n${content}\nreturn ${toReturn}\n})()`)
|
|
44
46
|
writeFileSync(join(root, 'index.js'), content + '\n' + `module.exports = ${toReturn}`)
|
package/src/node/node.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
class Node {
|
|
2
8
|
constructor(tagName, attributes = {}, parent = null) {
|
|
3
9
|
this.isSingle = false;
|
|
@@ -11,10 +17,12 @@ class Node {
|
|
|
11
17
|
this._dataset = null
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
get id() { return this.attributes.id
|
|
20
|
+
get id() { return this.attributes.id ? this.attributes.id : null; }
|
|
21
|
+
set id(newValue) { this.attributes.id = newValue; }
|
|
15
22
|
get className() {return this.attributes.class || null}
|
|
16
23
|
get parentNode() { return this.parent }
|
|
17
24
|
get ancestors() {
|
|
25
|
+
if(!this.parent) return []
|
|
18
26
|
const ancestors = []
|
|
19
27
|
let element = this.parent
|
|
20
28
|
while (element.tagName !== 'ROOT') {
|
|
@@ -24,17 +32,25 @@ class Node {
|
|
|
24
32
|
return ancestors.reverse()
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
get
|
|
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
|
+
}
|
|
28
44
|
get previousElementSibling() { return this.prev }
|
|
29
45
|
get prev() {
|
|
30
46
|
if (!this.childIndex) return null // if no index or index == 0
|
|
31
|
-
return this.parent.
|
|
47
|
+
return this.parent.children[this.childIndex - 1]
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
get nextElementSibling() { return this.next }
|
|
35
51
|
get next() {
|
|
36
52
|
if (!this.childIndex) return null
|
|
37
|
-
return this.parent.
|
|
53
|
+
return this.parent.children[this.childIndex + 1] || null
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
get dataset() {
|
|
@@ -53,8 +69,8 @@ class Node {
|
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
get outerHTML() {
|
|
56
|
-
const attrs = Object.entries(this.attributes).map(([key, val]) => `${key}="${val}"`).join(" ");
|
|
57
|
-
return `<${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}>`;
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
getAttribute(attrName) { return this.attributes[attrName] || null }
|
|
@@ -63,7 +79,7 @@ class Node {
|
|
|
63
79
|
|
|
64
80
|
remove() {
|
|
65
81
|
if (!this.parent) return
|
|
66
|
-
const index = this.
|
|
82
|
+
const index = this.childNodeIndex;
|
|
67
83
|
if (index !== null) this.parent.childNodes.splice(index, 1);
|
|
68
84
|
}
|
|
69
85
|
|
|
@@ -101,23 +117,44 @@ class Node {
|
|
|
101
117
|
insertAdjacentElement(position, newElement) {
|
|
102
118
|
if(newElement.tagName === 'ROOT' && newElement.childNodes.length > 0) newElement = newElement.childNodes[0]
|
|
103
119
|
const pos = position.toLowerCase();
|
|
104
|
-
if
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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")
|
|
127
|
+
if (pos === "beforebegin") insertBefore(this.parent.childNodes, this.childNodeIndex, newElement)
|
|
128
|
+
else if (pos === "afterend") this.parent.childNodes.splice(this.childNodeIndex + 1, 0, newElement);
|
|
129
|
+
newElement.parent = this.parent
|
|
109
130
|
return newElement
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
insertAdjacentHTML(position, html) {
|
|
113
134
|
const newNode = parseHTML(html);
|
|
114
|
-
|
|
135
|
+
newNode.childNodes.reverse().forEach(node => {
|
|
136
|
+
this.insertAdjacentElement(position, node);
|
|
137
|
+
});
|
|
138
|
+
return newNode
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
insertAdjacentText(position, text) {
|
|
118
142
|
return this.insertAdjacentElement(position, new TextNode(text));
|
|
119
143
|
}
|
|
120
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
|
+
|
|
121
158
|
set innerHTML(html) {
|
|
122
159
|
const parsed = parseHTML(html);
|
|
123
160
|
this.childNodes = parsed.childNodes;
|
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
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
function parseHTML(html) {
|
|
2
|
-
const root = new
|
|
2
|
+
const root = new Root();
|
|
3
3
|
const stack = [root];
|
|
4
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
|
+
}
|
|
5
26
|
|
|
6
27
|
function parseSpecial(startStr, endStr, n1, n2, tag) {
|
|
7
28
|
if (!html.startsWith(startStr, i)) return false
|
|
@@ -13,8 +34,10 @@ function parseHTML(html) {
|
|
|
13
34
|
}
|
|
14
35
|
|
|
15
36
|
while (i < html.length) {
|
|
37
|
+
if(i >= max) max = i;
|
|
38
|
+
else break;
|
|
39
|
+
if (parseScript()) continue
|
|
16
40
|
if (parseSpecial("<!--", "-->", 4, 3, '#comment')) continue
|
|
17
|
-
if (parseSpecial("<script", "</script>", 8, 9, 'script')) continue
|
|
18
41
|
if (parseSpecial("<style", "</style>", 7, 8, 'style')) continue
|
|
19
42
|
|
|
20
43
|
if (html.startsWith("<![CDATA[", i)) {
|
|
@@ -28,8 +51,10 @@ function parseHTML(html) {
|
|
|
28
51
|
}
|
|
29
52
|
|
|
30
53
|
if (html.startsWith("<", i)) {
|
|
31
|
-
if (currentText.
|
|
32
|
-
|
|
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]
|
|
33
58
|
currentText = "";
|
|
34
59
|
}
|
|
35
60
|
|
|
@@ -52,7 +77,6 @@ function parseHTML(html) {
|
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
const tagContent = html.substring(i + 1, tagEnd);
|
|
55
|
-
|
|
56
80
|
if (tagContent.startsWith("/")) stack.pop(); // It is close tag
|
|
57
81
|
else { // It is open tag
|
|
58
82
|
let isSelfClosing = tagContent.endsWith('/');
|
|
@@ -69,6 +93,6 @@ function parseHTML(html) {
|
|
|
69
93
|
i++;
|
|
70
94
|
}
|
|
71
95
|
}
|
|
72
|
-
if (currentText.trim()) stack[stack.length - 1].childNodes.push(new TextNode(currentText
|
|
96
|
+
if (currentText.trim() && stack[stack.length - 1]) stack[stack.length - 1].childNodes.push(new TextNode(currentText));
|
|
73
97
|
return root;
|
|
74
98
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
function checkElement(el,selector) {
|
|
2
2
|
if(selector == undefined) return true
|
|
3
3
|
if(el == null) return false
|
|
4
|
-
let {tag,classList,attributes,id,prev,ancestors,parents,prevAny} = selector
|
|
4
|
+
let {tag,classList,attribs:attributes,id,prev,ancestors,parents,prevAny} = selector
|
|
5
5
|
if(typeof el === 'string') return false
|
|
6
6
|
if(id !== undefined && el.id === null) return false
|
|
7
7
|
if(id && id !== el.id) return false
|
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,23 +4,24 @@
|
|
|
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="
|
|
8
|
-
<script src="
|
|
7
|
+
<script src="test.js"></script>
|
|
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>
|
|
17
|
-
<script src="./query.js"></script>
|
|
18
17
|
<script src="utils.js"></script>
|
|
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
|
-
<script src="parse-real.js"></script>
|
|
24
|
+
<!-- <script src="parse-real.js"></script> -->
|
|
24
25
|
<script>
|
|
25
26
|
runTests();
|
|
26
27
|
|
package/tests/node.js
CHANGED
|
@@ -130,7 +130,7 @@ describe('Content Manipulation', () => {
|
|
|
130
130
|
expect(rootNode.childNodes[2].tagName).equalTo('a');
|
|
131
131
|
|
|
132
132
|
childNode.insertAdjacentHTML('beforebegin', '<strong></strong>');
|
|
133
|
-
expect(rootNode.childNodes[
|
|
133
|
+
expect(rootNode.childNodes[1].tagName).equalTo('strong');
|
|
134
134
|
|
|
135
135
|
childNode.insertAdjacentText('afterend', 'Some text');
|
|
136
136
|
expect(typeof rootNode.childNodes[3].nodeValue).equalTo('string');
|
package/tests/parse-real.js
CHANGED
|
@@ -32,8 +32,9 @@ describe('Real data html1', async () => {
|
|
|
32
32
|
it('Text nodes check', () => {
|
|
33
33
|
const realParagraph = iframe.querySelector('p');
|
|
34
34
|
const parsedParagraph = parsedHTML.querySelector('p');
|
|
35
|
-
const real = realParagraph.textContent.trim().replace(/\n
|
|
36
|
-
const parsed = parsedParagraph.textContent.trim()
|
|
35
|
+
const real = realParagraph.textContent.trim().replace(/\n|\s/gm,'')
|
|
36
|
+
const parsed = parsedParagraph.textContent.trim().replace(/\n|\s/gm,'')
|
|
37
|
+
console.log({parsed,real})
|
|
37
38
|
assert(real === parsed, 'Text contents are the same');
|
|
38
39
|
});
|
|
39
40
|
|
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', () => {
|
|
@@ -225,11 +225,11 @@ describe('signle tags, script and style', () => {
|
|
|
225
225
|
<link rel="stylesheet" href="styles.css">
|
|
226
226
|
`;
|
|
227
227
|
const rootMetaLink = parseHTML(testMetaLink);
|
|
228
|
-
assert(rootMetaLink.
|
|
229
|
-
assert(rootMetaLink.
|
|
230
|
-
assert(rootMetaLink.
|
|
231
|
-
assert(rootMetaLink.
|
|
232
|
-
assert(rootMetaLink.
|
|
228
|
+
assert(rootMetaLink.children[0].tagName === "meta", "Test Meta/Link 1: Meta tag not created");
|
|
229
|
+
assert(rootMetaLink.children[0].getAttribute("charset") === "UTF-8", "Test Meta/Link 1: Meta content not correct");
|
|
230
|
+
assert(rootMetaLink.children[1].tagName === "link", "Test Meta/Link 2: Link tag not created");
|
|
231
|
+
assert(rootMetaLink.children[1].getAttribute("rel") === "stylesheet", "Test Meta/Link 2: Link rel attribute not correct");
|
|
232
|
+
assert(rootMetaLink.children[1].getAttribute("href") === "styles.css", "Test Meta/Link 2: Link href attribute not correct");
|
|
233
233
|
})
|
|
234
234
|
|
|
235
235
|
it('broken html structure', () => {
|