@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
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── CSS property shorthands ───────────────────────────────────────────────────
|
|
4
|
+
const CSS_PROP_MAP = {
|
|
5
|
+
bg: 'background', fg: 'color', p: 'padding',
|
|
6
|
+
px: 'padding-inline', py: 'padding-block',
|
|
7
|
+
pt: 'padding-top', pb: 'padding-bottom', pl: 'padding-left', pr: 'padding-right',
|
|
8
|
+
m: 'margin', mx: 'margin-inline', my: 'margin-block',
|
|
9
|
+
mt: 'margin-top', mb: 'margin-bottom', ml: 'margin-left', mr: 'margin-right',
|
|
10
|
+
radius: 'border-radius',
|
|
11
|
+
w: 'width', h: 'height',
|
|
12
|
+
'min-w': 'min-width', 'max-w': 'max-width', 'min-h': 'min-height', 'max-h': 'max-height',
|
|
13
|
+
gap: 'gap', 'col-gap': 'column-gap', 'row-gap': 'row-gap',
|
|
14
|
+
text: 'font-size', font: 'font-family', weight: 'font-weight',
|
|
15
|
+
tracking: 'letter-spacing', leading: 'line-height',
|
|
16
|
+
shadow: 'box-shadow', opacity: 'opacity',
|
|
17
|
+
border: 'border', outline: 'outline',
|
|
18
|
+
transition: 'transition', cursor: 'cursor', overflow: 'overflow',
|
|
19
|
+
'overflow-x': 'overflow-x', 'overflow-y': 'overflow-y',
|
|
20
|
+
z: 'z-index', transform: 'transform', content: 'content',
|
|
21
|
+
resize: 'resize', appearance: 'appearance',
|
|
22
|
+
'accent-color': 'accent-color', 'object-fit': 'object-fit',
|
|
23
|
+
direction: 'flex-direction', wrap: 'flex-wrap',
|
|
24
|
+
align: 'align-items', justify: 'justify-content',
|
|
25
|
+
'align-self': 'align-self', 'justify-self': 'justify-self',
|
|
26
|
+
grow: 'flex-grow', shrink: 'flex-shrink', basis: 'flex-basis',
|
|
27
|
+
order: 'order', cols: 'grid-template-columns', rows: 'grid-template-rows',
|
|
28
|
+
'col-span': 'grid-column', 'row-span': 'grid-row',
|
|
29
|
+
'place-items': 'place-items', 'place-content': 'place-content',
|
|
30
|
+
'list-style': 'list-style', 'text-align': 'text-align',
|
|
31
|
+
decoration: 'text-decoration', 'text-transform': 'text-transform',
|
|
32
|
+
'white-space': 'white-space', 'word-break': 'word-break',
|
|
33
|
+
'user-select': 'user-select', 'pointer-events': 'pointer-events',
|
|
34
|
+
'vertical-align': 'vertical-align', 'backdrop': 'backdrop-filter',
|
|
35
|
+
filter: 'filter', clip: 'clip-path', animation: 'animation',
|
|
36
|
+
position: 'position', top: 'top', right: 'right', bottom: 'bottom',
|
|
37
|
+
left: 'left', inset: 'inset', color: 'color', background: 'background',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── Boolean shorthands (no value needed) ─────────────────────────────────────
|
|
41
|
+
const CSS_BOOL_MAP = {
|
|
42
|
+
flex: 'display: flex', grid: 'display: grid',
|
|
43
|
+
block: 'display: block', inline: 'display: inline',
|
|
44
|
+
'inline-flex': 'display: inline-flex', 'inline-block': 'display: inline-block',
|
|
45
|
+
'inline-grid': 'display: inline-grid',
|
|
46
|
+
bold: 'font-weight: 700', italic: 'font-style: italic',
|
|
47
|
+
relative: 'position: relative', absolute: 'position: absolute',
|
|
48
|
+
fixed: 'position: fixed', sticky: 'position: sticky',
|
|
49
|
+
hidden: 'display: none', pointer: 'cursor: pointer',
|
|
50
|
+
underline: 'text-decoration: underline',
|
|
51
|
+
'line-through': 'text-decoration: line-through',
|
|
52
|
+
capitalize: 'text-transform: capitalize',
|
|
53
|
+
uppercase: 'text-transform: uppercase',
|
|
54
|
+
lowercase: 'text-transform: lowercase',
|
|
55
|
+
truncate: 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap',
|
|
56
|
+
'select-none': 'user-select: none',
|
|
57
|
+
'no-wrap': 'white-space: nowrap',
|
|
58
|
+
'no-list': 'list-style: none',
|
|
59
|
+
'no-outline': 'outline: none',
|
|
60
|
+
'no-border': 'border: none',
|
|
61
|
+
'no-bg': 'background: transparent',
|
|
62
|
+
'no-padding': 'padding: 0',
|
|
63
|
+
'no-margin': 'margin: 0',
|
|
64
|
+
'no-resize': 'resize: none',
|
|
65
|
+
center: 'text-align: center',
|
|
66
|
+
'items-center': 'align-items: center',
|
|
67
|
+
'justify-center': 'justify-content: center',
|
|
68
|
+
'place-center': 'place-items: center',
|
|
69
|
+
'flex-col': 'flex-direction: column',
|
|
70
|
+
'flex-row': 'flex-direction: row',
|
|
71
|
+
'flex-wrap': 'flex-wrap: wrap',
|
|
72
|
+
'flex-1': 'flex: 1',
|
|
73
|
+
'w-full': 'width: 100%',
|
|
74
|
+
'h-full': 'height: 100%',
|
|
75
|
+
'w-screen': 'width: 100vw',
|
|
76
|
+
'h-screen': 'height: 100vh',
|
|
77
|
+
'box-border': 'box-sizing: border-box',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function expandProp(name) {
|
|
81
|
+
const t = name.trim();
|
|
82
|
+
return CSS_PROP_MAP[t] || t;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stripQuotes(val) {
|
|
86
|
+
const v = val.trim();
|
|
87
|
+
if (v.length >= 2 &&
|
|
88
|
+
((v[0] === '"' && v[v.length - 1] === '"') ||
|
|
89
|
+
(v[0] === "'" && v[v.length - 1] === "'"))) {
|
|
90
|
+
return v.slice(1, -1);
|
|
91
|
+
}
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Resolve a child selector against its parent
|
|
96
|
+
function resolveSelector(sel, parent) {
|
|
97
|
+
sel = sel.trim();
|
|
98
|
+
if (!parent) return sel;
|
|
99
|
+
// At-rules and @keyframe frames inside @keyframes don't get prefixed
|
|
100
|
+
if (sel.startsWith('@')) return sel;
|
|
101
|
+
if (parent.startsWith('@keyframes') || parent.startsWith('@font-face')) return sel;
|
|
102
|
+
if (sel.includes('&')) return sel.replace(/&/g, parent);
|
|
103
|
+
return parent + ' ' + sel;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Expand a single declaration string (possibly multiple semicolon-separated)
|
|
107
|
+
function expandDecls(text) {
|
|
108
|
+
const result = [];
|
|
109
|
+
text.split(';').forEach(part => {
|
|
110
|
+
const d = part.trim();
|
|
111
|
+
if (!d || d.startsWith('//')) return;
|
|
112
|
+
if (CSS_BOOL_MAP[d]) {
|
|
113
|
+
CSS_BOOL_MAP[d].split(';').forEach(bd => { const t = bd.trim(); if (t) result.push(t); });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const ci = d.indexOf(':');
|
|
117
|
+
if (ci !== -1) {
|
|
118
|
+
const prop = expandProp(d.slice(0, ci));
|
|
119
|
+
const val = stripQuotes(d.slice(ci + 1));
|
|
120
|
+
if (val !== '') result.push(prop + ': ' + val);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Parse CSS block content into a flat CSS string ────────────────────────────
|
|
127
|
+
// Supports: nested selectors, &:pseudo, @keyframes, @media, boolean shorthands
|
|
128
|
+
function parseCssBlockContent(content, indentLevel) {
|
|
129
|
+
const ind = ' '.repeat(indentLevel || 0);
|
|
130
|
+
const ind1 = ind + ' ';
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
|
|
133
|
+
// We output in two passes:
|
|
134
|
+
// 1. Regular flattened rules (selector → [decls]) in a Map (preserves order)
|
|
135
|
+
// 2. At-rule blocks (@keyframes, @media, etc.) collected separately
|
|
136
|
+
|
|
137
|
+
const flatRules = new Map(); // selector → string[]
|
|
138
|
+
const atRuleSegs = []; // raw at-rule CSS strings (output after flat rules)
|
|
139
|
+
const selStack = [];
|
|
140
|
+
|
|
141
|
+
function curSel() { return selStack.length ? selStack[selStack.length - 1] : null; }
|
|
142
|
+
|
|
143
|
+
function addDecl(sel, d) {
|
|
144
|
+
if (!flatRules.has(sel)) flatRules.set(sel, []);
|
|
145
|
+
flatRules.get(sel).push(d);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function addDeclarationLine(line, sel) {
|
|
149
|
+
// Support multiple declarations on one line: "p: 10px; m: 0; bold"
|
|
150
|
+
if (line.includes(';')) {
|
|
151
|
+
line.split(';').forEach(part => {
|
|
152
|
+
const t = part.trim();
|
|
153
|
+
if (t) addDeclarationLine(t, sel);
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Boolean shorthand?
|
|
158
|
+
if (CSS_BOOL_MAP[line]) {
|
|
159
|
+
CSS_BOOL_MAP[line].split(';').forEach(bd => { const t = bd.trim(); if (t) addDecl(sel, t); });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const ci = line.indexOf(':');
|
|
163
|
+
if (ci !== -1) {
|
|
164
|
+
const prop = expandProp(line.slice(0, ci));
|
|
165
|
+
const val = stripQuotes(line.slice(ci + 1));
|
|
166
|
+
if (val !== '') addDecl(sel, prop + ': ' + val);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Collect a balanced brace block from `lines` starting at index i ──────
|
|
171
|
+
// Returns { inner: string, nextI: number }
|
|
172
|
+
function collectBlock(startI) {
|
|
173
|
+
let depth = 1;
|
|
174
|
+
let inner = '';
|
|
175
|
+
let j = startI;
|
|
176
|
+
while (j < lines.length && depth > 0) {
|
|
177
|
+
const ln = lines[j++];
|
|
178
|
+
for (const ch of ln) {
|
|
179
|
+
if (ch === '{') depth++;
|
|
180
|
+
else if (ch === '}') depth--;
|
|
181
|
+
}
|
|
182
|
+
if (depth > 0) inner += ln + '\n';
|
|
183
|
+
else {
|
|
184
|
+
// Last line: take content before final }
|
|
185
|
+
const ci = ln.lastIndexOf('}');
|
|
186
|
+
if (ci > 0) inner += ln.slice(0, ci) + '\n';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { inner, nextI: j };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let i = 0;
|
|
193
|
+
while (i < lines.length) {
|
|
194
|
+
const raw = lines[i++];
|
|
195
|
+
const line = raw.trim();
|
|
196
|
+
if (!line || line.startsWith('//')) continue;
|
|
197
|
+
|
|
198
|
+
const ob = line.indexOf('{');
|
|
199
|
+
const cb = line.lastIndexOf('}');
|
|
200
|
+
|
|
201
|
+
// ── @keyframes / @font-face — emit as nested block (not flattened) ──────
|
|
202
|
+
if (line.startsWith('@keyframes') || line.startsWith('@font-face')) {
|
|
203
|
+
const atSel = ob !== -1 ? line.slice(0, ob).trim() : line.trim();
|
|
204
|
+
let atBody = '';
|
|
205
|
+
|
|
206
|
+
if (ob !== -1 && cb !== -1 && cb > ob) {
|
|
207
|
+
// Inline: @keyframes name { from { ... } to { ... } }
|
|
208
|
+
atBody = line.slice(ob + 1, cb);
|
|
209
|
+
} else if (ob !== -1) {
|
|
210
|
+
const res = collectBlock(i);
|
|
211
|
+
i = res.nextI;
|
|
212
|
+
atBody = res.inner;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Expand property shorthands inside keyframe body (simple line pass)
|
|
216
|
+
const expandedBody = atBody.split('\n').map(ln => {
|
|
217
|
+
const t = ln.trim();
|
|
218
|
+
if (!t || t === '{' || t === '}') return ln;
|
|
219
|
+
const colonIdx = t.indexOf(':');
|
|
220
|
+
if (colonIdx !== -1 && !t.slice(0, colonIdx).includes('{')) {
|
|
221
|
+
const prop = expandProp(t.slice(0, colonIdx));
|
|
222
|
+
const val = stripQuotes(t.slice(colonIdx + 1));
|
|
223
|
+
return ln.replace(t, prop + ': ' + val);
|
|
224
|
+
}
|
|
225
|
+
return ln;
|
|
226
|
+
}).join('\n');
|
|
227
|
+
|
|
228
|
+
atRuleSegs.push(`${ind}${atSel} {\n${expandedBody}${ind}}\n`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── @media / @supports — emit as nested block, process inner rules ───────
|
|
233
|
+
if (line.startsWith('@media') || line.startsWith('@supports') || line.startsWith('@layer')) {
|
|
234
|
+
const atSel = ob !== -1 ? line.slice(0, ob).trim() : line.trim();
|
|
235
|
+
let atBody = '';
|
|
236
|
+
|
|
237
|
+
if (ob !== -1 && cb !== -1 && cb > ob) {
|
|
238
|
+
atBody = line.slice(ob + 1, cb);
|
|
239
|
+
} else if (ob !== -1) {
|
|
240
|
+
const res = collectBlock(i);
|
|
241
|
+
i = res.nextI;
|
|
242
|
+
atBody = res.inner;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const innerCss = parseCssBlockContent(atBody, (indentLevel || 0) + 1);
|
|
246
|
+
atRuleSegs.push(`${ind}${atSel} {\n${innerCss}${ind}}\n`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Inline rule: selector { decl; decl } on a single line ──────────────
|
|
251
|
+
if (ob !== -1 && cb !== -1 && cb > ob) {
|
|
252
|
+
const sel = line.slice(0, ob).trim();
|
|
253
|
+
const inner = line.slice(ob + 1, cb).trim();
|
|
254
|
+
const full = resolveSelector(sel, curSel());
|
|
255
|
+
if (!flatRules.has(full)) flatRules.set(full, []);
|
|
256
|
+
if (inner) expandDecls(inner).forEach(d => addDecl(full, d));
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Block opener: selector { ─────────────────────────────────────────────
|
|
261
|
+
if (ob !== -1) {
|
|
262
|
+
const sel = line.slice(0, ob).trim();
|
|
263
|
+
const full = resolveSelector(sel, curSel());
|
|
264
|
+
selStack.push(full);
|
|
265
|
+
if (!flatRules.has(full)) flatRules.set(full, []);
|
|
266
|
+
// Handle any declarations on the same line after {
|
|
267
|
+
const tail = line.slice(ob + 1).trim();
|
|
268
|
+
if (tail) expandDecls(tail).forEach(d => addDecl(full, d));
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Closing brace ────────────────────────────────────────────────────────
|
|
273
|
+
if (line === '}' || line === '};') {
|
|
274
|
+
selStack.pop();
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Declaration or boolean shorthand inside a rule block ─────────────────
|
|
279
|
+
if (selStack.length > 0) {
|
|
280
|
+
addDeclarationLine(line, curSel());
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Emit flat rules ───────────────────────────────────────────────────────
|
|
285
|
+
let css = '';
|
|
286
|
+
for (const [sel, decls] of flatRules) {
|
|
287
|
+
if (decls.length === 0) continue;
|
|
288
|
+
css += `${ind}${sel} {\n`;
|
|
289
|
+
for (const d of decls) css += `${ind1}${d};\n`;
|
|
290
|
+
css += `${ind}}\n`;
|
|
291
|
+
}
|
|
292
|
+
// ── Emit at-rule blocks (after flat rules) ────────────────────────────────
|
|
293
|
+
for (const seg of atRuleSegs) css += seg;
|
|
294
|
+
|
|
295
|
+
return css;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Main source preprocessor: replace css { } with CSS string ──────────────
|
|
299
|
+
class CssPreprocessor {
|
|
300
|
+
constructor(src) {
|
|
301
|
+
this.src = src;
|
|
302
|
+
this.pos = 0;
|
|
303
|
+
this.out = '';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
transform() {
|
|
307
|
+
while (this.pos < this.src.length) {
|
|
308
|
+
this.scan();
|
|
309
|
+
}
|
|
310
|
+
return this.out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
scan() {
|
|
314
|
+
const c = this.src[this.pos];
|
|
315
|
+
|
|
316
|
+
// Line comment — pass through unchanged
|
|
317
|
+
if (c === '/' && this.src[this.pos + 1] === '/') {
|
|
318
|
+
const end = this.src.indexOf('\n', this.pos);
|
|
319
|
+
if (end === -1) { this.out += this.src.slice(this.pos); this.pos = this.src.length; }
|
|
320
|
+
else { this.out += this.src.slice(this.pos, end + 1); this.pos = end + 1; }
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Block comment — pass through
|
|
325
|
+
if (c === '/' && this.src[this.pos + 1] === '*') {
|
|
326
|
+
const end = this.src.indexOf('*/', this.pos + 2);
|
|
327
|
+
if (end === -1) { this.out += this.src.slice(this.pos); this.pos = this.src.length; }
|
|
328
|
+
else { this.out += this.src.slice(this.pos, end + 2); this.pos = end + 2; }
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// String literals — pass through
|
|
333
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
334
|
+
this.passString(c);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Detect standalone css keyword followed (across whitespace) by {
|
|
339
|
+
if (c === 'c' && this.src.slice(this.pos, this.pos + 3) === 'css') {
|
|
340
|
+
const charBefore = this.pos > 0 ? this.src[this.pos - 1] : '\n';
|
|
341
|
+
const charAfter = this.src[this.pos + 3] || '';
|
|
342
|
+
// Must not be part of a longer identifier
|
|
343
|
+
if (!/[a-zA-Z0-9_]/.test(charBefore) && !/[a-zA-Z0-9_]/.test(charAfter)) {
|
|
344
|
+
let j = this.pos + 3;
|
|
345
|
+
// Skip whitespace/newlines looking for {
|
|
346
|
+
while (j < this.src.length &&
|
|
347
|
+
(this.src[j] === ' ' || this.src[j] === '\t' ||
|
|
348
|
+
this.src[j] === '\n' || this.src[j] === '\r')) j++;
|
|
349
|
+
|
|
350
|
+
if (this.src[j] === '{') {
|
|
351
|
+
j++; // consume opening {
|
|
352
|
+
let depth = 1;
|
|
353
|
+
let blockStart = j;
|
|
354
|
+
|
|
355
|
+
// Find matching closing }
|
|
356
|
+
while (j < this.src.length && depth > 0) {
|
|
357
|
+
const ch = this.src[j];
|
|
358
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
359
|
+
const q = ch; j++;
|
|
360
|
+
while (j < this.src.length && this.src[j] !== q) {
|
|
361
|
+
if (this.src[j] === '\\') j++;
|
|
362
|
+
j++;
|
|
363
|
+
}
|
|
364
|
+
j++;
|
|
365
|
+
} else if (ch === '{') { depth++; j++; }
|
|
366
|
+
else if (ch === '}') { depth--; j++; }
|
|
367
|
+
else j++;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const blockContent = this.src.slice(blockStart, j - 1);
|
|
371
|
+
const css = parseCssBlockContent(blockContent);
|
|
372
|
+
// Emit as backtick string (Flux raw multiline string, no interpolation)
|
|
373
|
+
this.out += '`' + css.replace(/\\/g, '\\\\').replace(/`/g, '\\`') + '`';
|
|
374
|
+
this.pos = j;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.out += c;
|
|
381
|
+
this.pos++;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
passString(quote) {
|
|
385
|
+
this.out += quote; this.pos++;
|
|
386
|
+
while (this.pos < this.src.length) {
|
|
387
|
+
const c = this.src[this.pos];
|
|
388
|
+
if (c === '\\') { this.out += c + (this.src[this.pos + 1] || ''); this.pos += 2; continue; }
|
|
389
|
+
if (c === quote) { this.out += c; this.pos++; return; }
|
|
390
|
+
this.out += c; this.pos++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function transformCss(src) {
|
|
396
|
+
return new CssPreprocessor(src).transform();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = { transformCss, parseCssBlockContent, CSS_PROP_MAP, CSS_BOOL_MAP };
|
package/src/formatter.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Flux Code Formatter ───────────────────────────────────────────────────────
|
|
4
|
+
// Normalizes Flux source code:
|
|
5
|
+
// • 4-space indentation
|
|
6
|
+
// • Strips trailing whitespace
|
|
7
|
+
// • Max 1 consecutive blank line
|
|
8
|
+
// • Blank line after top-level fn/class/type declarations
|
|
9
|
+
// • Spacing around binary operators
|
|
10
|
+
// • Consistent colon / arrow spacing
|
|
11
|
+
|
|
12
|
+
function format(source) {
|
|
13
|
+
const lines = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
14
|
+
|
|
15
|
+
// ── 1. Detect indentation unit from the first indented line ─────────────
|
|
16
|
+
let indentUnit = 4;
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const m = line.match(/^( +)\S/);
|
|
19
|
+
if (m) {
|
|
20
|
+
const w = m[1].length;
|
|
21
|
+
if (w > 0 && w < indentUnit) indentUnit = w;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (indentUnit < 1) indentUnit = 4;
|
|
26
|
+
|
|
27
|
+
// ── 2. Strip trailing whitespace + normalize indent to 4 spaces ─────────
|
|
28
|
+
const normalized = lines.map(line => {
|
|
29
|
+
const stripped = line.trimEnd();
|
|
30
|
+
if (!stripped) return '';
|
|
31
|
+
const m = stripped.match(/^( *)/);
|
|
32
|
+
const spaces = m ? m[1].length : 0;
|
|
33
|
+
const level = Math.round(spaces / indentUnit);
|
|
34
|
+
const content = stripped.trimStart();
|
|
35
|
+
|
|
36
|
+
// Normalize spacing around operators in the content (only when not a string literal line)
|
|
37
|
+
const formatted = content.startsWith('//') || content.startsWith('*')
|
|
38
|
+
? content
|
|
39
|
+
: normalizeOperators(content);
|
|
40
|
+
|
|
41
|
+
return ' '.repeat(level) + formatted;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ── 3. Collapse multiple blank lines → max 1 ────────────────────────────
|
|
45
|
+
const collapsed = [];
|
|
46
|
+
let blankRun = 0;
|
|
47
|
+
for (const line of normalized) {
|
|
48
|
+
if (line === '') {
|
|
49
|
+
blankRun++;
|
|
50
|
+
if (blankRun <= 1) collapsed.push('');
|
|
51
|
+
} else {
|
|
52
|
+
blankRun = 0;
|
|
53
|
+
collapsed.push(line);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 4. Add blank line after top-level fn/class/type/async fn blocks ──────
|
|
58
|
+
const withSpacing = [];
|
|
59
|
+
for (let i = 0; i < collapsed.length; i++) {
|
|
60
|
+
withSpacing.push(collapsed[i]);
|
|
61
|
+
const cur = collapsed[i].trimStart();
|
|
62
|
+
const next = (collapsed[i + 1] || '').trimStart();
|
|
63
|
+
const indent = collapsed[i].match(/^( *)/)[1].length;
|
|
64
|
+
|
|
65
|
+
// After dedented fn/class/type at indent=0 or indent=4, add blank line
|
|
66
|
+
if (indent <= 4 && /^(fn |async fn |class |type )/.test(cur)) {
|
|
67
|
+
// No blank needed if next line is blank already
|
|
68
|
+
if (next !== '' && !/^(fn |async fn |class |type )/.test(next)) {
|
|
69
|
+
// skip — next content comes right after declaration line, part of fn
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// After closing a fn/class block at top level (no indent dedent signal,
|
|
74
|
+
// but we detect when a non-indented line follows an indented block)
|
|
75
|
+
if (i > 0) {
|
|
76
|
+
const prevIndent = collapsed[i - 1].match(/^( *)/)[1].length;
|
|
77
|
+
if (prevIndent >= 4 && indent === 0 && cur !== '') {
|
|
78
|
+
// We just came back to top-level — add blank if last added wasn't blank
|
|
79
|
+
if (withSpacing[withSpacing.length - 2] !== '') {
|
|
80
|
+
withSpacing.splice(withSpacing.length - 1, 0, '');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── 5. Trim leading/trailing blank lines + ensure single trailing newline ─
|
|
87
|
+
let out = withSpacing;
|
|
88
|
+
while (out.length > 0 && out[0] === '') out.shift();
|
|
89
|
+
while (out.length > 0 && out[out.length - 1] === '') out.pop();
|
|
90
|
+
out.push('');
|
|
91
|
+
|
|
92
|
+
return out.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Operator spacing normalizer ──────────────────────────────────────────────
|
|
96
|
+
// Ensures spaces around binary operators without touching strings/comments
|
|
97
|
+
function normalizeOperators(line) {
|
|
98
|
+
// Skip lines that look like CSS / strings (heuristic: too many : or { })
|
|
99
|
+
if ((line.match(/:/g) || []).length > 2) return line;
|
|
100
|
+
|
|
101
|
+
let result = line;
|
|
102
|
+
|
|
103
|
+
// Add space around = (but not ==, !=, <=, >=, ->, =>, +=, -=, *=, /=, %=)
|
|
104
|
+
result = result.replace(/([^=!<>+\-*/%])=(?!=|>)/g, '$1 = ');
|
|
105
|
+
result = result.replace(/\s{2,}=/g, ' =');
|
|
106
|
+
|
|
107
|
+
// Normalize multiple spaces inside (but not at start = indentation)
|
|
108
|
+
result = result.replace(/([^\s]) +/g, '$1 ');
|
|
109
|
+
|
|
110
|
+
// Ensure space after comma
|
|
111
|
+
result = result.replace(/,(?!\s)/g, ', ');
|
|
112
|
+
|
|
113
|
+
// Ensure space before/after -> and =>
|
|
114
|
+
result = result.replace(/\s*->\s*/g, ' -> ');
|
|
115
|
+
result = result.replace(/\s*=>\s*/g, ' => ');
|
|
116
|
+
|
|
117
|
+
// Trim end
|
|
118
|
+
result = result.trimEnd();
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Diff output ──────────────────────────────────────────────────────────────
|
|
124
|
+
function diff(original, formatted) {
|
|
125
|
+
const origLines = original.split('\n');
|
|
126
|
+
const fmtLines = formatted.split('\n');
|
|
127
|
+
const changes = [];
|
|
128
|
+
|
|
129
|
+
const maxLen = Math.max(origLines.length, fmtLines.length);
|
|
130
|
+
for (let i = 0; i < maxLen; i++) {
|
|
131
|
+
const o = origLines[i];
|
|
132
|
+
const f = fmtLines[i];
|
|
133
|
+
if (o !== f) {
|
|
134
|
+
changes.push({ line: i + 1, original: o, formatted: f });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return changes;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { format, diff };
|