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.
Files changed (129) hide show
  1. package/Config.ts +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +332 -0
  4. package/app/Types.ts +1 -0
  5. package/app/models/README.md +9 -0
  6. package/app/routes/README.md +19 -0
  7. package/app/views/README.md +1 -0
  8. package/app/views/layout.html +1 -0
  9. package/bin/structured +114 -0
  10. package/build/Config.d.ts +2 -0
  11. package/build/Config.js +31 -0
  12. package/build/app/Types.d.ts +1 -0
  13. package/build/app/Types.js +1 -0
  14. package/build/app/models/Users.d.ts +0 -0
  15. package/build/app/models/Users.js +1 -0
  16. package/build/app/routes/Auth.d.ts +0 -0
  17. package/build/app/routes/Auth.js +1 -0
  18. package/build/app/routes/Test.d.ts +2 -0
  19. package/build/app/routes/Test.js +101 -0
  20. package/build/app/routes/Todo.d.ts +0 -0
  21. package/build/app/routes/Todo.js +1 -0
  22. package/build/app/routes/Upload.d.ts +0 -0
  23. package/build/app/routes/Upload.js +1 -0
  24. package/build/app/routes/Validation.d.ts +2 -0
  25. package/build/app/routes/Validation.js +34 -0
  26. package/build/app/views/components/ClientImport/ClientImport.client.d.ts +2 -0
  27. package/build/app/views/components/ClientImport/ClientImport.client.js +4 -0
  28. package/build/app/views/components/ClientImport/Export.d.ts +1 -0
  29. package/build/app/views/components/ClientImport/Export.js +1 -0
  30. package/build/app/views/components/Conditionals/Conditionals.client.d.ts +2 -0
  31. package/build/app/views/components/Conditionals/Conditionals.client.js +43 -0
  32. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.d.ts +8 -0
  33. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.js +7 -0
  34. package/build/app/views/components/ModelsTest/ModelsTest.client.d.ts +2 -0
  35. package/build/app/views/components/ModelsTest/ModelsTest.client.js +5 -0
  36. package/build/app/views/components/MultipartForm/MultipartForm.client.d.ts +0 -0
  37. package/build/app/views/components/MultipartForm/MultipartForm.client.js +1 -0
  38. package/build/app/views/components/PassObject/PassObject.d.ts +10 -0
  39. package/build/app/views/components/PassObject/PassObject.js +10 -0
  40. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.d.ts +6 -0
  41. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.js +6 -0
  42. package/build/app/views/components/RedrawAbort/RedrawAbort.client.d.ts +2 -0
  43. package/build/app/views/components/RedrawAbort/RedrawAbort.client.js +6 -0
  44. package/build/app/views/components/RedrawAbort/RedrawAbort.d.ts +8 -0
  45. package/build/app/views/components/RedrawAbort/RedrawAbort.js +8 -0
  46. package/build/app/views/components/ServerSideContext/ServerSideContext.d.ts +7 -0
  47. package/build/app/views/components/ServerSideContext/ServerSideContext.js +10 -0
  48. package/build/assets/ts/Export.d.ts +1 -0
  49. package/build/assets/ts/Export.js +1 -0
  50. package/build/index.d.ts +1 -0
  51. package/build/index.js +3 -0
  52. package/build/system/Helpers.d.ts +3 -0
  53. package/build/system/Helpers.js +72 -0
  54. package/build/system/Symbols.d.ts +3 -0
  55. package/build/system/Symbols.js +3 -0
  56. package/build/system/Types.d.ts +171 -0
  57. package/build/system/Types.js +1 -0
  58. package/build/system/Util.d.ts +20 -0
  59. package/build/system/Util.js +336 -0
  60. package/build/system/client/App.d.ts +7 -0
  61. package/build/system/client/App.js +8 -0
  62. package/build/system/client/Client.d.ts +6 -0
  63. package/build/system/client/Client.js +9 -0
  64. package/build/system/client/ClientComponent.d.ts +68 -0
  65. package/build/system/client/ClientComponent.js +734 -0
  66. package/build/system/client/DataStore.d.ts +22 -0
  67. package/build/system/client/DataStore.js +64 -0
  68. package/build/system/client/DataStoreView.d.ts +19 -0
  69. package/build/system/client/DataStoreView.js +56 -0
  70. package/build/system/client/EventEmitter.d.ts +7 -0
  71. package/build/system/client/EventEmitter.js +31 -0
  72. package/build/system/client/Net.d.ts +13 -0
  73. package/build/system/client/Net.js +39 -0
  74. package/build/system/client/NetRequest.d.ts +13 -0
  75. package/build/system/client/NetRequest.js +45 -0
  76. package/build/system/server/Application.d.ts +31 -0
  77. package/build/system/server/Application.js +171 -0
  78. package/build/system/server/Component.d.ts +27 -0
  79. package/build/system/server/Component.js +249 -0
  80. package/build/system/server/Components.d.ts +12 -0
  81. package/build/system/server/Components.js +77 -0
  82. package/build/system/server/Cookies.d.ts +6 -0
  83. package/build/system/server/Cookies.js +19 -0
  84. package/build/system/server/Document.d.ts +24 -0
  85. package/build/system/server/Document.js +107 -0
  86. package/build/system/server/DocumentHead.d.ts +32 -0
  87. package/build/system/server/DocumentHead.js +118 -0
  88. package/build/system/server/FormValidation.d.ts +16 -0
  89. package/build/system/server/FormValidation.js +197 -0
  90. package/build/system/server/Handlebars.d.ts +11 -0
  91. package/build/system/server/Handlebars.js +34 -0
  92. package/build/system/server/Request.d.ts +21 -0
  93. package/build/system/server/Request.js +356 -0
  94. package/build/system/server/Session.d.ts +23 -0
  95. package/build/system/server/Session.js +114 -0
  96. package/build/system/server/dom/DOMFragment.d.ts +4 -0
  97. package/build/system/server/dom/DOMFragment.js +6 -0
  98. package/build/system/server/dom/DOMNode.d.ts +31 -0
  99. package/build/system/server/dom/DOMNode.js +110 -0
  100. package/build/system/server/dom/HTMLParser.d.ts +21 -0
  101. package/build/system/server/dom/HTMLParser.js +204 -0
  102. package/index.ts +4 -0
  103. package/package.json +31 -0
  104. package/system/Helpers.ts +97 -0
  105. package/system/Symbols.ts +6 -0
  106. package/system/Types.ts +234 -0
  107. package/system/Util.ts +488 -0
  108. package/system/client/App.ts +11 -0
  109. package/system/client/Client.ts +9 -0
  110. package/system/client/ClientComponent.ts +1117 -0
  111. package/system/client/DataStore.ts +101 -0
  112. package/system/client/DataStoreView.ts +82 -0
  113. package/system/client/EventEmitter.ts +38 -0
  114. package/system/client/Net.ts +58 -0
  115. package/system/client/NetRequest.ts +64 -0
  116. package/system/server/Application.ts +230 -0
  117. package/system/server/Component.ts +404 -0
  118. package/system/server/Components.ts +111 -0
  119. package/system/server/Cookies.ts +29 -0
  120. package/system/server/Document.ts +163 -0
  121. package/system/server/DocumentHead.ts +150 -0
  122. package/system/server/FormValidation.ts +231 -0
  123. package/system/server/Handlebars.ts +51 -0
  124. package/system/server/Request.ts +497 -0
  125. package/system/server/Session.ts +151 -0
  126. package/system/server/dom/DOMFragment.ts +7 -0
  127. package/system/server/dom/DOMNode.ts +140 -0
  128. package/system/server/dom/HTMLParser.ts +238 -0
  129. 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,4 @@
1
+ import { DOMNode } from "./DOMNode.js";
2
+ export declare class DOMFragment extends DOMNode {
3
+ constructor();
4
+ }
@@ -0,0 +1,6 @@
1
+ import { DOMNode } from "./DOMNode.js";
2
+ export class DOMFragment extends DOMNode {
3
+ constructor() {
4
+ super('body');
5
+ }
6
+ }
@@ -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
@@ -0,0 +1,4 @@
1
+ import { Application } from "@structured/Application.js";
2
+ import { config } from './Config.js';
3
+
4
+ new Application(config);
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 &nbsp;
85
+ indent: function(...args) {
86
+ if (args.length === 1 && 'fn' in args[0]) {
87
+ // block
88
+ return args[0].fn(this).replaceAll(' ', '&nbsp;').replaceAll('\t', '&nbsp;'.repeat(4));
89
+ }
90
+ if (args.length === 2) {
91
+ return args[0].replaceAll(' ', '&nbsp;').replaceAll('\t', '&nbsp;'.repeat(4));
92
+ }
93
+ return '';
94
+ }
95
+ }
96
+
97
+ export default helpers;
@@ -0,0 +1,6 @@
1
+ // all framework related data will be stored under this session key
2
+ export const symbolSession: unique symbol = Symbol('session');
3
+
4
+ export const symbolAny: unique symbol = Symbol('*');
5
+
6
+ export const symbolArrays: unique symbol = Symbol.for('arrays');