swl-core 1.2.0
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 +182 -0
- package/bin/swls.js +55 -0
- package/examples/completo.css +138 -0
- package/examples/completo.swls +142 -0
- package/icons/swls.svg +11 -0
- package/index.js +1 -0
- package/lib/compiler.js +478 -0
- package/lib/dev-server.js +96 -0
- package/lib/framework.js +83 -0
- package/lib/graph-engine.js +46 -0
- package/package.json +22 -0
- package/system/install-assets.js +44 -0
- package/system/swls.xml +9 -0
- package/vscode-extension/.vscodeignore +3 -0
- package/vscode-extension/icons/icon-theme.json +14 -0
- package/vscode-extension/icons/swls-icon.svg +11 -0
- package/vscode-extension/language-configuration.json +26 -0
- package/vscode-extension/package.json +42 -0
- package/vscode-extension/snippets/swls.json +120 -0
- package/vscode-extension/src/extension.js +147 -0
- package/vscode-extension/syntaxes/swls.tmLanguage.json +72 -0
package/lib/compiler.js
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// ── Dicionários ───────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const PROPS = {
|
|
7
|
+
fundo:"background-color", bg:"background-color", cor:"color", "text-cor":"color",
|
|
8
|
+
fonte:"font", tamanho:"font-size", peso:"font-weight", estilo:"font-style",
|
|
9
|
+
"altura-linha":"line-height", "espacamento-letras":"letter-spacing", familia:"font-family",
|
|
10
|
+
decoracao:"text-decoration", "transformacao-texto":"text-transform", "alinhamento-texto":"text-align",
|
|
11
|
+
largura:"width", w:"width", altura:"height", h:"height",
|
|
12
|
+
"max-largura":"max-width", "max-w":"max-width", "min-largura":"min-width", "min-w":"min-width",
|
|
13
|
+
"max-altura":"max-height", "max-h":"max-height", "min-altura":"min-height", "min-h":"min-height",
|
|
14
|
+
"proporcao-aspecto":"aspect-ratio", aspecto:"aspect-ratio",
|
|
15
|
+
margem:"margin", m:"margin", "margem-topo":"margin-top", mt:"margin-top",
|
|
16
|
+
"margem-direita":"margin-right", mr:"margin-right", "margem-inferior":"margin-bottom", mb:"margin-bottom",
|
|
17
|
+
"margem-esquerda":"margin-left", ml:"margin-left",
|
|
18
|
+
padding:"padding", p:"padding", "padding-topo":"padding-top", pt:"padding-top",
|
|
19
|
+
"padding-direita":"padding-right", pr:"padding-right", "padding-inferior":"padding-bottom", pb:"padding-bottom",
|
|
20
|
+
"padding-esquerda":"padding-left", pl:"padding-left",
|
|
21
|
+
gap:"gap", alinhamento:"text-align", "alinhamento-vertical":"vertical-align",
|
|
22
|
+
"alinhamento-item":"align-items", "alinhamento-conteudo":"align-content", "alinhamento-self":"align-self",
|
|
23
|
+
justificacao:"justify-content", exibicao:"display", display:"display",
|
|
24
|
+
direcao:"flex-direction", envolvimento:"flex-wrap",
|
|
25
|
+
"flex-crescimento":"flex-grow", "flex-encolhimento":"flex-shrink", "flex-base":"flex-basis",
|
|
26
|
+
"espacamento-item":"gap", flex:"flex",
|
|
27
|
+
colunas:"grid-template-columns", linhas:"grid-template-rows", lacuna:"gap",
|
|
28
|
+
"auto-coloca":"grid-auto-flow",
|
|
29
|
+
"coluna-inicial":"grid-column-start", "coluna-final":"grid-column-end",
|
|
30
|
+
"linha-inicial":"grid-row-start", "linha-final":"grid-row-end",
|
|
31
|
+
borda:"border", "borda-topo":"border-top", "borda-direita":"border-right",
|
|
32
|
+
"borda-inferior":"border-bottom", "borda-esquerda":"border-left",
|
|
33
|
+
"borda-raio":"border-radius", "borda-cor":"border-color",
|
|
34
|
+
"borda-estilo":"border-style", "borda-largura":"border-width",
|
|
35
|
+
posicao:"position", pos:"position",
|
|
36
|
+
topo:"top", top:"top", direita:"right", right:"right",
|
|
37
|
+
inferior:"bottom", bottom:"bottom", esquerda:"left", left:"left",
|
|
38
|
+
"indice-z":"z-index", z:"z-index",
|
|
39
|
+
opacidade:"opacity", sombra:"box-shadow", "sombra-texto":"text-shadow",
|
|
40
|
+
filtro:"filter", transformacao:"transform",
|
|
41
|
+
backtamanho:"background-size", backposicao:"background-position",
|
|
42
|
+
overflow:"overflow", "overflow-x":"overflow-x", "overflow-y":"overflow-y",
|
|
43
|
+
cursor:"cursor", "pointer-eventos":"pointer-events",
|
|
44
|
+
transicao:"transition", "transicao-funcao":"transition-timing-function",
|
|
45
|
+
"transicao-atraso":"transition-delay", animar:"animation",
|
|
46
|
+
outline:"outline", visibilidade:"visibility", clip:"clip-path",
|
|
47
|
+
"objeto-fit":"object-fit", "objeto-posicao":"object-position",
|
|
48
|
+
"list-estilo":"list-style", "white-space":"white-space",
|
|
49
|
+
"word-wrap":"word-wrap", "word-break":"word-break",
|
|
50
|
+
hifens:"hyphens", "quebra-texto":"text-overflow",
|
|
51
|
+
"mistura-modo":"mix-blend-mode", perspectiva:"perspective",
|
|
52
|
+
"transformacao-origem":"transform-origin", "backface-visibilidade":"backface-visibility",
|
|
53
|
+
"conteudo-caixa":"box-sizing", "box-sizing":"box-sizing",
|
|
54
|
+
"fundo-imagem":"background-image", "fundo-repetir":"background-repeat",
|
|
55
|
+
"fundo-anexar":"background-attachment", "fundo-origem":"background-origin",
|
|
56
|
+
"fundo-clip":"background-clip", resize:"resize",
|
|
57
|
+
"coluna-conta":"column-count", "coluna-gap":"column-gap",
|
|
58
|
+
"coluna-regra":"column-rule", "linha-clamp":"line-clamp",
|
|
59
|
+
"aparencia":"appearance", "user-select":"user-select",
|
|
60
|
+
"scroll-comportamento":"scroll-behavior", "scroll-margem":"scroll-margin",
|
|
61
|
+
"scroll-padding":"scroll-padding",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const COLORS = {
|
|
65
|
+
branco:"#FFFFFF", white:"#FFFFFF", preto:"#000000", black:"#000000",
|
|
66
|
+
cinza:"#808080", gray:"#808080", "cinza-claro":"#D3D3D3", "cinza-escuro":"#696969",
|
|
67
|
+
vermelho:"#FF0000", red:"#FF0000",
|
|
68
|
+
azul:"#0066FF", blue:"#0066FF", "azul-claro":"#87CEEB", "azul-escuro":"#00008B",
|
|
69
|
+
verde:"#008000", green:"#008000", "verde-claro":"#90EE90",
|
|
70
|
+
amarelo:"#FFFF00", yellow:"#FFFF00",
|
|
71
|
+
laranja:"#FFA500", orange:"#FFA500",
|
|
72
|
+
roxo:"#800080", purple:"#800080", violeta:"#EE82EE",
|
|
73
|
+
rosa:"#FFC0CB", pink:"#FFC0CB",
|
|
74
|
+
marrom:"#A52A2A", brown:"#A52A2A",
|
|
75
|
+
ouro:"#FFD700", gold:"#FFD700", prata:"#C0C0C0", silver:"#C0C0C0",
|
|
76
|
+
transparente:"transparent", transparent:"transparent",
|
|
77
|
+
ciano:"#00FFFF", cyan:"#00FFFF", magenta:"#FF00FF",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const VALUES = {
|
|
81
|
+
nenhuma:"none", nenhum:"none", oculta:"hidden", auto:"auto",
|
|
82
|
+
herdado:"inherit", inicial:"initial", conteudo:"content",
|
|
83
|
+
bloco:"block", inline:"inline", "inline-bloco":"inline-block",
|
|
84
|
+
flex:"flex", grid:"grid",
|
|
85
|
+
linha:"row", coluna:"column", "linha-reversa":"row-reverse", "coluna-reversa":"column-reverse",
|
|
86
|
+
envolver:"wrap", "envolver-reverso":"wrap-reverse",
|
|
87
|
+
"flexo-inicio":"flex-start", inicio:"flex-start", "flexo-fim":"flex-end", fim:"flex-end",
|
|
88
|
+
distribuido:"space-between", espacado:"space-around", "distribuido-uniformemente":"space-evenly",
|
|
89
|
+
esticado:"stretch", centro:"center", meio:"center",
|
|
90
|
+
justificado:"justify", solida:"solid", pontilhada:"dotted", tracejada:"dashed",
|
|
91
|
+
dupla:"double", redonda:"50%",
|
|
92
|
+
relativa:"relative", absoluta:"absolute", fixa:"fixed", pegajosa:"sticky", estatica:"static",
|
|
93
|
+
escondido:"hidden", visivel:"visible",
|
|
94
|
+
ponteiro:"pointer", mao:"pointer", hand:"pointer",
|
|
95
|
+
"nao-permitido":"not-allowed", texto:"text", movimento:"move", normal:"normal",
|
|
96
|
+
suave:"ease", "suave-entrada":"ease-in", "suave-saida":"ease-out", "suave-entrada-saida":"ease-in-out",
|
|
97
|
+
linear:"linear", infinito:"infinite",
|
|
98
|
+
denso:"dense", underline:"underline", overline:"overline", "line-through":"line-through",
|
|
99
|
+
maiuscula:"uppercase", minuscula:"lowercase", capitalizar:"capitalize",
|
|
100
|
+
nowrap:"nowrap", pre:"pre", "pre-wrap":"pre-wrap",
|
|
101
|
+
"break-word":"break-word", "break-all":"break-all",
|
|
102
|
+
cover:"cover", contain:"contain", fill:"fill", "scale-down":"scale-down",
|
|
103
|
+
borda:"border-box", conteudo:"content-box",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const PSEUDOS = {
|
|
107
|
+
pairado:"hover", ativo:"active", focado:"focus", visitado:"visited",
|
|
108
|
+
desabilitado:"disabled", marcado:"checked", indeterminado:"indeterminate",
|
|
109
|
+
"primeiro-filho":"first-child", "ultimo-filho":"last-child",
|
|
110
|
+
primeiro:"first-child", ultimo:"last-child",
|
|
111
|
+
"nth-filho":"nth-child", "somente-filho":"only-child",
|
|
112
|
+
"primeiro-tipo":"first-of-type", "ultimo-tipo":"last-of-type",
|
|
113
|
+
preenchido:"valid", invalido:"invalid",
|
|
114
|
+
"modo-escuro":"prefers-color-scheme: dark",
|
|
115
|
+
"modo-claro":"prefers-color-scheme: light",
|
|
116
|
+
"focado-visivel":"focus-visible", placeholder:"placeholder",
|
|
117
|
+
antes:"before", depois:"after", selecao:"selection",
|
|
118
|
+
"sem-filhos":"empty", raiz:"root",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ── AST Nodes ─────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
class Node { constructor(type) { this.type = type; } }
|
|
124
|
+
class Program extends Node { constructor() { super("Program"); this.body = []; } }
|
|
125
|
+
class Variable extends Node { constructor(n, v) { super("Variable"); this.name = n; this.value = v; } }
|
|
126
|
+
class Import extends Node { constructor(p) { super("Import"); this.path = p; } }
|
|
127
|
+
class Property extends Node { constructor(n, v, l) { super("Property"); this.name = n; this.value = v; this.line = l; } }
|
|
128
|
+
class PseudoState extends Node { constructor(n) { super("PseudoState"); this.name = n; this.props = []; } }
|
|
129
|
+
class MediaQuery extends Node { constructor(c) { super("MediaQuery"); this.condition = c; this.rules = []; } }
|
|
130
|
+
|
|
131
|
+
class Rule extends Node {
|
|
132
|
+
constructor(sel) { super("Rule"); this.selector = sel; this.props = []; this.pseudos = []; this.children = []; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class Component extends Node {
|
|
136
|
+
constructor(name, parent) { super("Component"); this.name = name; this.parent = parent; this.body = []; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class Theme extends Node {
|
|
140
|
+
constructor(name) { super("Theme"); this.name = name; this.vars = []; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class Animation extends Node {
|
|
144
|
+
constructor(name) { super("Animation"); this.name = name; this.frames = []; }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class Frame extends Node {
|
|
148
|
+
constructor(pos) { super("Frame"); this.position = pos; this.props = []; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Lexer ─────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
class Lexer {
|
|
154
|
+
tokenize(code) {
|
|
155
|
+
const tokens = [];
|
|
156
|
+
let inBlock = false;
|
|
157
|
+
let ln = 0;
|
|
158
|
+
|
|
159
|
+
for (let raw of code.split("\n")) {
|
|
160
|
+
ln++;
|
|
161
|
+
let line = raw.trim();
|
|
162
|
+
if (line.includes("/*")) inBlock = true;
|
|
163
|
+
if (line.includes("*/")) { inBlock = false; continue; }
|
|
164
|
+
if (inBlock || !line || line.startsWith("//")) continue;
|
|
165
|
+
if (line.includes("//")) line = line.slice(0, line.indexOf("//")).trim();
|
|
166
|
+
if (!line) continue;
|
|
167
|
+
|
|
168
|
+
if (line.startsWith("@importar")) {
|
|
169
|
+
const m = line.match(/@importar\s+"([^"]+)"/);
|
|
170
|
+
if (m) tokens.push({ type:"IMPORT", value:m[1], line:ln });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (line.startsWith("@tema ")) {
|
|
174
|
+
const m = line.match(/^@tema\s+([\w-]+)/);
|
|
175
|
+
if (m) tokens.push({ type:"THEME", value:m[1], line:ln });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (line.startsWith("@componente ")) {
|
|
179
|
+
const m = line.match(/^@componente\s+([\w-]+)(?:\s+herda\s+([\w-]+))?/);
|
|
180
|
+
if (m) tokens.push({ type:"COMPONENT", value:m[1], parent:m[2]||null, line:ln });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (line.startsWith("@animar ")) {
|
|
184
|
+
const m = line.match(/^@animar\s+([\w-]+)/);
|
|
185
|
+
if (m) tokens.push({ type:"ANIMATION", value:m[1], line:ln });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (line.startsWith("@media ")) {
|
|
189
|
+
tokens.push({ type:"MEDIA", value:line.slice(7).trim(), line:ln });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (line.startsWith("@") && line.includes(":")) {
|
|
193
|
+
const m = line.match(/^@([\w-]+)\s*:\s*(.+)$/);
|
|
194
|
+
if (m) tokens.push({ type:"VARIABLE", name:m[1], value:m[2].trim(), line:ln });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (line === "(") { tokens.push({ type:"BLOCK_START", line:ln }); continue; }
|
|
198
|
+
if (line === ")") { tokens.push({ type:"BLOCK_END", line:ln }); continue; }
|
|
199
|
+
|
|
200
|
+
const ruleM = line.match(/^\((.+):$/);
|
|
201
|
+
if (ruleM) { tokens.push({ type:"RULE", selector:ruleM[1].trim(), line:ln }); continue; }
|
|
202
|
+
|
|
203
|
+
const pseudoM = line.match(/^&:([\w-]+)/);
|
|
204
|
+
if (pseudoM) { tokens.push({ type:"PSEUDO", value:pseudoM[1], line:ln }); continue; }
|
|
205
|
+
|
|
206
|
+
const frameM = line.match(/^(\d+%|de|para):$/);
|
|
207
|
+
if (frameM) { tokens.push({ type:"FRAME", value:frameM[1], line:ln }); continue; }
|
|
208
|
+
|
|
209
|
+
const propM = line.match(/^([\w-]+)\s+(.+)$/);
|
|
210
|
+
if (propM) { tokens.push({ type:"PROPERTY", name:propM[1], value:propM[2].trim(), line:ln }); continue; }
|
|
211
|
+
}
|
|
212
|
+
return tokens;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Parser ────────────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
class Parser {
|
|
219
|
+
parse(tokens) {
|
|
220
|
+
this.tokens = tokens;
|
|
221
|
+
this.pos = 0;
|
|
222
|
+
const prog = new Program();
|
|
223
|
+
while (this.pos < this.tokens.length) {
|
|
224
|
+
const t = this.tokens[this.pos];
|
|
225
|
+
switch (t.type) {
|
|
226
|
+
case "IMPORT": prog.body.push(new Import(t.value)); this.pos++; break;
|
|
227
|
+
case "VARIABLE": prog.body.push(new Variable(t.name, t.value)); this.pos++; break;
|
|
228
|
+
case "COMPONENT": prog.body.push(this.parseComponent()); break;
|
|
229
|
+
case "THEME": prog.body.push(this.parseTheme()); break;
|
|
230
|
+
case "ANIMATION": prog.body.push(this.parseAnimation()); break;
|
|
231
|
+
case "MEDIA": prog.body.push(this.parseMedia()); break;
|
|
232
|
+
case "RULE": prog.body.push(this.parseRule()); break;
|
|
233
|
+
default: this.pos++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return prog;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
cur() { return this.tokens[this.pos]; }
|
|
240
|
+
|
|
241
|
+
parseBlock(stopTypes = []) {
|
|
242
|
+
const nodes = [];
|
|
243
|
+
while (this.pos < this.tokens.length) {
|
|
244
|
+
const t = this.cur();
|
|
245
|
+
if (!t || stopTypes.includes(t.type) || t.type === "BLOCK_END") break;
|
|
246
|
+
if (t.type === "PROPERTY") { nodes.push(new Property(t.name, t.value, t.line)); this.pos++; continue; }
|
|
247
|
+
if (t.type === "PSEUDO") { nodes.push(this.parsePseudo()); continue; }
|
|
248
|
+
if (t.type === "MEDIA") { nodes.push(this.parseMedia()); continue; }
|
|
249
|
+
if (t.type === "RULE") { nodes.push(this.parseRule()); continue; }
|
|
250
|
+
if (t.type === "VARIABLE") { nodes.push(new Variable(t.name, t.value)); this.pos++; continue; }
|
|
251
|
+
this.pos++;
|
|
252
|
+
}
|
|
253
|
+
return nodes;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
parseRule() {
|
|
257
|
+
const t = this.tokens[this.pos++];
|
|
258
|
+
const rule = new Rule(t.selector);
|
|
259
|
+
this.parseBlock(["RULE","COMPONENT","THEME","ANIMATION","MEDIA"]).forEach(n => {
|
|
260
|
+
if (n instanceof Property) rule.props.push(n);
|
|
261
|
+
else if (n instanceof PseudoState) rule.pseudos.push(n);
|
|
262
|
+
else rule.children.push(n);
|
|
263
|
+
});
|
|
264
|
+
return rule;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
parsePseudo() {
|
|
268
|
+
const t = this.tokens[this.pos++];
|
|
269
|
+
const pseudo = new PseudoState(t.value);
|
|
270
|
+
this.parseBlock(["PSEUDO","RULE","MEDIA"]).forEach(n => {
|
|
271
|
+
if (n instanceof Property) pseudo.props.push(n);
|
|
272
|
+
});
|
|
273
|
+
return pseudo;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
parseComponent() {
|
|
277
|
+
const t = this.tokens[this.pos++];
|
|
278
|
+
const comp = new Component(t.value, t.parent);
|
|
279
|
+
this.parseBlock(["COMPONENT","RULE","THEME","ANIMATION","MEDIA","IMPORT"]).forEach(n => comp.body.push(n));
|
|
280
|
+
return comp;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
parseTheme() {
|
|
284
|
+
const t = this.tokens[this.pos++];
|
|
285
|
+
const theme = new Theme(t.name || t.value);
|
|
286
|
+
this.parseBlock(["THEME","RULE","COMPONENT","ANIMATION","MEDIA","IMPORT"]).forEach(n => {
|
|
287
|
+
if (n instanceof Variable) theme.vars.push(n);
|
|
288
|
+
});
|
|
289
|
+
return theme;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
parseAnimation() {
|
|
293
|
+
const t = this.tokens[this.pos++];
|
|
294
|
+
const anim = new Animation(t.value);
|
|
295
|
+
let frame = null;
|
|
296
|
+
while (this.pos < this.tokens.length) {
|
|
297
|
+
const cur = this.cur();
|
|
298
|
+
if (!cur || ["ANIMATION","BLOCK_END","RULE","COMPONENT","THEME","MEDIA","IMPORT"].includes(cur.type)) break;
|
|
299
|
+
if (cur.type === "FRAME") {
|
|
300
|
+
frame = new Frame(cur.value);
|
|
301
|
+
anim.frames.push(frame);
|
|
302
|
+
this.pos++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (cur.type === "PROPERTY" && frame) {
|
|
306
|
+
frame.props.push(new Property(cur.name, cur.value, cur.line));
|
|
307
|
+
}
|
|
308
|
+
this.pos++;
|
|
309
|
+
}
|
|
310
|
+
return anim;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
parseMedia() {
|
|
314
|
+
const t = this.tokens[this.pos++];
|
|
315
|
+
const media = new MediaQuery(t.value);
|
|
316
|
+
this.parseBlock(["MEDIA"]).forEach(n => {
|
|
317
|
+
if (n instanceof Rule) media.rules.push(n);
|
|
318
|
+
});
|
|
319
|
+
return media;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Variable Resolver ─────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
class VarResolver {
|
|
326
|
+
resolve(ast) {
|
|
327
|
+
this.vars = {};
|
|
328
|
+
for (const n of ast.body) {
|
|
329
|
+
if (n.type === "Variable") this.vars[n.name] = n.value;
|
|
330
|
+
if (n.type === "Theme") n.vars.forEach(v => { this.vars[v.name] = v.value; });
|
|
331
|
+
}
|
|
332
|
+
this.walk(ast);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
walk(node) {
|
|
336
|
+
if (!node) return;
|
|
337
|
+
const resolve = list => list && list.forEach(p => {
|
|
338
|
+
if (p.value) p.value = p.value.replace(/@([\w-]+)/g, (_, k) => this.vars[k] || `@${k}`);
|
|
339
|
+
});
|
|
340
|
+
switch (node.type) {
|
|
341
|
+
case "Program": node.body.forEach(c => this.walk(c)); break;
|
|
342
|
+
case "Rule": resolve(node.props); node.children.forEach(c => this.walk(c)); node.pseudos.forEach(p => this.walk(p)); break;
|
|
343
|
+
case "Component": resolve(node.body.filter(n => n.type === "Property")); node.body.forEach(n => this.walk(n)); break;
|
|
344
|
+
case "PseudoState":resolve(node.props); break;
|
|
345
|
+
case "MediaQuery": node.rules.forEach(r => this.walk(r)); break;
|
|
346
|
+
case "Animation": node.frames.forEach(f => resolve(f.props)); break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Generator ─────────────────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
class Generator {
|
|
354
|
+
val(raw) {
|
|
355
|
+
if (!raw) return "";
|
|
356
|
+
const v = raw.trim();
|
|
357
|
+
if (COLORS[v]) return COLORS[v];
|
|
358
|
+
if (VALUES[v]) return VALUES[v];
|
|
359
|
+
if (v.includes(" ")) {
|
|
360
|
+
return v.split(/\s+/).map(p => COLORS[p] || VALUES[p] || p).join(" ");
|
|
361
|
+
}
|
|
362
|
+
return v;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
prop(p) {
|
|
366
|
+
return ` ${PROPS[p.name] || p.name}: ${this.val(p.value)};\n`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
generate(ast) {
|
|
370
|
+
return ast.body.map(n => {
|
|
371
|
+
switch (n.type) {
|
|
372
|
+
case "Rule": return this.rule(n);
|
|
373
|
+
case "Component": return this.component(n);
|
|
374
|
+
case "Theme": return this.theme(n);
|
|
375
|
+
case "Animation": return this.animation(n);
|
|
376
|
+
case "MediaQuery": return this.media(n);
|
|
377
|
+
default: return "";
|
|
378
|
+
}
|
|
379
|
+
}).join("");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
rule(node, parent = "") {
|
|
383
|
+
const sel = parent ? `${parent} ${node.selector}` : node.selector;
|
|
384
|
+
let out = "";
|
|
385
|
+
const block = node.props.map(p => this.prop(p)).join("");
|
|
386
|
+
if (block) out += `${sel} {\n${block}}\n\n`;
|
|
387
|
+
node.pseudos.forEach(ps => {
|
|
388
|
+
const name = PSEUDOS[ps.name] || ps.name;
|
|
389
|
+
const isMedia = name.includes(":");
|
|
390
|
+
if (isMedia) {
|
|
391
|
+
out += `@media (${name}) {\n ${sel} {\n`;
|
|
392
|
+
ps.props.forEach(p => { out += " " + this.prop(p); });
|
|
393
|
+
out += " }\n}\n\n";
|
|
394
|
+
} else {
|
|
395
|
+
out += `${sel}:${name} {\n`;
|
|
396
|
+
ps.props.forEach(p => { out += this.prop(p); });
|
|
397
|
+
out += "}\n\n";
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
node.children.forEach(c => { out += this.rule(c, sel); });
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
component(comp) {
|
|
405
|
+
let out = "";
|
|
406
|
+
const block = comp.body.filter(n => n instanceof Property).map(p => this.prop(p)).join("");
|
|
407
|
+
if (block) out += `.${comp.name} {\n${block}}\n\n`;
|
|
408
|
+
comp.body.filter(n => n instanceof Rule).forEach(r => { out += this.rule(r, `.${comp.name}`); });
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
theme(theme) {
|
|
413
|
+
let out = `:root[data-tema="${theme.name}"] {\n`;
|
|
414
|
+
theme.vars.forEach(v => { out += ` --${v.name}: ${this.val(v.value)};\n`; });
|
|
415
|
+
return out + "}\n\n";
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
animation(anim) {
|
|
419
|
+
let out = `@keyframes ${anim.name} {\n`;
|
|
420
|
+
anim.frames.forEach(f => {
|
|
421
|
+
const pos = f.position === "de" ? "from" : f.position === "para" ? "to" : f.position;
|
|
422
|
+
out += ` ${pos} {\n`;
|
|
423
|
+
f.props.forEach(p => { out += " " + this.prop(p); });
|
|
424
|
+
out += " }\n";
|
|
425
|
+
});
|
|
426
|
+
return out + "}\n\n";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
media(mq) {
|
|
430
|
+
let out = `@media ${mq.condition} {\n`;
|
|
431
|
+
mq.rules.forEach(r => {
|
|
432
|
+
out += this.rule(r).split("\n").map(l => l ? " " + l : "").join("\n");
|
|
433
|
+
});
|
|
434
|
+
return out + "}\n\n";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── Compiler ──────────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
class Compiler {
|
|
441
|
+
constructor() {
|
|
442
|
+
this.lexer = new Lexer();
|
|
443
|
+
this.parser = new Parser();
|
|
444
|
+
this.vars = new VarResolver();
|
|
445
|
+
this.gen = new Generator();
|
|
446
|
+
this.plugins = [];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
use(plugin) {
|
|
450
|
+
if (plugin && typeof plugin.apply === "function") this.plugins.push(plugin);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
compile(source, filename = "unknown.swls") {
|
|
454
|
+
try {
|
|
455
|
+
let src = source;
|
|
456
|
+
this.plugins.forEach(p => p.beforeParse && (src = p.beforeParse(src) || src));
|
|
457
|
+
|
|
458
|
+
const tokens = this.lexer.tokenize(src);
|
|
459
|
+
let ast = this.parser.parse(tokens);
|
|
460
|
+
|
|
461
|
+
this.plugins.forEach(p => p.afterParse && (ast = p.afterParse(ast) || ast));
|
|
462
|
+
this.vars.resolve(ast);
|
|
463
|
+
|
|
464
|
+
let css = this.gen.generate(ast);
|
|
465
|
+
this.plugins.forEach(p => p.afterGenerate && (css = p.afterGenerate(css) || css));
|
|
466
|
+
|
|
467
|
+
return { success: true, css, errors: [] };
|
|
468
|
+
} catch (err) {
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
css: "",
|
|
472
|
+
errors: [{ message: err.message, file: filename, line: err.line || null }],
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = { Compiler, PROPS, COLORS, VALUES, PSEUDOS };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const WebSocket = require("ws");
|
|
3
|
+
|
|
4
|
+
class DevServer {
|
|
5
|
+
constructor(framework) {
|
|
6
|
+
this.fw = framework;
|
|
7
|
+
this.httpPort = 3000;
|
|
8
|
+
this.wsPort = 3030;
|
|
9
|
+
this.clients = new Set();
|
|
10
|
+
this.lastCSS = "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
start() {
|
|
14
|
+
this.ws = new WebSocket.Server({ port: this.wsPort });
|
|
15
|
+
this.ws.on("connection", (client) => {
|
|
16
|
+
this.clients.add(client);
|
|
17
|
+
client.send(JSON.stringify({ type: "connected" }));
|
|
18
|
+
client.on("close", () => this.clients.delete(client));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
this.http = http.createServer((req, res) => {
|
|
22
|
+
if (req.url === "/swls.css") {
|
|
23
|
+
const css = this.fw.getCurrentStyle();
|
|
24
|
+
res.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
|
|
25
|
+
return res.end(css);
|
|
26
|
+
}
|
|
27
|
+
if (req.url === "/" || req.url === "/index.html") {
|
|
28
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
29
|
+
return res.end(this.html());
|
|
30
|
+
}
|
|
31
|
+
res.writeHead(404); res.end("Not found");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.http.listen(this.httpPort, () => {
|
|
35
|
+
console.log(`📡 Dev Server: http://localhost:${this.httpPort}`);
|
|
36
|
+
console.log(`🔌 HMR WebSocket: ws://localhost:${this.wsPort}`);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
broadcast(file, css) {
|
|
41
|
+
this.lastCSS = css;
|
|
42
|
+
if (!this.clients.size) return;
|
|
43
|
+
const msg = JSON.stringify({ type: "hmr-update", file, css, ts: Date.now() });
|
|
44
|
+
for (const c of this.clients) {
|
|
45
|
+
if (c.readyState === WebSocket.OPEN) c.send(msg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
html() {
|
|
50
|
+
return `<!DOCTYPE html>
|
|
51
|
+
<html lang="pt-BR">
|
|
52
|
+
<head>
|
|
53
|
+
<meta charset="UTF-8">
|
|
54
|
+
<title>SWLS Dev Server</title>
|
|
55
|
+
<link rel="stylesheet" href="/swls.css" id="swls-style">
|
|
56
|
+
<style>
|
|
57
|
+
body { font-family: system-ui, sans-serif; background: #13141c; color: #f4f4f7; padding: 2rem; margin: 0; }
|
|
58
|
+
.box { max-width: 760px; margin: 0 auto; background: #1b1d2a; padding: 2rem; border-radius: 10px; border: 1px solid #2e3248; }
|
|
59
|
+
h1 { color: #0066FF; margin: 0 0 1rem; }
|
|
60
|
+
.badge { font-size: .75rem; padding: 3px 8px; border-radius: 12px; background: #007A3D; font-weight: bold; }
|
|
61
|
+
.log { background: #0c0d14; padding: 1rem; border-radius: 6px; font-family: monospace; font-size: .85rem;
|
|
62
|
+
color: #a2a6b8; height: 140px; overflow-y: auto; border: 1px solid #1f2130; margin-top: 1.5rem; }
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div class="box">
|
|
67
|
+
<h1>SWLS Dev Server <span class="badge" id="st">Conectando...</span></h1>
|
|
68
|
+
<p>Monitorando arquivos <code>.swls</code>. Alterações são injetadas sem recarregar a página.</p>
|
|
69
|
+
<div class="log" id="log">[SWLS] Inicializando...<br></div>
|
|
70
|
+
</div>
|
|
71
|
+
<script>
|
|
72
|
+
const log = (m) => { const el = document.getElementById('log'); el.innerHTML += \`[\${new Date().toLocaleTimeString()}] \${m}<br>\`; el.scrollTop = el.scrollHeight; };
|
|
73
|
+
const st = document.getElementById('st');
|
|
74
|
+
const ws = new WebSocket('ws://localhost:${this.wsPort}');
|
|
75
|
+
ws.onopen = () => { st.textContent = 'HMR Ativo'; st.style.background = '#007A3D'; log('Conectado.'); };
|
|
76
|
+
ws.onclose = () => { st.textContent = 'Desconectado'; st.style.background = '#B30000'; };
|
|
77
|
+
ws.onmessage = ({ data }) => {
|
|
78
|
+
const d = JSON.parse(data);
|
|
79
|
+
if (d.type === 'hmr-update') {
|
|
80
|
+
log('⚡ Patch recebido.');
|
|
81
|
+
let tag = document.getElementById('swls-style');
|
|
82
|
+
if (!tag) { tag = document.createElement('style'); tag.id = 'swls-style'; document.head.appendChild(tag); }
|
|
83
|
+
if (tag.tagName === 'LINK') {
|
|
84
|
+
const s = document.createElement('style');
|
|
85
|
+
s.id = 'swls-style'; s.textContent = d.css;
|
|
86
|
+
tag.replaceWith(s);
|
|
87
|
+
} else { tag.textContent = d.css; }
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
</script>
|
|
91
|
+
</body>
|
|
92
|
+
</html>`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = DevServer;
|
package/lib/framework.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { Compiler } = require("./compiler");
|
|
4
|
+
const GraphEngine = require("./graph-engine");
|
|
5
|
+
const DevServer = require("./dev-server");
|
|
6
|
+
|
|
7
|
+
class SWLSFramework {
|
|
8
|
+
constructor(entry) {
|
|
9
|
+
this.entry = path.resolve(entry);
|
|
10
|
+
this.graph = new GraphEngine();
|
|
11
|
+
this.compiler = new Compiler();
|
|
12
|
+
this.server = new DevServer(this);
|
|
13
|
+
this.cache = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
use(plugin) {
|
|
17
|
+
this.compiler.use(plugin);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
build(outputPath = null) {
|
|
22
|
+
const t0 = Date.now();
|
|
23
|
+
const source = fs.readFileSync(this.entry, "utf-8");
|
|
24
|
+
this.graph.update(this.entry, source);
|
|
25
|
+
|
|
26
|
+
const result = this.compiler.compile(source, this.entry);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
result.errors.forEach(e => console.error(`❌ ${e.message}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const out = outputPath ? path.resolve(outputPath) : this.entry.replace(/\.swls$/, ".css");
|
|
33
|
+
fs.writeFileSync(out, result.css);
|
|
34
|
+
this.cache.set(this.entry, result.css);
|
|
35
|
+
|
|
36
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
37
|
+
console.log("✅ SWLS Core v1.2.0 — Build concluído");
|
|
38
|
+
console.log(` Entrada : ${path.basename(this.entry)}`);
|
|
39
|
+
console.log(` Saída : ${path.basename(out)}`);
|
|
40
|
+
console.log(` Tempo : ${Date.now() - t0}ms`);
|
|
41
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
42
|
+
return result.css;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dev() {
|
|
46
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
47
|
+
console.log("🔄 SWLS Core v1.2.0 — Modo Dev");
|
|
48
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
49
|
+
this.build();
|
|
50
|
+
this.server.start();
|
|
51
|
+
this.watch();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
watch() {
|
|
55
|
+
console.log(`👀 Monitorando: ${path.basename(this.entry)}\n`);
|
|
56
|
+
fs.watch(this.entry, () => {
|
|
57
|
+
try {
|
|
58
|
+
const source = fs.readFileSync(this.entry, "utf-8");
|
|
59
|
+
this.graph.update(this.entry, source);
|
|
60
|
+
const affected = this.graph.getAffected(this.entry);
|
|
61
|
+
for (const file of affected) {
|
|
62
|
+
const src = fs.readFileSync(file, "utf-8");
|
|
63
|
+
const result = this.compiler.compile(src, file);
|
|
64
|
+
if (result.success) {
|
|
65
|
+
this.cache.set(file, result.css);
|
|
66
|
+
this.server.broadcast(file, result.css);
|
|
67
|
+
console.log(`⚡ HMR: ${path.basename(file)}`);
|
|
68
|
+
} else {
|
|
69
|
+
result.errors.forEach(e => console.error(`❌ ${path.basename(file)}: ${e.message}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error("⚠️ Erro no watch:", err.message);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getCurrentStyle() {
|
|
79
|
+
return this.cache.get(this.entry) || "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = SWLSFramework;
|