primate 0.5.1 → 0.6.1

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 (81) hide show
  1. package/README.md +21 -36
  2. package/debris.json +1 -2
  3. package/package.json +7 -10
  4. package/source/{server/App.js → App.js} +14 -19
  5. package/source/Bundler.js +43 -0
  6. package/source/{server/Directory.js → Directory.js} +0 -0
  7. package/source/{server/EagerPromise.js → EagerPromise.js} +0 -0
  8. package/source/{server/File.js → File.js} +0 -0
  9. package/source/Router.js +28 -0
  10. package/source/Server.js +103 -0
  11. package/source/Session.js +26 -0
  12. package/source/{server/attributes.js → attributes.js} +0 -0
  13. package/source/{server/cache.js → cache.js} +0 -0
  14. package/source/{server/conf.js → conf.js} +1 -1
  15. package/source/{server/crypto.js → crypto.js} +0 -0
  16. package/source/{server/domain → domain}/Domain.js +8 -10
  17. package/source/{server/domain → domain}/Field.js +0 -0
  18. package/source/{server/domain → domain}/Predicate.js +0 -0
  19. package/source/{server/domain → domain}/domains.js +0 -0
  20. package/source/{server/errors → errors}/InternalServer.js +0 -0
  21. package/source/{server/errors → errors}/Predicate.js +0 -0
  22. package/source/{server/errors.js → errors.js} +0 -0
  23. package/source/{server/exports.js → exports.js} +4 -2
  24. package/source/{server/extend_object.js → extend_object.js} +0 -0
  25. package/source/handlers/DOM/Node.js +184 -0
  26. package/source/{server/view → handlers/DOM}/Parser.js +22 -18
  27. package/source/handlers/html.js +27 -0
  28. package/source/handlers/http.js +6 -0
  29. package/source/handlers/redirect.js +10 -0
  30. package/source/{server/servers/http-codes.json → http-codes.json} +0 -0
  31. package/source/{server/invariants.js → invariants.js} +0 -0
  32. package/source/{server/log.js → log.js} +0 -0
  33. package/source/{server/servers/mimes.json → mimes.json} +0 -0
  34. package/source/preset/primate.json +12 -4
  35. package/source/preset/static/index.html +0 -2
  36. package/source/preset/stores/default.js +2 -0
  37. package/source/{server/sanitize.js → sanitize.js} +0 -0
  38. package/source/{server/store → store}/Memory.js +0 -0
  39. package/source/{server/store → store}/Store.js +1 -1
  40. package/source/{server/types → types}/Array.js +0 -0
  41. package/source/{server/types → types}/Boolean.js +0 -0
  42. package/source/{server/types → types}/Date.js +0 -0
  43. package/source/{server/types → types}/Domain.js +0 -0
  44. package/source/{server/types → types}/Instance.js +0 -0
  45. package/source/{server/types → types}/Number.js +0 -0
  46. package/source/{server/types → types}/Object.js +0 -0
  47. package/source/{server/types → types}/Primitive.js +0 -0
  48. package/source/{server/types → types}/Storeable.js +0 -0
  49. package/source/{server/types → types}/String.js +0 -0
  50. package/source/{server/types → types}/errors/Array.json +0 -0
  51. package/source/{server/types → types}/errors/Boolean.json +0 -0
  52. package/source/{server/types → types}/errors/Date.json +0 -0
  53. package/source/{server/types → types}/errors/Number.json +0 -0
  54. package/source/{server/types → types}/errors/Object.json +0 -0
  55. package/source/{server/types → types}/errors/String.json +0 -0
  56. package/source/{server/types.js → types.js} +0 -0
  57. package/source/client/Action.js +0 -157
  58. package/source/client/App.js +0 -16
  59. package/source/client/Client.js +0 -61
  60. package/source/client/Context.js +0 -47
  61. package/source/client/Element.js +0 -249
  62. package/source/client/Node.js +0 -13
  63. package/source/client/Session.js +0 -27
  64. package/source/client/View.js +0 -89
  65. package/source/client/document.js +0 -6
  66. package/source/client/exports.js +0 -15
  67. package/source/preset/client/Element.js +0 -2
  68. package/source/preset/client/app.js +0 -2
  69. package/source/preset/data/stores/default.js +0 -2
  70. package/source/server/Action.js +0 -100
  71. package/source/server/Bundler.js +0 -177
  72. package/source/server/Context.js +0 -97
  73. package/source/server/Projector.js +0 -86
  74. package/source/server/Router.js +0 -52
  75. package/source/server/Session.js +0 -45
  76. package/source/server/servers/Dynamic.js +0 -57
  77. package/source/server/servers/Server.js +0 -5
  78. package/source/server/servers/Static.js +0 -118
  79. package/source/server/servers/content-security-policy.json +0 -7
  80. package/source/server/view/TreeNode.js +0 -197
  81. package/source/server/view/View.js +0 -35
@@ -0,0 +1,184 @@
1
+ import Parser from "./Parser.js";
2
+
3
+ const replacement_regex = /^\$([0-9]*)$/;
4
+ const data_regex = /\${([^}]*)}/g;
5
+ const replace = (attribute, source) => {
6
+ if (attribute.includes(".")) {
7
+ const index = attribute.indexOf(".");
8
+ const left = attribute.slice(0, index);
9
+ const rest = attribute.slice(index+1);
10
+ if (source[left] !== undefined) {
11
+ return replace(rest, source[left]);
12
+ }
13
+ } else {
14
+ return source[attribute];
15
+ }
16
+ };
17
+ const fulfill = (attribute, source) => {
18
+ if (source === undefined) {
19
+ return undefined;
20
+ }
21
+ let value;
22
+ const matches = [...attribute.matchAll(data_regex)];
23
+ if (matches.length > 0) {
24
+ for (const match of matches) {
25
+ const [key] = match;
26
+ const new_value = replace(match[1], source);
27
+ value = attribute.replace(key, new_value);
28
+ }
29
+ } else {
30
+ value = replace(attribute, source);
31
+ }
32
+ return value;
33
+ };
34
+
35
+ export default class Node {
36
+ #data;
37
+
38
+ constructor(parent, content = "div", data) {
39
+ if (parent !== undefined) {
40
+ this.parent = parent;
41
+ this.parent.attach(this);
42
+ }
43
+ this.#data = data;
44
+ this.attributes = {};
45
+ if (content !== undefined) {
46
+ const [tag_name, ...attributes] = content.split(" ");
47
+ this.tag_name = tag_name;
48
+ for (const attribute of attributes
49
+ .map(a => a.replaceAll("\"", ""))
50
+ .filter(a => a.includes("="))) {
51
+ const position = attribute.indexOf("=");
52
+ const key = attribute.slice(0, position);
53
+ const value = attribute.slice(position+1);
54
+ this.attributes[key] = value;
55
+ if (this.data) {
56
+ const result = replacement_regex.exec(value);
57
+ if (result !== null) {
58
+ this.attributes[key] = this.data[result[1]];
59
+ }
60
+ }
61
+ }
62
+ }
63
+ this.children = [];
64
+ }
65
+
66
+ get data() {
67
+ if (this.#data !== undefined) {
68
+ return this.#data;
69
+ } else {
70
+ return this.parent?.data;
71
+ }
72
+ }
73
+
74
+ set data(value) {
75
+ this.#data = value;
76
+ }
77
+
78
+ attach(child) {
79
+ this.children.push(child);
80
+ child.parent = this;
81
+ }
82
+
83
+ replace(child, replacement) {
84
+ for (let i = 0; i < this.children.length; i++) {
85
+ if (child === this.children[i]) {
86
+ replacement.parent = this;
87
+ this.children.splice(i, 1, replacement);
88
+ }
89
+ }
90
+ }
91
+
92
+ async render() {
93
+ let tag = "<" + this.tag_name;
94
+ for (const [key, value] of Object.entries(this.attributes)) {
95
+ if (value === undefined) {
96
+ continue;
97
+ }
98
+ tag += " " + key + "=";
99
+ if (!value.startsWith("\"")) {
100
+ tag += "\"";
101
+ }
102
+ tag += value;
103
+ if (!value.endsWith("\"")) {
104
+ tag += "\"";
105
+ }
106
+ }
107
+ if (this.auto_closing) {
108
+ tag += "/>";
109
+ } else {
110
+ tag += ">";
111
+ for (const child of this.children) {
112
+ tag += await child.render();
113
+ }
114
+ if (this.text) {
115
+ tag += await this.text;
116
+ }
117
+ tag += "</" + this.tag_name + ">";
118
+ }
119
+ return tag;
120
+ }
121
+
122
+ compose(components) {
123
+ if (components[this.tag_name]) {
124
+ return Parser.parse(components[this.tag_name], this.attributes);
125
+ }
126
+ for (let i = 0; i < this.children.length; i++) {
127
+ this.children[i] = this.children[i].compose(components);
128
+ }
129
+ return this;
130
+ }
131
+
132
+ clone(parent, data) {
133
+ const cloned = new Node(parent, this.tag_name, data);
134
+ cloned.text = this.text;
135
+ for (const attribute in this.attributes) {
136
+ cloned.attributes[attribute] = this.attributes[attribute];
137
+ }
138
+ for (const child of this.children) {
139
+ child.clone(cloned);
140
+ }
141
+ }
142
+
143
+ async expand() {
144
+ if (this.attributes["data-for"] !== undefined) {
145
+ const key = this.attributes["data-for"];
146
+ delete this.attributes["data-for"];
147
+ const value = await this.data[key];
148
+ const arr = Array.isArray(value) ? value : [value];
149
+ const newparent = new Node();
150
+ for (const val of arr) {
151
+ this.clone(newparent, val);
152
+ }
153
+ this.parent.replace(this, newparent);
154
+ return newparent.expand();
155
+ }
156
+ for (const attribute in this.attributes) {
157
+ if (attribute.startsWith("data-")) {
158
+ const fulfilled = fulfill(this.attributes[attribute], this.data);
159
+ switch(attribute) {
160
+ case "data-value":
161
+ if (this.tag_name === "input") {
162
+ this.attributes.value = fulfilled;
163
+ } else {
164
+ this.text = fulfilled;
165
+ }
166
+ break;
167
+ case "data-href":
168
+ fulfill(this.attributes[attribute], this.data);
169
+ this.attributes.href = fulfilled;
170
+ break;
171
+ }
172
+ delete this.attributes[attribute];
173
+ }
174
+ }
175
+ for (let i = 0; i < this.children.length; i++) {
176
+ this.children[i] = await this.children[i].expand();
177
+ }
178
+ return this;
179
+ }
180
+
181
+ unfold(components) {
182
+ return this.compose(components).expand();
183
+ }
184
+ }
@@ -1,24 +1,24 @@
1
- import TreeNode from "./TreeNode.js";
1
+ import Node from "./Node.js";
2
2
 
3
- const OPEN_TAG = "open_tag";
4
- const CLOSE_TAG = "close_tag";
5
- const OPEN_AND_CLOSE_TAG = "open_and_close_tag";
3
+ const open_tag = "open_tag";
4
+ const close_tag = "close_tag";
5
+ const open_and_close_tag = "open_and_close_tag";
6
6
  const last_index = -1;
7
7
 
8
8
  export default class Parser {
9
- constructor(html) {
9
+ constructor(html, data) {
10
10
  this.html = html;
11
- this.result = [];
12
11
  this.index = 0;
12
+ this.result = [];
13
13
  this.buffer = "";
14
14
  this.balance = 0;
15
15
  this.reading_tag = false;
16
- this.node = new TreeNode();
16
+ this.node = new Node(undefined, undefined, data);
17
17
  this.tree = this.node;
18
18
  }
19
19
 
20
- at(index) {
21
- return this.html[index];
20
+ remove_whitespace() {
21
+ this.html = this.html.replace(/[\n\t\r]/gu, "");
22
22
  }
23
23
 
24
24
  get previous() {
@@ -33,12 +33,12 @@ export default class Parser {
33
33
  return this.at(this.index+1);
34
34
  }
35
35
 
36
- remove_whitespace() {
37
- this.html = this.html.replace(/[\n\t\r]/gu, "");
36
+ at(index) {
37
+ return this.html[index];
38
38
  }
39
39
 
40
40
  open_tag() {
41
- this.node = new TreeNode(this.node, this.buffer);
41
+ this.node = new Node(this.node, this.buffer);
42
42
  }
43
43
 
44
44
  close_tag() {
@@ -49,6 +49,7 @@ export default class Parser {
49
49
 
50
50
  open_and_close_tag() {
51
51
  this.open_tag();
52
+ this.node.auto_closing = true;
52
53
  this.close_tag();
53
54
  }
54
55
 
@@ -59,7 +60,7 @@ export default class Parser {
59
60
  this.reading_tag = false;
60
61
  // if the previous character is '/', it's an open and close tag
61
62
  if (this.previous === "/") {
62
- this.tag = OPEN_AND_CLOSE_TAG;
63
+ this.tag = open_and_close_tag;
63
64
  this.balance--;
64
65
  this.buffer = this.buffer.slice(0, last_index);
65
66
  }
@@ -77,17 +78,21 @@ export default class Parser {
77
78
  process_not_reading_tag() {
78
79
  // encountered '<'
79
80
  if (this.current === "<") {
81
+ this.node.text = this.buffer;
82
+ this.buffer = "";
80
83
  // mark as inside tag
81
84
  this.reading_tag = true;
82
85
  if (this.next === "/") {
83
86
  // next character is slash, this is a close tag
84
- this.tag = CLOSE_TAG;
87
+ this.tag = close_tag;
85
88
  this.balance--;
86
89
  } else {
87
90
  // this is an open tag (or open-and-close)
88
- this.tag = OPEN_TAG;
91
+ this.tag = open_tag;
89
92
  this.balance++;
90
93
  }
94
+ } else {
95
+ this.buffer += this.current;
91
96
  }
92
97
  }
93
98
 
@@ -109,14 +114,13 @@ export default class Parser {
109
114
  return this.tree;
110
115
  }
111
116
 
112
- // TODO: ignore comments in the analysed html
113
117
  parse() {
114
118
  this.remove_whitespace();
115
119
  do {} while (this.read());
116
120
  return this.return_checked();
117
121
  }
118
122
 
119
- static parse(html) {
120
- return new Parser(html).parse();
123
+ static parse(html, data) {
124
+ return new Parser(html, data).parse();
121
125
  }
122
126
  }
@@ -0,0 +1,27 @@
1
+ import Parser from "./DOM/Parser.js";
2
+ import conf from "../conf.js";
3
+ import Directory from "../Directory.js";
4
+ import File from "../File.js";
5
+
6
+ const last = -1;
7
+ const {paths: {"components": path}} = conf();
8
+ const components = {};
9
+ if (await File.exists(path)) {
10
+ const names = await Directory.list(path);
11
+ for (const name of names) {
12
+ components[name.slice(0, -5)] = await File.read(`${path}/${name}`);
13
+ }
14
+ }
15
+
16
+ export default async (strings, ...keys) => {
17
+ const awaited_keys = await Promise.all(keys);
18
+ const body = await (await Parser.parse(strings
19
+ .slice(0, last)
20
+ .map((string, i) => `${string}$${i}`)
21
+ .join("") + strings[strings.length+last], awaited_keys)
22
+ .unfold(components))
23
+ .render();
24
+ const code = 200;
25
+ const headers = {"Content-Type": "text/html"};
26
+ return {code, body, headers};
27
+ };
@@ -0,0 +1,6 @@
1
+ const http404 = () => {
2
+ const headers = {"Content-Type": "text/html"};
3
+ return {"code": 404, "body": "Page not found", headers};
4
+ };
5
+
6
+ export {http404};
@@ -0,0 +1,10 @@
1
+ const last = -1;
2
+
3
+ export default async (strings, ...keys) => {
4
+ const awaited_keys = await Promise.all(keys);
5
+ const Location = strings
6
+ .slice(0, last)
7
+ .map((string, i) => string + awaited_keys[i])
8
+ .join("") + strings[strings.length+last];
9
+ return {"code": 302, "headers": {Location}};
10
+ };
File without changes
File without changes
File without changes
@@ -13,13 +13,21 @@
13
13
  "ssl": {
14
14
  "key": "ssl/default.key",
15
15
  "cert": "ssl/default.crt"
16
- }
16
+ },
17
+ "csp": {
18
+ "default-src": "'self'",
19
+ "object-src": "'none'",
20
+ "frame-ancestors": "'none'",
21
+ "form-action": "'self'",
22
+ "base-uri": "'self'"
23
+ },
24
+ "same-site": "Strict"
17
25
  },
18
26
  "paths": {
19
- "client": "client",
20
27
  "domains": "domains",
21
28
  "public": "public",
22
- "server": "server",
23
- "static": "static"
29
+ "static": "static",
30
+ "routes": "routes",
31
+ "components": "components"
24
32
  }
25
33
  }
@@ -2,9 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <title>Primate app</title>
5
- <base href="${conf.base}" />
6
5
  <meta charset="utf-8" />
7
- ${client}
8
6
  </head>
9
7
  <body></body>
10
8
  </html>
@@ -0,0 +1,2 @@
1
+ import MemoryStore from "../../store/Memory.js";
2
+ export default new MemoryStore();
File without changes
File without changes
@@ -1,5 +1,5 @@
1
1
  import {resolve} from "path";
2
- const preset = "../../preset/data/stores";
2
+ const preset = "../preset/stores";
3
3
 
4
4
  export default class Store {
5
5
  constructor(conf = {}) {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,157 +0,0 @@
1
- import Element from "../Element.js";
2
- import View from "./View.js";
3
- import {origin_base, origin} from "./document.js";
4
-
5
- export default class Action {
6
- constructor(name, session) {
7
- this.name = name;
8
- this.session = session;
9
- this.listeners = [];
10
- }
11
-
12
- before() {}
13
-
14
- enter(data) {
15
- if (data.location) {
16
- location.href = data.location;
17
- } else {
18
- this.create_view(data);
19
- }
20
- }
21
-
22
- create_view(data, root, save_history = true) {
23
- if (save_history) {
24
- if (data.url !== document.location.href.replace(origin, "")) {
25
- history.pushState({}, "", data.url);
26
- }
27
- }
28
- this.view = new View(data, this, root);
29
- }
30
-
31
- create_and_return_view(data, root) {
32
- return new View(data, this, root);
33
- }
34
-
35
- get_form_data(target) {
36
- const payload = {};
37
- const {elements} = target;
38
- for (let i = 0; i < elements.length; i++) {
39
- const element = elements[i];
40
- const type = Element.value(element, "type");
41
- const data_value = Element.value(element, "data-value");
42
- switch (element.localName) {
43
- case "input":
44
- case "textarea":
45
- if (type === "checkbox") {
46
- payload[data_value] = element.checked;
47
- } else if (type === "file") {
48
- payload[data_value] = element.getAttribute("base64");
49
- } else if (type === "submit") {
50
- // do nothing
51
- } else {
52
- payload[data_value] = element.value;
53
- }
54
- break;
55
- case "select":
56
- payload[data_value] = Element
57
- .value(element.children[element.selectedIndex], "value");
58
- break;
59
- default:
60
- break;
61
- }
62
- }
63
- return payload;
64
- }
65
-
66
- async onsubmit(event) {
67
- const {target} = event;
68
- const stop = this.fire("submit", event);
69
- if (stop) {
70
- return event.preventDefault();
71
- }
72
- event.preventDefault();
73
- const data = await this.write(target.action, this.get_form_data(target));
74
- return data.type === "update"
75
- ? this.view.update(data.payload)
76
- : this.session.run(data);
77
- }
78
-
79
- async onclick(event) {
80
- const stop = await this.fire("click", event);
81
- if (stop) {
82
- return event.preventDefault();
83
- }
84
- let target;
85
- if (event.target.shadowRoot !== null &&
86
- event.target.shadowRoot.activeElement !== null) {
87
- target = event.target.shadowRoot.activeElement.closest("a");
88
- } else {
89
- target = event.target.closest("a");
90
- }
91
- if (target !== null) {
92
- const href = Element.value(target, "href");
93
- const url = new URL(href, origin_base);
94
- if (event.button === 0 && url.href.startsWith(origin_base)) {
95
- event.preventDefault();
96
- if (href !== "") {
97
- await this.session.run(await this.read(href));
98
- }
99
- }
100
- }
101
- }
102
-
103
- onchange() {
104
- const stop = this.fire("change", event);
105
- if (stop) {
106
- event.preventDefault();
107
- }
108
- }
109
-
110
- oninput() {
111
- this.fire("input", event);
112
- }
113
-
114
- fire(type, event) {
115
- const listeners = this.listeners[type];
116
- if (listeners !== undefined) {
117
- const {target} = event;
118
- for (const listener of listeners) {
119
- if (event.target.closest(listener.selector)) {
120
- listener.listener(event, target);
121
- return true;
122
- }
123
- }
124
- }
125
-
126
- return false;
127
- }
128
-
129
- on(selector, event, listener) {
130
- this.listeners[event] = this.listeners[event] ?? [];
131
- const index = this.listeners[event]
132
- .findIndex(listener => listener.selector === selector);
133
- const object = {selector, listener};
134
- const not_found = -1;
135
- if (index === not_found) {
136
- this.listeners[event].push(object);
137
- } else {
138
- this.listeners[event].splice(index, 1, object);
139
- }
140
- }
141
-
142
- _(selector) {
143
- return new Element(selector, this);
144
- }
145
-
146
- get selector() {
147
- return selector => new Element(selector, this);
148
- }
149
-
150
- read(pathname) {
151
- return this.session.read(pathname);
152
- }
153
-
154
- write(pathname, payload) {
155
- return this.session.write(pathname, payload);
156
- }
157
- }
@@ -1,16 +0,0 @@
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
- }
@@ -1,61 +0,0 @@
1
- import Session from "./Session.js";
2
- import {base, host, origin_base} from "./document.js";
3
-
4
- const events = ["submit", "click", "change", "input"];
5
- let back = undefined;
6
- const location = `wss://${host}${base}`;
7
-
8
- export default class Client {
9
- constructor(conf) {
10
- this.conf = conf;
11
- this.session = new Session(this);
12
-
13
- window.onpopstate = async event => {
14
- const {pathname, search} = event.target.location;
15
- return this.session.run(await this.read(pathname + search));
16
- };
17
-
18
- for (const key of events) {
19
- window.addEventListener(key, event => this.session.on(key, event));
20
- }
21
- }
22
-
23
- open() {
24
- return this.connection?.readyState === 1 ? this
25
- : new Promise(resolve => {
26
- this.connection = new WebSocket(location);
27
- this.connection.addEventListener("message", ({data}) => data === "open"
28
- ? resolve(this)
29
- : this.receive(JSON.parse(data)));
30
- });
31
- }
32
-
33
- async receive(data) {
34
- if (back === undefined) {
35
- await this.session.execute(data);
36
- } else {
37
- back(data);
38
- back = undefined;
39
- }
40
- }
41
-
42
- read(url) {
43
- return this.send("read", url);
44
- }
45
-
46
- write(url, payload) {
47
- return this.send("write", url, payload);
48
- }
49
-
50
- async send(type, url, payload = {}) {
51
- await this.open();
52
- const {pathname, search} = new URL(url, origin_base);
53
- const _url = pathname + search;
54
- return new Promise(resolve => {
55
- back = data => resolve(data);
56
- this.connection.send(JSON.stringify({
57
- type, pathname, search, payload, "url": _url,
58
- }));
59
- });
60
- }
61
- }