htmv 0.0.32 → 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/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 -29
- 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 -44
|
@@ -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,35 +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 = isset(props[propName]);
|
|
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
|
}
|
|
38
|
-
function isset(prop) {
|
|
39
|
-
if (Array.isArray(prop)) {
|
|
40
|
-
return prop.length > 0;
|
|
41
|
-
}
|
|
42
|
-
return prop !== undefined && prop !== null;
|
|
43
|
-
}
|
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,50 +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 = isset(props[propName]);
|
|
48
|
-
|
|
49
|
-
if (isNegated ? !exists : exists) return innerContent;
|
|
50
|
-
return "";
|
|
51
|
-
},
|
|
52
|
-
);
|
|
53
|
-
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, {
|
|
54
24
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
55
25
|
});
|
|
56
26
|
}
|
|
57
|
-
|
|
58
|
-
function isset(prop: unknown) {
|
|
59
|
-
if (Array.isArray(prop)) {
|
|
60
|
-
return prop.length > 0;
|
|
61
|
-
}
|
|
62
|
-
return prop !== undefined && prop !== null;
|
|
63
|
-
}
|