htmv 0.0.32 → 0.0.34

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.
@@ -0,0 +1,3 @@
1
+ import type { RootNode } from "./renderer";
2
+ import type { Token } from "./tokenizer";
3
+ export declare function parse(tokens: Token[]): RootNode;
@@ -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,41 @@
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(resolvePropertyPath(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
+ function resolvePropertyPath(path) {
30
+ const [variable, ...properties] = path.split(".");
31
+ if (variable === undefined)
32
+ throw new Error("Missing variable name on interpolation");
33
+ let result = context[variable];
34
+ for (const property of properties) {
35
+ if (typeof result !== "object" || result === null)
36
+ throw new Error("Property access attempt on non-object.");
37
+ result = result[property];
38
+ }
39
+ return result;
40
+ }
41
+ }
@@ -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 replacedCode = code
13
- .replace(/<For\s+(\w+)\s+in\s+(\w+)>([\s\S]*?)<\/For>/g, (_, itemName, listName, innerContent) => {
14
- const list = props[listName];
15
- if (!Array.isArray(list))
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
@@ -2,7 +2,7 @@
2
2
  "name": "htmv",
3
3
  "main": "dist/index.js",
4
4
  "type": "module",
5
- "version": "0.0.32",
5
+ "version": "0.0.34",
6
6
  "devDependencies": {
7
7
  "@biomejs/biome": "2.3.3",
8
8
  "@types/bun": "latest"
@@ -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,81 @@
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(resolvePropertyPath(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
+
69
+ function resolvePropertyPath(path: string) {
70
+ const [variable, ...properties] = path.split(".");
71
+ if (variable === undefined)
72
+ throw new Error("Missing variable name on interpolation");
73
+ let result = context[variable];
74
+ for (const property of properties) {
75
+ if (typeof result !== "object" || result === null)
76
+ throw new Error("Property access attempt on non-object.");
77
+ result = (result as Record<string, unknown>)[property];
78
+ }
79
+ return result;
80
+ }
81
+ }
@@ -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 replacedCode = code
18
- .replace(
19
- /<For\s+(\w+)\s+in\s+(\w+)>([\s\S]*?)<\/For>/g,
20
- (_, itemName: string, listName: string, innerContent: string) => {
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
- }