-
Notifications
You must be signed in to change notification settings - Fork 0
/
ryuji.ts
287 lines (275 loc) · 11.5 KB
/
ryuji.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import { readFileSync } from "fs";
export const SYNTAX_REGEX = /\[\[ [a-zA-Z0-9.:/\*\-_!]+ \]\]/g;
export type file_extension = `.${string}`;
export interface ForLoopInfo {
index: number,
total: number,
current: number,
var_value: any, //value we are looping over
iter_var_name?: string,
index_var_name?: string,
}
export class Renderer {
templates_dir: string;
components_dir: string;
file_extension: file_extension;
constructor(templates_dir: string, components_dir: string, file_extension: file_extension=".html") {
this.templates_dir = templates_dir;
this.components_dir = components_dir;
this.file_extension = file_extension
}
static remove_empty_lines(text: string): string {
let lines: string[] = text.split("\n");
let new_lines: string[] = [];
for (let i=0; i < lines.length; i++) {
if (lines[i].trim() === "") continue;
new_lines.push(lines[i]);
}
return new_lines.join("\n");
}
static concat_path(path1: string, path2: string): string {
if (path1.endsWith("/") && path2.startsWith("/")) {
return `${path1.slice(0, path1.length-1)}${path2}`;
} else if ((!path1.endsWith("/") && path2.startsWith("/")) || (path1.endsWith("/") && !path2.startsWith("/"))) {
return `${path1}${path2}`;
} else if (!path1.endsWith("/") && !path2.startsWith("/")) {
return `${path1}/${path2}`;
}
}
static sanitize(non_html: string): string {
return non_html.replaceAll("<", "<").replaceAll(">", ">");
}
static check_var_name_legality(var_name: string, dot_allowed: boolean=true) {
//I try to avoid regex if I can
let legal_chars: string[] = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_", "."];
if (!dot_allowed) legal_chars.pop();
let legal_var_name: boolean = var_name.toLowerCase().split("").every((char) => legal_chars.includes(char));
if (!legal_var_name) throw Error(`Variable name "${var_name}" has illegal characters`);
}
static get_var(var_name: string, vars?: any): any { //probably a string but guarantee
if (typeof vars === "undefined") throw Error(`Variable "${var_name}" is undefined`);
Renderer.check_var_name_legality(var_name);
let splitted: string[] = var_name.split(".")
let var_value = vars;
for (let i=0; i < splitted.length; i++) {
var_value = var_value?.[splitted[i]];
}
if (typeof var_value === "undefined") throw Error(`Variable "${var_name}" is undefined`);
return var_value;
}
render(template_contents: string, vars?: any, recursion_layer: number=0): string {
let matches = [...template_contents.matchAll(SYNTAX_REGEX)];
if (matches.length === 0) {
return template_contents;
}
let _iterations: number = 0;
let rendered: string = template_contents.slice(0, matches[0].index);
let index: number = 0;
let for_loops: ForLoopInfo[] = [];
//let offset: number = 0; //I guess we don't need the offset
while (true) {
if (index === matches.length) break;
if (_iterations > 75000) console.log("Passed 75000 iterations while rendering, infinite loop?");
let match = matches[index];
//[[ content ]]
let exp_parts = match[0].slice(3, match[0].length-3).split(":");
if (exp_parts[0] === "component") {
//we do not want get into an infinite recursion loop with components referring to each other
if (recursion_layer > 5) throw Error("Components more than 5 layers deep, components may be referencing each other in infinite loop.");
if (typeof exp_parts[1] !== "string") throw Error("`component:` statement missing component file name afterwards");
let file_name: string = exp_parts[1];
if (!file_name.includes(".")) {
file_name += this.file_extension;
}
rendered += this.render_template(Renderer.concat_path(this.components_dir, file_name), vars, recursion_layer+1);
} else if (exp_parts[0] === "for") {
if (for_loops[for_loops.length-1]?.index === index) {
//for loop already exists, just continue and do nothing
} else {
//variables in for loops are not scoped because that would be too much work
if (typeof exp_parts[1] !== "string") throw Error("`for:` statement missing variable name to loop over afterwards");
let var_name: string = exp_parts[1];
let var_value = Renderer.get_var(var_name, vars);
//set iter variable (optional) (you know, the "post" in "for post in posts")
//(I don't know what the actual name of that thing is)
if (typeof exp_parts[2] === "string") {
let iter_var_name: string = exp_parts[2];
Renderer.check_var_name_legality(iter_var_name, false);
vars[iter_var_name] = var_value[0];
}
if (typeof exp_parts[3] === "string") {
//set index count
let index_var_name: string = exp_parts[3];
Renderer.check_var_name_legality(index_var_name, false);
vars[index_var_name] = 0;
}
if (typeof exp_parts[4] === "string") {
//set max count
let max_var_name: string = exp_parts[4];
Renderer.check_var_name_legality(max_var_name, false);
vars[max_var_name] = var_value.length-1;
}
//add to for loops
for_loops.push({
index,
total: var_value.length,
current: 0,
var_value,
iter_var_name: exp_parts[2],
index_var_name: exp_parts[3],
});
//make sure thing we are iterating over isn't empty
if (var_value.length === 0) {
//skip straight to the endfor
let sliced = matches.slice(index+1, matches.length);
let new_index: number;
let extra_fors: number = 0;
for (let i=0; i < sliced.length; i++) {
if (sliced[i][0].startsWith("[[ for:")) {
extra_fors++;
} else if (sliced[i][0] === "[[ endfor ]]") {
if (extra_fors === 0) {
new_index = i;
break;
}
extra_fors--;
}
}
if (typeof new_index === "undefined") throw Error("`for:` statement missing an `[[ endfor ]]`");
index += new_index+1;
continue;
}
}
} else if (exp_parts[0] === "endfor") {
//check if for loop is over, if not, go back to for
let current_loop: ForLoopInfo = for_loops[for_loops.length-1];
current_loop.current++;
if (current_loop.current >= current_loop.total) {
//for loop ended, onwards! oh yeah, also remove the current for loop info
for_loops.pop();
} else {
//update iter var
if (current_loop.iter_var_name) {
vars[current_loop.iter_var_name] = current_loop.var_value[current_loop.current];
}
if (current_loop.index_var_name) {
vars[current_loop.index_var_name] = current_loop.current;
}
//go back to start of for loop index
index = current_loop.index;
continue;
}
} else if (exp_parts[0] === "if") {
if (typeof exp_parts[1] !== "string") throw Error("`if:` statement missing variable name afterwards");
let var_name: string = exp_parts[1];
let var_value = Renderer.get_var(var_name, vars);
let condition_pass: boolean;
if (typeof exp_parts[2] !== "string") {
//make sure var is truthy
if (var_value) {
condition_pass = true;
} else {
condition_pass = false;
}
} else {
//compare with second var
let var_name2: string = exp_parts[2];
let if_in: boolean = false;
let if_not: boolean = false;
//*! is valid
if (var_name2.startsWith("*")) {
var_name2 = var_name2.slice(1, var_name2.length);
if_in = true;
}
if (var_name2.startsWith("!")) {
var_name2 = var_name2.slice(1, var_name2.length);
if_not = true;
}
let var_value2 = Renderer.get_var(var_name2, vars);
if (if_in) {
var_value2 = var_value2.find((ele) => ele === var_value);
}
if (if_not) {
//make sure the two compared variables are NOT equal
if (var_value !== var_value2) {
condition_pass = true;
} else {
condition_pass = false;
}
} else {
//regular comparison statement
if (var_value === var_value2) {
condition_pass = true;
} else {
condition_pass = false;
}
}
}
if (!condition_pass) { //failed condition
//skip to the endif
let sliced = matches.slice(index+1, matches.length);
let new_index: number;
let extra_ifs: number = 0;
for (let i=0; i < sliced.length; i++) {
if (sliced[i][0].startsWith("[[ if:")) {
extra_ifs++;
} else if (sliced[i][0] === "[[ endif ]]") {
if (extra_ifs === 0) {
new_index = i;
break;
}
extra_ifs--;
}
}
if (typeof new_index === "undefined") throw Error("`if:` statement missing an `[[ endif ]]`");
index += new_index+1;
continue;
}
} else if (exp_parts[0] === "endif") {
//yup, nothing here
} else { //html:<variable name> or <variable name>
//variable
let var_name: string;
if (exp_parts[0] === "html") {
if (typeof exp_parts[1] !== "string") throw Error("`html:` statement missing variable name afterwards");
var_name = exp_parts[1];
} else {
var_name = exp_parts[0];
}
//convert to string
let var_value: string = String(Renderer.get_var(var_name, vars));
//add indentation
let current_lines: string[] = rendered.split("\n");
let current_last: string = current_lines[current_lines.length-1];
let indentation: number = 0;
for (let i=0; i < current_last.length; i++) {
if (current_last[i] !== " ") break;
indentation++;
}
let var_lines: string[] = var_value.split("\n");
let var_first: string = var_lines.shift();
//append spaces
var_value = var_lines.length === 0 ? var_first : var_first+"\n"+var_lines.map((var_line) => " ".repeat(indentation)+var_line).join("\n");
if (exp_parts[0] === "html") {
//variable but not sanitized
rendered += var_value;
} else {
rendered += Renderer.sanitize(var_value);
}
}
//add the html that comes after this, up until the next template syntax match thing
rendered += template_contents.slice(match.index+match[0].length, matches[index+1]?.index ? matches[index+1].index : template_contents.length);
index++;
_iterations++;
}
return rendered;
}
render_template(template_name: string, vars?: any, recursion_layer: number=0): string {
if (!template_name.includes(".")) {
template_name += this.file_extension;
}
let path_to_template: string = Renderer.concat_path(this.templates_dir, template_name);
const template_contents: string = readFileSync(path_to_template, "utf-8");
return this.render(template_contents, vars, recursion_layer);
}
}