@xnoxs/flux-lang 3.1.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/CHANGELOG.md +103 -0
- package/README.md +1089 -0
- package/bin/flux.js +1397 -0
- package/dist/flux.cjs.js +6664 -0
- package/dist/flux.esm.js +6674 -0
- package/dist/flux.min.js +263 -0
- package/index.d.ts +202 -0
- package/index.js +26 -0
- package/package.json +77 -0
- package/scripts/build.js +76 -0
- package/src/bundler.js +216 -0
- package/src/checker.js +322 -0
- package/src/codegen.js +785 -0
- package/src/css-preprocessor.js +399 -0
- package/src/formatter.js +140 -0
- package/src/jsx.js +480 -0
- package/src/lexer.js +518 -0
- package/src/linter.js +758 -0
- package/src/mangler.js +280 -0
- package/src/parser.js +1671 -0
- package/src/self/bundler.flux +167 -0
- package/src/self/bundler.js +187 -0
- package/src/self/checker.flux +249 -0
- package/src/self/checker.js +338 -0
- package/src/self/codegen.flux +555 -0
- package/src/self/codegen.js +784 -0
- package/src/self/css-preprocessor.flux +373 -0
- package/src/self/css-preprocessor.js +387 -0
- package/src/self/formatter.flux +93 -0
- package/src/self/formatter.js +114 -0
- package/src/self/jsx.flux +430 -0
- package/src/self/jsx.js +396 -0
- package/src/self/lexer.flux +529 -0
- package/src/self/lexer.js +709 -0
- package/src/self/lexer.stage2.js +700 -0
- package/src/self/linter.flux +515 -0
- package/src/self/linter.js +804 -0
- package/src/self/mangler.flux +253 -0
- package/src/self/mangler.js +348 -0
- package/src/self/parser.flux +1146 -0
- package/src/self/parser.js +1571 -0
- package/src/self/sourcemap.flux +66 -0
- package/src/self/sourcemap.js +72 -0
- package/src/self/stdlib.flux +356 -0
- package/src/self/stdlib.js +396 -0
- package/src/self/test-runner.flux +201 -0
- package/src/self/test-runner.js +132 -0
- package/src/self/transpiler.flux +123 -0
- package/src/self/transpiler.js +83 -0
- package/src/self/type-checker.flux +821 -0
- package/src/self/type-checker.js +1106 -0
- package/src/sourcemap.js +82 -0
- package/src/stdlib.js +436 -0
- package/src/test-runner.js +239 -0
- package/src/transpiler.js +172 -0
- package/src/type-checker.js +1206 -0
package/src/jsx.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Flux JSX Preprocessor ─────────────────────────────────────────────────────
|
|
4
|
+
// Transforms JSX syntax in Flux source to _fluxH() calls before lexing.
|
|
5
|
+
// <div class="x">text {expr}</div> → _fluxH("div",{"class":"x"},"text ",expr)
|
|
6
|
+
// <br /> → _fluxH("br",null)
|
|
7
|
+
// <>...</> → _fluxH("",null,...)
|
|
8
|
+
|
|
9
|
+
// ── JSX utility style props → inline style object ────────────────────────────
|
|
10
|
+
// These prop names on JSX elements get compiled into the style={} attribute.
|
|
11
|
+
// Value props: bg="color" p="20px" radius="8px" w="100%" text="1rem" …
|
|
12
|
+
// Bool props: flex grid bold italic pointer hidden relative absolute …
|
|
13
|
+
const JSX_STYLE_VALUE_PROPS = {
|
|
14
|
+
bg: v => `background:${v}`,
|
|
15
|
+
fg: v => `color:${v}`,
|
|
16
|
+
color: v => `color:${v}`,
|
|
17
|
+
p: v => `padding:${v}`,
|
|
18
|
+
px: v => `paddingLeft:${v},paddingRight:${v}`,
|
|
19
|
+
py: v => `paddingTop:${v},paddingBottom:${v}`,
|
|
20
|
+
pt: v => `paddingTop:${v}`,
|
|
21
|
+
pb: v => `paddingBottom:${v}`,
|
|
22
|
+
pl: v => `paddingLeft:${v}`,
|
|
23
|
+
pr: v => `paddingRight:${v}`,
|
|
24
|
+
m: v => `margin:${v}`,
|
|
25
|
+
mx: v => `marginLeft:${v},marginRight:${v}`,
|
|
26
|
+
my: v => `marginTop:${v},marginBottom:${v}`,
|
|
27
|
+
mt: v => `marginTop:${v}`,
|
|
28
|
+
mb: v => `marginBottom:${v}`,
|
|
29
|
+
ml: v => `marginLeft:${v}`,
|
|
30
|
+
mr: v => `marginRight:${v}`,
|
|
31
|
+
radius: v => `borderRadius:${v}`,
|
|
32
|
+
w: v => `width:${v}`,
|
|
33
|
+
h: v => `height:${v}`,
|
|
34
|
+
'min-w': v => `minWidth:${v}`,
|
|
35
|
+
'max-w': v => `maxWidth:${v}`,
|
|
36
|
+
'min-h': v => `minHeight:${v}`,
|
|
37
|
+
'max-h': v => `maxHeight:${v}`,
|
|
38
|
+
gap: v => `gap:${v}`,
|
|
39
|
+
'col-gap': v => `columnGap:${v}`,
|
|
40
|
+
'row-gap': v => `rowGap:${v}`,
|
|
41
|
+
text: v => `fontSize:${v}`,
|
|
42
|
+
font: v => `fontFamily:${v}`,
|
|
43
|
+
weight: v => `fontWeight:${v}`,
|
|
44
|
+
tracking: v => `letterSpacing:${v}`,
|
|
45
|
+
leading: v => `lineHeight:${v}`,
|
|
46
|
+
shadow: v => `boxShadow:${v}`,
|
|
47
|
+
opacity: v => `opacity:${v}`,
|
|
48
|
+
border: v => `border:${v}`,
|
|
49
|
+
outline: v => `outline:${v}`,
|
|
50
|
+
transition: v => `transition:${v}`,
|
|
51
|
+
cursor: v => `cursor:${v}`,
|
|
52
|
+
overflow: v => `overflow:${v}`,
|
|
53
|
+
z: v => `zIndex:${v}`,
|
|
54
|
+
transform: v => `transform:${v}`,
|
|
55
|
+
direction: v => `flexDirection:${v}`,
|
|
56
|
+
align: v => `alignItems:${v}`,
|
|
57
|
+
justify: v => `justifyContent:${v}`,
|
|
58
|
+
'align-self': v => `alignSelf:${v}`,
|
|
59
|
+
'place-items': v => `placeItems:${v}`,
|
|
60
|
+
grow: v => `flexGrow:${v}`,
|
|
61
|
+
shrink: v => `flexShrink:${v}`,
|
|
62
|
+
basis: v => `flexBasis:${v}`,
|
|
63
|
+
cols: v => `gridTemplateColumns:${v}`,
|
|
64
|
+
rows: v => `gridTemplateRows:${v}`,
|
|
65
|
+
inset: v => `inset:${v}`,
|
|
66
|
+
top: v => `top:${v}`,
|
|
67
|
+
right: v => `right:${v}`,
|
|
68
|
+
bottom: v => `bottom:${v}`,
|
|
69
|
+
left: v => `left:${v}`,
|
|
70
|
+
'object-fit': v => `objectFit:${v}`,
|
|
71
|
+
'line-height': v => `lineHeight:${v}`,
|
|
72
|
+
'text-align': v => `textAlign:${v}`,
|
|
73
|
+
decoration: v => `textDecoration:${v}`,
|
|
74
|
+
clip: v => `clipPath:${v}`,
|
|
75
|
+
filter: v => `filter:${v}`,
|
|
76
|
+
backdrop: v => `backdropFilter:${v}`,
|
|
77
|
+
animation: v => `animation:${v}`,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const JSX_STYLE_BOOL_PROPS = {
|
|
81
|
+
flex: 'display:"flex"',
|
|
82
|
+
grid: 'display:"grid"',
|
|
83
|
+
block: 'display:"block"',
|
|
84
|
+
'inline-flex': 'display:"inline-flex"',
|
|
85
|
+
'inline-block': 'display:"inline-block"',
|
|
86
|
+
bold: 'fontWeight:700',
|
|
87
|
+
italic: 'fontStyle:"italic"',
|
|
88
|
+
underline: 'textDecoration:"underline"',
|
|
89
|
+
pointer: 'cursor:"pointer"',
|
|
90
|
+
hidden: 'display:"none"',
|
|
91
|
+
relative: 'position:"relative"',
|
|
92
|
+
absolute: 'position:"absolute"',
|
|
93
|
+
fixed: 'position:"fixed"',
|
|
94
|
+
sticky: 'position:"sticky"',
|
|
95
|
+
'flex-col': 'flexDirection:"column"',
|
|
96
|
+
'flex-row': 'flexDirection:"row"',
|
|
97
|
+
'flex-wrap': 'flexWrap:"wrap"',
|
|
98
|
+
'flex-1': 'flex:1',
|
|
99
|
+
'w-full': 'width:"100%"',
|
|
100
|
+
'h-full': 'height:"100%"',
|
|
101
|
+
center: 'textAlign:"center"',
|
|
102
|
+
truncate: 'overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"',
|
|
103
|
+
'select-none': 'userSelect:"none"',
|
|
104
|
+
'no-wrap': 'whiteSpace:"nowrap"',
|
|
105
|
+
'no-list': 'listStyle:"none"',
|
|
106
|
+
'no-outline': 'outline:"none"',
|
|
107
|
+
'no-border': 'border:"none"',
|
|
108
|
+
'box-border': 'boxSizing:"border-box"',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Set of all known style util prop names (for quick lookup)
|
|
112
|
+
const ALL_STYLE_PROPS = new Set([
|
|
113
|
+
...Object.keys(JSX_STYLE_VALUE_PROPS),
|
|
114
|
+
...Object.keys(JSX_STYLE_BOOL_PROPS),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
class JsxPreprocessor {
|
|
118
|
+
constructor(src) {
|
|
119
|
+
this.src = src;
|
|
120
|
+
this.pos = 0;
|
|
121
|
+
this.out = '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Entry ─────────────────────────────────────────────────────
|
|
125
|
+
transform() {
|
|
126
|
+
while (this.pos < this.src.length) {
|
|
127
|
+
this.scanTop();
|
|
128
|
+
}
|
|
129
|
+
this.hasJsx = this.out !== this.src;
|
|
130
|
+
return this.out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Top-level scan: skip strings/comments, detect JSX ─────────
|
|
134
|
+
scanTop() {
|
|
135
|
+
const c = this.src[this.pos];
|
|
136
|
+
|
|
137
|
+
// Line comment
|
|
138
|
+
if (c === '/' && this.src[this.pos + 1] === '/') {
|
|
139
|
+
const end = this.src.indexOf('\n', this.pos);
|
|
140
|
+
if (end === -1) { this.out += this.src.slice(this.pos); this.pos = this.src.length; }
|
|
141
|
+
else { this.out += this.src.slice(this.pos, end + 1); this.pos = end + 1; }
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Block comment
|
|
146
|
+
if (c === '/' && this.src[this.pos + 1] === '*') {
|
|
147
|
+
const end = this.src.indexOf('*/', this.pos + 2);
|
|
148
|
+
if (end === -1) { this.out += this.src.slice(this.pos); this.pos = this.src.length; }
|
|
149
|
+
else { this.out += this.src.slice(this.pos, end + 2); this.pos = end + 2; }
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Double-quoted string (with possible interpolation)
|
|
154
|
+
if (c === '"') { this.passString('"'); return; }
|
|
155
|
+
if (c === "'") { this.passString("'"); return; }
|
|
156
|
+
if (c === '`') { this.passTemplateLit(); return; }
|
|
157
|
+
|
|
158
|
+
// Potential JSX open
|
|
159
|
+
if (c === '<' && this.isJsxStart()) {
|
|
160
|
+
const jsxCode = this.parseJsxElement();
|
|
161
|
+
this.out += jsxCode;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.out += c;
|
|
166
|
+
this.pos++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Heuristic: is '<' here in expression-start position? ──────
|
|
170
|
+
// Previous non-whitespace char must be = ( [ { , : > \n or end of keyword
|
|
171
|
+
isJsxStart() {
|
|
172
|
+
// Next char must be a letter (tag name) or > (fragment)
|
|
173
|
+
const next = this.src[this.pos + 1] || '';
|
|
174
|
+
if (!(next === '>' || (next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) return false;
|
|
175
|
+
|
|
176
|
+
// Scan back past spaces/tabs (not newlines — they're significant)
|
|
177
|
+
let i = this.pos - 1;
|
|
178
|
+
while (i >= 0 && (this.src[i] === ' ' || this.src[i] === '\t')) i--;
|
|
179
|
+
if (i < 0) return true;
|
|
180
|
+
|
|
181
|
+
const prev = this.src[i];
|
|
182
|
+
|
|
183
|
+
// Obvious expression-start punctuation (? for ternary: cond ? <jsx> : ...)
|
|
184
|
+
if ('=([{,:>\n?'.includes(prev)) return true;
|
|
185
|
+
|
|
186
|
+
// Check if the previous token is a keyword that precedes an expression:
|
|
187
|
+
// return, not, and, or, val, var, in, await, yield, else
|
|
188
|
+
if (/[a-z]/.test(prev)) {
|
|
189
|
+
let j = i;
|
|
190
|
+
while (j >= 0 && /[a-z]/.test(this.src[j])) j--;
|
|
191
|
+
const word = this.src.slice(j + 1, i + 1);
|
|
192
|
+
const exprKeywords = new Set(['return','not','and','or','val','var','await','yield','else','in','throw']);
|
|
193
|
+
if (exprKeywords.has(word)) return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Pass-through string literal ────────────────────────────────
|
|
200
|
+
passString(quote) {
|
|
201
|
+
this.out += quote;
|
|
202
|
+
this.pos++;
|
|
203
|
+
while (this.pos < this.src.length) {
|
|
204
|
+
const c = this.src[this.pos];
|
|
205
|
+
if (c === '\\') { this.out += c + (this.src[this.pos + 1] || ''); this.pos += 2; continue; }
|
|
206
|
+
if (c === quote) { this.out += c; this.pos++; return; }
|
|
207
|
+
this.out += c; this.pos++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Pass-through template literal ─────────────────────────────
|
|
212
|
+
passTemplateLit() {
|
|
213
|
+
this.out += '`'; this.pos++;
|
|
214
|
+
while (this.pos < this.src.length) {
|
|
215
|
+
const c = this.src[this.pos];
|
|
216
|
+
if (c === '\\') { this.out += c + (this.src[this.pos + 1] || ''); this.pos += 2; continue; }
|
|
217
|
+
if (c === '`') { this.out += c; this.pos++; return; }
|
|
218
|
+
this.out += c; this.pos++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Parse a full JSX element, return Flux/JS source string ────
|
|
223
|
+
parseJsxElement() {
|
|
224
|
+
this.pos++; // consume '<'
|
|
225
|
+
|
|
226
|
+
// Fragment: <>...</>
|
|
227
|
+
if (this.src[this.pos] === '>') {
|
|
228
|
+
this.pos++;
|
|
229
|
+
const children = this.parseJsxChildren('');
|
|
230
|
+
this.expectClose('');
|
|
231
|
+
const childStr = children.length ? ',' + children.join(',') : '';
|
|
232
|
+
return `_fluxH("",null${childStr})`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const tag = this.readTagName();
|
|
236
|
+
const attrs = this.parseJsxAttrs();
|
|
237
|
+
|
|
238
|
+
// Self-closing
|
|
239
|
+
if (this.src[this.pos] === '/' && this.src[this.pos + 1] === '>') {
|
|
240
|
+
this.pos += 2;
|
|
241
|
+
return `_fluxH("${tag}",${attrs})`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Opening close >
|
|
245
|
+
if (this.src[this.pos] === '>') {
|
|
246
|
+
this.pos++;
|
|
247
|
+
const children = this.parseJsxChildren(tag);
|
|
248
|
+
this.expectClose(tag);
|
|
249
|
+
const childStr = children.length ? ',' + children.join(',') : '';
|
|
250
|
+
return `_fluxH("${tag}",${attrs}${childStr})`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fallback — not valid JSX, pass through
|
|
254
|
+
return `<${tag}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Read tag/component name ────────────────────────────────────
|
|
258
|
+
readTagName() {
|
|
259
|
+
let name = '';
|
|
260
|
+
while (this.pos < this.src.length) {
|
|
261
|
+
const c = this.src[this.pos];
|
|
262
|
+
if (/[a-zA-Z0-9\-_\.]/.test(c)) { name += c; this.pos++; }
|
|
263
|
+
else break;
|
|
264
|
+
}
|
|
265
|
+
return name;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Parse attributes → JS object literal string ───────────────
|
|
269
|
+
// Also handles utility style props: bg="…" p="…" flex bold …
|
|
270
|
+
parseJsxAttrs() {
|
|
271
|
+
this.skipWs();
|
|
272
|
+
const pairs = []; // regular attr pairs: "name":value
|
|
273
|
+
const styleParts = []; // accumulated style object key:value pairs
|
|
274
|
+
let explicitStyle = null; // content of explicit style={…} if present
|
|
275
|
+
|
|
276
|
+
while (this.pos < this.src.length) {
|
|
277
|
+
const c = this.src[this.pos];
|
|
278
|
+
if (c === '>' || (c === '/' && this.src[this.pos + 1] === '>')) break;
|
|
279
|
+
|
|
280
|
+
this.skipWs();
|
|
281
|
+
if (!(/[a-zA-Z_\-]/.test(this.src[this.pos]))) break;
|
|
282
|
+
|
|
283
|
+
// Read attr name (allow - in names for data-*, aria-*, and util props like min-w)
|
|
284
|
+
let attrName = '';
|
|
285
|
+
while (this.pos < this.src.length && /[a-zA-Z0-9\-_:]/.test(this.src[this.pos])) {
|
|
286
|
+
attrName += this.src[this.pos++];
|
|
287
|
+
}
|
|
288
|
+
this.skipWs();
|
|
289
|
+
|
|
290
|
+
// ── No value: boolean attr OR boolean style util ──────────
|
|
291
|
+
if (this.src[this.pos] !== '=') {
|
|
292
|
+
if (JSX_STYLE_BOOL_PROPS[attrName] !== undefined) {
|
|
293
|
+
// Boolean style util prop
|
|
294
|
+
styleParts.push(JSX_STYLE_BOOL_PROPS[attrName]);
|
|
295
|
+
} else {
|
|
296
|
+
pairs.push(`"${attrName}":true`);
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.pos++; // consume '='
|
|
302
|
+
this.skipWs();
|
|
303
|
+
|
|
304
|
+
const ch = this.src[this.pos];
|
|
305
|
+
let rawVal = null; // string value (unquoted)
|
|
306
|
+
let exprVal = null; // expression value (JS expr string)
|
|
307
|
+
|
|
308
|
+
if (ch === '"' || ch === "'") {
|
|
309
|
+
const q = ch; this.pos++;
|
|
310
|
+
let val = '';
|
|
311
|
+
while (this.pos < this.src.length && this.src[this.pos] !== q) {
|
|
312
|
+
if (this.src[this.pos] === '\\') { val += this.src[this.pos] + this.src[this.pos + 1]; this.pos += 2; }
|
|
313
|
+
else { val += this.src[this.pos++]; }
|
|
314
|
+
}
|
|
315
|
+
this.pos++; // close quote
|
|
316
|
+
rawVal = val;
|
|
317
|
+
} else if (ch === '{') {
|
|
318
|
+
exprVal = this.readBraced();
|
|
319
|
+
} else {
|
|
320
|
+
// fallback boolean
|
|
321
|
+
if (JSX_STYLE_BOOL_PROPS[attrName] !== undefined) {
|
|
322
|
+
styleParts.push(JSX_STYLE_BOOL_PROPS[attrName]);
|
|
323
|
+
} else {
|
|
324
|
+
pairs.push(`"${attrName}":true`);
|
|
325
|
+
}
|
|
326
|
+
this.skipWs();
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Route to style util or regular attr ───────────────────
|
|
331
|
+
if (rawVal !== null && JSX_STYLE_VALUE_PROPS[attrName]) {
|
|
332
|
+
// Utility value prop: bg="…" p="…" radius="…" …
|
|
333
|
+
const expanded = JSX_STYLE_VALUE_PROPS[attrName](JSON.stringify(rawVal));
|
|
334
|
+
styleParts.push(expanded);
|
|
335
|
+
} else if (exprVal !== null && attrName === 'style') {
|
|
336
|
+
// Explicit style={…} — capture for later merging
|
|
337
|
+
explicitStyle = exprVal;
|
|
338
|
+
} else {
|
|
339
|
+
// Regular attr
|
|
340
|
+
if (rawVal !== null) {
|
|
341
|
+
pairs.push(`"${attrName}":${JSON.stringify(rawVal)}`);
|
|
342
|
+
} else {
|
|
343
|
+
pairs.push(`"${attrName}":${exprVal}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this.skipWs();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Build style entry (merge explicit + utility) ──────────
|
|
351
|
+
if (styleParts.length > 0 || explicitStyle !== null) {
|
|
352
|
+
const utilObj = styleParts.length > 0 ? `{${styleParts.join(',')}}` : null;
|
|
353
|
+
if (explicitStyle !== null && utilObj !== null) {
|
|
354
|
+
pairs.push(`"style":Object.assign(${explicitStyle},${utilObj})`);
|
|
355
|
+
} else if (explicitStyle !== null) {
|
|
356
|
+
pairs.push(`"style":${explicitStyle}`);
|
|
357
|
+
} else {
|
|
358
|
+
pairs.push(`"style":{${styleParts.join(',')}}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return pairs.length ? `{${pairs.join(',')}}` : 'null';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Read {…} balanced braces, return inner content ────────────
|
|
366
|
+
readBraced() {
|
|
367
|
+
this.pos++; // consume '{'
|
|
368
|
+
let depth = 1, content = '';
|
|
369
|
+
while (this.pos < this.src.length && depth > 0) {
|
|
370
|
+
const c = this.src[this.pos];
|
|
371
|
+
if (c === '{') depth++;
|
|
372
|
+
if (c === '}') { depth--; if (depth === 0) { this.pos++; break; } }
|
|
373
|
+
content += c; this.pos++;
|
|
374
|
+
}
|
|
375
|
+
return content.trim();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Parse JSX children until </tag> ───────────────────────────
|
|
379
|
+
parseJsxChildren(tag) {
|
|
380
|
+
const children = [];
|
|
381
|
+
|
|
382
|
+
while (this.pos < this.src.length) {
|
|
383
|
+
// End tag
|
|
384
|
+
if (this.src[this.pos] === '<' && this.src[this.pos + 1] === '/') break;
|
|
385
|
+
|
|
386
|
+
// Nested element
|
|
387
|
+
if (this.src[this.pos] === '<') {
|
|
388
|
+
const next = this.src[this.pos + 1] || '';
|
|
389
|
+
if ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z') || next === '>') {
|
|
390
|
+
children.push(this.parseJsxElement());
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Expression interpolation {expr}
|
|
396
|
+
if (this.src[this.pos] === '{') {
|
|
397
|
+
const expr = this.readBraced();
|
|
398
|
+
if (expr.trim()) children.push(expr);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Text node
|
|
403
|
+
let text = '';
|
|
404
|
+
while (this.pos < this.src.length) {
|
|
405
|
+
const c = this.src[this.pos];
|
|
406
|
+
if (c === '<' || c === '{') break;
|
|
407
|
+
text += c; this.pos++;
|
|
408
|
+
}
|
|
409
|
+
const trimmed = text.replace(/\s+/g, ' ').trim();
|
|
410
|
+
if (trimmed) children.push(JSON.stringify(trimmed));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return children;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── Expect and consume </tag> ──────────────────────────────────
|
|
417
|
+
expectClose(tag) {
|
|
418
|
+
// Consume </tagName>
|
|
419
|
+
if (this.src[this.pos] === '<' && this.src[this.pos + 1] === '/') {
|
|
420
|
+
this.pos += 2;
|
|
421
|
+
// Skip tag name
|
|
422
|
+
while (this.pos < this.src.length && this.src[this.pos] !== '>') this.pos++;
|
|
423
|
+
if (this.src[this.pos] === '>') this.pos++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Skip whitespace (not newlines) ────────────────────────────
|
|
428
|
+
skipWs() {
|
|
429
|
+
while (this.pos < this.src.length && (this.src[this.pos] === ' ' || this.src[this.pos] === '\t')) this.pos++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── _fluxH runtime helper (browser) ──────────────────────────────────────────
|
|
434
|
+
const FLUX_H_BROWSER = `
|
|
435
|
+
function _fluxH(tag,props,...children){
|
|
436
|
+
if(tag===""){const f=document.createDocumentFragment();children.flat(Infinity).forEach(c=>{if(c==null)return;f.appendChild(c instanceof Node?c:document.createTextNode(String(c)));});return f;}
|
|
437
|
+
const el=document.createElement(tag);
|
|
438
|
+
if(props){for(const[k,v]of Object.entries(props)){if(k==="class"||k==="className"){el.className=v;}else if(k==="style"&&typeof v==="object"){Object.assign(el.style,v);}else if(k.startsWith("on")&&typeof v==="function"){el.addEventListener(k.slice(2).toLowerCase(),v);}else if(typeof v==="boolean"){if(v)el.setAttribute(k,"");}else{el.setAttribute(k,String(v));}}}
|
|
439
|
+
children.flat(Infinity).forEach(c=>{if(c==null)return;el.appendChild(c instanceof Node?c:document.createTextNode(String(c)));});
|
|
440
|
+
return el;
|
|
441
|
+
}`;
|
|
442
|
+
|
|
443
|
+
// ── _fluxH runtime helper (server/Node.js) ───────────────────────────────────
|
|
444
|
+
const FLUX_H_SERVER = `
|
|
445
|
+
function _fluxH(tag,props,...children){
|
|
446
|
+
const VOID=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);
|
|
447
|
+
if(tag==="")return children.flat(Infinity).map(c=>c==null?"":String(c)).join("");
|
|
448
|
+
let attrs="";
|
|
449
|
+
if(props){for(const[k,v]of Object.entries(props)){if(k==="class"||k==="className")attrs+=\` class="\${v}"\`;else if(k==="style"&&typeof v==="object")attrs+=\` style="\${Object.entries(v).map(([p,val])=>p.replace(/[A-Z]/g,m=>"-"+m.toLowerCase())+":"+val).join(";")}"\`;else if(typeof v!=="function"&&typeof v!=="boolean")attrs+=\` \${k}="\${String(v).replace(/"/g,""")}"\`;else if(v===true)attrs+=\` \${k}\`;}}
|
|
450
|
+
const inner=children.flat(Infinity).map(c=>c==null?"":String(c)).join("");
|
|
451
|
+
if(VOID.has(tag))return\`<\${tag}\${attrs}>\`;
|
|
452
|
+
return\`<\${tag}\${attrs}>\${inner}</\${tag}>\`;
|
|
453
|
+
}`;
|
|
454
|
+
|
|
455
|
+
// ── _fluxCSS helper (inject <style> tag in browser) ──────────────────────────
|
|
456
|
+
const FLUX_CSS_BROWSER = `
|
|
457
|
+
function _fluxCSS(css){const s=document.createElement("style");s.textContent=css;document.head.appendChild(s);return css;}`;
|
|
458
|
+
|
|
459
|
+
const FLUX_CSS_SERVER = `
|
|
460
|
+
function _fluxCSS(css){return css;}`;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Transform Flux source with JSX syntax into plain Flux source.
|
|
464
|
+
* @param {string} src - raw Flux source (may contain JSX)
|
|
465
|
+
* @param {object} opts - { target: 'browser' | 'server' }
|
|
466
|
+
* @returns {{ source: string, hasJsx: boolean, runtimeHelpers: string }}
|
|
467
|
+
*/
|
|
468
|
+
function transformJsx(src, opts = {}) {
|
|
469
|
+
const target = opts.target || 'browser';
|
|
470
|
+
const proc = new JsxPreprocessor(src);
|
|
471
|
+
const source = proc.transform();
|
|
472
|
+
const hasJsx = proc.hasJsx || src.includes('_fluxH') || src.includes('_fluxCSS');
|
|
473
|
+
|
|
474
|
+
const hHelper = target === 'browser' ? FLUX_H_BROWSER : FLUX_H_SERVER;
|
|
475
|
+
const cssHelper = target === 'browser' ? FLUX_CSS_BROWSER : FLUX_CSS_SERVER;
|
|
476
|
+
|
|
477
|
+
return { source, hasJsx, runtimeHelpers: hHelper + cssHelper };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = { transformJsx, JsxPreprocessor, FLUX_H_BROWSER, FLUX_H_SERVER, FLUX_CSS_BROWSER, FLUX_CSS_SERVER };
|