htmv 0.0.31 → 0.0.33
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/README.md +5 -1
- package/dist/next-parser/parser.d.ts +3 -0
- package/dist/next-parser/parser.js +103 -0
- package/dist/next-parser/renderer.d.ts +26 -0
- package/dist/next-parser/renderer.js +29 -0
- package/dist/next-parser/tokenizer.d.ts +23 -0
- package/dist/next-parser/tokenizer.js +77 -0
- package/dist/views.js +7 -23
- package/package.json +1 -1
- package/src/next-parser/parser.ts +113 -0
- package/src/next-parser/renderer.ts +68 -0
- package/src/next-parser/tokenizer.ts +123 -0
- package/src/views.ts +7 -38
package/README.md
CHANGED
|
@@ -109,4 +109,8 @@ bunx htmv@latest gen view MyCoolView --path cool_stuff/my_custom_views_folder
|
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
# Hot reloading
|
|
112
|
-
Having to restart the server every time you make a change can be quite tedious. HTMV takes care of this thanks to Bun. Just develop with `bun dev` and it should work out of the box! Note that this does not include hot reloading in the browser. As of now, you have to refresh the page to see new changes. It doesn't update in real time.
|
|
112
|
+
Having to restart the server every time you make a change can be quite tedious. HTMV takes care of this thanks to Bun. Just develop with `bun dev` and it should work out of the box! Note that this does not include hot reloading in the browser. As of now, you have to refresh the page to see new changes. It doesn't update in real time.
|
|
113
|
+
|
|
114
|
+
# Still have questions?
|
|
115
|
+
How about asking the DeepWiki instead?
|
|
116
|
+
[](https://deepwiki.com/Fabrisdev/htmv)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export function parse(tokens) {
|
|
2
|
+
let i = 0;
|
|
3
|
+
const root = {
|
|
4
|
+
type: "root",
|
|
5
|
+
children: parseChildren(),
|
|
6
|
+
};
|
|
7
|
+
return root;
|
|
8
|
+
function parseChildren(untilTag) {
|
|
9
|
+
const nodes = [];
|
|
10
|
+
while (i < tokens.length) {
|
|
11
|
+
const token = tokens[i];
|
|
12
|
+
if (untilTag &&
|
|
13
|
+
token?.type === "close" &&
|
|
14
|
+
token.tag.toLocaleLowerCase() === untilTag) {
|
|
15
|
+
i++;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
if (token?.type === "text") {
|
|
19
|
+
nodes.push({
|
|
20
|
+
type: "text",
|
|
21
|
+
text: token.text,
|
|
22
|
+
});
|
|
23
|
+
i++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (token?.type === "interpolation") {
|
|
27
|
+
nodes.push({
|
|
28
|
+
type: "interpolation",
|
|
29
|
+
value: token.value,
|
|
30
|
+
});
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (token?.type === "open") {
|
|
35
|
+
const tag = token.tag.toLowerCase();
|
|
36
|
+
i++;
|
|
37
|
+
if (tag === "for") {
|
|
38
|
+
const nextToken = tokens[i]; //should be arguments token
|
|
39
|
+
i++;
|
|
40
|
+
if (nextToken?.type !== "arguments")
|
|
41
|
+
throw new Error("Missing arguments in for");
|
|
42
|
+
const [itemName, _in, listName] = nextToken.value;
|
|
43
|
+
if (itemName === undefined ||
|
|
44
|
+
_in === undefined ||
|
|
45
|
+
listName === undefined)
|
|
46
|
+
throw new Error("Incorrect amount of arguments in for. Expected 3 arguments");
|
|
47
|
+
if (_in !== "in")
|
|
48
|
+
throw new Error("Expected reserved word in.");
|
|
49
|
+
const children = parseChildren(tag);
|
|
50
|
+
nodes.push({
|
|
51
|
+
type: "for",
|
|
52
|
+
children,
|
|
53
|
+
itemName,
|
|
54
|
+
listName,
|
|
55
|
+
});
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (tag === "isset") {
|
|
59
|
+
const nextToken = tokens[i]; //should be arguments token
|
|
60
|
+
i++;
|
|
61
|
+
if (nextToken?.type !== "arguments")
|
|
62
|
+
throw new Error("Missing arguments in isset");
|
|
63
|
+
const [itemName] = nextToken.value;
|
|
64
|
+
if (itemName === undefined)
|
|
65
|
+
throw new Error("Expected item name");
|
|
66
|
+
const children = parseChildren(tag);
|
|
67
|
+
nodes.push({
|
|
68
|
+
type: "isset",
|
|
69
|
+
children,
|
|
70
|
+
itemName,
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const nextToken = tokens[i]; //may be arguments token
|
|
75
|
+
if (nextToken?.type === "arguments") {
|
|
76
|
+
const args = nextToken.value.join(" ");
|
|
77
|
+
nodes.push({
|
|
78
|
+
type: "text",
|
|
79
|
+
text: `<${tag} ${args}>`,
|
|
80
|
+
});
|
|
81
|
+
i++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
nodes.push({
|
|
85
|
+
type: "text",
|
|
86
|
+
text: `<${tag}>`,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (token?.type === "close") {
|
|
91
|
+
if (token.tag === "for" || token.tag === "isset")
|
|
92
|
+
continue;
|
|
93
|
+
const tag = token.tag;
|
|
94
|
+
nodes.push({
|
|
95
|
+
type: "text",
|
|
96
|
+
text: `</${tag}>`,
|
|
97
|
+
});
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return nodes;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type Node = RootNode | TextNode | InterpolationNode | IssetNode | ForNode;
|
|
2
|
+
export interface RootNode {
|
|
3
|
+
type: "root";
|
|
4
|
+
children: Node[];
|
|
5
|
+
}
|
|
6
|
+
interface TextNode {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
interface InterpolationNode {
|
|
11
|
+
type: "interpolation";
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
interface IssetNode {
|
|
15
|
+
type: "isset";
|
|
16
|
+
itemName: string;
|
|
17
|
+
children: Node[];
|
|
18
|
+
}
|
|
19
|
+
interface ForNode {
|
|
20
|
+
type: "for";
|
|
21
|
+
listName: string;
|
|
22
|
+
itemName: string;
|
|
23
|
+
children: Node[];
|
|
24
|
+
}
|
|
25
|
+
export declare function render(node: Node, context: Record<string, unknown>): string;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function render(node, context) {
|
|
2
|
+
if (node.type === "text") {
|
|
3
|
+
return node.text;
|
|
4
|
+
}
|
|
5
|
+
if (node.type === "for") {
|
|
6
|
+
const list = context[node.listName];
|
|
7
|
+
if (Array.isArray(list)) {
|
|
8
|
+
const output = list.map((item) => {
|
|
9
|
+
return node.children
|
|
10
|
+
.map((childrenNode) => render(childrenNode, { ...context, [node.itemName]: item }))
|
|
11
|
+
.join("");
|
|
12
|
+
});
|
|
13
|
+
return output.join("");
|
|
14
|
+
}
|
|
15
|
+
throw new Error("La lista pasada no es un array");
|
|
16
|
+
}
|
|
17
|
+
if (node.type === "interpolation") {
|
|
18
|
+
return String(context[node.value]);
|
|
19
|
+
}
|
|
20
|
+
if (node.type === "isset") {
|
|
21
|
+
if (context[node.itemName] !== undefined &&
|
|
22
|
+
context[node.itemName] !== null) {
|
|
23
|
+
return node.children.map((node) => render(node, context)).join("");
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
const output = node.children.map((node) => render(node, context));
|
|
28
|
+
return output.join("");
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type Token = TextToken | InterpolationToken | OpenToken | CloseToken | ArgumentsToken;
|
|
2
|
+
type TextToken = {
|
|
3
|
+
type: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
type InterpolationToken = {
|
|
7
|
+
type: "interpolation";
|
|
8
|
+
value: string;
|
|
9
|
+
};
|
|
10
|
+
type OpenToken = {
|
|
11
|
+
type: "open";
|
|
12
|
+
tag: string;
|
|
13
|
+
};
|
|
14
|
+
type CloseToken = {
|
|
15
|
+
type: "close";
|
|
16
|
+
tag: string;
|
|
17
|
+
};
|
|
18
|
+
type ArgumentsToken = {
|
|
19
|
+
type: "arguments";
|
|
20
|
+
value: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare function tokenize(input: string): Token[];
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function tokenize(input) {
|
|
2
|
+
const tokens = [];
|
|
3
|
+
let textBuffer = "";
|
|
4
|
+
for (let i = 0; i < input.length; i++) {
|
|
5
|
+
const char = input[i];
|
|
6
|
+
if (char === "{") {
|
|
7
|
+
pushTextBuffer();
|
|
8
|
+
const read = readTill("}", i, input);
|
|
9
|
+
if (!read.success)
|
|
10
|
+
throw new Error("Unable to find closing }");
|
|
11
|
+
const { value, finishingPosition } = read;
|
|
12
|
+
tokens.push({
|
|
13
|
+
type: "interpolation",
|
|
14
|
+
value,
|
|
15
|
+
});
|
|
16
|
+
i = finishingPosition;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (char === "<") {
|
|
20
|
+
pushTextBuffer();
|
|
21
|
+
const read = readTill(">", i, input);
|
|
22
|
+
if (!read.success)
|
|
23
|
+
throw new Error("Unable to find closing '>'");
|
|
24
|
+
const { value, finishingPosition } = read;
|
|
25
|
+
const values = value.trim().split(/\s+/);
|
|
26
|
+
const tag = values[0];
|
|
27
|
+
if (tag === undefined)
|
|
28
|
+
throw new Error("Unable to find tag name");
|
|
29
|
+
if (tag[0] === "/") {
|
|
30
|
+
const tagName = tag.substring(1);
|
|
31
|
+
tokens.push({
|
|
32
|
+
type: "close",
|
|
33
|
+
tag: tagName,
|
|
34
|
+
});
|
|
35
|
+
i = finishingPosition;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const args = values.slice(1);
|
|
39
|
+
tokens.push({
|
|
40
|
+
type: "open",
|
|
41
|
+
tag,
|
|
42
|
+
});
|
|
43
|
+
if (args.length > 0) {
|
|
44
|
+
tokens.push({
|
|
45
|
+
type: "arguments",
|
|
46
|
+
value: args,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
i = finishingPosition;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
textBuffer += char;
|
|
53
|
+
}
|
|
54
|
+
pushTextBuffer();
|
|
55
|
+
return tokens;
|
|
56
|
+
function pushTextBuffer() {
|
|
57
|
+
if (textBuffer.length > 0) {
|
|
58
|
+
tokens.push({
|
|
59
|
+
type: "text",
|
|
60
|
+
text: textBuffer,
|
|
61
|
+
});
|
|
62
|
+
textBuffer = "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function readTill(charToSearchFor, startingPosition, text) {
|
|
67
|
+
const finishingPosition = text.indexOf(charToSearchFor, startingPosition + 1);
|
|
68
|
+
if (finishingPosition === -1) {
|
|
69
|
+
return { success: false };
|
|
70
|
+
}
|
|
71
|
+
const substring = text.substring(startingPosition + 1, finishingPosition);
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
value: substring,
|
|
75
|
+
finishingPosition,
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/views.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parse } from "./next-parser/parser";
|
|
4
|
+
import { render } from "./next-parser/renderer";
|
|
5
|
+
import { tokenize } from "./next-parser/tokenizer";
|
|
3
6
|
let viewsPath = "";
|
|
4
7
|
export function setViewsPath(path) {
|
|
5
8
|
viewsPath = path;
|
|
@@ -9,29 +12,10 @@ export async function view(view, props) {
|
|
|
9
12
|
throw new Error("Views folder path not yet configured. Use `Htmv.setup` before rendering a view.");
|
|
10
13
|
const filePath = path.join(viewsPath, `${view}.html`);
|
|
11
14
|
const code = await fs.readFile(filePath, "utf-8");
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error(`${listName} on view ${view} is not an array. If you wish for the For to not do anything pass an empty array instead.`);
|
|
17
|
-
return list
|
|
18
|
-
.map((item) => innerContent.replace(new RegExp(`{${itemName}}`, "g"), String(item)))
|
|
19
|
-
.join("");
|
|
20
|
-
})
|
|
21
|
-
.replace(/{(\w+)}/g, (_, propName) => {
|
|
22
|
-
return props[propName];
|
|
23
|
-
})
|
|
24
|
-
.replace(/<Isset\s+(!?\w+)>([\s\S]*?)<\/Isset>/g, (_, propNameWithPrefix, innerContent) => {
|
|
25
|
-
const isNegated = propNameWithPrefix.startsWith("!");
|
|
26
|
-
const propName = isNegated
|
|
27
|
-
? propNameWithPrefix.slice(1)
|
|
28
|
-
: propNameWithPrefix;
|
|
29
|
-
const exists = props[propName] !== undefined && props[propName] !== null;
|
|
30
|
-
if (isNegated ? !exists : exists)
|
|
31
|
-
return innerContent;
|
|
32
|
-
return "";
|
|
33
|
-
});
|
|
34
|
-
return new Response(replacedCode, {
|
|
15
|
+
const tokens = tokenize(code);
|
|
16
|
+
const root = parse(tokens);
|
|
17
|
+
const rendered = render(root, props);
|
|
18
|
+
return new Response(rendered, {
|
|
35
19
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
36
20
|
});
|
|
37
21
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Node, RootNode } from "./renderer";
|
|
2
|
+
import type { Token } from "./tokenizer";
|
|
3
|
+
|
|
4
|
+
export function parse(tokens: Token[]) {
|
|
5
|
+
let i = 0;
|
|
6
|
+
|
|
7
|
+
const root: RootNode = {
|
|
8
|
+
type: "root",
|
|
9
|
+
children: parseChildren(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return root;
|
|
13
|
+
|
|
14
|
+
function parseChildren(untilTag?: string): Node[] {
|
|
15
|
+
const nodes: Node[] = [];
|
|
16
|
+
while (i < tokens.length) {
|
|
17
|
+
const token = tokens[i];
|
|
18
|
+
if (
|
|
19
|
+
untilTag &&
|
|
20
|
+
token?.type === "close" &&
|
|
21
|
+
token.tag.toLocaleLowerCase() === untilTag
|
|
22
|
+
) {
|
|
23
|
+
i++;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
if (token?.type === "text") {
|
|
27
|
+
nodes.push({
|
|
28
|
+
type: "text",
|
|
29
|
+
text: token.text,
|
|
30
|
+
});
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (token?.type === "interpolation") {
|
|
35
|
+
nodes.push({
|
|
36
|
+
type: "interpolation",
|
|
37
|
+
value: token.value,
|
|
38
|
+
});
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (token?.type === "open") {
|
|
43
|
+
const tag = token.tag.toLowerCase();
|
|
44
|
+
i++;
|
|
45
|
+
if (tag === "for") {
|
|
46
|
+
const nextToken = tokens[i]; //should be arguments token
|
|
47
|
+
i++;
|
|
48
|
+
if (nextToken?.type !== "arguments")
|
|
49
|
+
throw new Error("Missing arguments in for");
|
|
50
|
+
const [itemName, _in, listName] = nextToken.value;
|
|
51
|
+
if (
|
|
52
|
+
itemName === undefined ||
|
|
53
|
+
_in === undefined ||
|
|
54
|
+
listName === undefined
|
|
55
|
+
)
|
|
56
|
+
throw new Error(
|
|
57
|
+
"Incorrect amount of arguments in for. Expected 3 arguments",
|
|
58
|
+
);
|
|
59
|
+
if (_in !== "in") throw new Error("Expected reserved word in.");
|
|
60
|
+
|
|
61
|
+
const children = parseChildren(tag);
|
|
62
|
+
nodes.push({
|
|
63
|
+
type: "for",
|
|
64
|
+
children,
|
|
65
|
+
itemName,
|
|
66
|
+
listName,
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (tag === "isset") {
|
|
71
|
+
const nextToken = tokens[i]; //should be arguments token
|
|
72
|
+
i++;
|
|
73
|
+
if (nextToken?.type !== "arguments")
|
|
74
|
+
throw new Error("Missing arguments in isset");
|
|
75
|
+
const [itemName] = nextToken.value;
|
|
76
|
+
if (itemName === undefined) throw new Error("Expected item name");
|
|
77
|
+
const children = parseChildren(tag);
|
|
78
|
+
nodes.push({
|
|
79
|
+
type: "isset",
|
|
80
|
+
children,
|
|
81
|
+
itemName,
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const nextToken = tokens[i]; //may be arguments token
|
|
86
|
+
if (nextToken?.type === "arguments") {
|
|
87
|
+
const args = nextToken.value.join(" ");
|
|
88
|
+
nodes.push({
|
|
89
|
+
type: "text",
|
|
90
|
+
text: `<${tag} ${args}>`,
|
|
91
|
+
});
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
nodes.push({
|
|
96
|
+
type: "text",
|
|
97
|
+
text: `<${tag}>`,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (token?.type === "close") {
|
|
102
|
+
if (token.tag === "for" || token.tag === "isset") continue;
|
|
103
|
+
const tag = token.tag;
|
|
104
|
+
nodes.push({
|
|
105
|
+
type: "text",
|
|
106
|
+
text: `</${tag}>`,
|
|
107
|
+
});
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return nodes;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type Node =
|
|
2
|
+
| RootNode
|
|
3
|
+
| TextNode
|
|
4
|
+
| InterpolationNode
|
|
5
|
+
| IssetNode
|
|
6
|
+
| ForNode;
|
|
7
|
+
|
|
8
|
+
export interface RootNode {
|
|
9
|
+
type: "root";
|
|
10
|
+
children: Node[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TextNode {
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface InterpolationNode {
|
|
19
|
+
type: "interpolation";
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface IssetNode {
|
|
24
|
+
type: "isset";
|
|
25
|
+
itemName: string;
|
|
26
|
+
children: Node[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ForNode {
|
|
30
|
+
type: "for";
|
|
31
|
+
listName: string;
|
|
32
|
+
itemName: string;
|
|
33
|
+
children: Node[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function render(node: Node, context: Record<string, unknown>): string {
|
|
37
|
+
if (node.type === "text") {
|
|
38
|
+
return node.text;
|
|
39
|
+
}
|
|
40
|
+
if (node.type === "for") {
|
|
41
|
+
const list = context[node.listName];
|
|
42
|
+
if (Array.isArray(list)) {
|
|
43
|
+
const output = list.map((item) => {
|
|
44
|
+
return node.children
|
|
45
|
+
.map((childrenNode) =>
|
|
46
|
+
render(childrenNode, { ...context, [node.itemName]: item }),
|
|
47
|
+
)
|
|
48
|
+
.join("");
|
|
49
|
+
});
|
|
50
|
+
return output.join("");
|
|
51
|
+
}
|
|
52
|
+
throw new Error("La lista pasada no es un array");
|
|
53
|
+
}
|
|
54
|
+
if (node.type === "interpolation") {
|
|
55
|
+
return String(context[node.value]);
|
|
56
|
+
}
|
|
57
|
+
if (node.type === "isset") {
|
|
58
|
+
if (
|
|
59
|
+
context[node.itemName] !== undefined &&
|
|
60
|
+
context[node.itemName] !== null
|
|
61
|
+
) {
|
|
62
|
+
return node.children.map((node) => render(node, context)).join("");
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
const output = node.children.map((node) => render(node, context));
|
|
67
|
+
return output.join("");
|
|
68
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type Token =
|
|
2
|
+
| TextToken
|
|
3
|
+
| InterpolationToken
|
|
4
|
+
| OpenToken
|
|
5
|
+
| CloseToken
|
|
6
|
+
| ArgumentsToken;
|
|
7
|
+
|
|
8
|
+
type TextToken = {
|
|
9
|
+
type: "text";
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type InterpolationToken = {
|
|
14
|
+
type: "interpolation";
|
|
15
|
+
value: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type OpenToken = {
|
|
19
|
+
type: "open";
|
|
20
|
+
tag: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CloseToken = {
|
|
24
|
+
type: "close";
|
|
25
|
+
tag: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ArgumentsToken = {
|
|
29
|
+
type: "arguments";
|
|
30
|
+
value: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function tokenize(input: string): Token[] {
|
|
34
|
+
const tokens: Token[] = [];
|
|
35
|
+
let textBuffer = "";
|
|
36
|
+
for (let i = 0; i < input.length; i++) {
|
|
37
|
+
const char = input[i];
|
|
38
|
+
if (char === "{") {
|
|
39
|
+
pushTextBuffer();
|
|
40
|
+
const read = readTill("}", i, input);
|
|
41
|
+
if (!read.success) throw new Error("Unable to find closing }");
|
|
42
|
+
const { value, finishingPosition } = read;
|
|
43
|
+
tokens.push({
|
|
44
|
+
type: "interpolation",
|
|
45
|
+
value,
|
|
46
|
+
});
|
|
47
|
+
i = finishingPosition;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (char === "<") {
|
|
52
|
+
pushTextBuffer();
|
|
53
|
+
const read = readTill(">", i, input);
|
|
54
|
+
if (!read.success) throw new Error("Unable to find closing '>'");
|
|
55
|
+
const { value, finishingPosition } = read;
|
|
56
|
+
const values = value.trim().split(/\s+/);
|
|
57
|
+
const tag = values[0];
|
|
58
|
+
if (tag === undefined) throw new Error("Unable to find tag name");
|
|
59
|
+
if (tag[0] === "/") {
|
|
60
|
+
const tagName = tag.substring(1);
|
|
61
|
+
tokens.push({
|
|
62
|
+
type: "close",
|
|
63
|
+
tag: tagName,
|
|
64
|
+
});
|
|
65
|
+
i = finishingPosition;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const args = values.slice(1);
|
|
69
|
+
tokens.push({
|
|
70
|
+
type: "open",
|
|
71
|
+
tag,
|
|
72
|
+
});
|
|
73
|
+
if (args.length > 0) {
|
|
74
|
+
tokens.push({
|
|
75
|
+
type: "arguments",
|
|
76
|
+
value: args,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
i = finishingPosition;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
textBuffer += char;
|
|
85
|
+
}
|
|
86
|
+
pushTextBuffer();
|
|
87
|
+
return tokens;
|
|
88
|
+
|
|
89
|
+
function pushTextBuffer() {
|
|
90
|
+
if (textBuffer.length > 0) {
|
|
91
|
+
tokens.push({
|
|
92
|
+
type: "text",
|
|
93
|
+
text: textBuffer,
|
|
94
|
+
});
|
|
95
|
+
textBuffer = "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readTill(
|
|
101
|
+
charToSearchFor: string,
|
|
102
|
+
startingPosition: number,
|
|
103
|
+
text: string,
|
|
104
|
+
):
|
|
105
|
+
| {
|
|
106
|
+
success: true;
|
|
107
|
+
value: string;
|
|
108
|
+
finishingPosition: number;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
success: false;
|
|
112
|
+
} {
|
|
113
|
+
const finishingPosition = text.indexOf(charToSearchFor, startingPosition + 1);
|
|
114
|
+
if (finishingPosition === -1) {
|
|
115
|
+
return { success: false };
|
|
116
|
+
}
|
|
117
|
+
const substring = text.substring(startingPosition + 1, finishingPosition);
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
value: substring,
|
|
121
|
+
finishingPosition,
|
|
122
|
+
};
|
|
123
|
+
}
|
package/src/views.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parse } from "./next-parser/parser";
|
|
4
|
+
import { render } from "./next-parser/renderer";
|
|
5
|
+
import { tokenize } from "./next-parser/tokenizer";
|
|
3
6
|
|
|
4
7
|
let viewsPath = "";
|
|
5
8
|
|
|
@@ -14,44 +17,10 @@ export async function view(view: string, props: Record<string, unknown>) {
|
|
|
14
17
|
);
|
|
15
18
|
const filePath = path.join(viewsPath, `${view}.html`);
|
|
16
19
|
const code = await fs.readFile(filePath, "utf-8");
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const list = props[listName];
|
|
22
|
-
if (!Array.isArray(list))
|
|
23
|
-
throw new Error(
|
|
24
|
-
`${listName} on view ${view} is not an array. If you wish for the For to not do anything pass an empty array instead.`,
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
return list
|
|
28
|
-
.map((item) =>
|
|
29
|
-
innerContent.replace(
|
|
30
|
-
new RegExp(`{${itemName}}`, "g"),
|
|
31
|
-
String(item),
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
.join("");
|
|
35
|
-
},
|
|
36
|
-
)
|
|
37
|
-
.replace(/{(\w+)}/g, (_, propName) => {
|
|
38
|
-
return props[propName] as string;
|
|
39
|
-
})
|
|
40
|
-
.replace(
|
|
41
|
-
/<Isset\s+(!?\w+)>([\s\S]*?)<\/Isset>/g,
|
|
42
|
-
(_, propNameWithPrefix: string, innerContent: string) => {
|
|
43
|
-
const isNegated = propNameWithPrefix.startsWith("!");
|
|
44
|
-
const propName = isNegated
|
|
45
|
-
? propNameWithPrefix.slice(1)
|
|
46
|
-
: propNameWithPrefix;
|
|
47
|
-
const exists =
|
|
48
|
-
props[propName] !== undefined && props[propName] !== null;
|
|
49
|
-
|
|
50
|
-
if (isNegated ? !exists : exists) return innerContent;
|
|
51
|
-
return "";
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
return new Response(replacedCode, {
|
|
20
|
+
const tokens = tokenize(code);
|
|
21
|
+
const root = parse(tokens);
|
|
22
|
+
const rendered = render(root, props);
|
|
23
|
+
return new Response(rendered, {
|
|
55
24
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
56
25
|
});
|
|
57
26
|
}
|