primate 0.0.1 → 0.3.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.
Files changed (75) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +50 -0
  3. package/package.json +15 -1
  4. package/source/client/Action.js +159 -0
  5. package/source/client/App.js +16 -0
  6. package/source/client/Base.js +5 -0
  7. package/source/client/Client.js +65 -0
  8. package/source/client/Context.js +53 -0
  9. package/source/client/Element.js +245 -0
  10. package/source/client/Node.js +13 -0
  11. package/source/client/View.js +90 -0
  12. package/source/client/document.js +6 -0
  13. package/source/client/exports.js +15 -0
  14. package/source/preset/client/Element.js +2 -0
  15. package/source/preset/client/app.js +2 -0
  16. package/source/preset/data/stores/default.js +2 -0
  17. package/source/preset/primate.json +31 -0
  18. package/source/preset/static/index.html +10 -0
  19. package/source/server/Action.js +118 -0
  20. package/source/server/App.js +42 -0
  21. package/source/server/Base.js +35 -0
  22. package/source/server/Bundler.js +180 -0
  23. package/source/server/Context.js +90 -0
  24. package/source/server/Crypto.js +8 -0
  25. package/source/server/Directory.js +35 -0
  26. package/source/server/File.js +117 -0
  27. package/source/server/Router.js +61 -0
  28. package/source/server/Session.js +69 -0
  29. package/source/server/attributes.js +11 -0
  30. package/source/server/cache.js +17 -0
  31. package/source/server/conf.js +33 -0
  32. package/source/server/domain/Domain.js +277 -0
  33. package/source/server/domain/Field.js +115 -0
  34. package/source/server/domain/domains.js +15 -0
  35. package/source/server/errors/Fallback.js +1 -0
  36. package/source/server/errors/InternalServer.js +1 -0
  37. package/source/server/errors/Predicate.js +1 -0
  38. package/source/server/errors.js +3 -0
  39. package/source/server/exports.js +27 -0
  40. package/source/server/fallback.js +11 -0
  41. package/source/server/invariants.js +19 -0
  42. package/source/server/log.js +22 -0
  43. package/source/server/promises/Eager.js +49 -0
  44. package/source/server/promises/Meta.js +42 -0
  45. package/source/server/promises.js +2 -0
  46. package/source/server/servers/Dynamic.js +51 -0
  47. package/source/server/servers/Server.js +5 -0
  48. package/source/server/servers/Static.js +113 -0
  49. package/source/server/servers/content-security-policy.json +8 -0
  50. package/source/server/servers/http-codes.json +5 -0
  51. package/source/server/servers/mimes.json +12 -0
  52. package/source/server/store/Memory.js +60 -0
  53. package/source/server/store/Store.js +17 -0
  54. package/source/server/types/Array.js +33 -0
  55. package/source/server/types/Boolean.js +29 -0
  56. package/source/server/types/Date.js +20 -0
  57. package/source/server/types/Domain.js +16 -0
  58. package/source/server/types/File.js +20 -0
  59. package/source/server/types/Instance.js +8 -0
  60. package/source/server/types/Number.js +45 -0
  61. package/source/server/types/Object.js +12 -0
  62. package/source/server/types/Primitive.js +7 -0
  63. package/source/server/types/Storeable.js +51 -0
  64. package/source/server/types/String.js +49 -0
  65. package/source/server/types/errors/Array.json +7 -0
  66. package/source/server/types/errors/Boolean.json +5 -0
  67. package/source/server/types/errors/Date.json +3 -0
  68. package/source/server/types/errors/Number.json +9 -0
  69. package/source/server/types/errors/Object.json +3 -0
  70. package/source/server/types/errors/String.json +11 -0
  71. package/source/server/types.js +7 -0
  72. package/source/server/utils/extend_object.js +10 -0
  73. package/source/server/view/Parser.js +122 -0
  74. package/source/server/view/TreeNode.js +195 -0
  75. package/source/server/view/View.js +30 -0
package/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2022, Primate core team <core@primatejs.com>. All rights
2
+ reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its contributors
15
+ may be used to endorse or promote products derived from this software without
16
+ specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Primate: a JavaScript framework
2
+
3
+ ⚠️ **This software is currently its initial development phase. It is not
4
+ considered production-ready. Expect breakage.** ⚠️
5
+
6
+ Primate is a server-client JavaScript framework for web applications aimed at
7
+ relieving you of dealing with repetitive, error-prone tasks and letting you
8
+ concentrate on writing effective, expressive code.
9
+
10
+ ## Installing
11
+
12
+ ```
13
+ yarn add primate
14
+ ```
15
+
16
+ ## Concepts
17
+
18
+ * **Minimal** just one dependency ([ws][])
19
+ * **Simple** only native web technologies (JavaScript, HTML)
20
+ * **Full-stack** server and client, both in JavaScript
21
+ * **Layered** separation of data (domains), logic (actions) and
22
+ presentation (views)
23
+ * **Correct** data verification using field definitions in domains
24
+ * **Secure** serving hash-verified scripts on secure HTTP with a strong CSP
25
+ * **Roles** access control with contexts
26
+ * **Sessions** using cookies that are `HttpOnly`, `Secure` and `Path`-limited
27
+ * **Sane defaults** minimally opinionated with good, overrideable defaults
28
+ for most features
29
+ * **Data linking** modeling `1:1`, `1:n` and `n:m` relationships on domains via
30
+ ad-hoc getters
31
+
32
+ ## Getting started
33
+
34
+ Coming soon.
35
+
36
+ ## Resources
37
+
38
+ Coming soon.
39
+
40
+ ## Versioning
41
+
42
+ This software is still in its initial development phase and is not considered
43
+ stable or recommended for production. Once we release a stable version `1`, we
44
+ will move over to semantic versioning.
45
+
46
+ ## License
47
+
48
+ BSD-3-Clause
49
+
50
+ [ws]: https://github.com/websockets/ws
package/package.json CHANGED
@@ -1,9 +1,23 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.0.1",
3
+ "version": "0.3.0",
4
4
  "author": "Primate core team <core@primatejs.com>",
5
5
  "homepage": "https://primatejs.com",
6
+ "description": "Server-client framework",
6
7
  "license": "BSD-3-Clause",
8
+ "dependencies": {
9
+ "ws": "^8.4.0"
10
+ },
11
+ "scripts": {
12
+ "test": "node --experimental-json-modules node_modules/stick stick.json"
13
+ },
14
+ "devDependencies": {
15
+ "eslint": "^8.6.0",
16
+ "eslint-plugin-json": "^3.1.0",
17
+ "stick": "../stick"
18
+ },
19
+ "type": "module",
20
+ "exports": "./source/server/exports.js",
7
21
  "engines": {
8
22
  "node": ">=17.3.0"
9
23
  }
@@ -0,0 +1,159 @@
1
+ import Element from "../Element.js";
2
+ import View from "./View.js";
3
+ import Base from "./Base.js";
4
+ import {origin_base} from "./document.js";
5
+
6
+ export default class Action extends Base {
7
+ constructor(name, context) {
8
+ super();
9
+ this.name = name;
10
+ this.context = context;
11
+ this.listeners = [];
12
+ }
13
+
14
+ before() {}
15
+
16
+ async enter(data) {
17
+ if (data.location) {
18
+ location.href = data.location;
19
+ } else {
20
+ this.create_view(data);
21
+ }
22
+ }
23
+
24
+ create_view(data, root, save_history = true) {
25
+ if (save_history) {
26
+ const pathname = data.pathname === "" ? "/" : data.pathname;
27
+ window.location.pathname !== pathname
28
+ && history.pushState({}, "", pathname);
29
+ }
30
+ this.view = new View(data, this, root);
31
+ }
32
+
33
+ create_and_return_view(data, root, save_history) {
34
+ return new View(data, this, root);
35
+ }
36
+
37
+ get_form_data(target) {
38
+ const payload = {};
39
+ const elements = target.elements;
40
+ for (let i = 0; i < elements.length; i++) {
41
+ const element = elements[i];
42
+ const type = Element.value(element, "type");
43
+ const data_value = Element.value(element, "data-value");
44
+ switch (element.localName) {
45
+ case "input":
46
+ case "textarea":
47
+ if (type === "checkbox") {
48
+ payload[data_value] = element.checked;
49
+ } else if (type === "file") {
50
+ payload[data_value] = element.getAttribute("base64");
51
+ } else if (type === "submit") {
52
+ // do nothing
53
+ } else {
54
+ payload[data_value] = element.value;
55
+ }
56
+ break;
57
+ case "select":
58
+ payload[data_value] = Element
59
+ .value(element.children[element.selectedIndex], "value");
60
+ break;
61
+ default:
62
+ break;
63
+ }
64
+ }
65
+ return payload;
66
+ }
67
+
68
+ async onsubmit(event) {
69
+ const target = event.target;
70
+ const stop = this.fire("submit", event);
71
+ if (stop) {
72
+ return event.preventDefault();
73
+ }
74
+ event.preventDefault();
75
+ const data = await this.write(target.action, this.get_form_data(target));
76
+ return data.pathname === document.location.href.replace(origin_base, "")
77
+ ? this.view.update(data.payload)
78
+ : this.context.run(data);
79
+ }
80
+
81
+ async onclick(event) {
82
+ const stop = await this.fire("click", event);
83
+ if (stop) {
84
+ return event.preventDefault();
85
+ }
86
+ let target;
87
+ if (event.target.shadowRoot !== null &&
88
+ event.target.shadowRoot.activeElement !== null) {
89
+ target = event.target.shadowRoot.activeElement.closest("a");
90
+ } else {
91
+ target = event.target.closest("a");
92
+ }
93
+ if (target !== null) {
94
+ const href = Element.value(target, "href");
95
+ const url = new URL(href, origin_base);
96
+ if (event.button === 0 && url.href.startsWith(origin_base)) {
97
+ event.preventDefault();
98
+ if (href !== "") {
99
+ const data = await this.read(href);
100
+ await this.context.run(data);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ onchange() {
107
+ const stop = this.fire("change", event);
108
+ if (stop) {
109
+ event.preventDefault();
110
+ }
111
+ }
112
+
113
+ oninput() {
114
+ const stop = this.fire("input", event);
115
+ }
116
+
117
+ fire(type, event) {
118
+ const listeners = this.listeners[type];
119
+ if (listeners !== undefined) {
120
+ const target = event.target;
121
+ for (const listener of listeners) {
122
+ if (event.target.closest(listener.selector)) {
123
+ listener.listener(event, target);
124
+ return true;
125
+ }
126
+ }
127
+ }
128
+
129
+ return false;
130
+ }
131
+
132
+ on(selector, event, listener) {
133
+ this.listeners[event] = this.listeners[event] ?? [];
134
+ const index = this.listeners[event]
135
+ .findIndex(listener => listener.selector === selector);
136
+ const object = {selector, listener};
137
+ if (index === -1) {
138
+ this.listeners[event].push(object);
139
+ } else {
140
+ this.listeners[event].splice(index, 1, object);
141
+ }
142
+ }
143
+
144
+ _(selector) {
145
+ return new Element(selector, this);
146
+ }
147
+
148
+ get selector() {
149
+ return selector => new Element(selector, this);
150
+ }
151
+
152
+ read(pathname) {
153
+ return this.context.read(pathname);
154
+ }
155
+
156
+ write(pathname, payload) {
157
+ return this.context.write(pathname, payload);
158
+ }
159
+ }
@@ -0,0 +1,16 @@
1
+ import Client from "./Client.js";
2
+
3
+ export default class App {
4
+ constructor() {
5
+ this.definitions = {};
6
+ }
7
+
8
+ run() {
9
+ this.client = new Client();
10
+ this.client.open();
11
+ }
12
+
13
+ define(name, predicate) {
14
+ this.definitions[name] = predicate;
15
+ }
16
+ }
@@ -0,0 +1,5 @@
1
+ export default class Base {
2
+ Class() {
3
+ return this.constructor;
4
+ }
5
+ }
@@ -0,0 +1,65 @@
1
+ import Context from "./Context.js";
2
+ import {contexts} from "./exports.js";
3
+ import {base, host, origin_base} from "./document.js";
4
+
5
+ const events = ["submit", "click", "change", "input"];
6
+ let back = undefined;
7
+ const location = `wss://${host}${base}`;
8
+
9
+ export default class Client {
10
+ constructor(conf) {
11
+ this.conf = conf;
12
+
13
+ window.onpopstate = async event => {
14
+ const {pathname, search} = event.target.location;
15
+ return this.execute(await this.read(pathname + search));
16
+ };
17
+
18
+ for (const key of events) {
19
+ window.addEventListener(key, event => this.context.on(key, event));
20
+ }
21
+ }
22
+
23
+ open() {
24
+ return this.connection?.readyState === 1 ? this
25
+ : new Promise(resolve => {
26
+ const connection = new WebSocket(location);
27
+ connection.addEventListener("message", ({data}) => data === "open"
28
+ ? resolve(this)
29
+ : this.receive(JSON.parse(data)));
30
+ this.connection = connection;
31
+ });
32
+ }
33
+
34
+ async receive(data) {
35
+ if (back !== undefined) {
36
+ back(data);
37
+ back = undefined;
38
+ } else {
39
+ await this.execute(data);
40
+ }
41
+ }
42
+
43
+ async execute(data) {
44
+ const {context} = data;
45
+ this.context = new (contexts[context] ?? Context)(context, this);
46
+ await this.context.run(data);
47
+ }
48
+
49
+ read(url) {
50
+ return this.send("read", url);
51
+ }
52
+
53
+ write(url, payload) {
54
+ return this.send("write", url, payload);
55
+ }
56
+
57
+ async send(type, url, payload = {}) {
58
+ await this.open();
59
+ const {pathname, search} = new URL(url, origin_base);
60
+ return new Promise(resolve => {
61
+ back = data => resolve(data);
62
+ this.connection.send(JSON.stringify({type, pathname, search, payload}));
63
+ });
64
+ }
65
+ }
@@ -0,0 +1,53 @@
1
+ import Base from "./Base.js";
2
+ import Action from "./Action.js";
3
+ import {routes} from "./exports.js";
4
+
5
+ export default class Context extends Base {
6
+ constructor(name, client) {
7
+ super();
8
+ this.name = name;
9
+ this.actions = {};
10
+ this.used_actions = [];
11
+ this.client = client;
12
+ }
13
+
14
+ async run(data) {
15
+ const {namespace, action} = data;
16
+ const route = `${namespace}/${action}`;
17
+ const module = routes?.[namespace]?.[action] ?? Action;
18
+ this.actions[route] = new module(action, this);
19
+ const nextaction = this.actions[route];
20
+ nextaction.enter(data);
21
+ await this.Class().before(nextaction);
22
+ nextaction.before();
23
+ this.action = nextaction;
24
+ }
25
+
26
+ static before(action) {
27
+ return action;
28
+ }
29
+
30
+ static prepare(action, data) {
31
+ return data;
32
+ }
33
+
34
+ read(pathname) {
35
+ return this.client.read(pathname);
36
+ }
37
+
38
+ write(pathname, payload) {
39
+ return this.client.write(pathname, payload);
40
+ }
41
+
42
+ on(key, event) {
43
+ this.action[`on${key}`](event);
44
+ }
45
+
46
+ push_action() {
47
+ this.used_actions.push(this.action);
48
+ }
49
+
50
+ pop_action() {
51
+ this.action = this.used_actions.pop();
52
+ }
53
+ }
@@ -0,0 +1,245 @@
1
+ const file_listener = event => reader.readAsDataURL(event.target.files[0]);
2
+ const reader = new FileReader();
3
+
4
+ const replace = (attribute, source) => {
5
+ if (attribute.includes(".")) {
6
+ const index = attribute.indexOf(".");
7
+ const left = attribute.slice(0, index);
8
+ const rest = attribute.slice(index+1);
9
+ if (source[left] !== undefined) {
10
+ return replace(rest, source[left]);
11
+ }
12
+ } else {
13
+ return source[attribute];
14
+ }
15
+ };
16
+
17
+ const data_prefix_length = 5;
18
+ const data_regex = /\${([^}]*)}/g;
19
+ const fulfill = (attribute, source) => {
20
+ if (source === undefined) {
21
+ return undefined;
22
+ }
23
+ let value = attribute.value;
24
+ const matches = [...value.matchAll(data_regex)];
25
+ if (matches.length > 0) {
26
+ for (const match of matches) {
27
+ const key = match[0];
28
+ const new_value = replace(match[1], source);
29
+ value = value.replace(key, new_value);
30
+ }
31
+ } else {
32
+ value = replace(value, source);
33
+ }
34
+ return value;
35
+ };
36
+
37
+
38
+ const $selector = Symbol("$selector");
39
+ const $element = Symbol("$element");
40
+ const $action = Symbol("$action");
41
+
42
+ const proxy_handler = {
43
+ "get": (target, property, receiver) => {
44
+ if (property === "element") {
45
+ return target.element;
46
+ }
47
+ if (property === "on" || property === "insert") {
48
+ return target[property].bind(target);
49
+ }
50
+ if (target.element !== null) {
51
+ return (...args) => target[property] !== undefined
52
+ ? target[property](...args)
53
+ : receiver;
54
+ } else {
55
+ return () => receiver;
56
+ }
57
+ },
58
+ };
59
+
60
+ export default class Element {
61
+ constructor(selector, action) {
62
+ this[$action] = action;
63
+ if (typeof selector === "string") {
64
+ this.construct_by_selector(selector);
65
+ } else {
66
+ this.construct_by_element(selector);
67
+ }
68
+ return new Proxy(this, proxy_handler);
69
+ }
70
+
71
+ construct_by_selector(selector) {
72
+ this[$selector] = selector;
73
+ }
74
+
75
+ construct_by_element(element) {
76
+ this[$element] = element;
77
+ }
78
+
79
+ static value(element, name) {
80
+ const attribute = element.attributes.getNamedItem(name);
81
+ return attribute === null ? null : attribute.value;
82
+ }
83
+
84
+ static evaluate(attribute, source, data) {
85
+ const element = new this(source);
86
+ const attribute_name = attribute.name.slice(data_prefix_length);
87
+ element.resolve(attribute_name)(fulfill(attribute, data), attribute_name);
88
+ }
89
+
90
+ resolve(name) {
91
+ return (this[name] ?? this.default).bind(this);
92
+ }
93
+
94
+ get element() {
95
+ return this[$element] ?? document.querySelector(this[$selector]);
96
+ }
97
+
98
+ each(callback) {
99
+ document.querySelectorAll(this[$selector]).forEach(callback);
100
+ }
101
+
102
+ get(name) {
103
+ return this.element.getAttribute(name);
104
+ }
105
+
106
+ has(attribute) {
107
+ return this.element[attribute] !== undefined;
108
+ }
109
+
110
+ show() {
111
+ return this.class("show");
112
+ }
113
+
114
+ hide() {
115
+ return this.class("show", false);
116
+ }
117
+
118
+ insert() {
119
+ const element = document.createElement("div");
120
+ if (this[$selector].startsWith(".")) {
121
+ element.className = this[$selector].slice(1);
122
+ }
123
+ document.body.append(element);
124
+ return this;
125
+ }
126
+
127
+ replace(child) {
128
+ this.element.innerHTML = "";
129
+ return this.append(child);
130
+ }
131
+
132
+ append(child) {
133
+ if (child instanceof HTMLElement) {
134
+ this.element.appendChild(child);
135
+ } else {
136
+ this.element.innerHTML += child;
137
+ }
138
+ return this;
139
+ }
140
+
141
+ set(name, value) {
142
+ this.element.setAttribute(name, value);
143
+ return this;
144
+ }
145
+
146
+ class(classes, set) {
147
+ classes.split(" ").forEach(name => {
148
+ this.element.classList.toggle(name, set !== false);
149
+ })
150
+ return this;
151
+ }
152
+
153
+ remove() {
154
+ this.element.remove();
155
+ return this;
156
+ }
157
+
158
+ remove_attribute(name) {
159
+ this.element.removeAttribute(name);
160
+ return this;
161
+ }
162
+
163
+ click() {
164
+ this.element.click();
165
+ return this;
166
+ }
167
+
168
+ on(event, handler) {
169
+ this[$action].on(this[$selector], event, handler);
170
+ return this;
171
+ }
172
+
173
+ defined(value) {
174
+ return value === undefined && this.remove();
175
+ }
176
+
177
+ html(value) {
178
+ if (value !== undefined) {
179
+ this.element.innerHTML = value;
180
+ }
181
+ return this;
182
+ }
183
+
184
+ options(value) {
185
+ value instanceof Array && value.forEach(option => {
186
+ if (this.element.children[option._id] === undefined) {
187
+ const newElement = document.createElement("option");
188
+ newElement.setAttribute("value", option._id);
189
+ newElement.innerHTML = option.name;
190
+ this.append(newElement);
191
+ }
192
+ });
193
+ }
194
+
195
+ if(value) {
196
+ return value !== true && this.remove();
197
+ }
198
+
199
+ value(value) {
200
+ const name = `${this.element.localName}_value`;
201
+ this[name]?.(value) ?? this.default_value(value);
202
+ }
203
+
204
+ select_value(value) {
205
+ this.element.value = value === undefined ? "" : value;
206
+ }
207
+
208
+ input_value(value) {
209
+ const type = Element.value(this.element, "type");
210
+ this[`input_${type}_value`]?.(value) ?? this.input_input_value(value);
211
+ }
212
+
213
+ textarea_value(value) {
214
+ return this.input_value(value);
215
+ }
216
+
217
+ input_checkbox_value(value) {
218
+ if (value === true) {
219
+ this.element.checked = true;
220
+ }
221
+ }
222
+
223
+ input_file_value(value) {
224
+ const element = this.element;
225
+ if (value !== undefined) {
226
+ element.setAttribute("base64", value);
227
+ }
228
+ reader.onload = ({target}) => element.setAttribute("base64", target.result);
229
+ element.addEventListener("change", file_listener);
230
+ }
231
+
232
+ input_input_value(value) {
233
+ if (value !== undefined) {
234
+ this.element.value = value;
235
+ }
236
+ }
237
+
238
+ default_value(value) {
239
+ this.element.innerText = value ?? "";
240
+ }
241
+
242
+ default(value, attribute) {
243
+ value !== undefined && this.element.setAttribute(attribute, value);
244
+ }
245
+ }
@@ -0,0 +1,13 @@
1
+ export default class Node {
2
+ constructor(node) {
3
+ this.node = node;
4
+ }
5
+
6
+ get attributes() {
7
+ return [...this.node.attributes];
8
+ }
9
+
10
+ get children() {
11
+ return [...this.node.children];
12
+ }
13
+ }