structured-fw 0.7.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/Config.ts +47 -0
- package/LICENSE +21 -0
- package/README.md +332 -0
- package/app/Types.ts +1 -0
- package/app/models/README.md +9 -0
- package/app/routes/README.md +19 -0
- package/app/views/README.md +1 -0
- package/app/views/layout.html +1 -0
- package/bin/structured +114 -0
- package/build/Config.d.ts +2 -0
- package/build/Config.js +31 -0
- package/build/app/Types.d.ts +1 -0
- package/build/app/Types.js +1 -0
- package/build/app/models/Users.d.ts +0 -0
- package/build/app/models/Users.js +1 -0
- package/build/app/routes/Auth.d.ts +0 -0
- package/build/app/routes/Auth.js +1 -0
- package/build/app/routes/Test.d.ts +2 -0
- package/build/app/routes/Test.js +101 -0
- package/build/app/routes/Todo.d.ts +0 -0
- package/build/app/routes/Todo.js +1 -0
- package/build/app/routes/Upload.d.ts +0 -0
- package/build/app/routes/Upload.js +1 -0
- package/build/app/routes/Validation.d.ts +2 -0
- package/build/app/routes/Validation.js +34 -0
- package/build/app/views/components/ClientImport/ClientImport.client.d.ts +2 -0
- package/build/app/views/components/ClientImport/ClientImport.client.js +4 -0
- package/build/app/views/components/ClientImport/Export.d.ts +1 -0
- package/build/app/views/components/ClientImport/Export.js +1 -0
- package/build/app/views/components/Conditionals/Conditionals.client.d.ts +2 -0
- package/build/app/views/components/Conditionals/Conditionals.client.js +43 -0
- package/build/app/views/components/FormTest/FormTestNested/FormTestNested.d.ts +8 -0
- package/build/app/views/components/FormTest/FormTestNested/FormTestNested.js +7 -0
- package/build/app/views/components/ModelsTest/ModelsTest.client.d.ts +2 -0
- package/build/app/views/components/ModelsTest/ModelsTest.client.js +5 -0
- package/build/app/views/components/MultipartForm/MultipartForm.client.d.ts +0 -0
- package/build/app/views/components/MultipartForm/MultipartForm.client.js +1 -0
- package/build/app/views/components/PassObject/PassObject.d.ts +10 -0
- package/build/app/views/components/PassObject/PassObject.js +10 -0
- package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.d.ts +6 -0
- package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.js +6 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.client.d.ts +2 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.client.js +6 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.d.ts +8 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.js +8 -0
- package/build/app/views/components/ServerSideContext/ServerSideContext.d.ts +7 -0
- package/build/app/views/components/ServerSideContext/ServerSideContext.js +10 -0
- package/build/assets/ts/Export.d.ts +1 -0
- package/build/assets/ts/Export.js +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +3 -0
- package/build/system/Helpers.d.ts +3 -0
- package/build/system/Helpers.js +72 -0
- package/build/system/Symbols.d.ts +3 -0
- package/build/system/Symbols.js +3 -0
- package/build/system/Types.d.ts +171 -0
- package/build/system/Types.js +1 -0
- package/build/system/Util.d.ts +20 -0
- package/build/system/Util.js +336 -0
- package/build/system/client/App.d.ts +7 -0
- package/build/system/client/App.js +8 -0
- package/build/system/client/Client.d.ts +6 -0
- package/build/system/client/Client.js +9 -0
- package/build/system/client/ClientComponent.d.ts +68 -0
- package/build/system/client/ClientComponent.js +734 -0
- package/build/system/client/DataStore.d.ts +22 -0
- package/build/system/client/DataStore.js +64 -0
- package/build/system/client/DataStoreView.d.ts +19 -0
- package/build/system/client/DataStoreView.js +56 -0
- package/build/system/client/EventEmitter.d.ts +7 -0
- package/build/system/client/EventEmitter.js +31 -0
- package/build/system/client/Net.d.ts +13 -0
- package/build/system/client/Net.js +39 -0
- package/build/system/client/NetRequest.d.ts +13 -0
- package/build/system/client/NetRequest.js +45 -0
- package/build/system/server/Application.d.ts +31 -0
- package/build/system/server/Application.js +171 -0
- package/build/system/server/Component.d.ts +27 -0
- package/build/system/server/Component.js +249 -0
- package/build/system/server/Components.d.ts +12 -0
- package/build/system/server/Components.js +77 -0
- package/build/system/server/Cookies.d.ts +6 -0
- package/build/system/server/Cookies.js +19 -0
- package/build/system/server/Document.d.ts +24 -0
- package/build/system/server/Document.js +107 -0
- package/build/system/server/DocumentHead.d.ts +32 -0
- package/build/system/server/DocumentHead.js +118 -0
- package/build/system/server/FormValidation.d.ts +16 -0
- package/build/system/server/FormValidation.js +197 -0
- package/build/system/server/Handlebars.d.ts +11 -0
- package/build/system/server/Handlebars.js +34 -0
- package/build/system/server/Request.d.ts +21 -0
- package/build/system/server/Request.js +356 -0
- package/build/system/server/Session.d.ts +23 -0
- package/build/system/server/Session.js +114 -0
- package/build/system/server/dom/DOMFragment.d.ts +4 -0
- package/build/system/server/dom/DOMFragment.js +6 -0
- package/build/system/server/dom/DOMNode.d.ts +31 -0
- package/build/system/server/dom/DOMNode.js +110 -0
- package/build/system/server/dom/HTMLParser.d.ts +21 -0
- package/build/system/server/dom/HTMLParser.js +204 -0
- package/index.ts +4 -0
- package/package.json +31 -0
- package/system/Helpers.ts +97 -0
- package/system/Symbols.ts +6 -0
- package/system/Types.ts +234 -0
- package/system/Util.ts +488 -0
- package/system/client/App.ts +11 -0
- package/system/client/Client.ts +9 -0
- package/system/client/ClientComponent.ts +1117 -0
- package/system/client/DataStore.ts +101 -0
- package/system/client/DataStoreView.ts +82 -0
- package/system/client/EventEmitter.ts +38 -0
- package/system/client/Net.ts +58 -0
- package/system/client/NetRequest.ts +64 -0
- package/system/server/Application.ts +230 -0
- package/system/server/Component.ts +404 -0
- package/system/server/Components.ts +111 -0
- package/system/server/Cookies.ts +29 -0
- package/system/server/Document.ts +163 -0
- package/system/server/DocumentHead.ts +150 -0
- package/system/server/FormValidation.ts +231 -0
- package/system/server/Handlebars.ts +51 -0
- package/system/server/Request.ts +497 -0
- package/system/server/Session.ts +151 -0
- package/system/server/dom/DOMFragment.ts +7 -0
- package/system/server/dom/DOMNode.ts +140 -0
- package/system/server/dom/HTMLParser.ts +238 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { randomString } from '../Util.js';
|
|
2
|
+
export class Session {
|
|
3
|
+
constructor(app) {
|
|
4
|
+
this.enabled = false;
|
|
5
|
+
this.sessions = {};
|
|
6
|
+
this.application = app;
|
|
7
|
+
this.application.on('beforeRequestHandler', async (ctx) => {
|
|
8
|
+
if (this.enabled) {
|
|
9
|
+
const sessionCookie = ctx.cookies[this.application.config.session.cookieName];
|
|
10
|
+
const invalidSessionId = sessionCookie && !this.sessions[sessionCookie];
|
|
11
|
+
if (!sessionCookie || invalidSessionId) {
|
|
12
|
+
this.sessionInit(ctx);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
ctx.sessionId = sessionCookie;
|
|
16
|
+
if (ctx.sessionId) {
|
|
17
|
+
this.application.cookies.set(ctx.response, this.application.config.session.cookieName, ctx.sessionId, this.application.config.session.durationSeconds);
|
|
18
|
+
this.sessions[ctx.sessionId].lastRequest = new Date().getTime();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
this.garbageCollect();
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
this.enabled = true;
|
|
27
|
+
}
|
|
28
|
+
stop() {
|
|
29
|
+
this.enabled = false;
|
|
30
|
+
}
|
|
31
|
+
sessionInit(ctx) {
|
|
32
|
+
ctx.sessionId = this.generateId();
|
|
33
|
+
this.application.cookies.set(ctx.response, this.application.config.session.cookieName, ctx.sessionId, this.application.config.session.durationSeconds);
|
|
34
|
+
const sessionEntry = {
|
|
35
|
+
sessionId: ctx.sessionId,
|
|
36
|
+
lastRequest: new Date().getTime(),
|
|
37
|
+
data: {}
|
|
38
|
+
};
|
|
39
|
+
this.sessions[ctx.sessionId] = sessionEntry;
|
|
40
|
+
}
|
|
41
|
+
generateId() {
|
|
42
|
+
return randomString(this.application.config.session.keyLength);
|
|
43
|
+
}
|
|
44
|
+
garbageCollect() {
|
|
45
|
+
const time = new Date().getTime();
|
|
46
|
+
const sessDurationMilliseconds = this.application.config.session.garbageCollectAfterSeconds * 1000;
|
|
47
|
+
for (const sessionId in this.sessions) {
|
|
48
|
+
const sess = this.sessions[sessionId];
|
|
49
|
+
if (time - sess.lastRequest > sessDurationMilliseconds) {
|
|
50
|
+
delete this.sessions[sessionId];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
this.garbageCollect();
|
|
55
|
+
}, this.application.config.session.garbageCollectIntervalSeconds * 1000);
|
|
56
|
+
}
|
|
57
|
+
setValue(sessionId, key, value) {
|
|
58
|
+
if (sessionId === undefined) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (this.sessions[sessionId]) {
|
|
62
|
+
const session = this.sessions[sessionId];
|
|
63
|
+
session.data[key] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
getValue(sessionId, key) {
|
|
67
|
+
if (sessionId === undefined) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (this.sessions[sessionId]) {
|
|
71
|
+
const session = this.sessions[sessionId];
|
|
72
|
+
return typeof session.data[key] !== 'undefined' ? session.data[key] : null;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
getClear(sessionId, key) {
|
|
77
|
+
const val = this.getValue(sessionId, key);
|
|
78
|
+
this.removeValue(sessionId, key);
|
|
79
|
+
return val;
|
|
80
|
+
}
|
|
81
|
+
removeValue(sessionId, key) {
|
|
82
|
+
if (sessionId === undefined) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (this.sessions[sessionId] && this.sessions[sessionId].data[key]) {
|
|
86
|
+
delete this.sessions[sessionId].data[key];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
clear(sessionId) {
|
|
90
|
+
if (sessionId === undefined) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.sessions[sessionId]) {
|
|
94
|
+
this.sessions[sessionId].data = {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
extract(sessionId, keys) {
|
|
98
|
+
if (sessionId === undefined) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
const data = {};
|
|
102
|
+
keys.forEach((key) => {
|
|
103
|
+
if (typeof key === 'string') {
|
|
104
|
+
data[key] = this.getValue(sessionId, key);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const keyInSession = Object.keys(key)[0];
|
|
108
|
+
const keyReturned = key[keyInSession];
|
|
109
|
+
data[keyReturned] = this.getValue(sessionId, keyInSession);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type DOMNodeAttribute = {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string | true;
|
|
4
|
+
};
|
|
5
|
+
type JSONNode = {
|
|
6
|
+
name: string;
|
|
7
|
+
children: Array<JSONNode>;
|
|
8
|
+
attributes: Record<string, DOMNodeAttribute>;
|
|
9
|
+
strings: Array<string>;
|
|
10
|
+
};
|
|
11
|
+
export declare const selfClosingTags: ReadonlyArray<string>;
|
|
12
|
+
export declare class DOMNode {
|
|
13
|
+
tagName: string;
|
|
14
|
+
parentNode: DOMNode | null;
|
|
15
|
+
children: Array<DOMNode | string>;
|
|
16
|
+
attributes: Array<DOMNodeAttribute>;
|
|
17
|
+
attributeMap: Record<string, DOMNodeAttribute>;
|
|
18
|
+
style: Partial<CSSStyleDeclaration>;
|
|
19
|
+
selfClosing: boolean;
|
|
20
|
+
constructor(tagName: string);
|
|
21
|
+
appendChild(node: DOMNode | string): void;
|
|
22
|
+
setAttribute(attributeName: string, attributeValue: string | true): void;
|
|
23
|
+
hasAttribute(attributeName: string): boolean;
|
|
24
|
+
queryByTagName(...tagNames: Array<string>): Array<DOMNode>;
|
|
25
|
+
queryByHasAttribute(...attributeNames: Array<string>): Array<DOMNode>;
|
|
26
|
+
get innerHTML(): string;
|
|
27
|
+
set innerHTML(html: string);
|
|
28
|
+
get outerHTML(): string;
|
|
29
|
+
toObject(): JSONNode;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { HTMLParser } from "./HTMLParser.js";
|
|
2
|
+
export const selfClosingTags = ['br', 'hr', 'input', 'img', 'link', 'meta', 'source', 'embed', 'path', 'area'];
|
|
3
|
+
export class DOMNode {
|
|
4
|
+
constructor(tagName) {
|
|
5
|
+
this.parentNode = null;
|
|
6
|
+
this.children = [];
|
|
7
|
+
this.attributes = [];
|
|
8
|
+
this.attributeMap = {};
|
|
9
|
+
this.style = {};
|
|
10
|
+
this.tagName = tagName;
|
|
11
|
+
this.selfClosing = selfClosingTags.includes(tagName);
|
|
12
|
+
}
|
|
13
|
+
appendChild(node) {
|
|
14
|
+
if (typeof node !== 'string') {
|
|
15
|
+
node.parentNode = this;
|
|
16
|
+
}
|
|
17
|
+
this.children.push(node);
|
|
18
|
+
}
|
|
19
|
+
setAttribute(attributeName, attributeValue) {
|
|
20
|
+
const attributeExisting = this.attributeMap[attributeName];
|
|
21
|
+
if (!attributeExisting) {
|
|
22
|
+
const attribute = {
|
|
23
|
+
name: attributeName,
|
|
24
|
+
value: attributeValue
|
|
25
|
+
};
|
|
26
|
+
this.attributeMap[attributeName] = attribute;
|
|
27
|
+
this.attributes.push(attribute);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
attributeExisting.value = attributeValue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
hasAttribute(attributeName) {
|
|
34
|
+
return attributeName in this.attributeMap;
|
|
35
|
+
}
|
|
36
|
+
queryByTagName(...tagNames) {
|
|
37
|
+
let nodes = [];
|
|
38
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
39
|
+
const child = this.children[i];
|
|
40
|
+
if (typeof child === 'string') {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (tagNames.includes(child.tagName)) {
|
|
44
|
+
nodes.push(child);
|
|
45
|
+
}
|
|
46
|
+
nodes = nodes.concat(child.queryByTagName(...tagNames));
|
|
47
|
+
}
|
|
48
|
+
return nodes;
|
|
49
|
+
}
|
|
50
|
+
queryByHasAttribute(...attributeNames) {
|
|
51
|
+
let nodes = [];
|
|
52
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
53
|
+
const child = this.children[i];
|
|
54
|
+
if (typeof child === 'string') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (attributeNames.some((attributeName) => {
|
|
58
|
+
return child.hasAttribute(attributeName);
|
|
59
|
+
})) {
|
|
60
|
+
nodes.push(child);
|
|
61
|
+
}
|
|
62
|
+
nodes = nodes.concat(child.queryByHasAttribute(...attributeNames));
|
|
63
|
+
}
|
|
64
|
+
return nodes;
|
|
65
|
+
}
|
|
66
|
+
get innerHTML() {
|
|
67
|
+
return this.children.reduce((html, child) => {
|
|
68
|
+
if (typeof child === 'string') {
|
|
69
|
+
return html += child;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return html += child.outerHTML;
|
|
73
|
+
}
|
|
74
|
+
}, '');
|
|
75
|
+
}
|
|
76
|
+
set innerHTML(html) {
|
|
77
|
+
const fragment = new HTMLParser(html).dom();
|
|
78
|
+
this.children = fragment.children;
|
|
79
|
+
}
|
|
80
|
+
get outerHTML() {
|
|
81
|
+
const attributes = this.attributes.reduce((attributes, attribute) => {
|
|
82
|
+
attributes += ` ${attribute.name}${attribute.value === true ? '' : `="${attribute.value}"`}`;
|
|
83
|
+
return attributes;
|
|
84
|
+
}, '');
|
|
85
|
+
const style = Object.keys(this.style).reduce((style, styleName) => {
|
|
86
|
+
const styleValue = this.style[styleName];
|
|
87
|
+
if (styleValue?.toString().trim().length === 0) {
|
|
88
|
+
return style;
|
|
89
|
+
}
|
|
90
|
+
;
|
|
91
|
+
style += ` ${styleName}: ${styleValue};`;
|
|
92
|
+
return style;
|
|
93
|
+
}, '');
|
|
94
|
+
return `<${this.tagName}${attributes}${style.trim().length > 0 ? ` style="${style}"` : ''}>${this.selfClosing ? '' : `${this.innerHTML}</${this.tagName}>`}`;
|
|
95
|
+
}
|
|
96
|
+
toObject() {
|
|
97
|
+
return {
|
|
98
|
+
name: this.tagName,
|
|
99
|
+
children: this.children.filter((child) => {
|
|
100
|
+
return typeof child !== 'string';
|
|
101
|
+
}).map((child) => {
|
|
102
|
+
return child.toObject();
|
|
103
|
+
}),
|
|
104
|
+
attributes: this.attributeMap,
|
|
105
|
+
strings: this.children.filter((child) => {
|
|
106
|
+
return typeof child === 'string';
|
|
107
|
+
})
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DOMFragment } from "./DOMFragment.js";
|
|
2
|
+
export declare class HTMLParser {
|
|
3
|
+
private readonly html;
|
|
4
|
+
private offset;
|
|
5
|
+
private context;
|
|
6
|
+
private state;
|
|
7
|
+
private tokenCurrent;
|
|
8
|
+
private fragment;
|
|
9
|
+
private attributeOpenQuote;
|
|
10
|
+
private attributeNameCurrent;
|
|
11
|
+
private attributeContext;
|
|
12
|
+
constructor(html: string);
|
|
13
|
+
private char;
|
|
14
|
+
private lastChar;
|
|
15
|
+
parse(): boolean;
|
|
16
|
+
private line;
|
|
17
|
+
private isLetter;
|
|
18
|
+
private isNumber;
|
|
19
|
+
dom(): DOMFragment;
|
|
20
|
+
private error;
|
|
21
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { DOMFragment } from "./DOMFragment.js";
|
|
2
|
+
import { DOMNode } from "./DOMNode.js";
|
|
3
|
+
export class HTMLParser {
|
|
4
|
+
constructor(html) {
|
|
5
|
+
this.offset = 0;
|
|
6
|
+
this.state = 'idle';
|
|
7
|
+
this.tokenCurrent = '';
|
|
8
|
+
this.fragment = new DOMFragment();
|
|
9
|
+
this.attributeOpenQuote = '"';
|
|
10
|
+
this.attributeNameCurrent = '';
|
|
11
|
+
this.attributeContext = null;
|
|
12
|
+
this.html = html;
|
|
13
|
+
this.context = this.fragment;
|
|
14
|
+
while (this.parse()) {
|
|
15
|
+
this.offset++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
char() {
|
|
19
|
+
return this.html.charAt(this.offset);
|
|
20
|
+
}
|
|
21
|
+
lastChar() {
|
|
22
|
+
return this.offset === this.html.length - 1;
|
|
23
|
+
}
|
|
24
|
+
parse() {
|
|
25
|
+
if (this.offset >= this.html.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const char = this.char();
|
|
29
|
+
const charCode = char.charCodeAt(0);
|
|
30
|
+
if (this.state === 'idle') {
|
|
31
|
+
if (char === ' ') {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (char === '<') {
|
|
35
|
+
this.state = 'tagStart';
|
|
36
|
+
this.tokenCurrent = '';
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
this.state = 'text';
|
|
40
|
+
this.tokenCurrent = char;
|
|
41
|
+
}
|
|
42
|
+
else if (this.state === 'tagStart') {
|
|
43
|
+
if (char === '/') {
|
|
44
|
+
this.state = 'tagClose';
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (this.isLetter(charCode)) {
|
|
48
|
+
this.state = 'tagOpen';
|
|
49
|
+
this.tokenCurrent = char;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (this.state === 'tagOpen') {
|
|
54
|
+
if (char === '\n') {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (char === '/') {
|
|
58
|
+
if (this.tokenCurrent.length === 0) {
|
|
59
|
+
throw this.error(`Unexpected tag closing sequence "</", expected opening tag`);
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (char === '>') {
|
|
64
|
+
if (this.tokenCurrent.length === 0) {
|
|
65
|
+
throw this.error(`Found an empty HTML tag <>`);
|
|
66
|
+
}
|
|
67
|
+
const node = new DOMNode(this.tokenCurrent);
|
|
68
|
+
this.context.appendChild(node);
|
|
69
|
+
this.state = 'idle';
|
|
70
|
+
this.tokenCurrent = '';
|
|
71
|
+
if (!node.selfClosing) {
|
|
72
|
+
this.context = node;
|
|
73
|
+
}
|
|
74
|
+
this.attributeContext = node;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (char === ' ') {
|
|
78
|
+
if (this.tokenCurrent.length === 0) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
this.state = 'attributeName';
|
|
82
|
+
const node = new DOMNode(this.tokenCurrent);
|
|
83
|
+
this.context.appendChild(node);
|
|
84
|
+
this.tokenCurrent = '';
|
|
85
|
+
if (!node.selfClosing) {
|
|
86
|
+
this.context = node;
|
|
87
|
+
}
|
|
88
|
+
this.attributeContext = node;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (char !== '_' && !this.isLetter(charCode) && (this.tokenCurrent.length > 0 && !this.isNumber(charCode))) {
|
|
92
|
+
throw this.error(`Expected a-Z after HTML opening tag`);
|
|
93
|
+
}
|
|
94
|
+
this.tokenCurrent += char;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
else if (this.state === 'tagClose') {
|
|
98
|
+
if (char === '/') {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (char === '>') {
|
|
102
|
+
if (this.tokenCurrent !== this.context.tagName) {
|
|
103
|
+
throw this.error(`Found closing tag ${this.tokenCurrent}, expected ${this.context.tagName}`);
|
|
104
|
+
}
|
|
105
|
+
this.context = this.context.parentNode || this.fragment;
|
|
106
|
+
this.state = 'idle';
|
|
107
|
+
}
|
|
108
|
+
this.tokenCurrent += char;
|
|
109
|
+
}
|
|
110
|
+
else if (this.state === 'text') {
|
|
111
|
+
if (char === '<') {
|
|
112
|
+
this.state = 'tagStart';
|
|
113
|
+
this.context.appendChild(this.tokenCurrent);
|
|
114
|
+
this.tokenCurrent = '';
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
this.tokenCurrent += char;
|
|
118
|
+
if (this.lastChar() && this.tokenCurrent.length > 0) {
|
|
119
|
+
this.context.appendChild(this.tokenCurrent);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (this.state === 'attributeName') {
|
|
123
|
+
const boundsChar = char === ' ' || char === '\n' || char === '\t';
|
|
124
|
+
if (boundsChar || char === '=' || char === '>') {
|
|
125
|
+
if (char === '=') {
|
|
126
|
+
this.state = 'attributeValueStart';
|
|
127
|
+
}
|
|
128
|
+
else if (char === '>') {
|
|
129
|
+
this.state = 'idle';
|
|
130
|
+
}
|
|
131
|
+
if (this.tokenCurrent.length > 0) {
|
|
132
|
+
if (this.attributeContext !== null && this.tokenCurrent.trim().length > 0) {
|
|
133
|
+
this.attributeContext.setAttribute(this.tokenCurrent, true);
|
|
134
|
+
}
|
|
135
|
+
this.attributeNameCurrent = this.tokenCurrent;
|
|
136
|
+
this.tokenCurrent = '';
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!boundsChar) {
|
|
141
|
+
this.tokenCurrent += char;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (this.state === 'attributeValueStart') {
|
|
145
|
+
if (char === '"' || char === "'") {
|
|
146
|
+
this.state = 'attributeValue';
|
|
147
|
+
this.attributeOpenQuote = char;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (this.state === 'attributeValue') {
|
|
152
|
+
if (char === this.attributeOpenQuote) {
|
|
153
|
+
if (this.attributeContext) {
|
|
154
|
+
this.attributeContext.setAttribute(this.attributeNameCurrent, this.tokenCurrent);
|
|
155
|
+
}
|
|
156
|
+
this.tokenCurrent = '';
|
|
157
|
+
this.attributeNameCurrent = '';
|
|
158
|
+
this.state = 'attributeEnd';
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
this.tokenCurrent += char;
|
|
162
|
+
}
|
|
163
|
+
else if (this.state === 'attributeEnd') {
|
|
164
|
+
if (char === '>') {
|
|
165
|
+
this.state = 'idle';
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (char === ' ' || char === '\n') {
|
|
169
|
+
this.state = 'attributeName';
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
else if (char === '/') {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
throw this.error(`Unexpected character ${char} after attribute value`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
line() {
|
|
182
|
+
return this.html.substring(0, this.offset).split('\n').length;
|
|
183
|
+
}
|
|
184
|
+
isLetter(charCode) {
|
|
185
|
+
const isLowerCase = charCode > 96 && charCode < 123;
|
|
186
|
+
if (isLowerCase) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
const isUpperCase = charCode > 64 && charCode < 91;
|
|
190
|
+
if (isUpperCase) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
isNumber(charCode) {
|
|
196
|
+
return charCode > 47 && charCode < 58;
|
|
197
|
+
}
|
|
198
|
+
dom() {
|
|
199
|
+
return this.fragment;
|
|
200
|
+
}
|
|
201
|
+
error(message) {
|
|
202
|
+
return new Error(`HTMLParser: ${message}\nLine ${this.line()}\nHTML:\n${this.html}\n`);
|
|
203
|
+
}
|
|
204
|
+
}
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "structured-fw",
|
|
3
|
+
"displayName": "Structured framework",
|
|
4
|
+
"description": "Production-tested Node.js framework for creating performant server-side rendered web apps and APIs, with a sane amount of client side abstraction.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Julijan Andjelic",
|
|
7
|
+
"email": "julijan.andjelic@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "build/index",
|
|
11
|
+
"version": "0.7.2",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"develop": "tsc --watch",
|
|
14
|
+
"startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
|
|
15
|
+
"start": "cd build && node index.js",
|
|
16
|
+
"postinstall": "tsc"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"structured": "./bin/structured"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/mime-types": "^2.1.4",
|
|
23
|
+
"@types/node": "^22.9.3",
|
|
24
|
+
"typescript": "^5.7.2"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"handlebars": "^4.7.8",
|
|
28
|
+
"mime-types": "^3.0.0",
|
|
29
|
+
"ts-md5": "^1.3.1"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { HelperDelegate } from "handlebars";
|
|
2
|
+
import { attributeValueToString, objectEach } from "./Util.js";
|
|
3
|
+
|
|
4
|
+
const helpers: Record<string, HelperDelegate> = {
|
|
5
|
+
|
|
6
|
+
// {{{htmlTag tagName}}} outputs <tagName></tagName>
|
|
7
|
+
htmlTag : function(...args) {
|
|
8
|
+
// output a tag with given name
|
|
9
|
+
return `<${args[0]}></${args[0]}>`;
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
// {{{layoutComponent componentName data}}} outputs <tagName data-use="data.key0,data.key1..."></tagName>
|
|
13
|
+
layoutComponent: function(...args) {
|
|
14
|
+
// output a tag with given name
|
|
15
|
+
if (args.length < 2 || args.length > 4) {
|
|
16
|
+
console.warn('layoutComponent expects 1 - 3 arguments (componentName, data?, attributes?) got ' + (args.length - 1));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const argsUsed = args.slice(0, args.length - 1);
|
|
20
|
+
|
|
21
|
+
const componentName = argsUsed[0];
|
|
22
|
+
const data = argsUsed[1];
|
|
23
|
+
const attributes = argsUsed[2];
|
|
24
|
+
const dataAttributes: Array<string> = [];
|
|
25
|
+
let attributesString = '';
|
|
26
|
+
|
|
27
|
+
if (attributes) {
|
|
28
|
+
// got attributes
|
|
29
|
+
if (attributes) {
|
|
30
|
+
const attrNames = Object.keys(attributes);
|
|
31
|
+
attributesString = attrNames.map((attrName) => {
|
|
32
|
+
const val = attributes[attrName];
|
|
33
|
+
if (typeof val === 'string' || typeof val === 'number') {
|
|
34
|
+
return `${attrName}="${val}"`
|
|
35
|
+
}
|
|
36
|
+
if (val === true) {
|
|
37
|
+
return attrName;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}).filter((val) => val !== null).join(' ');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (data) {
|
|
45
|
+
objectEach(data, (key, val) => {
|
|
46
|
+
dataAttributes.push(`data-${key as string}="${attributeValueToString(key as string, val)}"`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return `<${componentName} ${dataAttributes.length > 0 ? dataAttributes.join(' ') : ''} ${attributesString}></${componentName}>`;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// JSON.stringify the given object
|
|
54
|
+
json: function(...args) {
|
|
55
|
+
if (args.length > 1) {
|
|
56
|
+
if (typeof args[0] === 'object' && args[0] !== null) {
|
|
57
|
+
return JSON.stringify(args[0]);
|
|
58
|
+
}
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
return '';
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// used as <div {{{attr [attrName] [attrValue]}}}></div>
|
|
65
|
+
// returns data-[attrName]="attributeValueToString([attrValue])"
|
|
66
|
+
// valu can be of any type and will be preserved since it is encoded using attributeValueToString
|
|
67
|
+
attr: function(key: string, val: any) {
|
|
68
|
+
return `data-${key}="${attributeValueToString(key, val)}"`;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// converts newline characters to <br>
|
|
72
|
+
nl2br: function(...args) {
|
|
73
|
+
if (args.length === 1 && 'fn' in args[0]) {
|
|
74
|
+
// block
|
|
75
|
+
return (args[0].fn(this) || '').replaceAll('\n', '<br>');
|
|
76
|
+
}
|
|
77
|
+
if (args.length === 2) {
|
|
78
|
+
if (typeof args[0] !== 'string') {return '';}
|
|
79
|
+
return args[0].replaceAll('\n', '<br>');
|
|
80
|
+
}
|
|
81
|
+
return '';
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// preserve indentation in given string by replacing space with
|
|
85
|
+
indent: function(...args) {
|
|
86
|
+
if (args.length === 1 && 'fn' in args[0]) {
|
|
87
|
+
// block
|
|
88
|
+
return args[0].fn(this).replaceAll(' ', ' ').replaceAll('\t', ' '.repeat(4));
|
|
89
|
+
}
|
|
90
|
+
if (args.length === 2) {
|
|
91
|
+
return args[0].replaceAll(' ', ' ').replaceAll('\t', ' '.repeat(4));
|
|
92
|
+
}
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default helpers;
|