round-core 0.1.2 → 0.1.3

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,525 @@
1
+ // Transformer for .round files
2
+ // Handles custom syntax like:
3
+ // {if(cond){ ... }} -> {cond ? (...) : null}
4
+ // if(cond){ ... } (bare in JSX) -> {cond ? (...) : null}
5
+ // {for(item in list){ ... }} -> {list.map(item => (...))}
6
+ // {switch(cond) { case ... }} -> {function() { switch ... }}
7
+
8
+ export function transform(code, initialDepth = 0) {
9
+ let result = '';
10
+ let i = 0;
11
+ let jsxDepth = initialDepth;
12
+
13
+ // --- Helpers ---
14
+
15
+ function parseBlock(str, startIndex) {
16
+ let open = 0;
17
+ let startBlockIndex = -1;
18
+
19
+ let inSingle = false, inDouble = false, inTemplate = false;
20
+ let inCommentLine = false, inCommentMulti = false;
21
+
22
+ for (let j = startIndex; j < str.length; j++) {
23
+ const ch = str[j];
24
+ const prev = j > 0 ? str[j - 1] : '';
25
+ const next = j < str.length - 1 ? str[j + 1] : '';
26
+
27
+ if (inCommentLine) {
28
+ if (ch === '\n' || ch === '\r') inCommentLine = false;
29
+ continue;
30
+ }
31
+ if (inCommentMulti) {
32
+ if (ch === '*' && next === '/') { inCommentMulti = false; j++; }
33
+ continue;
34
+ }
35
+ if (inTemplate) {
36
+ if (ch === '`' && prev !== '\\') inTemplate = false;
37
+ continue;
38
+ }
39
+ if (inSingle) {
40
+ if (ch === '\'' && prev !== '\\') inSingle = false;
41
+ continue;
42
+ }
43
+ if (inDouble) {
44
+ if (ch === '"' && prev !== '\\') inDouble = false;
45
+ continue;
46
+ }
47
+
48
+ if (ch === '/' && next === '/') { inCommentLine = true; j++; continue; }
49
+ if (ch === '/' && next === '*') { inCommentMulti = true; j++; continue; }
50
+ if (ch === '`') { inTemplate = true; continue; }
51
+ if (ch === '\'') { inSingle = true; continue; }
52
+ if (ch === '"') { inDouble = true; continue; }
53
+
54
+ if (ch === '{') {
55
+ if (open === 0) startBlockIndex = j;
56
+ open++;
57
+ } else if (ch === '}') {
58
+ open--;
59
+ if (open === 0) {
60
+ return { start: startBlockIndex, end: j };
61
+ }
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function consumeWhitespace(str, idx) {
68
+ while (idx < str.length && /\s/.test(str[idx])) idx++;
69
+ return idx;
70
+ }
71
+
72
+ function extractCondition(str, startIndex) {
73
+ if (str[startIndex] !== '(') return null;
74
+ let depth = 1;
75
+ let j = startIndex + 1;
76
+ let inSingle = false, inDouble = false, inTemplate = false;
77
+
78
+ while (j < str.length && depth > 0) {
79
+ const ch = str[j], prev = str[j - 1] || '';
80
+ if (!inDouble && !inTemplate && ch === '\'' && prev !== '\\') inSingle = !inSingle;
81
+ else if (!inSingle && !inTemplate && ch === '"' && prev !== '\\') inDouble = !inDouble;
82
+ else if (!inSingle && !inDouble && ch === '`' && prev !== '\\') inTemplate = !inTemplate;
83
+
84
+ if (!inSingle && !inDouble && !inTemplate) {
85
+ if (ch === '(') depth++;
86
+ else if (ch === ')') depth--;
87
+ }
88
+ j++;
89
+ }
90
+ if (depth !== 0) return null;
91
+ return { cond: str.substring(startIndex + 1, j - 1), end: j };
92
+ }
93
+
94
+ // --- Control Flow Handlers ---
95
+
96
+ function handleIf(currI, isBare = false) {
97
+ // If bare, currI is at 'i' of 'if'. If not bare, currI is at '{'.
98
+ let startPtr = currI;
99
+ if (!isBare) {
100
+ startPtr = consumeWhitespace(code, currI + 1);
101
+ }
102
+
103
+ // Strict verification
104
+ if (!code.startsWith('if', startPtr)) return null;
105
+
106
+ let ptr = startPtr + 2;
107
+ ptr = consumeWhitespace(code, ptr);
108
+ if (code[ptr] !== '(') return null;
109
+
110
+ const cases = [];
111
+ let elseContent = null;
112
+ let currentPtr = ptr;
113
+ let first = true;
114
+
115
+ while (true) {
116
+ if (!first) {
117
+ if (!code.startsWith('if', currentPtr)) break;
118
+ currentPtr += 2;
119
+ currentPtr = consumeWhitespace(code, currentPtr);
120
+ }
121
+ first = false;
122
+
123
+ const condRes = extractCondition(code, currentPtr);
124
+ if (!condRes) return null;
125
+
126
+ currentPtr = consumeWhitespace(code, condRes.end);
127
+ if (code[currentPtr] !== '{') return null;
128
+
129
+ const block = parseBlock(code, currentPtr);
130
+ if (!block) return null;
131
+
132
+ const rawContent = code.substring(block.start + 1, block.end);
133
+ // RECURSIVE: content wrapped in fragment, so depth=1
134
+ const transformedContent = transform(rawContent, 1);
135
+
136
+ cases.push({ cond: condRes.cond, content: transformedContent });
137
+
138
+ currentPtr = block.end + 1;
139
+ currentPtr = consumeWhitespace(code, currentPtr);
140
+
141
+ if (code.startsWith('else', currentPtr)) {
142
+ currentPtr += 4;
143
+ currentPtr = consumeWhitespace(code, currentPtr);
144
+ if (code.startsWith('if', currentPtr)) {
145
+ continue;
146
+ } else if (code[currentPtr] === '{') {
147
+ const elseBlock = parseBlock(code, currentPtr);
148
+ if (!elseBlock) return null;
149
+ const rawElse = code.substring(elseBlock.start + 1, elseBlock.end);
150
+ elseContent = transform(rawElse, 1);
151
+ currentPtr = elseBlock.end + 1;
152
+ break;
153
+ } else {
154
+ return null;
155
+ }
156
+ } else {
157
+ break;
158
+ }
159
+ }
160
+
161
+ // If not bare, consume closing '}'. If bare, we are done.
162
+ let endIdx = currentPtr;
163
+ if (!isBare) {
164
+ endIdx = consumeWhitespace(code, endIdx);
165
+ if (code[endIdx] !== '}') return null;
166
+ endIdx++;
167
+ }
168
+
169
+ let expr = '';
170
+ for (let idx = 0; idx < cases.length; idx++) {
171
+ const c = cases[idx];
172
+ let cond = c.cond.trim();
173
+ const isSimplePath = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(cond);
174
+ if (isSimplePath && !cond.endsWith(')')) {
175
+ cond = `((typeof (${cond}) === 'function' && typeof (${cond}).peek === 'function' && ('value' in (${cond}))) ? (${cond})() : (${cond}))`;
176
+ }
177
+ const body = `<Fragment>${c.content}</Fragment>`;
178
+ expr += `(${cond}) ? (${body}) : `;
179
+ }
180
+ expr += elseContent ? `(<Fragment>${elseContent}</Fragment>)` : 'null';
181
+
182
+ // Always wrap in Thunk `{(() => ...)}`
183
+ return { end: endIdx, replacement: `{(() => ${expr})}` };
184
+ }
185
+
186
+ function handleFor(currI, isBare = false) {
187
+ let ptr = currI;
188
+ if (!isBare) ptr = consumeWhitespace(code, currI + 1);
189
+
190
+ if (!code.startsWith('for', ptr)) return null;
191
+ ptr += 3;
192
+ ptr = consumeWhitespace(code, ptr);
193
+
194
+ const condRes = extractCondition(code, ptr);
195
+ if (!condRes) return null;
196
+
197
+ const forCond = condRes.cond;
198
+ const inMatch = forCond.match(/^\s*(\S+)\s+in\s+(.+)$/);
199
+ if (!inMatch) return null;
200
+
201
+ const item = inMatch[1].trim();
202
+ const list = inMatch[2].trim();
203
+
204
+ ptr = consumeWhitespace(code, condRes.end);
205
+ if (code[ptr] !== '{') return null;
206
+
207
+ const block = parseBlock(code, ptr);
208
+ if (!block) return null;
209
+
210
+ const rawContent = code.substring(block.start + 1, block.end);
211
+ const transformedContent = transform(rawContent, 1);
212
+
213
+ let endIdx = block.end + 1;
214
+ if (!isBare) {
215
+ endIdx = consumeWhitespace(code, endIdx);
216
+ if (code[endIdx] !== '}') return null;
217
+ endIdx++;
218
+ }
219
+
220
+ const replacement = `{(() => ${list}.map(${item} => (<Fragment>${transformedContent}</Fragment>)))}`;
221
+ return { end: endIdx, replacement };
222
+ }
223
+
224
+ function handleSwitch(currI, isBare = false) {
225
+ let ptr = currI;
226
+ if (!isBare) ptr = consumeWhitespace(code, currI + 1);
227
+
228
+ if (!code.startsWith('switch', ptr)) return null;
229
+ ptr += 6;
230
+ ptr = consumeWhitespace(code, ptr);
231
+
232
+ const condRes = extractCondition(code, ptr);
233
+ if (!condRes) return null;
234
+ const cond = condRes.cond;
235
+
236
+ ptr = consumeWhitespace(code, condRes.end);
237
+ if (code[ptr] !== '{') return null;
238
+
239
+ const block = parseBlock(code, ptr);
240
+ if (!block) return null;
241
+
242
+ const rawContent = code.substring(block.start + 1, block.end);
243
+ const transformedInner = transform(rawContent, 0);
244
+
245
+ const finalContent = transformedInner.replace(/(case\s+.*?:|default:)([\s\S]*?)(?=case\s+.*?:|default:|$)/g, (m, label, body) => {
246
+ const trimmed = body.trim();
247
+ if (!trimmed) return m;
248
+ if (trimmed.startsWith('return ')) return m;
249
+ return `${label} return (<Fragment>${body}</Fragment>);`;
250
+ });
251
+
252
+ let endIdx = block.end + 1;
253
+ if (!isBare) {
254
+ endIdx = consumeWhitespace(code, endIdx);
255
+ if (code[endIdx] !== '}') return null;
256
+ endIdx++;
257
+ }
258
+
259
+ // Fix Reactivity: Return a function (Thunk) instead of IIFE result
260
+ // { function() { ... } }
261
+ const replacement = `{function() { __ROUND_SWITCH_TOKEN__(${cond}) { ${finalContent} } }}`;
262
+ return { end: endIdx, replacement };
263
+ }
264
+
265
+ // --- Main Parser Loop ---
266
+
267
+ let inSingle = false, inDouble = false, inTemplate = false;
268
+ let inCommentLine = false, inCommentMulti = false;
269
+
270
+ // Track JSX opening tag state to avoid transforming code inside attribute expressions
271
+ let inOpeningTag = false; // True when between <Tag and > (parsing attributes)
272
+ let attrBraceDepth = 0; // Brace nesting depth inside ={...} expressions
273
+ let prevWasEquals = false; // Track if previous non-whitespace char was '='
274
+
275
+ while (i < code.length) {
276
+ const ch = code[i];
277
+ const next = i < code.length - 1 ? code[i + 1] : '';
278
+ const prev = i > 0 ? code[i - 1] : '';
279
+
280
+ if (inCommentLine) {
281
+ result += ch;
282
+ if (ch === '\n' || ch === '\r') inCommentLine = false;
283
+ i++; continue;
284
+ }
285
+ if (inCommentMulti) {
286
+ result += ch;
287
+ if (ch === '*' && next === '/') { inCommentMulti = false; result += '/'; i += 2; continue; }
288
+ i++; continue;
289
+ }
290
+ if (inTemplate) {
291
+ result += ch;
292
+ if (ch === '`' && prev !== '\\') inTemplate = false;
293
+ i++; continue;
294
+ }
295
+ if (inSingle) {
296
+ result += ch;
297
+ if (ch === '\'' && prev !== '\\') inSingle = false;
298
+ i++; continue;
299
+ }
300
+ if (inDouble) {
301
+ result += ch;
302
+ if (ch === '"' && prev !== '\\') inDouble = false;
303
+ i++; continue;
304
+ }
305
+
306
+ if (ch === '/' && next === '/') { inCommentLine = true; result += '//'; i += 2; continue; }
307
+ if (ch === '/' && next === '*') { inCommentMulti = true; result += '/*'; i += 2; continue; }
308
+ if (ch === '`') { inTemplate = true; result += ch; i++; continue; }
309
+ if (ch === '\'') { inSingle = true; result += ch; i++; continue; }
310
+ if (ch === '"') { inDouble = true; result += ch; i++; continue; }
311
+
312
+ // Track attribute expression braces BEFORE other logic
313
+ if (inOpeningTag) {
314
+ if (ch === '=' && !attrBraceDepth) {
315
+ prevWasEquals = true;
316
+ result += ch;
317
+ i++;
318
+ continue;
319
+ }
320
+ if (ch === '{') {
321
+ if (prevWasEquals || attrBraceDepth > 0) {
322
+ // Entering or continuing inside an attribute expression
323
+ attrBraceDepth++;
324
+ }
325
+ prevWasEquals = false;
326
+ result += ch;
327
+ i++;
328
+ continue;
329
+ }
330
+ if (ch === '}' && attrBraceDepth > 0) {
331
+ attrBraceDepth--;
332
+ result += ch;
333
+ i++;
334
+ continue;
335
+ }
336
+ if (!/\s/.test(ch)) {
337
+ prevWasEquals = false;
338
+ }
339
+ // End of opening tag
340
+ if (ch === '>' && attrBraceDepth === 0) {
341
+ inOpeningTag = false;
342
+ result += ch;
343
+ i++;
344
+ continue;
345
+ }
346
+ // Self-closing tag
347
+ if (ch === '/' && next === '>' && attrBraceDepth === 0) {
348
+ inOpeningTag = false;
349
+ if (jsxDepth > 0) jsxDepth--;
350
+ result += '/>';
351
+ i += 2;
352
+ continue;
353
+ }
354
+ }
355
+
356
+ // JSX tag detection (only when NOT inside an opening tag already)
357
+ if (ch === '<' && !inOpeningTag) {
358
+ const isOpenTag = /[a-zA-Z0-9_$]/.test(next);
359
+ const isCloseTag = next === '/';
360
+ const isFragment = next === '>';
361
+
362
+ if (isOpenTag) {
363
+ jsxDepth++;
364
+ inOpeningTag = true;
365
+ attrBraceDepth = 0;
366
+ prevWasEquals = false;
367
+ } else if (isCloseTag) {
368
+ // Closing tag </tag>
369
+ if (jsxDepth > 0) jsxDepth--;
370
+ } else if (isFragment) {
371
+ // Fragment <>
372
+ jsxDepth++;
373
+ }
374
+ }
375
+
376
+ // Fragment closing </>
377
+ if (ch === '<' && next === '/' && code[i + 2] === '>') {
378
+ if (jsxDepth > 0) jsxDepth--;
379
+ result += '</>';
380
+ i += 3;
381
+ continue;
382
+ }
383
+
384
+ // ONLY transform when in JSX children context (not in opening tag, not in attr expression)
385
+ if (jsxDepth > 0 && !inOpeningTag && attrBraceDepth === 0) {
386
+ let processed = false;
387
+
388
+ // 1. Handlers for { control }
389
+ if (ch === '{') {
390
+ let ptr = consumeWhitespace(code, i + 1);
391
+ if (code.startsWith('if', ptr)) {
392
+ const res = handleIf(i, false);
393
+ if (res) { result += res.replacement; i = res.end; processed = true; }
394
+ } else if (code.startsWith('for', ptr)) {
395
+ const res = handleFor(i, false);
396
+ if (res) { result += res.replacement; i = res.end; processed = true; }
397
+ } else if (code.startsWith('switch', ptr)) {
398
+ const res = handleSwitch(i, false);
399
+ if (res) { result += res.replacement; i = res.end; processed = true; }
400
+ }
401
+ }
402
+
403
+ // 2. Handlers for bare control flow (implicit nesting)
404
+ // Strict check: must look like "if (" inside a code block
405
+ else if (ch === 'i' && code.startsWith('if', i)) {
406
+ // Verify it is followed by (
407
+ let ptr = consumeWhitespace(code, i + 2);
408
+ if (code[ptr] === '(') {
409
+ const res = handleIf(i, true);
410
+ if (res) { result += res.replacement; i = res.end; processed = true; }
411
+ }
412
+ } else if (ch === 'f' && code.startsWith('for', i)) {
413
+ let ptr = consumeWhitespace(code, i + 3);
414
+ if (code[ptr] === '(') {
415
+ const res = handleFor(i, true);
416
+ if (res) { result += res.replacement; i = res.end; processed = true; }
417
+ }
418
+ } else if (ch === 's' && code.startsWith('switch', i)) {
419
+ let ptr = consumeWhitespace(code, i + 6);
420
+ if (code[ptr] === '(') {
421
+ const res = handleSwitch(i, true);
422
+ if (res) { result += res.replacement; i = res.end; processed = true; }
423
+ }
424
+ }
425
+
426
+ if (processed) continue;
427
+ }
428
+
429
+ result += ch;
430
+ i++;
431
+ }
432
+
433
+ // --- Helpers for global transforms ---
434
+
435
+ function findJsxTagEnd(str, startIndex) {
436
+ let inSingle = false, inDouble = false, inTemplate = false;
437
+ let braceDepth = 0;
438
+ for (let k = startIndex; k < str.length; k++) {
439
+ const c = str[k];
440
+ const p = k > 0 ? str[k - 1] : '';
441
+ if (!inDouble && !inTemplate && c === '\'' && p !== '\\') inSingle = !inSingle;
442
+ else if (!inSingle && !inTemplate && c === '"' && p !== '\\') inDouble = !inDouble;
443
+ else if (!inSingle && !inDouble && c === '`' && p !== '\\') inTemplate = !inTemplate;
444
+ if (inSingle || inDouble || inTemplate) continue;
445
+ if (c === '{') braceDepth++;
446
+ else if (c === '}') braceDepth = Math.max(0, braceDepth - 1);
447
+ else if (c === '>' && braceDepth === 0) return k;
448
+ }
449
+ return -1;
450
+ }
451
+
452
+ function transformSuspenseBlocks(str) {
453
+ let out = str;
454
+ let cursor = 0;
455
+ while (true) {
456
+ const openIndex = out.indexOf('<Suspense', cursor);
457
+ if (openIndex === -1) break;
458
+ const openEnd = findJsxTagEnd(out, openIndex);
459
+ if (openEnd === -1) break;
460
+ const openTagText = out.slice(openIndex, openEnd + 1);
461
+ if (/\/>\s*$/.test(openTagText)) { cursor = openEnd + 1; continue; }
462
+ let depth = 1, k = openEnd + 1, closeStart = -1;
463
+ while (k < out.length) {
464
+ if (out.slice(k).startsWith('<Suspense')) { depth++; k += 9; }
465
+ else if (out.slice(k).startsWith('</Suspense>')) {
466
+ depth--; if (depth === 0) { closeStart = k; break; } k += 11;
467
+ } else k++;
468
+ }
469
+ if (closeStart === -1) break;
470
+ const inner = out.slice(openEnd + 1, closeStart);
471
+ const wrapped = `{(() => (<Fragment>${inner}</Fragment>))}`;
472
+ out = out.slice(0, openEnd + 1) + wrapped + out.slice(closeStart);
473
+ cursor = closeStart + wrapped.length + 11;
474
+ }
475
+ return out;
476
+ }
477
+
478
+ function transformProviderBlocks(str) {
479
+ let out = str;
480
+ let cursor = 0;
481
+ while (true) {
482
+ const dot = out.indexOf('.Provider', cursor);
483
+ if (dot === -1) break;
484
+ const lt = out.lastIndexOf('<', dot);
485
+ if (lt === -1) break;
486
+ const openEnd = findJsxTagEnd(out, lt);
487
+ if (openEnd === -1) break;
488
+ const tagText = out.slice(lt, openEnd + 1);
489
+ if (/\/>\s*$/.test(tagText)) { cursor = openEnd + 1; continue; }
490
+ const m = tagText.match(/^<\s*([A-Za-z_$][\w$]*\.Provider)\b/);
491
+ if (!m) { cursor = openEnd + 1; continue; }
492
+ const tagName = m[1];
493
+ const closeTag = `</${tagName}>`;
494
+ let depth = 1, k = openEnd + 1, closeStart = -1;
495
+ while (k < out.length) {
496
+ const nOpen = out.indexOf(`<${tagName}`, k);
497
+ const nClose = out.indexOf(closeTag, k);
498
+ if (nClose === -1) break;
499
+ if (nOpen !== -1 && nOpen < nClose) {
500
+ const innerEnd = findJsxTagEnd(out, nOpen);
501
+ if (innerEnd !== -1 && !/\/>\s*$/.test(out.slice(nOpen, innerEnd + 1))) depth++;
502
+ k = innerEnd + 1; continue;
503
+ }
504
+ depth--;
505
+ if (depth === 0) { closeStart = nClose; break; }
506
+ k = nClose + closeTag.length;
507
+ }
508
+ if (closeStart === -1) break;
509
+ const inner = out.slice(openEnd + 1, closeStart);
510
+ const wrapped = `{(() => (<Fragment>${inner}</Fragment>))}`;
511
+ out = out.slice(0, openEnd + 1) + wrapped + out.slice(closeStart);
512
+ cursor = closeStart + wrapped.length + closeTag.length;
513
+ }
514
+ return out;
515
+ }
516
+
517
+ result = transformSuspenseBlocks(result);
518
+ result = transformProviderBlocks(result);
519
+
520
+ result = result
521
+ .replace(/\{\s*([A-Za-z_$][\w$]*)\s*\(\s*\)\s*\}/g, '{() => $1()}')
522
+ .replace(/=\{\s*([A-Za-z_$][\w$]*)\s*\(\s*\)\s*\}/g, '={' + '() => $1()}');
523
+
524
+ return result.replace(/__ROUND_SWITCH_TOKEN__/g, 'switch');
525
+ }
@@ -0,0 +1,8 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ export interface RoundPluginOptions {
4
+ configPath?: string;
5
+ restartOnConfigChange?: boolean;
6
+ }
7
+
8
+ export default function RoundPlugin(options?: RoundPluginOptions): Plugin;