free-framework 5.0.0 → 5.0.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/compiler/generator.js +88 -199
- package/compiler/lexer.js +2 -1
- package/compiler/parser.js +16 -13
- package/package.json +1 -1
- package/templates/app-template/package.json +1 -1
- package/templates/app-template/resources/views/counter.free +3 -6
- package/templates/app-template/resources/views/docs.free +24 -34
- package/templates/app-template/resources/views/header.free +1 -1
- package/templates/app-template/resources/views/home.free +1 -1
- package/templates/app-template/routes/web.free +4 -4
- package/templates/app-template/.free/app.js +0 -1555
- /package/templates/app-template/{resources → public}/css/app.css +0 -0
package/compiler/generator.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* compiler/generator.js
|
|
3
|
-
* Code Generator for Free Ultra (
|
|
3
|
+
* Code Generator for Free Ultra (v5.0.0 Convergence).
|
|
4
4
|
* Optimized for Islands Architecture and Component Composition.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -22,8 +22,7 @@ function generate(ast) {
|
|
|
22
22
|
"const modelsRegistry = {};\n";
|
|
23
23
|
|
|
24
24
|
const CleanCSS = require('clean-css');
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
|
|
27
26
|
const components = {};
|
|
28
27
|
const actions = {};
|
|
29
28
|
let globalCSS = "";
|
|
@@ -98,205 +97,101 @@ function generate(ast) {
|
|
|
98
97
|
ast.forEach(node => {
|
|
99
98
|
if (node.type === 'action') {
|
|
100
99
|
output += `\nserver.app.post("/_free/action/${node.name}", async (req, res) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (node.params && node.params.length > 0) {
|
|
111
|
-
output += ` const { ${node.params.join(', ')} } = body;\n`;
|
|
112
|
-
}
|
|
113
|
-
output += " " + node.code.replace(/res\.locals/g, 'res.context') + "\n" +
|
|
114
|
-
" })();\n" +
|
|
115
|
-
" if (!res.headersSent) res.json({ success: true, result });\n" +
|
|
116
|
-
" } catch(e) {\n" +
|
|
117
|
-
" if (!res.headersSent) res.status(500).json({ success: false, error: e.message });\n" +
|
|
118
|
-
" }\n" +
|
|
119
|
-
"});\n";
|
|
100
|
+
const body = req.body || {};
|
|
101
|
+
const logic = async (body, res) => { ${node.code} };
|
|
102
|
+
try {
|
|
103
|
+
const result = await logic(body, res);
|
|
104
|
+
if (!res.completed) res.json({ success: true, data: result });
|
|
105
|
+
} catch(e) {
|
|
106
|
+
if (!res.completed) res.status(500).json({ success: false, error: e.message });
|
|
107
|
+
}
|
|
108
|
+
});\n`;
|
|
120
109
|
}
|
|
121
110
|
});
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
output += `\nserver.renderRegistry = {\n`;
|
|
127
|
-
Object.keys(components).forEach((name, i) => {
|
|
128
|
-
output += ` ${name}: render${name}${i === Object.keys(components).length - 1 ? '' : ','}\n`;
|
|
129
|
-
});
|
|
130
|
-
output += `};\n`;
|
|
131
|
-
|
|
132
|
-
output += `\nserver.middleware((req, res, next) => {
|
|
133
|
-
if (!res.context) res.context = {};
|
|
134
|
-
res.context.csrfToken = crypto.randomBytes(32).toString('hex');
|
|
135
|
-
next();
|
|
136
|
-
});\n`;
|
|
137
|
-
|
|
138
|
-
// Actually emit the component rendering functions (hoisted)
|
|
139
|
-
Object.values(components).forEach(node => {
|
|
140
|
-
output += generateUltraComponent(node);
|
|
112
|
+
output += "\n// Components (Islands Animation)\n";
|
|
113
|
+
Object.keys(components).forEach(name => {
|
|
114
|
+
output += generateComponentCode(components[name]);
|
|
141
115
|
});
|
|
142
116
|
|
|
143
|
-
|
|
144
|
-
output += `
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
117
|
+
output += `\n// Routes & Global Rendering\n`;
|
|
118
|
+
output += `const helpers = {
|
|
119
|
+
renderComponent: (name, props, h) => {
|
|
120
|
+
const comp = islandsRegistry[name];
|
|
121
|
+
return comp ? comp(props, h) : \`<!-- error: \${name} not found -->\`;
|
|
122
|
+
},
|
|
123
|
+
e: (val) => String(val).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
124
|
+
};\n`;
|
|
125
|
+
|
|
126
|
+
output += `const islandsRegistry = {\n`;
|
|
127
|
+
Object.keys(components).forEach(name => {
|
|
128
|
+
output += ` '${name}': render_${name},\n`;
|
|
152
129
|
});
|
|
153
|
-
}
|
|
154
|
-
if (server.namedMiddlewares['auth']) server.namedMiddlewares['AuthGuard'] = server.namedMiddlewares['auth'];
|
|
155
|
-
\n`;
|
|
130
|
+
output += `};\n\n`;
|
|
156
131
|
|
|
157
132
|
ast.forEach(node => {
|
|
158
133
|
if (node.type === 'route') {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (req.headers['content-type'] && req.headers['content-type'].includes('application/json')) {
|
|
181
|
-
body = await req.json().catch(() => ({}));
|
|
182
|
-
} else {
|
|
183
|
-
body = await req.urlencoded().catch(() => ({}));
|
|
184
|
-
}
|
|
134
|
+
const method = node.method.toLowerCase() === 'get' || node.method === 'Get' ? 'get' : node.method.toLowerCase();
|
|
135
|
+
if (node.handler && node.handler.type === 'action_ref') {
|
|
136
|
+
output += `server.app.${method}('${node.path}', async (req, res) => {
|
|
137
|
+
const actionResults = await fetch(\`http://localhost:\${process.env.PORT || 3000}/_free/action/${node.handler.name}\`, { method: 'POST', body: JSON.stringify(req.body) }).then(r => r.json());
|
|
138
|
+
res.header('Content-Type', 'text/html');
|
|
139
|
+
res.send(render_${node.view}({ user: req.user, ...actionResults.data }, helpers));
|
|
140
|
+
});\n`;
|
|
141
|
+
} else if (node.handler) {
|
|
142
|
+
output += `server.app.${method}('${node.path}', async (req, res) => {
|
|
143
|
+
const logic = async (req, res) => { ${node.handler} };
|
|
144
|
+
const viewToRender = await logic(req, res);
|
|
145
|
+
if (viewToRender && islandsRegistry[viewToRender.name]) {
|
|
146
|
+
res.header('Content-Type', 'text/html');
|
|
147
|
+
res.send(islandsRegistry[viewToRender.name]({ user: req.user, ...req.body }, helpers));
|
|
148
|
+
}
|
|
149
|
+
});\n`;
|
|
150
|
+
} else if (node.view) {
|
|
151
|
+
output += `server.app.${method}('${node.path}', async (req, res) => {
|
|
152
|
+
res.header('Content-Type', 'text/html');
|
|
153
|
+
res.send(render_${node.view}({ user: req.user }, helpers));
|
|
154
|
+
});\n`;
|
|
185
155
|
}
|
|
186
|
-
} catch(e) {}
|
|
187
|
-
|
|
188
|
-
const result = await (async () => {\n` +
|
|
189
|
-
" " + handlerCode.replace(/res\.locals/g, 'res.context') + "\n" +
|
|
190
|
-
" })();\n\n" +
|
|
191
|
-
" if (res.headersSent) return;\n" +
|
|
192
|
-
"\n" +
|
|
193
|
-
" // If it's a PAGE route (has a view), render HTML\n" +
|
|
194
|
-
" if (" + (node.view ? 'true' : 'false') + ") {\n" +
|
|
195
|
-
" const stylesRegistry = " + JSON.stringify(stylesRegistry) + ";\n" +
|
|
196
|
-
" const helpers = {\n" +
|
|
197
|
-
" renderComponent: (name, props = {}) => {\n" +
|
|
198
|
-
" server.renderRegistry = server.renderRegistry || {\n" +
|
|
199
|
-
" " + Object.keys(components).map(name => `${name}: render${name}`).join(',\n ') + "\n" +
|
|
200
|
-
" };\n" +
|
|
201
|
-
" return server.renderRegistry[name] ? server.renderRegistry[name](props, helpers) : '<!-- Component ' + name + ' not found -->';\n" +
|
|
202
|
-
" },\n" +
|
|
203
|
-
" getScopedCSS: (used = []) => {\n" +
|
|
204
|
-
" return used.map(name => stylesRegistry[name] || '').join('\\n');\n" +
|
|
205
|
-
" }\n" +
|
|
206
|
-
" };\n" +
|
|
207
|
-
" const props = result || (res.context && res.context.props ? res.context.props : {});\n" +
|
|
208
|
-
" const pageHtml = helpers.renderComponent('" + viewName + "', props);\n" +
|
|
209
|
-
" const scopedCSS = helpers.getScopedCSS(['" + viewName + "', 'Header']);\n" +
|
|
210
|
-
" if (req.headers['x-free-partial']) {\n" +
|
|
211
|
-
" return res.json({ title: 'Free Ultra | " + viewName + "', content: pageHtml, url: req.url, css: scopedCSS });\n" +
|
|
212
|
-
" }\n" +
|
|
213
|
-
" const headerHtml = helpers.renderComponent('Header', props);\n" +
|
|
214
|
-
" const fullHTML = `<!DOCTYPE html>\n" +
|
|
215
|
-
"<html lang=\"en\">\n" +
|
|
216
|
-
"<head>\n" +
|
|
217
|
-
" <meta charset=\"UTF-8\">\n" +
|
|
218
|
-
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
|
|
219
|
-
" <title>Free Ultra | " + viewName + "</title>\n" +
|
|
220
|
-
" <meta name=\"csrf-token\" content=\"${res.context.csrfToken || ''}\">\n" +
|
|
221
|
-
" <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n" +
|
|
222
|
-
" <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n" +
|
|
223
|
-
" <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;900&display=swap\" rel=\"stylesheet\">\n" +
|
|
224
|
-
" <link rel=\"stylesheet\" href=\"/css/app.css\">\n" +
|
|
225
|
-
" <style>\n" +
|
|
226
|
-
" :root { --primary:#fff; --bg:#000; --border:#27272a; }\n" +
|
|
227
|
-
" body { margin:0; font-family:'Inter', system-ui, sans-serif; background:var(--bg); color:#fff; -webkit-font-smoothing:antialiased; }\n" +
|
|
228
|
-
" ${scopedCSS}\n" +
|
|
229
|
-
" </style>\n" +
|
|
230
|
-
" <script src=\"/free-runtime.js\" defer></script>\n" +
|
|
231
|
-
"</head>\n" +
|
|
232
|
-
"<body>\n" +
|
|
233
|
-
" ${headerHtml}\n" +
|
|
234
|
-
" <main id=\"free-app-root\">\n" +
|
|
235
|
-
" ${pageHtml}\n" +
|
|
236
|
-
" </main>\n" +
|
|
237
|
-
" ${process.env.NODE_ENV !== 'production' ? `\n" +
|
|
238
|
-
" <script>\n" +
|
|
239
|
-
" (function connectHMR() {\n" +
|
|
240
|
-
" const ws = new WebSocket('ws://' + location.host + '/_free_hmr');\n" +
|
|
241
|
-
" ws.onclose = () => { \n" +
|
|
242
|
-
" setTimeout(() => fetch('/').then(() => window.location.reload()).catch(connectHMR), 500); \n" +
|
|
243
|
-
" };\n" +
|
|
244
|
-
" console.log('[Free Engine] ⚡ HMR Active');\n" +
|
|
245
|
-
" })();\n" +
|
|
246
|
-
" </script>` : ''}\n" +
|
|
247
|
-
"</body>\n" +
|
|
248
|
-
"</html>`;\n" +
|
|
249
|
-
" res.header('Content-Type', 'text/html').send(fullHTML);\n" +
|
|
250
|
-
" } else {\n" +
|
|
251
|
-
" res.json({ success: true, result });\n" +
|
|
252
|
-
" }\n" +
|
|
253
|
-
" });\n";
|
|
254
156
|
}
|
|
255
157
|
});
|
|
256
158
|
|
|
257
|
-
output += `\
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
159
|
+
output += `\n// Global Finalizing\n`;
|
|
160
|
+
output += `const cssMinifier = new CleanCSS({});\n`;
|
|
161
|
+
output += `const minifiedCSS = cssMinifier.minify(\`${globalCSS}\`).styles;\n`;
|
|
162
|
+
output += `server.app.get('/_free/styles.css', (req, res) => {
|
|
163
|
+
res.header('Content-Type', 'text/css');
|
|
164
|
+
res.send(minifiedCSS);
|
|
165
|
+
});\n`;
|
|
262
166
|
|
|
263
|
-
|
|
264
|
-
`;
|
|
167
|
+
output += `\nserver.start(process.env.PORT || 3000);\n`;
|
|
265
168
|
|
|
266
169
|
return output;
|
|
267
170
|
}
|
|
268
171
|
|
|
269
|
-
function
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
let code = `\nfunction render${node.name}(props = { }, helpers = { }) {\n`;
|
|
276
|
-
code += ` const e = (str) => String(str).replace(/[&<>'"]/g, m => ({'&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[m]));\n`;
|
|
277
|
-
code += ` const state = {\n`;
|
|
278
|
-
node.states.forEach(s => {
|
|
279
|
-
let val = s.default;
|
|
280
|
-
const isExpression = typeof val === 'string' && (val.includes('props.') || val.includes('||') || val.includes('&&') || val === 'true' || val === 'false' || val === 'null' || val.trim().startsWith('{') || val.trim().startsWith('['));
|
|
281
|
-
const finalVal = (isExpression || typeof val === 'number') ? val : `"${val}"`;
|
|
282
|
-
code += ` "${s.name}": ${finalVal},\n`;
|
|
283
|
-
});
|
|
284
|
-
code += ` };\n`;
|
|
285
|
-
if (stateNames.length > 0) {
|
|
286
|
-
code += ` const { ${stateNames.join(', ')} } = state;\n`;
|
|
287
|
-
}
|
|
288
|
-
|
|
172
|
+
function generateComponentCode(node) {
|
|
173
|
+
let stateNames = node.states.map(s => s.name);
|
|
174
|
+
let isInteractive = hasInteractivity(node.body);
|
|
175
|
+
let code = `\nfunction render_${node.name}(props = {}, helpers) {\n`;
|
|
176
|
+
code += ` const { e } = helpers;\n`;
|
|
177
|
+
code += ` let html = "";\n`;
|
|
289
178
|
code += ` const _events = [];\n`;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
179
|
+
|
|
180
|
+
// Inject state initializers for the island
|
|
181
|
+
if (node.states.length > 0) {
|
|
182
|
+
code += ` const state = { \n`;
|
|
183
|
+
node.states.forEach(s => {
|
|
184
|
+
code += ` ${s.name}: ${s.default},\n`;
|
|
185
|
+
});
|
|
186
|
+
code += ` };\n`;
|
|
187
|
+
} else {
|
|
188
|
+
code += ` const state = {};\n`;
|
|
296
189
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
190
|
+
|
|
191
|
+
code += ` html += "<div class='free-component' data-component='${node.name}'";\n`;
|
|
192
|
+
if (isInteractive) {
|
|
193
|
+
code += ` html += " data-island='true'";\n`;
|
|
194
|
+
code += ` html += " data-state='" + JSON.stringify(state).replace(/'/g, "'") + "'";\n`;
|
|
300
195
|
}
|
|
301
196
|
|
|
302
197
|
code += ` html += ">";\n`;
|
|
@@ -355,7 +250,7 @@ function generateTagCode(tag, stateNames) {
|
|
|
355
250
|
let propsStr = "{";
|
|
356
251
|
if (tag.attributes) {
|
|
357
252
|
Object.keys(tag.attributes).forEach((a, i) => {
|
|
358
|
-
let val = tag.attributes[a].replace(
|
|
253
|
+
let val = tag.attributes[a].replace(/([^\\]|^)\{([a-zA-Z0-9_$.]+)\}/g, '$1${$2}');
|
|
359
254
|
propsStr += (i === 0 ? "" : ", ") + '"' + a + '": `' + val + '`';
|
|
360
255
|
});
|
|
361
256
|
}
|
|
@@ -378,7 +273,7 @@ function generateTagCode(tag, stateNames) {
|
|
|
378
273
|
}
|
|
379
274
|
code += ` html += " data-on-${eventName}='${uniqueId}'";\n`;
|
|
380
275
|
} else {
|
|
381
|
-
let val = tag.attributes[a].replace(
|
|
276
|
+
let val = tag.attributes[a].replace(/([^\\]|^)\{([a-zA-Z0-9_$.]+)\}/g, '$1${e($2)}');
|
|
382
277
|
code += " html += \" \" + " + JSON.stringify(a) + " + \"='\" + `" + val + "` + \"'\";\n";
|
|
383
278
|
}
|
|
384
279
|
});
|
|
@@ -398,23 +293,17 @@ function generateTagCode(tag, stateNames) {
|
|
|
398
293
|
code += ` html += ">";\n`;
|
|
399
294
|
|
|
400
295
|
if (tag.content) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return `\${e(${p})}`;
|
|
411
|
-
});
|
|
412
|
-
// Remove escape characters from \{ and \}
|
|
413
|
-
cont = cont.replace(/\\\{/g, '{').replace(/\\\}/g, '}');
|
|
414
|
-
code += ` html += \`${cont}\`;\n`;
|
|
296
|
+
// Ultra-safe: only interpolate if it's a simple variable-like name with no whitespace
|
|
297
|
+
let val = tag.content.replace(/([^\\]|^)\{([a-zA-Z0-9_$.]+)\}/g, '$1${e($2)}');
|
|
298
|
+
// Handle escaped braces
|
|
299
|
+
val = val.replace(/\\\{/g, '{').replace(/\\\}/g, '}');
|
|
300
|
+
code += ` html += \`${val}\`;\n`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (tag.children) {
|
|
304
|
+
code += generateComponentBody(tag.children, stateNames);
|
|
415
305
|
}
|
|
416
306
|
|
|
417
|
-
if (tag.children) code += generateComponentBody(tag.children, stateNames);
|
|
418
307
|
code += ` html += "</${tag.name}>";\n`;
|
|
419
308
|
return code;
|
|
420
309
|
}
|
package/compiler/lexer.js
CHANGED
|
@@ -24,7 +24,8 @@ const KEYWORDS = new Set([
|
|
|
24
24
|
'middleware', 'auth', 'login', 'register', 'upload', 'path', 'maxSize', 'error',
|
|
25
25
|
'text', 'action', 'validate', 'store', 'onMount', 'onDestroy', 'queue', 'mail',
|
|
26
26
|
'loop', 'condition', 'script', 'if', 'for',
|
|
27
|
-
'get', 'post', 'put', 'delete', 'patch', 'head', 'options'
|
|
27
|
+
'get', 'post', 'put', 'delete', 'patch', 'head', 'options',
|
|
28
|
+
'Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options'
|
|
28
29
|
]);
|
|
29
30
|
|
|
30
31
|
function tokenize(code, filename = 'unknown.free') {
|
package/compiler/parser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* compiler/parser.js
|
|
3
|
-
* Parser for the Free Framework (
|
|
3
|
+
* Parser for the Free Framework (v5.0.0 Convergence).
|
|
4
4
|
* Standardized braced-body parsing and raw code slicing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ function parse(tokens, filename = 'unknown.free', source = '') {
|
|
|
21
21
|
if (token.type === 'KEYWORD') {
|
|
22
22
|
if (token.value === 'route') {
|
|
23
23
|
ast.push(parseRoute(tokens, filename, source));
|
|
24
|
-
} else if (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'].includes(token.value)) {
|
|
24
|
+
} else if (['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options'].includes(token.value)) {
|
|
25
25
|
tokens.unshift(token); // put back the method
|
|
26
26
|
ast.push(parseRoute(tokens, filename, source));
|
|
27
27
|
} else if (token.value === 'model') {
|
|
@@ -175,7 +175,7 @@ function parseComponent(tokens, filename, source) {
|
|
|
175
175
|
body.push({ type: next.value, code: parseActionBody(tokens, source) });
|
|
176
176
|
} else if (next.value === 'style') {
|
|
177
177
|
body.push({ type: 'style', content: parseActionBody(tokens, source) });
|
|
178
|
-
} else if (next.type === 'IDENTIFIER') {
|
|
178
|
+
} else if (next.type === 'IDENTIFIER' || next.type === 'KEYWORD') {
|
|
179
179
|
body.push(parseTag(next, tokens, filename, source));
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -194,7 +194,6 @@ function parseComponentBody(tokens, filename, source) {
|
|
|
194
194
|
let item, list = "";
|
|
195
195
|
|
|
196
196
|
if (isLoop) {
|
|
197
|
-
// loop list item {
|
|
198
197
|
const loopTokens = [];
|
|
199
198
|
while (tokens.length > 0 && tokens[0].value !== '{') {
|
|
200
199
|
loopTokens.push(tokens.shift());
|
|
@@ -203,7 +202,6 @@ function parseComponentBody(tokens, filename, source) {
|
|
|
203
202
|
item = loopTokens.pop().value;
|
|
204
203
|
list = loopTokens.map(t => (t.type === 'STRING' ? `"${t.value.replace(/"/g, '')}"` : t.value)).join('');
|
|
205
204
|
} else {
|
|
206
|
-
// for item in list {
|
|
207
205
|
item = tokens.shift().value;
|
|
208
206
|
if (tokens[0] && tokens[0].value === 'in') tokens.shift();
|
|
209
207
|
while (tokens.length > 0 && tokens[0].value !== '{') {
|
|
@@ -235,7 +233,12 @@ function parseComponentBody(tokens, filename, source) {
|
|
|
235
233
|
}
|
|
236
234
|
children.push({ type: 'tag', name: 'a', attributes: attrs, content: txt });
|
|
237
235
|
} else if (next.type === 'IDENTIFIER' || next.type === 'KEYWORD') {
|
|
238
|
-
|
|
236
|
+
if (['class', 'id', 'style'].includes(next.value)) {
|
|
237
|
+
// Skip attribute-like identifiers inside bodies to prevent malformed HTML tags
|
|
238
|
+
if (tokens[0] && (tokens[0].type === 'STRING' || tokens[0].type === 'IDENTIFIER')) tokens.shift();
|
|
239
|
+
} else {
|
|
240
|
+
children.push(parseTag(next, tokens, filename, source));
|
|
241
|
+
}
|
|
239
242
|
}
|
|
240
243
|
}
|
|
241
244
|
if (tokens[0] && tokens[0].value === '}') tokens.shift();
|
|
@@ -251,13 +254,11 @@ function parseTag(token, tokens, filename, source) {
|
|
|
251
254
|
|
|
252
255
|
while (tokens.length > 0) {
|
|
253
256
|
const next = tokens[0];
|
|
254
|
-
// Break on braces or other symbols (except - for identifiers)
|
|
255
257
|
if (next.value === '{' || next.value === '}' || (next.type === 'SYMBOL' && next.value !== '-' && next.value !== '=')) break;
|
|
256
258
|
|
|
257
259
|
const attrNameToken = tokens.shift();
|
|
258
260
|
const attrName = attrNameToken.value;
|
|
259
261
|
|
|
260
|
-
// Skip = if present
|
|
261
262
|
if (tokens[0] && tokens[0].value === '=') tokens.shift();
|
|
262
263
|
|
|
263
264
|
if (tokens[0] && tokens[0].value === '{') {
|
|
@@ -318,7 +319,11 @@ function parseTag(token, tokens, filename, source) {
|
|
|
318
319
|
}
|
|
319
320
|
children.push({ type: 'tag', name: 'a', attributes: attrs, content: txt });
|
|
320
321
|
} else if (next.type === 'IDENTIFIER' || next.type === 'KEYWORD') {
|
|
321
|
-
|
|
322
|
+
if (['class', 'id', 'style'].includes(next.value)) {
|
|
323
|
+
if (tokens[0] && (tokens[0].type === 'STRING' || tokens[0].type === 'IDENTIFIER')) tokens.shift();
|
|
324
|
+
} else {
|
|
325
|
+
children.push(parseTag(next, tokens, filename, source));
|
|
326
|
+
}
|
|
322
327
|
}
|
|
323
328
|
}
|
|
324
329
|
if (tokens[0] && tokens[0].value === '}') tokens.shift();
|
|
@@ -347,18 +352,16 @@ function parseAction(tokens, source) {
|
|
|
347
352
|
let name = nameToken.value;
|
|
348
353
|
let params = [];
|
|
349
354
|
|
|
350
|
-
// Support for parameterized actions: action myAction(id, name) { ... }
|
|
351
355
|
if (tokens[0] && tokens[0].value === '(') {
|
|
352
|
-
tokens.shift();
|
|
356
|
+
tokens.shift();
|
|
353
357
|
while (tokens.length > 0 && tokens[0].value !== ')') {
|
|
354
358
|
const pt = tokens.shift();
|
|
355
359
|
if (pt.value !== ',') {
|
|
356
360
|
params.push(pt.value);
|
|
357
361
|
}
|
|
358
362
|
}
|
|
359
|
-
if (tokens[0] && tokens[0].value === ')') tokens.shift();
|
|
363
|
+
if (tokens[0] && tokens[0].value === ')') tokens.shift();
|
|
360
364
|
} else if (name.includes('(')) {
|
|
361
|
-
// Handle case where lexer smushed it together: myAction(id)
|
|
362
365
|
const parts = name.split('(');
|
|
363
366
|
name = parts[0];
|
|
364
367
|
const argPart = parts[1].replace(')', '');
|
package/package.json
CHANGED
|
@@ -3,15 +3,12 @@ component Counter {
|
|
|
3
3
|
count: 0
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
div {
|
|
7
|
-
class "p-4 bg-gray-800 rounded-lg text-center"
|
|
6
|
+
div class="p-4 bg-gray-800 rounded-lg text-center" {
|
|
8
7
|
h2 { text "Reactive Counter" }
|
|
9
|
-
p {
|
|
10
|
-
class "text-4xl font-black text-primary my-4"
|
|
8
|
+
p class="text-4xl font-black text-primary my-4" {
|
|
11
9
|
text "{count}"
|
|
12
10
|
}
|
|
13
|
-
button {
|
|
14
|
-
class "px-6 py-2 bg-primary text-dark font-bold rounded-full hover:scale-105 transition-transform"
|
|
11
|
+
button class="px-6 py-2 bg-primary text-dark font-bold rounded-full hover:scale-105 transition-transform" {
|
|
15
12
|
text "Increment"
|
|
16
13
|
on-click { state.count++ }
|
|
17
14
|
}
|
|
@@ -2,7 +2,7 @@ component Docs {
|
|
|
2
2
|
div class="min-h-screen bg-black text-zinc-300 font-sans selection:bg-white selection:text-black" {
|
|
3
3
|
// Navigation / Header Space
|
|
4
4
|
div class="container-pro pt-32 pb-20" {
|
|
5
|
-
span class="badge-pro mb-4 animate-pulse" { text "
|
|
5
|
+
span class="badge-pro mb-4 animate-pulse" { text "v5.0.1 | High-Octane Docs" }
|
|
6
6
|
h1 class="text-6xl md:text-8xl font-black text-white tracking-tighter mb-8 leading-none" {
|
|
7
7
|
text "Free Ultra"
|
|
8
8
|
br;
|
|
@@ -22,7 +22,6 @@ component Docs {
|
|
|
22
22
|
// Content Sections
|
|
23
23
|
div class="container-pro space-y-32 pb-40" {
|
|
24
24
|
|
|
25
|
-
// 1. Core Philosophy
|
|
26
25
|
section id="philosophy" {
|
|
27
26
|
div class="grid md:grid-cols-2 gap-16 items-start" {
|
|
28
27
|
div {
|
|
@@ -35,26 +34,30 @@ component Docs {
|
|
|
35
34
|
text " to ensure your pages load instantly while remaining fully interactive."
|
|
36
35
|
}
|
|
37
36
|
ul class="space-y-4 text-sm" {
|
|
38
|
-
li class="flex gap-3" {
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
li class="flex gap-3" {
|
|
38
|
+
span class="text-white" { text "•" }
|
|
39
|
+
text "Blazing-fast SSR powered by HyperExpress."
|
|
40
|
+
}
|
|
41
|
+
li class="flex gap-3" {
|
|
42
|
+
span class="text-white" { text "•" }
|
|
43
|
+
text "Selective Hydration: Only JavaScript that's needed enters the browser."
|
|
44
|
+
}
|
|
45
|
+
li class="flex gap-3" {
|
|
46
|
+
span class="text-white" { text "•" }
|
|
47
|
+
text "Unified Syntax: The .free language unifies HTML, CSS, and Logic."
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
text "component Fast \{ \n"
|
|
48
|
-
text " h1 \{ text 'Zero Delay' \}\n"
|
|
49
|
-
text " // Static by default\n"
|
|
50
|
-
text "\}\n"
|
|
51
|
-
}
|
|
51
|
+
div class="card-pro bg-zinc-900/20" {
|
|
52
|
+
pre {
|
|
53
|
+
code {
|
|
54
|
+
text "// Pure Performance\ncomponent Fast { \n h1 { text 'Zero Delay' }\n // Static by default\n}\n"
|
|
52
55
|
}
|
|
53
56
|
}
|
|
57
|
+
}
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// 2. The Free Language
|
|
58
61
|
section id="language" {
|
|
59
62
|
h2 class="text-4xl font-bold text-white mb-12 tracking-tight" { text "2. The .free Language" }
|
|
60
63
|
div class="grid md:grid-cols-3 gap-8" {
|
|
@@ -85,7 +88,6 @@ component Docs {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
// 3. Routing & ORM
|
|
89
91
|
section id="orm" {
|
|
90
92
|
div class="grid md:grid-cols-2 gap-16" {
|
|
91
93
|
div {
|
|
@@ -109,18 +111,13 @@ component Docs {
|
|
|
109
111
|
div class="card-pro bg-zinc-950" {
|
|
110
112
|
pre {
|
|
111
113
|
code {
|
|
112
|
-
text "get
|
|
113
|
-
text "post \"/api/login\" \{ \n"
|
|
114
|
-
text " const user = await ORM.find('User', \{ email: body.email \});\n"
|
|
115
|
-
text " return \{ success: !!user \};\n"
|
|
116
|
-
text "\}"
|
|
114
|
+
text "get /dashboard -> Dashboard\n\npost /api/login { \n const user = await ORM.find('User', { email: body.email });\n return { success: !!user };\n}"
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
// 4. CLI Power-user
|
|
124
121
|
section id="cli" {
|
|
125
122
|
h2 class="text-4xl font-bold text-white mb-12 tracking-tight" { text "4. CLI Power-user Guide" }
|
|
126
123
|
div class="grid grid-cols-1 md:grid-cols-2 gap-4" {
|
|
@@ -147,15 +144,15 @@ component Docs {
|
|
|
147
144
|
}
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
// 5. IDE Extensions
|
|
151
147
|
section id="vscode" {
|
|
152
148
|
div class="bg-zinc-900/10 border border-zinc-800 rounded-3xl p-12" {
|
|
153
149
|
h2 class="text-4xl font-bold text-white mb-8 tracking-tight" { text "5. IDE Ecosystem" }
|
|
154
|
-
|
|
155
150
|
div class="grid md:grid-cols-2 gap-12" {
|
|
156
151
|
div {
|
|
157
152
|
h3 class="text-2xl font-bold text-white mb-4" { text "Visual Studio Code" }
|
|
158
|
-
p class="mb-6 text-sm" {
|
|
153
|
+
p class="mb-6 text-sm" {
|
|
154
|
+
text "Get syntax highlighting, auto-completion, and deep linting for .free files."
|
|
155
|
+
}
|
|
159
156
|
div class="space-y-4" {
|
|
160
157
|
a href="https://marketplace.visualstudio.com/items?itemName=dev-omartolba.free-vscode" class="block text-zinc-400 hover:text-white transition-colors flex items-center gap-2" {
|
|
161
158
|
text "→ Install from VS Marketplace"
|
|
@@ -181,20 +178,13 @@ component Docs {
|
|
|
181
178
|
}
|
|
182
179
|
}
|
|
183
180
|
|
|
184
|
-
// Footer
|
|
185
181
|
footer class="border-t border-zinc-900 py-20" {
|
|
186
182
|
div class="container-pro text-center" {
|
|
187
183
|
p class="text-sm text-zinc-600" {
|
|
188
184
|
text "Free Ultra Framework © 2026. Built for professionals. "
|
|
189
185
|
a href="https://omar-fathy.xyz/free-docs/guide.php" class="text-zinc-400 hover:text-white" { text "Official Encyclopedia" }
|
|
190
186
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
style {
|
|
196
|
-
html { scroll-behavior: smooth; }
|
|
197
|
-
.hero-title { background: linear-gradient(to right, #fff, #555); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
198
|
-
.badge-pro { display: inline-block; }
|
|
187
|
+
}
|
|
188
|
+
</footer>
|
|
199
189
|
}
|
|
200
190
|
}
|