pulse-js-framework 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.
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "pulse-js-framework",
3
+ "version": "1.0.0",
4
+ "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "pulse": "cli/index.js"
9
+ },
10
+ "exports": {
11
+ ".": "./index.js",
12
+ "./runtime": "./runtime/index.js",
13
+ "./runtime/*": "./runtime/*.js",
14
+ "./compiler": "./compiler/index.js",
15
+ "./vite": "./loader/vite-plugin.js"
16
+ },
17
+ "files": [
18
+ "index.js",
19
+ "cli/",
20
+ "runtime/",
21
+ "compiler/",
22
+ "loader/",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "test": "node --test test/*.js"
28
+ },
29
+ "keywords": [
30
+ "framework",
31
+ "frontend",
32
+ "reactive",
33
+ "declarative",
34
+ "dom",
35
+ "pulse",
36
+ "css-selectors",
37
+ "dsl",
38
+ "ui",
39
+ "javascript",
40
+ "spa",
41
+ "signals"
42
+ ],
43
+ "author": "Your Name <your.email@example.com>",
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/your-username/pulse-framework.git"
48
+ },
49
+ "homepage": "https://github.com/your-username/pulse-framework#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/your-username/pulse-framework/issues"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "dependencies": {},
57
+ "devDependencies": {}
58
+ }
package/runtime/dom.js ADDED
@@ -0,0 +1,484 @@
1
+ /**
2
+ * Pulse DOM - Declarative DOM manipulation
3
+ *
4
+ * Creates DOM elements using CSS selector-like syntax
5
+ * and provides reactive bindings
6
+ */
7
+
8
+ import { effect, pulse, batch } from './pulse.js';
9
+
10
+ /**
11
+ * Parse a CSS selector-like string into element configuration
12
+ * Supports: tag, #id, .class, [attr=value]
13
+ *
14
+ * Examples:
15
+ * "div" -> { tag: "div" }
16
+ * "#app" -> { tag: "div", id: "app" }
17
+ * ".container" -> { tag: "div", classes: ["container"] }
18
+ * "button.primary.large" -> { tag: "button", classes: ["primary", "large"] }
19
+ * "input[type=text][placeholder=Name]" -> { tag: "input", attrs: { type: "text", placeholder: "Name" } }
20
+ */
21
+ function parseSelector(selector) {
22
+ const config = {
23
+ tag: 'div',
24
+ id: null,
25
+ classes: [],
26
+ attrs: {}
27
+ };
28
+
29
+ if (!selector || selector === '') return config;
30
+
31
+ // Match tag name at the start
32
+ const tagMatch = selector.match(/^([a-zA-Z][a-zA-Z0-9-]*)/);
33
+ if (tagMatch) {
34
+ config.tag = tagMatch[1];
35
+ selector = selector.slice(tagMatch[0].length);
36
+ }
37
+
38
+ // Match ID
39
+ const idMatch = selector.match(/#([a-zA-Z][a-zA-Z0-9-_]*)/);
40
+ if (idMatch) {
41
+ config.id = idMatch[1];
42
+ selector = selector.replace(idMatch[0], '');
43
+ }
44
+
45
+ // Match classes
46
+ const classMatches = selector.matchAll(/\.([a-zA-Z][a-zA-Z0-9-_]*)/g);
47
+ for (const match of classMatches) {
48
+ config.classes.push(match[1]);
49
+ }
50
+
51
+ // Match attributes
52
+ const attrMatches = selector.matchAll(/\[([a-zA-Z][a-zA-Z0-9-_]*)(?:=([^\]]+))?\]/g);
53
+ for (const match of attrMatches) {
54
+ const key = match[1];
55
+ let value = match[2] || '';
56
+ // Remove quotes if present
57
+ if ((value.startsWith('"') && value.endsWith('"')) ||
58
+ (value.startsWith("'") && value.endsWith("'"))) {
59
+ value = value.slice(1, -1);
60
+ }
61
+ config.attrs[key] = value;
62
+ }
63
+
64
+ return config;
65
+ }
66
+
67
+ /**
68
+ * Create a DOM element from a selector
69
+ */
70
+ export function el(selector, ...children) {
71
+ const config = parseSelector(selector);
72
+ const element = document.createElement(config.tag);
73
+
74
+ if (config.id) {
75
+ element.id = config.id;
76
+ }
77
+
78
+ if (config.classes.length > 0) {
79
+ element.className = config.classes.join(' ');
80
+ }
81
+
82
+ for (const [key, value] of Object.entries(config.attrs)) {
83
+ element.setAttribute(key, value);
84
+ }
85
+
86
+ // Process children
87
+ for (const child of children) {
88
+ appendChild(element, child);
89
+ }
90
+
91
+ return element;
92
+ }
93
+
94
+ /**
95
+ * Append a child to an element, handling various types
96
+ */
97
+ function appendChild(parent, child) {
98
+ if (child == null || child === false) return;
99
+
100
+ if (typeof child === 'string' || typeof child === 'number') {
101
+ parent.appendChild(document.createTextNode(String(child)));
102
+ } else if (child instanceof Node) {
103
+ parent.appendChild(child);
104
+ } else if (Array.isArray(child)) {
105
+ for (const c of child) {
106
+ appendChild(parent, c);
107
+ }
108
+ } else if (typeof child === 'function') {
109
+ // Reactive child - create a placeholder and update it
110
+ const placeholder = document.createComment('pulse');
111
+ parent.appendChild(placeholder);
112
+ let currentNodes = [];
113
+
114
+ effect(() => {
115
+ const result = child();
116
+
117
+ // Remove old nodes
118
+ for (const node of currentNodes) {
119
+ node.remove();
120
+ }
121
+ currentNodes = [];
122
+
123
+ // Add new nodes
124
+ if (result != null && result !== false) {
125
+ const fragment = document.createDocumentFragment();
126
+ if (typeof result === 'string' || typeof result === 'number') {
127
+ const textNode = document.createTextNode(String(result));
128
+ fragment.appendChild(textNode);
129
+ currentNodes.push(textNode);
130
+ } else if (result instanceof Node) {
131
+ fragment.appendChild(result);
132
+ currentNodes.push(result);
133
+ } else if (Array.isArray(result)) {
134
+ for (const r of result) {
135
+ if (r instanceof Node) {
136
+ fragment.appendChild(r);
137
+ currentNodes.push(r);
138
+ } else if (r != null && r !== false) {
139
+ const textNode = document.createTextNode(String(r));
140
+ fragment.appendChild(textNode);
141
+ currentNodes.push(textNode);
142
+ }
143
+ }
144
+ }
145
+ placeholder.parentNode.insertBefore(fragment, placeholder.nextSibling);
146
+ }
147
+ });
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Create a reactive text node
153
+ */
154
+ export function text(getValue) {
155
+ if (typeof getValue === 'function') {
156
+ const node = document.createTextNode('');
157
+ effect(() => {
158
+ node.textContent = String(getValue());
159
+ });
160
+ return node;
161
+ }
162
+ return document.createTextNode(String(getValue));
163
+ }
164
+
165
+ /**
166
+ * Bind an attribute reactively
167
+ */
168
+ export function bind(element, attr, getValue) {
169
+ if (typeof getValue === 'function') {
170
+ effect(() => {
171
+ const value = getValue();
172
+ if (value == null || value === false) {
173
+ element.removeAttribute(attr);
174
+ } else if (value === true) {
175
+ element.setAttribute(attr, '');
176
+ } else {
177
+ element.setAttribute(attr, String(value));
178
+ }
179
+ });
180
+ } else {
181
+ element.setAttribute(attr, String(getValue));
182
+ }
183
+ return element;
184
+ }
185
+
186
+ /**
187
+ * Bind a property reactively
188
+ */
189
+ export function prop(element, propName, getValue) {
190
+ if (typeof getValue === 'function') {
191
+ effect(() => {
192
+ element[propName] = getValue();
193
+ });
194
+ } else {
195
+ element[propName] = getValue;
196
+ }
197
+ return element;
198
+ }
199
+
200
+ /**
201
+ * Bind CSS class reactively
202
+ */
203
+ export function cls(element, className, condition) {
204
+ if (typeof condition === 'function') {
205
+ effect(() => {
206
+ if (condition()) {
207
+ element.classList.add(className);
208
+ } else {
209
+ element.classList.remove(className);
210
+ }
211
+ });
212
+ } else if (condition) {
213
+ element.classList.add(className);
214
+ }
215
+ return element;
216
+ }
217
+
218
+ /**
219
+ * Bind style property reactively
220
+ */
221
+ export function style(element, prop, getValue) {
222
+ if (typeof getValue === 'function') {
223
+ effect(() => {
224
+ element.style[prop] = getValue();
225
+ });
226
+ } else {
227
+ element.style[prop] = getValue;
228
+ }
229
+ return element;
230
+ }
231
+
232
+ /**
233
+ * Attach an event listener
234
+ */
235
+ export function on(element, event, handler, options) {
236
+ element.addEventListener(event, handler, options);
237
+ return element;
238
+ }
239
+
240
+ /**
241
+ * Create a reactive list
242
+ */
243
+ export function list(getItems, template, keyFn = (item, i) => i) {
244
+ const container = document.createDocumentFragment();
245
+ const startMarker = document.createComment('list-start');
246
+ const endMarker = document.createComment('list-end');
247
+
248
+ container.appendChild(startMarker);
249
+ container.appendChild(endMarker);
250
+
251
+ let itemNodes = new Map(); // key -> { nodes: Node[], cleanup: Function }
252
+
253
+ effect(() => {
254
+ const items = typeof getItems === 'function' ? getItems() : getItems.get();
255
+ const newKeys = new Set();
256
+
257
+ // Build new list
258
+ const fragment = document.createDocumentFragment();
259
+ const newItemNodes = new Map();
260
+
261
+ items.forEach((item, index) => {
262
+ const key = keyFn(item, index);
263
+ newKeys.add(key);
264
+
265
+ if (itemNodes.has(key)) {
266
+ // Reuse existing nodes
267
+ const existing = itemNodes.get(key);
268
+ for (const node of existing.nodes) {
269
+ fragment.appendChild(node);
270
+ }
271
+ newItemNodes.set(key, existing);
272
+ } else {
273
+ // Create new nodes
274
+ const result = template(item, index);
275
+ const nodes = Array.isArray(result) ? result : [result];
276
+ for (const node of nodes) {
277
+ fragment.appendChild(node);
278
+ }
279
+ newItemNodes.set(key, { nodes, cleanup: null });
280
+ }
281
+ });
282
+
283
+ // Remove old items
284
+ for (const [key, entry] of itemNodes) {
285
+ if (!newKeys.has(key)) {
286
+ for (const node of entry.nodes) {
287
+ node.remove();
288
+ }
289
+ if (entry.cleanup) entry.cleanup();
290
+ }
291
+ }
292
+
293
+ // Clear between markers
294
+ let current = startMarker.nextSibling;
295
+ while (current && current !== endMarker) {
296
+ const next = current.nextSibling;
297
+ current.remove();
298
+ current = next;
299
+ }
300
+
301
+ // Insert new fragment
302
+ endMarker.parentNode?.insertBefore(fragment, endMarker);
303
+ itemNodes = newItemNodes;
304
+ });
305
+
306
+ return container;
307
+ }
308
+
309
+ /**
310
+ * Conditional rendering
311
+ */
312
+ export function when(condition, thenTemplate, elseTemplate = null) {
313
+ const container = document.createDocumentFragment();
314
+ const marker = document.createComment('when');
315
+ container.appendChild(marker);
316
+
317
+ let currentNodes = [];
318
+ let currentCleanup = null;
319
+
320
+ effect(() => {
321
+ const show = typeof condition === 'function' ? condition() : condition.get();
322
+
323
+ // Cleanup previous
324
+ for (const node of currentNodes) {
325
+ node.remove();
326
+ }
327
+ if (currentCleanup) currentCleanup();
328
+ currentNodes = [];
329
+ currentCleanup = null;
330
+
331
+ // Render new
332
+ const template = show ? thenTemplate : elseTemplate;
333
+ if (template) {
334
+ const result = typeof template === 'function' ? template() : template;
335
+ if (result) {
336
+ const nodes = Array.isArray(result) ? result : [result];
337
+ const fragment = document.createDocumentFragment();
338
+ for (const node of nodes) {
339
+ if (node instanceof Node) {
340
+ fragment.appendChild(node);
341
+ currentNodes.push(node);
342
+ }
343
+ }
344
+ marker.parentNode?.insertBefore(fragment, marker.nextSibling);
345
+ }
346
+ }
347
+ });
348
+
349
+ return container;
350
+ }
351
+
352
+ /**
353
+ * Switch/case rendering
354
+ */
355
+ export function match(getValue, cases) {
356
+ const marker = document.createComment('match');
357
+ let currentNodes = [];
358
+
359
+ effect(() => {
360
+ const value = typeof getValue === 'function' ? getValue() : getValue.get();
361
+
362
+ // Remove old nodes
363
+ for (const node of currentNodes) {
364
+ node.remove();
365
+ }
366
+ currentNodes = [];
367
+
368
+ // Find matching case
369
+ const template = cases[value] ?? cases.default;
370
+ if (template) {
371
+ const result = typeof template === 'function' ? template() : template;
372
+ if (result) {
373
+ const nodes = Array.isArray(result) ? result : [result];
374
+ const fragment = document.createDocumentFragment();
375
+ for (const node of nodes) {
376
+ if (node instanceof Node) {
377
+ fragment.appendChild(node);
378
+ currentNodes.push(node);
379
+ }
380
+ }
381
+ marker.parentNode?.insertBefore(fragment, marker.nextSibling);
382
+ }
383
+ }
384
+ });
385
+
386
+ return marker;
387
+ }
388
+
389
+ /**
390
+ * Two-way binding for form inputs
391
+ */
392
+ export function model(element, pulseValue) {
393
+ const tagName = element.tagName.toLowerCase();
394
+ const type = element.type?.toLowerCase();
395
+
396
+ if (tagName === 'input' && (type === 'checkbox' || type === 'radio')) {
397
+ // Checkbox/Radio
398
+ effect(() => {
399
+ element.checked = pulseValue.get();
400
+ });
401
+ element.addEventListener('change', () => {
402
+ pulseValue.set(element.checked);
403
+ });
404
+ } else if (tagName === 'select') {
405
+ // Select
406
+ effect(() => {
407
+ element.value = pulseValue.get();
408
+ });
409
+ element.addEventListener('change', () => {
410
+ pulseValue.set(element.value);
411
+ });
412
+ } else {
413
+ // Text input, textarea, etc.
414
+ effect(() => {
415
+ if (element.value !== pulseValue.get()) {
416
+ element.value = pulseValue.get();
417
+ }
418
+ });
419
+ element.addEventListener('input', () => {
420
+ pulseValue.set(element.value);
421
+ });
422
+ }
423
+
424
+ return element;
425
+ }
426
+
427
+ /**
428
+ * Mount an element to a target
429
+ */
430
+ export function mount(target, element) {
431
+ if (typeof target === 'string') {
432
+ target = document.querySelector(target);
433
+ }
434
+ if (!target) {
435
+ throw new Error('Mount target not found');
436
+ }
437
+ target.appendChild(element);
438
+ return () => {
439
+ element.remove();
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Create a component factory
445
+ */
446
+ export function component(setup) {
447
+ return (props = {}) => {
448
+ const state = {};
449
+ const methods = {};
450
+
451
+ const ctx = {
452
+ state,
453
+ methods,
454
+ props,
455
+ pulse,
456
+ el,
457
+ text,
458
+ list,
459
+ when,
460
+ on,
461
+ bind,
462
+ model
463
+ };
464
+
465
+ return setup(ctx);
466
+ };
467
+ }
468
+
469
+ export default {
470
+ el,
471
+ text,
472
+ bind,
473
+ prop,
474
+ cls,
475
+ style,
476
+ on,
477
+ list,
478
+ when,
479
+ match,
480
+ model,
481
+ mount,
482
+ component,
483
+ parseSelector
484
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Pulse Runtime - Main exports
3
+ */
4
+
5
+ export * from './pulse.js';
6
+ export * from './dom.js';
7
+ export * from './router.js';
8
+ export * from './store.js';
9
+
10
+ export { default as PulseCore } from './pulse.js';
11
+ export { default as PulseDOM } from './dom.js';
12
+ export { default as PulseRouter } from './router.js';
13
+ export { default as PulseStore } from './store.js';