luxaura 1.0.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.
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Luxaura Parser
5
+ * Converts .lux file source into an Abstract Syntax Tree (AST)
6
+ *
7
+ * Block order in a .lux file:
8
+ * props → input declarations
9
+ * state → reactive local data
10
+ * server → backend Node.js logic (stripped from client build)
11
+ * style → visual properties
12
+ * view → UI component tree
13
+ */
14
+
15
+ const BLOCK_NAMES = ['props', 'state', 'server', 'style', 'view'];
16
+
17
+ class LuxParser {
18
+ constructor(source, filename = 'unknown.lux') {
19
+ this.source = source;
20
+ this.filename = filename;
21
+ this.lines = source.split('\n');
22
+ this.pos = 0;
23
+ }
24
+
25
+ /** Main entry — returns full AST */
26
+ parse() {
27
+ const ast = {
28
+ type: 'LuxFile',
29
+ filename: this.filename,
30
+ props: [],
31
+ state: [],
32
+ server: null,
33
+ style: [],
34
+ view: null,
35
+ };
36
+
37
+ while (this.pos < this.lines.length) {
38
+ const line = this.lines[this.pos];
39
+ const trimmed = line.trim();
40
+
41
+ if (!trimmed || trimmed.startsWith('#')) {
42
+ this.pos++;
43
+ continue;
44
+ }
45
+
46
+ const blockName = BLOCK_NAMES.find(b => trimmed === b);
47
+ if (blockName) {
48
+ this.pos++;
49
+ const blockLines = this._collectBlock(0);
50
+ switch (blockName) {
51
+ case 'props': ast.props = this._parseProps(blockLines); break;
52
+ case 'state': ast.state = this._parseState(blockLines); break;
53
+ case 'server': ast.server = this._parseServer(blockLines); break;
54
+ case 'style': ast.style = this._parseStyle(blockLines); break;
55
+ case 'view': ast.view = this._parseView(blockLines); break;
56
+ }
57
+ } else {
58
+ this.pos++;
59
+ }
60
+ }
61
+
62
+ return ast;
63
+ }
64
+
65
+ /** Collect all lines that belong to the current block (indented deeper than baseIndent) */
66
+ _collectBlock(baseIndent) {
67
+ const blockLines = [];
68
+ while (this.pos < this.lines.length) {
69
+ const line = this.lines[this.pos];
70
+ const trimmed = line.trim();
71
+ if (!trimmed || trimmed.startsWith('#')) {
72
+ blockLines.push(line);
73
+ this.pos++;
74
+ continue;
75
+ }
76
+ const indent = this._indentLevel(line);
77
+ if (indent <= baseIndent && trimmed && !trimmed.startsWith('#')) {
78
+ // Check if this is a new top-level block name
79
+ if (BLOCK_NAMES.includes(trimmed)) break;
80
+ // Could be content at the same level — stop if we're collecting a sub-block
81
+ if (baseIndent === 0) break;
82
+ }
83
+ blockLines.push(line);
84
+ this.pos++;
85
+ }
86
+ return blockLines;
87
+ }
88
+
89
+ _indentLevel(line) {
90
+ let count = 0;
91
+ for (const ch of line) {
92
+ if (ch === ' ') count++;
93
+ else if (ch === '\t') count += 4;
94
+ else break;
95
+ }
96
+ return count;
97
+ }
98
+
99
+ // ─── Block Parsers ─────────────────────────────────────────────────────────
100
+
101
+ _parseProps(lines) {
102
+ const props = [];
103
+ for (const line of lines) {
104
+ const trimmed = line.trim();
105
+ if (!trimmed || trimmed.startsWith('#')) continue;
106
+ // Format: name: Type or name: Type = default
107
+ const match = trimmed.match(/^(\w+)\s*:\s*(\w+)(?:\s*=\s*(.+))?$/);
108
+ if (match) {
109
+ props.push({
110
+ type: 'Prop',
111
+ name: match[1],
112
+ propType: match[2],
113
+ defaultValue: match[3] ? this._parseValue(match[3]) : undefined,
114
+ });
115
+ }
116
+ }
117
+ return props;
118
+ }
119
+
120
+ _parseState(lines) {
121
+ const state = [];
122
+ for (const line of lines) {
123
+ const trimmed = line.trim();
124
+ if (!trimmed || trimmed.startsWith('#')) continue;
125
+ // Format: name: value
126
+ const match = trimmed.match(/^(\w+)\s*:\s*(.+)$/);
127
+ if (match) {
128
+ state.push({
129
+ type: 'StateVar',
130
+ name: match[1],
131
+ value: this._parseValue(match[2]),
132
+ });
133
+ }
134
+ }
135
+ return state;
136
+ }
137
+
138
+ _parseServer(lines) {
139
+ // Server block is raw Node.js code — preserve as-is for compilation
140
+ return {
141
+ type: 'ServerBlock',
142
+ raw: lines.map(l => l.replace(/^\s{4}/, '')).join('\n'), // dedent one level
143
+ functions: this._extractServerFunctions(lines),
144
+ };
145
+ }
146
+
147
+ _extractServerFunctions(lines) {
148
+ const fns = [];
149
+ for (let i = 0; i < lines.length; i++) {
150
+ const trimmed = lines[i].trim();
151
+ const match = trimmed.match(/^def\s+(\w+)\s*\(([^)]*)\)\s*:$/);
152
+ if (match) {
153
+ const name = match[1];
154
+ const params = match[2].split(',').map(p => p.trim()).filter(Boolean);
155
+ // Collect function body
156
+ const bodyLines = [];
157
+ i++;
158
+ while (i < lines.length) {
159
+ const indent = this._indentLevel(lines[i]);
160
+ if (indent < 8 && lines[i].trim()) break;
161
+ bodyLines.push(lines[i].replace(/^\s{8}/, ''));
162
+ i++;
163
+ }
164
+ i--; // rewind since outer loop will increment
165
+ fns.push({ type: 'ServerFunction', name, params, body: bodyLines.join('\n') });
166
+ }
167
+ }
168
+ return fns;
169
+ }
170
+
171
+ _parseStyle(lines) {
172
+ const rules = [];
173
+ let currentSelector = null;
174
+ let currentProps = {};
175
+
176
+ for (let i = 0; i < lines.length; i++) {
177
+ const line = lines[i];
178
+ const trimmed = line.trim();
179
+ if (!trimmed || trimmed.startsWith('#')) continue;
180
+
181
+ const indent = this._indentLevel(line);
182
+
183
+ if (indent <= 4) {
184
+ // Selector (e.g., `self`, `.my-class`, `Title`)
185
+ if (currentSelector !== null) {
186
+ rules.push({ type: 'StyleRule', selector: currentSelector, props: currentProps });
187
+ }
188
+ currentSelector = trimmed;
189
+ currentProps = {};
190
+ } else {
191
+ // Property: value
192
+ const match = trimmed.match(/^([\w-]+)\s*:\s*(.+)$/);
193
+ if (match) {
194
+ currentProps[match[1]] = this._parseStyleValue(match[2]);
195
+ }
196
+ }
197
+ }
198
+ if (currentSelector !== null) {
199
+ rules.push({ type: 'StyleRule', selector: currentSelector, props: currentProps });
200
+ }
201
+ return rules;
202
+ }
203
+
204
+ _parseView(lines) {
205
+ if (!lines.length) return null;
206
+ const cleaned = lines.filter(l => l.trim() && !l.trim().startsWith('#'));
207
+ if (!cleaned.length) return null;
208
+ // Detect the minimum indentation level of the block so the tree
209
+ // builder knows what counts as the "root" level.
210
+ const baseIndent = Math.min(...cleaned.map(l => this._indentLevel(l)));
211
+ return this._parseComponentTree(cleaned, baseIndent).nodes[0] || null;
212
+ }
213
+
214
+ _parseComponentTree(lines, baseIndent) {
215
+ const nodes = [];
216
+ let i = 0;
217
+
218
+ while (i < lines.length) {
219
+ const line = lines[i];
220
+ const trimmed = line.trim();
221
+ if (!trimmed) { i++; continue; }
222
+
223
+ const indent = this._indentLevel(line);
224
+ if (indent < baseIndent) break;
225
+ if (indent > baseIndent) { i++; continue; }
226
+
227
+ const node = this._parseComponentLine(trimmed);
228
+
229
+ // Collect children (lines deeper than current indent)
230
+ const childLines = [];
231
+ i++;
232
+ while (i < lines.length) {
233
+ const childIndent = this._indentLevel(lines[i]);
234
+ const childTrimmed = lines[i].trim();
235
+ if (!childTrimmed) { i++; continue; }
236
+ if (childIndent <= indent) break;
237
+ childLines.push(lines[i]);
238
+ i++;
239
+ }
240
+
241
+ if (childLines.length) {
242
+ const result = this._parseComponentTree(childLines, indent + 4);
243
+ node.children = result.nodes;
244
+ node.events = [...(node.events || []), ...result.events];
245
+ }
246
+
247
+ nodes.push(node);
248
+ }
249
+
250
+ return { nodes, events: [] };
251
+ }
252
+
253
+ _parseComponentLine(text) {
254
+ // Event handler: on click:
255
+ const eventMatch = text.match(/^on\s+(\w+)\s*:/);
256
+ if (eventMatch) {
257
+ return { type: 'EventHandler', event: eventMatch[1], children: [] };
258
+ }
259
+
260
+ // Component with bound prop: Title "{propName}" — check BEFORE plain string
261
+ const boundMatch = text.match(/^(\w+)\s+"\{([^}]+)\}"$/);
262
+ if (boundMatch) {
263
+ return { type: 'Component', name: boundMatch[1], binding: boundMatch[2], props: {}, children: [] };
264
+ }
265
+
266
+ // Component with string arg: Title "Hello"
267
+ const strArgMatch = text.match(/^(\w+)\s+"([^"]*)"$/);
268
+ if (strArgMatch) {
269
+ return { type: 'Component', name: strArgMatch[1], text: strArgMatch[2], props: {}, children: [] };
270
+ }
271
+
272
+ // Component with props: Input type:"text" placeholder:"Name"
273
+ const propsMatch = text.match(/^(\w+)\s+(.+)$/);
274
+ if (propsMatch) {
275
+ const name = propsMatch[1];
276
+ const propsRaw = propsMatch[2];
277
+ const props = this._parseInlineProps(propsRaw);
278
+ return { type: 'Component', name, props, children: [] };
279
+ }
280
+
281
+ // Bare component: Box
282
+ return { type: 'Component', name: text, props: {}, children: [] };
283
+ }
284
+
285
+ _parseInlineProps(raw) {
286
+ const props = {};
287
+ // Match key:"value" or key:{binding} or key:value
288
+ const re = /(\w+)\s*:\s*(?:"([^"]*)"|\{([^}]+)\}|(\S+))/g;
289
+ let m;
290
+ while ((m = re.exec(raw)) !== null) {
291
+ const key = m[1];
292
+ const val = m[2] !== undefined ? m[2]
293
+ : m[3] !== undefined ? { binding: m[3] }
294
+ : this._parseValue(m[4]);
295
+ props[key] = val;
296
+ }
297
+ return props;
298
+ }
299
+
300
+ // ─── Value Helpers ──────────────────────────────────────────────────────────
301
+
302
+ _parseValue(raw) {
303
+ if (raw === 'true') return true;
304
+ if (raw === 'false') return false;
305
+ if (raw === 'null') return null;
306
+ if (!isNaN(Number(raw))) return Number(raw);
307
+ if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1);
308
+ if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
309
+ return raw; // treat as identifier / expression
310
+ }
311
+
312
+ _parseStyleValue(raw) {
313
+ const num = Number(raw);
314
+ if (!isNaN(num)) return num;
315
+ return raw;
316
+ }
317
+ }
318
+
319
+ module.exports = { LuxParser };
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Luxaura Runtime Engine
3
+ * ~20kb client-side library
4
+ * Handles: component registry, reactive state, DOM diffing, RPC tunnel, routing
5
+ *
6
+ * Window global: window.__Lux__
7
+ */
8
+
9
+ (function (global) {
10
+ 'use strict';
11
+
12
+ // ─── Utilities ─────────────────────────────────────────────────────────────
13
+
14
+ let _uidCounter = 0;
15
+ const uid = () => ++_uidCounter;
16
+
17
+ function deepClone(obj) {
18
+ return JSON.parse(JSON.stringify(obj));
19
+ }
20
+
21
+ function debounce(fn, ms) {
22
+ let t;
23
+ return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
24
+ }
25
+
26
+ // ─── CSRF Token ────────────────────────────────────────────────────────────
27
+
28
+ function getCsrfToken() {
29
+ let token = sessionStorage.getItem('__lux_csrf__');
30
+ if (!token) {
31
+ token = Array.from(crypto.getRandomValues(new Uint8Array(16)))
32
+ .map(b => b.toString(16).padStart(2, '0')).join('');
33
+ sessionStorage.setItem('__lux_csrf__', token);
34
+ }
35
+ return token;
36
+ }
37
+
38
+ // ─── Reactive State (Proxy-based) ──────────────────────────────────────────
39
+
40
+ class ReactiveState {
41
+ constructor(initial, onChange) {
42
+ this._raw = deepClone(initial);
43
+ this._listeners = [];
44
+ if (onChange) this._listeners.push(onChange);
45
+
46
+ this._proxy = new Proxy(this._raw, {
47
+ set: (target, key, value) => {
48
+ target[key] = value;
49
+ this._notify();
50
+ return true;
51
+ },
52
+ });
53
+ }
54
+
55
+ get proxy() { return this._proxy; }
56
+
57
+ get(key) { return this._raw[key]; }
58
+
59
+ set(key, value) {
60
+ this._raw[key] = value;
61
+ this._notify();
62
+ }
63
+
64
+ _notify() {
65
+ this._listeners.forEach(fn => fn(this._raw));
66
+ }
67
+ }
68
+
69
+ // ─── Simple Virtual DOM Differ ─────────────────────────────────────────────
70
+
71
+ function patch(container, newHTML) {
72
+ // Lightweight: use innerHTML for initial render,
73
+ // diff only changed text nodes and attributes on update
74
+ const parser = new DOMParser();
75
+ const newDoc = parser.parseFromString(`<body>${newHTML}</body>`, 'text/html');
76
+ const newBody = newDoc.body;
77
+ diffNodes(container, newBody);
78
+ }
79
+
80
+ function diffNodes(oldParent, newParent) {
81
+ const oldChildren = Array.from(oldParent.childNodes);
82
+ const newChildren = Array.from(newParent.childNodes);
83
+
84
+ const maxLen = Math.max(oldChildren.length, newChildren.length);
85
+
86
+ for (let i = 0; i < maxLen; i++) {
87
+ const oldNode = oldChildren[i];
88
+ const newNode = newChildren[i];
89
+
90
+ if (!oldNode && newNode) {
91
+ oldParent.appendChild(newNode.cloneNode(true));
92
+ continue;
93
+ }
94
+ if (oldNode && !newNode) {
95
+ oldParent.removeChild(oldNode);
96
+ continue;
97
+ }
98
+ if (oldNode.nodeType !== newNode.nodeType ||
99
+ oldNode.nodeName !== newNode.nodeName) {
100
+ oldParent.replaceChild(newNode.cloneNode(true), oldNode);
101
+ continue;
102
+ }
103
+ if (oldNode.nodeType === Node.TEXT_NODE) {
104
+ if (oldNode.textContent !== newNode.textContent) {
105
+ oldNode.textContent = newNode.textContent;
106
+ }
107
+ continue;
108
+ }
109
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
110
+ diffAttributes(oldNode, newNode);
111
+ diffNodes(oldNode, newNode);
112
+ }
113
+ }
114
+ }
115
+
116
+ function diffAttributes(oldEl, newEl) {
117
+ // Remove old attrs
118
+ for (const attr of Array.from(oldEl.attributes)) {
119
+ if (!newEl.hasAttribute(attr.name)) oldEl.removeAttribute(attr.name);
120
+ }
121
+ // Set new/changed attrs
122
+ for (const attr of Array.from(newEl.attributes)) {
123
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
124
+ oldEl.setAttribute(attr.name, attr.value);
125
+ }
126
+ }
127
+ }
128
+
129
+ // ─── RPC Tunnel (server.fn() calls) ────────────────────────────────────────
130
+
131
+ const rpc = new Proxy({}, {
132
+ get(_, fnName) {
133
+ return async (...args) => {
134
+ const resp = await fetch('/__lux_rpc__', {
135
+ method: 'POST',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ 'X-Lux-CSRF': getCsrfToken(),
139
+ },
140
+ body: JSON.stringify({ fn: fnName, args }),
141
+ });
142
+ if (!resp.ok) throw new Error(`RPC ${fnName} failed: ${resp.status}`);
143
+ return resp.json();
144
+ };
145
+ },
146
+ });
147
+
148
+ // ─── Router ────────────────────────────────────────────────────────────────
149
+
150
+ const router = {
151
+ params: {},
152
+ query: {},
153
+
154
+ navigate(url) {
155
+ history.pushState({}, '', url);
156
+ window.dispatchEvent(new PopStateEvent('popstate'));
157
+ },
158
+
159
+ _parseQuery(search) {
160
+ const q = {};
161
+ new URLSearchParams(search).forEach((v, k) => { q[k] = v; });
162
+ return q;
163
+ },
164
+
165
+ _init() {
166
+ this.query = this._parseQuery(location.search);
167
+ window.addEventListener('popstate', () => {
168
+ this.query = this._parseQuery(location.search);
169
+ });
170
+ },
171
+ };
172
+
173
+ // ─── Auto-Responsiveness (Physics Engine) ──────────────────────────────────
174
+
175
+ const physics = {
176
+ _init() {
177
+ this._applyMobileTargets();
178
+ window.addEventListener('resize', debounce(() => this._applyMobileTargets(), 100));
179
+ },
180
+
181
+ _applyMobileTargets() {
182
+ const isMobile = window.innerWidth < 768;
183
+ document.querySelectorAll('.lux-action, .lux-input, .lux-switch').forEach(el => {
184
+ if (isMobile) {
185
+ el.style.minHeight = '44px';
186
+ el.style.minWidth = '44px';
187
+ } else {
188
+ el.style.minHeight = '';
189
+ el.style.minWidth = '';
190
+ }
191
+ });
192
+
193
+ // Auto-stack Row → Column on mobile
194
+ document.querySelectorAll('.lux-row').forEach(el => {
195
+ el.style.flexDirection = isMobile ? 'column' : 'row';
196
+ });
197
+ },
198
+ };
199
+
200
+ // ─── Storage Wrapper ───────────────────────────────────────────────────────
201
+
202
+ const storage = {
203
+ local: { get: k => localStorage.getItem(k), set: (k,v) => localStorage.setItem(k,v), remove: k => localStorage.removeItem(k) },
204
+ session: { get: k => sessionStorage.getItem(k), set: (k,v) => sessionStorage.setItem(k,v), remove: k => sessionStorage.removeItem(k) },
205
+ };
206
+
207
+ // ─── HTTP Helper ───────────────────────────────────────────────────────────
208
+
209
+ const http = {
210
+ async get(url, opts = {}) {
211
+ const r = await fetch(url, { method: 'GET', ...opts });
212
+ return r.json();
213
+ },
214
+ async post(url, body, opts = {}) {
215
+ const r = await fetch(url, {
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
218
+ body: JSON.stringify(body),
219
+ ...opts,
220
+ });
221
+ return r.json();
222
+ },
223
+ };
224
+
225
+ // ─── Auth Stub ─────────────────────────────────────────────────────────────
226
+
227
+ const auth = {
228
+ async login(credentials) { return rpc.login(credentials); },
229
+ async logout() { return rpc.logout(); },
230
+ async user() { return rpc.currentUser(); },
231
+ };
232
+
233
+ // ─── Component Context ─────────────────────────────────────────────────────
234
+
235
+ class ComponentContext {
236
+ constructor(definition, props = {}) {
237
+ this._uid = uid();
238
+ this._def = definition;
239
+ this._props = props;
240
+ this._state = new ReactiveState(
241
+ deepClone(definition.state || {}),
242
+ () => this._scheduleUpdate()
243
+ );
244
+ this._el = null;
245
+ this._updateScheduled = false;
246
+
247
+ // Public interface available in render / event handlers
248
+ this.server = rpc;
249
+ this.router = router;
250
+ this.http = http;
251
+ this.auth = auth;
252
+ this.storage = storage;
253
+ }
254
+
255
+ get(key) {
256
+ if (key in this._props) return this._props[key];
257
+ return this._state.get(key);
258
+ }
259
+
260
+ set(key, value) {
261
+ this._state.set(key, value);
262
+ }
263
+
264
+ _scheduleUpdate() {
265
+ if (this._updateScheduled) return;
266
+ this._updateScheduled = true;
267
+ requestAnimationFrame(() => {
268
+ this._updateScheduled = false;
269
+ this._update();
270
+ });
271
+ }
272
+
273
+ _update() {
274
+ if (!this._el) return;
275
+ const newHTML = this._def.render(this);
276
+ patch(this._el, newHTML);
277
+ }
278
+
279
+ mount(selector) {
280
+ const container = typeof selector === 'string'
281
+ ? document.querySelector(selector)
282
+ : selector;
283
+ if (!container) throw new Error(`Luxaura: mount target not found: ${selector}`);
284
+ this._el = container;
285
+ container.innerHTML = this._def.render(this);
286
+ if (this._def.mounted) this._def.mounted(container, this);
287
+ return this;
288
+ }
289
+ }
290
+
291
+ // ─── Lux Core ──────────────────────────────────────────────────────────────
292
+
293
+ const registry = {};
294
+
295
+ const Lux = {
296
+ version: '1.0.0',
297
+
298
+ define(name, definition) {
299
+ registry[name] = definition;
300
+ return this;
301
+ },
302
+
303
+ mount(name, selector, props = {}) {
304
+ const def = registry[name];
305
+ if (!def) throw new Error(`Luxaura: component "${name}" not defined.`);
306
+ const ctx = new ComponentContext(def, props);
307
+ ctx.mount(selector);
308
+ return ctx;
309
+ },
310
+
311
+ /** Bind an event to a rendered component element */
312
+ on(container, componentName, event, handler) {
313
+ const selector = `[data-lux-id^="${componentName.toLowerCase()}-"]`;
314
+ container.addEventListener(event, async (e) => {
315
+ const target = e.target.closest(selector);
316
+ if (!target) return;
317
+ const ctx = target.__luxCtx__;
318
+ await handler(ctx || {});
319
+ });
320
+ },
321
+
322
+ /** Two-way binding for Input elements */
323
+ bind(container, ctx) {
324
+ container.addEventListener('input', e => {
325
+ const key = e.target.dataset.luxBind;
326
+ if (key) ctx.set(key, e.target.value);
327
+ });
328
+ },
329
+
330
+ router,
331
+ http,
332
+ auth,
333
+ storage,
334
+ rpc,
335
+
336
+ _init() {
337
+ router._init();
338
+ physics._init();
339
+ },
340
+ };
341
+
342
+ // Boot
343
+ if (document.readyState === 'loading') {
344
+ document.addEventListener('DOMContentLoaded', () => Lux._init());
345
+ } else {
346
+ Lux._init();
347
+ }
348
+
349
+ global.__Lux__ = Lux;
350
+ })(window);