htmv 0.0.55 → 0.0.56
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/compiler/parser.js +52 -0
- package/dist/compiler/renderer.d.ts +7 -1
- package/dist/compiler/renderer.js +9 -0
- package/dist/index.js +2 -0
- package/dist/views-registry.d.ts +3 -0
- package/dist/views-registry.js +31 -0
- package/dist/views.d.ts +2 -1
- package/dist/views.js +6 -4
- package/package.json +1 -1
package/dist/compiler/parser.js
CHANGED
|
@@ -74,6 +74,34 @@ export function parse(tokens) {
|
|
|
74
74
|
});
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
|
+
const pathName = token.tag.split("/").at(-1);
|
|
78
|
+
if (pathName !== undefined && startsWithUppercase(pathName)) {
|
|
79
|
+
// component
|
|
80
|
+
const rawArgs = [];
|
|
81
|
+
let nextToken = tokens[i];
|
|
82
|
+
while (nextToken?.type === "arguments") {
|
|
83
|
+
rawArgs.push(nextToken.value);
|
|
84
|
+
i++;
|
|
85
|
+
nextToken = tokens[i];
|
|
86
|
+
}
|
|
87
|
+
if (rawArgs.at(-1) === "/") {
|
|
88
|
+
// is self closing
|
|
89
|
+
nodes.push({
|
|
90
|
+
type: "component",
|
|
91
|
+
path: token.tag,
|
|
92
|
+
props: rawArgsToProps(rawArgs),
|
|
93
|
+
children: [],
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
nodes.push({
|
|
98
|
+
type: "component",
|
|
99
|
+
path: token.tag,
|
|
100
|
+
props: rawArgsToProps(rawArgs.slice(0, -1)),
|
|
101
|
+
children: parseChildren(tag),
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
77
105
|
nodes.push({
|
|
78
106
|
type: "text",
|
|
79
107
|
text: `<${tag} `,
|
|
@@ -155,3 +183,27 @@ export function parse(tokens) {
|
|
|
155
183
|
}
|
|
156
184
|
}
|
|
157
185
|
}
|
|
186
|
+
function startsWithUppercase(text) {
|
|
187
|
+
return text[0] === text[0]?.toUpperCase();
|
|
188
|
+
}
|
|
189
|
+
function rawArgsToProps(rawArgs) {
|
|
190
|
+
const props = {};
|
|
191
|
+
for (const arg of rawArgs) {
|
|
192
|
+
if (arg === "/")
|
|
193
|
+
continue;
|
|
194
|
+
const eqIndex = arg.indexOf("=");
|
|
195
|
+
if (eqIndex === -1) {
|
|
196
|
+
// boolean prop
|
|
197
|
+
props[arg] = true;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const key = arg.slice(0, eqIndex);
|
|
201
|
+
let value = arg.slice(eqIndex + 1);
|
|
202
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
203
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
204
|
+
value = value.slice(1, -1);
|
|
205
|
+
}
|
|
206
|
+
props[key] = value;
|
|
207
|
+
}
|
|
208
|
+
return props;
|
|
209
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export type Node = RootNode | TextNode | InterpolationNode | IssetNode | ForNode | AttributeBindingNode;
|
|
1
|
+
export type Node = RootNode | TextNode | InterpolationNode | IssetNode | ForNode | AttributeBindingNode | ComponentNode;
|
|
2
|
+
export interface ComponentNode {
|
|
3
|
+
type: "component";
|
|
4
|
+
path: string;
|
|
5
|
+
props: Record<string, unknown>;
|
|
6
|
+
children: Node[];
|
|
7
|
+
}
|
|
2
8
|
export interface RootNode {
|
|
3
9
|
type: "root";
|
|
4
10
|
children: Node[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { view } from "../views";
|
|
1
2
|
export function render(node, context) {
|
|
2
3
|
if (node.type === "attr-binding") {
|
|
3
4
|
const prop = resolvePropertyPath(node.expr);
|
|
@@ -32,6 +33,14 @@ export function render(node, context) {
|
|
|
32
33
|
}
|
|
33
34
|
return "";
|
|
34
35
|
}
|
|
36
|
+
if (node.type === "component") {
|
|
37
|
+
const renderedView = view(node.path, {
|
|
38
|
+
...node.props,
|
|
39
|
+
children: node.children.map((node) => render(node, context)).join(""),
|
|
40
|
+
...context,
|
|
41
|
+
}).body;
|
|
42
|
+
return renderedView ?? "";
|
|
43
|
+
}
|
|
35
44
|
const output = node.children.map((node) => render(node, context));
|
|
36
45
|
return output.join("");
|
|
37
46
|
function resolvePropertyPath(path) {
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createApp } from "./app";
|
|
2
2
|
import { registerRoutes } from "./routing/routing";
|
|
3
3
|
import { setViewsPath } from "./views";
|
|
4
|
+
import { registerViews } from "./views-registry";
|
|
4
5
|
export { view } from "./views";
|
|
5
6
|
export async function setup(paths) {
|
|
6
7
|
setViewsPath(paths.views);
|
|
8
|
+
await registerViews();
|
|
7
9
|
const app = createApp(paths.public);
|
|
8
10
|
await registerRoutes(app, paths.routes);
|
|
9
11
|
app.listen(paths.port);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { viewsPath } from "./views";
|
|
4
|
+
export const viewRegistry = {};
|
|
5
|
+
export function addToViewRegistry(name, code) {
|
|
6
|
+
viewRegistry[name] = code;
|
|
7
|
+
}
|
|
8
|
+
export async function registerViews() {
|
|
9
|
+
const files = await deepReadDir(viewsPath);
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
if (file.endsWith(".html")) {
|
|
12
|
+
const name = file.slice(0, -".html".length);
|
|
13
|
+
const code = await fs.readFile(name, "utf-8");
|
|
14
|
+
addToViewRegistry(name, code);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function deepReadDir(dir) {
|
|
19
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
20
|
+
const files = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = path.join(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
files.push(...(await deepReadDir(fullPath)));
|
|
25
|
+
}
|
|
26
|
+
else if (entry.isFile()) {
|
|
27
|
+
files.push(fullPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return files;
|
|
31
|
+
}
|
package/dist/views.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { HttpResponse } from "./http/response";
|
|
2
|
+
export declare let viewsPath: string;
|
|
2
3
|
export declare function setViewsPath(path: string): void;
|
|
3
|
-
export declare function view(view: string, props?: Record<string, unknown>):
|
|
4
|
+
export declare function view(view: string, props?: Record<string, unknown>): HttpResponse;
|
package/dist/views.js
CHANGED
|
@@ -3,15 +3,17 @@ import path from "node:path";
|
|
|
3
3
|
import { parse } from "./compiler/parser";
|
|
4
4
|
import { render } from "./compiler/renderer";
|
|
5
5
|
import { tokenize } from "./compiler/tokenizer";
|
|
6
|
-
|
|
6
|
+
import { viewRegistry } from "./views-registry";
|
|
7
|
+
export let viewsPath = "";
|
|
7
8
|
export function setViewsPath(path) {
|
|
8
9
|
viewsPath = path;
|
|
9
10
|
}
|
|
10
|
-
export
|
|
11
|
+
export function view(view, props) {
|
|
11
12
|
if (viewsPath === "")
|
|
12
13
|
throw new Error("Views folder path not yet configured. Use `Htmv.setup` before rendering a view.");
|
|
13
|
-
const
|
|
14
|
-
|
|
14
|
+
const code = viewRegistry[view];
|
|
15
|
+
if (code === undefined)
|
|
16
|
+
throw new Error(`View ${view} not found. Did you use the correct name?`);
|
|
15
17
|
const tokens = tokenize(code);
|
|
16
18
|
const root = parse(tokens);
|
|
17
19
|
const rendered = render(root, props ? props : {});
|