pulse-js-framework 1.0.0 → 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/compiler/lexer.js CHANGED
@@ -197,6 +197,19 @@ export class Lexer {
197
197
  }
198
198
  }
199
199
 
200
+ /**
201
+ * Peek at the current word without advancing position
202
+ */
203
+ peekWord() {
204
+ let word = '';
205
+ let i = this.pos;
206
+ while (i < this.source.length && /[a-zA-Z0-9_$]/.test(this.source[i])) {
207
+ word += this.source[i];
208
+ i++;
209
+ }
210
+ return word;
211
+ }
212
+
200
213
  /**
201
214
  * Read a string literal
202
215
  */
@@ -507,8 +520,12 @@ export class Lexer {
507
520
 
508
521
  // Identifiers, keywords, and selectors
509
522
  if (/[a-zA-Z_$]/.test(char)) {
510
- // Check if we're in view context and this could be a selector
511
- if (this.isViewContext() && this.couldBeSelector()) {
523
+ // First check if this is a keyword - keywords take precedence
524
+ const word = this.peekWord();
525
+ if (KEYWORDS[word]) {
526
+ this.tokens.push(this.readIdentifier());
527
+ } else if (this.isViewContext() && this.couldBeSelector()) {
528
+ // Only treat as selector if not a keyword
512
529
  this.tokens.push(this.readSelector());
513
530
  } else {
514
531
  this.tokens.push(this.readIdentifier());
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Pulse Native Bridge
3
+ * Provides unified API for native mobile features
4
+ * Zero dependencies - works on Android, iOS, and Web
5
+ */
6
+
7
+ (function() {
8
+ 'use strict';
9
+
10
+ // Detect platform
11
+ const isAndroid = typeof window.PulseNative !== 'undefined';
12
+ const isIOS = typeof window.webkit?.messageHandlers?.PulseNative !== 'undefined';
13
+ const isNative = isAndroid || isIOS;
14
+
15
+ // Callback registry for async operations (iOS)
16
+ const callbacks = new Map();
17
+ let callbackId = 0;
18
+
19
+ /**
20
+ * Generate unique callback ID
21
+ */
22
+ function generateCallbackId() {
23
+ return `cb_${++callbackId}_${Date.now()}`;
24
+ }
25
+
26
+ /**
27
+ * Global callback handler (called from native iOS)
28
+ */
29
+ window.__pulseNativeCallback = function(id, response) {
30
+ const callback = callbacks.get(id);
31
+ if (callback) {
32
+ callbacks.delete(id);
33
+ if (response.success) {
34
+ callback.resolve(response.data);
35
+ } else {
36
+ callback.reject(new Error(response.error));
37
+ }
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Call native method (iOS)
43
+ */
44
+ function callNativeIOS(action, args) {
45
+ return new Promise((resolve, reject) => {
46
+ const id = generateCallbackId();
47
+ callbacks.set(id, { resolve, reject });
48
+
49
+ window.webkit.messageHandlers.PulseNative.postMessage({
50
+ action,
51
+ args,
52
+ callbackId: id
53
+ });
54
+
55
+ // Timeout after 30 seconds
56
+ setTimeout(() => {
57
+ if (callbacks.has(id)) {
58
+ callbacks.delete(id);
59
+ reject(new Error('Native call timeout'));
60
+ }
61
+ }, 30000);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Call native method (Android - synchronous)
67
+ */
68
+ function callNativeAndroid(action, args) {
69
+ return new Promise((resolve, reject) => {
70
+ try {
71
+ let result;
72
+
73
+ switch (action) {
74
+ // Storage
75
+ case 'setItem':
76
+ window.PulseNative.setItem(args.key, args.value);
77
+ resolve();
78
+ break;
79
+ case 'getItem':
80
+ result = window.PulseNative.getItem(args.key);
81
+ resolve(result);
82
+ break;
83
+ case 'removeItem':
84
+ window.PulseNative.removeItem(args.key);
85
+ resolve();
86
+ break;
87
+ case 'clearStorage':
88
+ window.PulseNative.clearStorage();
89
+ resolve();
90
+ break;
91
+ case 'getAllKeys':
92
+ result = window.PulseNative.getAllKeys();
93
+ resolve(result ? result.split(',').filter(k => k) : []);
94
+ break;
95
+
96
+ // Device
97
+ case 'getDeviceInfo':
98
+ result = window.PulseNative.getDeviceInfo();
99
+ resolve(JSON.parse(result));
100
+ break;
101
+ case 'getNetworkStatus':
102
+ result = window.PulseNative.getNetworkStatus();
103
+ resolve(JSON.parse(result));
104
+ break;
105
+
106
+ // UI
107
+ case 'showToast':
108
+ window.PulseNative.showToast(args.message, args.isLong || false);
109
+ resolve();
110
+ break;
111
+ case 'vibrate':
112
+ window.PulseNative.vibrate(args.duration || 100);
113
+ resolve();
114
+ break;
115
+
116
+ // Clipboard
117
+ case 'copyToClipboard':
118
+ window.PulseNative.copyToClipboard(args.text);
119
+ resolve();
120
+ break;
121
+ case 'getClipboardText':
122
+ result = window.PulseNative.getClipboardText();
123
+ resolve(result);
124
+ break;
125
+
126
+ // App Lifecycle
127
+ case 'exitApp':
128
+ window.PulseNative.exitApp();
129
+ resolve();
130
+ break;
131
+ case 'minimizeApp':
132
+ window.PulseNative.minimizeApp();
133
+ resolve();
134
+ break;
135
+
136
+ default:
137
+ reject(new Error(`Unknown action: ${action}`));
138
+ }
139
+ } catch (error) {
140
+ reject(error);
141
+ }
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Call native method
147
+ */
148
+ function callNative(action, args = {}) {
149
+ if (!isNative) {
150
+ return Promise.reject(new Error('Not running in native environment'));
151
+ }
152
+ return isIOS ? callNativeIOS(action, args) : callNativeAndroid(action, args);
153
+ }
154
+
155
+ // =========================================================================
156
+ // Public API
157
+ // =========================================================================
158
+
159
+ const PulseMobile = {
160
+ /**
161
+ * Platform detection
162
+ */
163
+ isNative,
164
+ isAndroid,
165
+ isIOS,
166
+
167
+ get platform() {
168
+ if (isAndroid) return 'android';
169
+ if (isIOS) return 'ios';
170
+ return 'web';
171
+ },
172
+
173
+ // =========================================================================
174
+ // Storage API - Native key-value storage
175
+ // =========================================================================
176
+ Storage: {
177
+ async setItem(key, value) {
178
+ if (!isNative) {
179
+ localStorage.setItem(key, value);
180
+ return;
181
+ }
182
+ return callNative('setItem', { key, value: String(value) });
183
+ },
184
+
185
+ async getItem(key) {
186
+ if (!isNative) {
187
+ return localStorage.getItem(key);
188
+ }
189
+ return callNative('getItem', { key });
190
+ },
191
+
192
+ async removeItem(key) {
193
+ if (!isNative) {
194
+ localStorage.removeItem(key);
195
+ return;
196
+ }
197
+ return callNative('removeItem', { key });
198
+ },
199
+
200
+ async clear() {
201
+ if (!isNative) {
202
+ localStorage.clear();
203
+ return;
204
+ }
205
+ return callNative('clearStorage');
206
+ },
207
+
208
+ async keys() {
209
+ if (!isNative) {
210
+ return Object.keys(localStorage);
211
+ }
212
+ return callNative('getAllKeys');
213
+ },
214
+
215
+ async getObject(key) {
216
+ const value = await this.getItem(key);
217
+ if (!value) return null;
218
+ try {
219
+ return JSON.parse(value);
220
+ } catch {
221
+ return null;
222
+ }
223
+ },
224
+
225
+ async setObject(key, value) {
226
+ return this.setItem(key, JSON.stringify(value));
227
+ }
228
+ },
229
+
230
+ // =========================================================================
231
+ // Device API - Device information
232
+ // =========================================================================
233
+ Device: {
234
+ async getInfo() {
235
+ if (!isNative) {
236
+ return {
237
+ platform: 'web',
238
+ userAgent: navigator.userAgent,
239
+ language: navigator.language,
240
+ online: navigator.onLine
241
+ };
242
+ }
243
+ return callNative('getDeviceInfo');
244
+ },
245
+
246
+ async getNetworkStatus() {
247
+ if (!isNative) {
248
+ return {
249
+ connected: navigator.onLine,
250
+ type: 'unknown'
251
+ };
252
+ }
253
+ return callNative('getNetworkStatus');
254
+ },
255
+
256
+ onNetworkChange(callback) {
257
+ window.addEventListener('online', () => callback({ connected: true }));
258
+ window.addEventListener('offline', () => callback({ connected: false }));
259
+ window.addEventListener('pulse:networkChange', (e) => callback(e.detail));
260
+ }
261
+ },
262
+
263
+ // =========================================================================
264
+ // UI API - Native UI interactions
265
+ // =========================================================================
266
+ UI: {
267
+ async showToast(message, isLong = false) {
268
+ if (!isNative) {
269
+ // Web fallback
270
+ const toast = document.createElement('div');
271
+ toast.textContent = message;
272
+ toast.style.cssText = `
273
+ position: fixed;
274
+ bottom: 20px;
275
+ left: 50%;
276
+ transform: translateX(-50%);
277
+ background: rgba(0,0,0,0.8);
278
+ color: white;
279
+ padding: 12px 24px;
280
+ border-radius: 8px;
281
+ z-index: 99999;
282
+ font-size: 14px;
283
+ font-family: system-ui, sans-serif;
284
+ animation: pulse-toast-in 0.3s ease;
285
+ `;
286
+ document.body.appendChild(toast);
287
+ setTimeout(() => {
288
+ toast.style.animation = 'pulse-toast-out 0.3s ease forwards';
289
+ setTimeout(() => toast.remove(), 300);
290
+ }, isLong ? 3500 : 2000);
291
+ return;
292
+ }
293
+ return callNative('showToast', { message, isLong });
294
+ },
295
+
296
+ async vibrate(duration = 100) {
297
+ if (!isNative) {
298
+ if (navigator.vibrate) {
299
+ navigator.vibrate(duration);
300
+ }
301
+ return;
302
+ }
303
+ return callNative('vibrate', { duration });
304
+ }
305
+ },
306
+
307
+ // =========================================================================
308
+ // Clipboard API
309
+ // =========================================================================
310
+ Clipboard: {
311
+ async copy(text) {
312
+ if (!isNative) {
313
+ if (navigator.clipboard) {
314
+ return navigator.clipboard.writeText(text);
315
+ }
316
+ // Fallback
317
+ const textarea = document.createElement('textarea');
318
+ textarea.value = text;
319
+ textarea.style.position = 'fixed';
320
+ textarea.style.opacity = '0';
321
+ document.body.appendChild(textarea);
322
+ textarea.select();
323
+ document.execCommand('copy');
324
+ document.body.removeChild(textarea);
325
+ return;
326
+ }
327
+ return callNative('copyToClipboard', { text });
328
+ },
329
+
330
+ async read() {
331
+ if (!isNative) {
332
+ if (navigator.clipboard) {
333
+ return navigator.clipboard.readText();
334
+ }
335
+ return '';
336
+ }
337
+ return callNative('getClipboardText');
338
+ }
339
+ },
340
+
341
+ // =========================================================================
342
+ // App Lifecycle
343
+ // =========================================================================
344
+ App: {
345
+ async exit() {
346
+ if (!isNative || isIOS) {
347
+ console.warn('Cannot exit app on this platform');
348
+ return;
349
+ }
350
+ return callNative('exitApp');
351
+ },
352
+
353
+ async minimize() {
354
+ if (!isNative) {
355
+ console.warn('Cannot minimize app on web');
356
+ return;
357
+ }
358
+ return callNative('minimizeApp');
359
+ },
360
+
361
+ onPause(callback) {
362
+ document.addEventListener('visibilitychange', () => {
363
+ if (document.hidden) callback();
364
+ });
365
+ window.addEventListener('pulse:pause', callback);
366
+ },
367
+
368
+ onResume(callback) {
369
+ document.addEventListener('visibilitychange', () => {
370
+ if (!document.hidden) callback();
371
+ });
372
+ window.addEventListener('pulse:resume', callback);
373
+ },
374
+
375
+ onBackButton(callback) {
376
+ window.addEventListener('pulse:backButton', callback);
377
+ }
378
+ }
379
+ };
380
+
381
+ // =========================================================================
382
+ // CSS for toast animation
383
+ // =========================================================================
384
+ const style = document.createElement('style');
385
+ style.textContent = `
386
+ @keyframes pulse-toast-in {
387
+ from { opacity: 0; transform: translateX(-50%) translateY(20px); }
388
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
389
+ }
390
+ @keyframes pulse-toast-out {
391
+ from { opacity: 1; transform: translateX(-50%) translateY(0); }
392
+ to { opacity: 0; transform: translateX(-50%) translateY(20px); }
393
+ }
394
+ `;
395
+ document.head.appendChild(style);
396
+
397
+ // =========================================================================
398
+ // Initialization
399
+ // =========================================================================
400
+
401
+ window.initPulseNative = function() {
402
+ console.log('[PulseMobile] Initialized on', PulseMobile.platform);
403
+ window.dispatchEvent(new CustomEvent('pulse:ready', {
404
+ detail: { platform: PulseMobile.platform }
405
+ }));
406
+ };
407
+
408
+ // Auto-init on web (native calls this after bridge is ready)
409
+ if (!isNative) {
410
+ if (document.readyState === 'complete') {
411
+ setTimeout(window.initPulseNative, 0);
412
+ } else {
413
+ window.addEventListener('load', () => setTimeout(window.initPulseNative, 0));
414
+ }
415
+ }
416
+
417
+ // Export globally
418
+ window.PulseMobile = PulseMobile;
419
+
420
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -20,11 +20,16 @@
20
20
  "runtime/",
21
21
  "compiler/",
22
22
  "loader/",
23
+ "mobile/",
23
24
  "README.md",
24
25
  "LICENSE"
25
26
  ],
26
27
  "scripts": {
27
- "test": "node --test test/*.js"
28
+ "test": "npm run test:compiler && npm run test:pulse && npm run test:dom",
29
+ "test:compiler": "node test/compiler.test.js",
30
+ "test:pulse": "node test/pulse.test.js",
31
+ "test:dom": "node test/dom.test.js",
32
+ "build:netlify": "node scripts/build-netlify.js"
28
33
  },
29
34
  "keywords": [
30
35
  "framework",
@@ -44,15 +49,17 @@
44
49
  "license": "MIT",
45
50
  "repository": {
46
51
  "type": "git",
47
- "url": "git+https://github.com/your-username/pulse-framework.git"
52
+ "url": "git+https://github.com/vincenthirtz/pulse-js-framework.git"
48
53
  },
49
- "homepage": "https://github.com/your-username/pulse-framework#readme",
54
+ "homepage": "https://github.com/vincenthirtz/pulse-js-framework#readme",
50
55
  "bugs": {
51
- "url": "https://github.com/your-username/pulse-framework/issues"
56
+ "url": "https://github.com/vincenthirtz/pulse-js-framework/issues"
52
57
  },
53
58
  "engines": {
54
59
  "node": ">=18.0.0"
55
60
  },
56
61
  "dependencies": {},
57
- "devDependencies": {}
62
+ "devDependencies": {
63
+ "linkedom": "^0.16.8"
64
+ }
58
65
  }