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/README.md +63 -0
- package/cli/build.js +124 -5
- package/cli/index.js +32 -5
- package/cli/mobile.js +1473 -0
- package/compiler/lexer.js +19 -2
- package/mobile/bridge/pulse-native.js +420 -0
- package/package.json +13 -6
- package/runtime/dom.js +363 -33
- package/runtime/index.js +2 -0
- package/runtime/native.js +368 -0
- package/runtime/pulse.js +247 -13
- package/runtime/router.js +10 -1
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
|
-
//
|
|
511
|
-
|
|
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.
|
|
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": "
|
|
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/
|
|
52
|
+
"url": "git+https://github.com/vincenthirtz/pulse-js-framework.git"
|
|
48
53
|
},
|
|
49
|
-
"homepage": "https://github.com/
|
|
54
|
+
"homepage": "https://github.com/vincenthirtz/pulse-js-framework#readme",
|
|
50
55
|
"bugs": {
|
|
51
|
-
"url": "https://github.com/
|
|
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
|
}
|