@ijo-elaja/rev.js 0.4.0 → 0.5.1

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 CHANGED
@@ -23,6 +23,7 @@ A highly opionionated, lightweight, frontend<sup>1</sup> only solution.
23
23
  - Slugs (dynamic data based routing)
24
24
  - Components
25
25
  - Inline JavaScript
26
+ - Client-Side Conditional Rendering
26
27
 
27
28
  ### Planned/Unplanned
28
29
 
@@ -92,7 +93,9 @@ new Rev({
92
93
  // this is 100% optional, but can be useful
93
94
  // when you want more functionality than just
94
95
  // what rev.js provides by default
95
- elysia: (app) => app
96
+ // note that typescript will complain without
97
+ // the `as any` if you actually make changes
98
+ elysia: (app) => app as any
96
99
  });
97
100
  ```
98
101
 
package/index.ts CHANGED
@@ -109,12 +109,19 @@ enum EvalstMode {
109
109
  LAYOUT,
110
110
  PAGE,
111
111
  SCRIPT,
112
+ CLIENT_CONDITIONAL,
113
+ }
114
+
115
+ interface ConditionalBranch {
116
+ condition: string | null;
117
+ content: string;
112
118
  }
113
119
 
114
120
  interface EvalstElement {
115
121
  match: string;
116
122
  mode: EvalstMode;
117
123
  content: string;
124
+ branches?: ConditionalBranch[];
118
125
  }
119
126
 
120
127
  interface EvalsElement {
@@ -124,6 +131,100 @@ interface EvalsElement {
124
131
  isJs: boolean;
125
132
  }
126
133
 
134
+ const parseConditionalBlock = (
135
+ content: string,
136
+ startPos: number,
137
+ ): { branches: ConditionalBranch[]; endPos: number; match: string } => {
138
+ const ifMatch = content
139
+ .slice(startPos)
140
+ .match(/^\{\{#\s*if\s+(.+?)\s*#\}\}/);
141
+ if (!ifMatch) throw new Error("Expected {{# if condition #}}");
142
+
143
+ const branches: ConditionalBranch[] = [];
144
+ const fullMatch = [ifMatch[0]];
145
+ let condition: string | null = ifMatch[1].replaceAll("~", "__EVAL_STATE__");
146
+ let currentPos = startPos + ifMatch[0].length;
147
+ let branchContent = "";
148
+
149
+ while (currentPos < content.length) {
150
+ const remaining = content.slice(currentPos);
151
+
152
+ const elseIfMatch = remaining.match(
153
+ /^\{\{#\s*else\s+if\s+(.+?)\s*#\}\}/,
154
+ );
155
+ if (elseIfMatch) {
156
+ branches.push({ condition, content: branchContent });
157
+ fullMatch.push(elseIfMatch[0]);
158
+ condition = elseIfMatch[1].replaceAll("~", "__EVAL_STATE__");
159
+ currentPos += elseIfMatch[0].length;
160
+ branchContent = "";
161
+ continue;
162
+ }
163
+
164
+ const elseMatch = remaining.match(/^\{\{#\s*else\s*#\}\}/);
165
+ if (elseMatch) {
166
+ branches.push({ condition, content: branchContent });
167
+ fullMatch.push(elseMatch[0]);
168
+ condition = null;
169
+ currentPos += elseMatch[0].length;
170
+ branchContent = "";
171
+ continue;
172
+ }
173
+
174
+ const endifMatch = remaining.match(/^\{\{#\s*endif\s*#\}\}/);
175
+ if (endifMatch) {
176
+ branches.push({ condition, content: branchContent });
177
+ fullMatch.push(endifMatch[0]);
178
+ currentPos += endifMatch[0].length;
179
+ return {
180
+ branches,
181
+ endPos: currentPos,
182
+ match: fullMatch.join(""),
183
+ };
184
+ }
185
+
186
+ branchContent += remaining[0];
187
+ currentPos++;
188
+ }
189
+
190
+ throw new Error("Unclosed {{# if #}} block");
191
+ };
192
+
193
+ const generateConditionalHTML = (
194
+ branches: ConditionalBranch[],
195
+ containerId: string,
196
+ state: Record<string, any>,
197
+ ): string => {
198
+ let html = `<div id="${containerId}">`;
199
+
200
+ branches.forEach((branch, idx) => {
201
+ html += `<div data-conditional-branch="${idx}" data-condition="${branch.condition || "else"}" style="display: none;">${branch.content}</div>`;
202
+ });
203
+
204
+ html += `</div>`;
205
+
206
+ let conditionScript = "";
207
+ for (let i = 0; i < branches.length; i++) {
208
+ const branch = branches[i];
209
+ if (i === 0) {
210
+ conditionScript += `if (${branch.condition}) { document.querySelector('[data-conditional-branch="${i}"]').style.display = "block"; }`;
211
+ } else if (branch.condition !== null) {
212
+ conditionScript += ` else if (${branch.condition}) { document.querySelector('[data-conditional-branch="${i}"]').style.display = "block"; }`;
213
+ } else {
214
+ conditionScript += ` else { document.querySelector('[data-conditional-branch="${i}"]').style.display = "block"; }`;
215
+ }
216
+ }
217
+
218
+ html += `<script>
219
+ (function() {
220
+ let __EVAL_STATE__ = ${JSON.stringify(state)};
221
+ ${conditionScript}
222
+ })();
223
+ </script>`;
224
+
225
+ return html;
226
+ };
227
+
127
228
  const evaluate = async (
128
229
  str: string,
129
230
  initialState?: Record<string, any>,
@@ -131,11 +232,12 @@ const evaluate = async (
131
232
  ): Promise<string> => {
132
233
  let processedPageContent = str;
133
234
  const evalst: EvalstElement[] = [];
235
+ let __EVAL_STATE__ = initialState || {};
236
+
134
237
  const regex = /{{ ?(.*?)? ?}}/gims;
135
238
  let m;
136
- let __EVAL_STATE__ = initialState || {};
137
239
 
138
- while ((m = regex.exec(str)) !== null) {
240
+ while ((m = regex.exec(processedPageContent)) !== null) {
139
241
  if (m.index === regex.lastIndex) {
140
242
  regex.lastIndex++;
141
243
  }
@@ -195,7 +297,7 @@ const evaluate = async (
195
297
  const code = `let __EVAL_STATE__=${JSON.stringify(
196
298
  __EVAL_STATE__,
197
299
  )};${toEval}`;
198
- const data = await Bun.$`bun -e '${code}'`.json();
300
+ const data = await Bun.$`bun -e ${code}`.json();
199
301
  // copy result into __EVAL_STATE__
200
302
  Object.assign(__EVAL_STATE__, data);
201
303
  evalst.push({
@@ -239,6 +341,30 @@ const evaluate = async (
239
341
  );
240
342
  });
241
343
 
344
+ let conditionalIndex = 0;
345
+ let pos = 0;
346
+ while (pos < processedPageContent.length) {
347
+ const ifMatch = processedPageContent.slice(pos).match(/\{\{#\s*if\s+/);
348
+ if (!ifMatch || ifMatch.index === undefined) break;
349
+
350
+ const ifPos = pos + ifMatch.index;
351
+ const parsed = parseConditionalBlock(processedPageContent, ifPos);
352
+
353
+ const conditionalHTML = generateConditionalHTML(
354
+ parsed.branches,
355
+ `cond-${conditionalIndex}`,
356
+ __EVAL_STATE__,
357
+ );
358
+
359
+ processedPageContent = processedPageContent.replace(
360
+ parsed.match,
361
+ conditionalHTML,
362
+ );
363
+
364
+ pos = ifPos + conditionalHTML.length;
365
+ conditionalIndex++;
366
+ }
367
+
242
368
  return processedPageContent;
243
369
  };
244
370
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ijo-elaja/rev.js",
3
3
  "module": "index.ts",
4
- "version": "0.4.0",
4
+ "version": "0.5.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -1,3 +0,0 @@
1
- <meta charset="UTF-8" />
2
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3
- <title>Title</title>
@@ -1,9 +0,0 @@
1
- // actually just a basic rev.js app
2
-
3
- import Rev from "../..";
4
-
5
- new Rev({
6
- port: 8080,
7
- showDebug: true,
8
- rootDir: __dirname,
9
- });
@@ -1 +0,0 @@
1
- <i style="color: red !important">404 Not Found</i>
@@ -1,17 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <!-- this is a component (very useful for sharing heads between pages) -->
5
- {{ %Head% }}
6
- </head>
7
- <body>
8
- <header>
9
- <section>
10
- <h1><a href="/">test site</a></h1>
11
- <small class="muted">subheadings are a thing</small>
12
- </section>
13
- </header>
14
- <!-- this is a built-in component -->
15
- <main>{{ %Outlet% }}</main>
16
- </body>
17
- </html>
@@ -1,3 +0,0 @@
1
- content would go here
2
- <br />
3
- <a href="/whatever/wow!">oh boy, i wonder where this goes?</a>
@@ -1 +0,0 @@
1
- [slug]
@@ -1,16 +0,0 @@
1
- rev.js supports slugs!
2
-
3
- <br />
4
- <br />
5
-
6
- <b>{{ < decodeURIComponent(~.slug) > }}</b>
7
-
8
- <br />
9
- <br />
10
-
11
- <a href="./{{ < ~.slug + ' ' + ~.slug > }}">click me (trust)</a>
12
-
13
- <br />
14
- <br />
15
-
16
- only one per folder in the fs for now