@lokascript/core 1.1.3 → 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.
@@ -34,8 +34,14 @@ export const AVAILABLE_COMMANDS = [
34
34
  // Utility
35
35
  'log',
36
36
  'call',
37
+ 'copy',
38
+ 'beep',
37
39
  // Navigation
38
40
  'go',
41
+ 'push',
42
+ 'push-url',
43
+ 'replace',
44
+ 'replace-url',
39
45
  // Focus
40
46
  'focus',
41
47
  'blur',
@@ -43,6 +49,13 @@ export const AVAILABLE_COMMANDS = [
43
49
  'return',
44
50
  'break',
45
51
  'continue',
52
+ 'halt',
53
+ 'exit',
54
+ 'throw',
55
+ // Advanced execution
56
+ 'js',
57
+ // DOM morphing (requires morphlex import)
58
+ 'morph',
46
59
  ] as const;
47
60
 
48
61
  /**
@@ -58,33 +71,23 @@ export const AVAILABLE_BLOCKS = ['if', 'repeat', 'for', 'while', 'fetch'] as con
58
71
  export const FULL_RUNTIME_ONLY_COMMANDS = [
59
72
  // Advanced execution
60
73
  'async',
61
- 'js',
62
74
  // DOM operations (complex)
63
75
  'make',
64
76
  'swap',
65
- 'morph',
66
77
  'process-partials',
67
78
  // Data binding
68
79
  'bind',
69
80
  'persist',
70
81
  'default',
71
- // Utility (complex)
72
- 'beep',
82
+ // Utility (complex - need runtime integration)
73
83
  'tell',
74
- 'copy',
75
84
  'pick',
76
- // Navigation (complex)
77
- 'push-url',
78
- 'replace-url',
79
- // Control flow (advanced)
80
- 'halt',
81
- 'exit',
82
- 'throw',
85
+ // Conditional (already have 'if' block)
83
86
  'unless',
84
- // Animation (advanced)
87
+ // Animation (advanced - needs helpers)
85
88
  'settle',
86
89
  'measure',
87
- // Behaviors
90
+ // Behaviors (requires registry)
88
91
  'install',
89
92
  ] as const;
90
93
 
@@ -259,6 +259,34 @@ const COMMAND_IMPLEMENTATIONS_TS: Record<string, string> = {
259
259
  return content;
260
260
  }`,
261
261
 
262
+ morph: `
263
+ case 'morph': {
264
+ const targets = await getTarget();
265
+ const content = await evaluate(cmd.args[0], ctx);
266
+ const contentStr = String(content);
267
+ const isOuter = cmd.modifier === 'over';
268
+
269
+ for (const target of targets) {
270
+ try {
271
+ if (isOuter) {
272
+ morphlexMorph(target, contentStr);
273
+ } else {
274
+ morphlexMorphInner(target, contentStr);
275
+ }
276
+ } catch (error) {
277
+ // Fallback to innerHTML/outerHTML if morph fails
278
+ console.warn('[LokaScript] Morph failed, falling back:', error);
279
+ if (isOuter) {
280
+ target.outerHTML = contentStr;
281
+ } else {
282
+ target.innerHTML = contentStr;
283
+ }
284
+ }
285
+ }
286
+ ctx.it = targets.length === 1 ? targets[0] : targets;
287
+ return ctx.it;
288
+ }`,
289
+
262
290
  take: `
263
291
  case 'take': {
264
292
  const className = getClassName(await evaluate(cmd.args[0], ctx));
@@ -350,6 +378,201 @@ const COMMAND_IMPLEMENTATIONS_TS: Record<string, string> = {
350
378
  case 'continue': {
351
379
  throw { type: 'continue' };
352
380
  }`,
381
+
382
+ // === Control Flow Commands ===
383
+ halt: `
384
+ case 'halt': {
385
+ // Check for "halt the event" pattern
386
+ const firstArg = cmd.args[0];
387
+ let targetEvent = ctx.event;
388
+ if (firstArg?.type === 'identifier' && firstArg.name === 'the' && cmd.args[1]?.name === 'event') {
389
+ targetEvent = ctx.event;
390
+ } else if (firstArg) {
391
+ const evaluated = await evaluate(firstArg, ctx);
392
+ if (evaluated?.preventDefault) targetEvent = evaluated;
393
+ }
394
+
395
+ if (targetEvent && typeof targetEvent.preventDefault === 'function') {
396
+ targetEvent.preventDefault();
397
+ targetEvent.stopPropagation();
398
+ return { halted: true, eventHalted: true };
399
+ }
400
+
401
+ // Regular halt - stop execution
402
+ const haltError = new Error('HALT_EXECUTION');
403
+ (haltError as any).isHalt = true;
404
+ throw haltError;
405
+ }`,
406
+
407
+ exit: `
408
+ case 'exit': {
409
+ const exitError = new Error('EXIT_COMMAND');
410
+ (exitError as any).isExit = true;
411
+ throw exitError;
412
+ }`,
413
+
414
+ throw: `
415
+ case 'throw': {
416
+ const message = cmd.args[0] ? await evaluate(cmd.args[0], ctx) : 'Error';
417
+ const errorToThrow = message instanceof Error ? message : new Error(String(message));
418
+ throw errorToThrow;
419
+ }`,
420
+
421
+ // === Debug Commands ===
422
+ beep: `
423
+ case 'beep': {
424
+ const values = await Promise.all(cmd.args.map((a: any) => evaluate(a, ctx)));
425
+ const displayValues = values.length > 0 ? values : [ctx.it];
426
+
427
+ for (const val of displayValues) {
428
+ const typeInfo = val === null ? 'null' :
429
+ val === undefined ? 'undefined' :
430
+ Array.isArray(val) ? \`Array[\${val.length}]\` :
431
+ val instanceof Element ? \`Element<\${val.tagName.toLowerCase()}>\` :
432
+ typeof val;
433
+ console.log('[beep]', typeInfo + ':', val);
434
+ }
435
+ return displayValues[0];
436
+ }`,
437
+
438
+ // === JavaScript Execution ===
439
+ js: `
440
+ case 'js': {
441
+ const codeArg = cmd.args[0];
442
+ let jsCode = '';
443
+
444
+ if (codeArg.type === 'string') {
445
+ jsCode = codeArg.value;
446
+ } else if (codeArg.type === 'template') {
447
+ jsCode = await evaluate(codeArg, ctx);
448
+ } else {
449
+ jsCode = String(await evaluate(codeArg, ctx));
450
+ }
451
+
452
+ // Build context object for the Function
453
+ const jsContext = {
454
+ me: ctx.me,
455
+ it: ctx.it,
456
+ event: ctx.event,
457
+ target: ctx.target || ctx.me,
458
+ locals: Object.fromEntries(ctx.locals),
459
+ globals: Object.fromEntries(globalVars),
460
+ document: typeof document !== 'undefined' ? document : undefined,
461
+ window: typeof window !== 'undefined' ? window : undefined,
462
+ };
463
+
464
+ try {
465
+ const fn = new Function('ctx', \`with(ctx) { return (async () => { \${jsCode} })(); }\`);
466
+ const result = await fn(jsContext);
467
+ ctx.it = result;
468
+ return result;
469
+ } catch (error) {
470
+ console.error('[js] Execution error:', error);
471
+ throw error;
472
+ }
473
+ }`,
474
+
475
+ // === Clipboard Commands ===
476
+ copy: `
477
+ case 'copy': {
478
+ const source = await evaluate(cmd.args[0], ctx);
479
+ let textToCopy = '';
480
+
481
+ if (typeof source === 'string') {
482
+ textToCopy = source;
483
+ } else if (source instanceof Element) {
484
+ textToCopy = source.textContent || '';
485
+ } else {
486
+ textToCopy = String(source);
487
+ }
488
+
489
+ try {
490
+ if (navigator.clipboard) {
491
+ await navigator.clipboard.writeText(textToCopy);
492
+ } else {
493
+ // Fallback for older browsers
494
+ const textarea = document.createElement('textarea');
495
+ textarea.value = textToCopy;
496
+ textarea.style.cssText = 'position:fixed;top:0;left:-9999px';
497
+ document.body.appendChild(textarea);
498
+ textarea.select();
499
+ document.execCommand('copy');
500
+ document.body.removeChild(textarea);
501
+ }
502
+
503
+ if (ctx.me instanceof Element) {
504
+ ctx.me.dispatchEvent(new CustomEvent('copy:success', {
505
+ bubbles: true,
506
+ detail: { text: textToCopy }
507
+ }));
508
+ }
509
+ ctx.it = textToCopy;
510
+ return textToCopy;
511
+ } catch (error) {
512
+ if (ctx.me instanceof Element) {
513
+ ctx.me.dispatchEvent(new CustomEvent('copy:error', {
514
+ bubbles: true,
515
+ detail: { error }
516
+ }));
517
+ }
518
+ throw error;
519
+ }
520
+ }`,
521
+
522
+ // === URL Navigation Commands ===
523
+ push: `
524
+ case 'push':
525
+ case 'push-url': {
526
+ // Handle "push url '/path'" pattern
527
+ let urlArg = cmd.args[0];
528
+ if (urlArg?.type === 'identifier' && urlArg.name === 'url') {
529
+ urlArg = cmd.args[1];
530
+ }
531
+
532
+ const url = String(await evaluate(urlArg, ctx));
533
+ let title = '';
534
+
535
+ // Check for "with title" modifier
536
+ if (cmd.modifiers?.title) {
537
+ title = String(await evaluate(cmd.modifiers.title, ctx));
538
+ }
539
+
540
+ window.history.pushState(null, '', url);
541
+ if (title) document.title = title;
542
+
543
+ window.dispatchEvent(new CustomEvent('lokascript:pushurl', {
544
+ detail: { url, title }
545
+ }));
546
+
547
+ return { url, title, mode: 'push' };
548
+ }`,
549
+
550
+ replace: `
551
+ case 'replace':
552
+ case 'replace-url': {
553
+ // Handle "replace url '/path'" pattern
554
+ let urlArg = cmd.args[0];
555
+ if (urlArg?.type === 'identifier' && urlArg.name === 'url') {
556
+ urlArg = cmd.args[1];
557
+ }
558
+
559
+ const url = String(await evaluate(urlArg, ctx));
560
+ let title = '';
561
+
562
+ // Check for "with title" modifier
563
+ if (cmd.modifiers?.title) {
564
+ title = String(await evaluate(cmd.modifiers.title, ctx));
565
+ }
566
+
567
+ window.history.replaceState(null, '', url);
568
+ if (title) document.title = title;
569
+
570
+ window.dispatchEvent(new CustomEvent('lokascript:replaceurl', {
571
+ detail: { url, title }
572
+ }));
573
+
574
+ return { url, title, mode: 'replace' };
575
+ }`,
353
576
  };
354
577
 
355
578
  /**
@@ -466,6 +689,11 @@ export const STYLE_COMMANDS = ['set', 'put', 'increment', 'decrement'];
466
689
  */
467
690
  export const ELEMENT_ARRAY_COMMANDS = ['put', 'increment', 'decrement'];
468
691
 
692
+ /**
693
+ * Commands that require morphlex import for DOM morphing
694
+ */
695
+ export const MORPH_COMMANDS = ['morph'];
696
+
469
697
  /**
470
698
  * Get command implementations for the specified format.
471
699
  * @param format 'ts' for TypeScript, 'js' for JavaScript
@@ -105,7 +105,19 @@ describe('Bundle Generator Validation', () => {
105
105
  expect(AVAILABLE_COMMANDS).toContain('remove');
106
106
  expect(AVAILABLE_COMMANDS).toContain('break');
107
107
  expect(AVAILABLE_COMMANDS).toContain('continue');
108
- expect(AVAILABLE_COMMANDS.length).toBeGreaterThan(20);
108
+ // New commands added for hybrid-plus
109
+ expect(AVAILABLE_COMMANDS).toContain('js');
110
+ expect(AVAILABLE_COMMANDS).toContain('copy');
111
+ expect(AVAILABLE_COMMANDS).toContain('beep');
112
+ expect(AVAILABLE_COMMANDS).toContain('halt');
113
+ expect(AVAILABLE_COMMANDS).toContain('exit');
114
+ expect(AVAILABLE_COMMANDS).toContain('throw');
115
+ expect(AVAILABLE_COMMANDS).toContain('push');
116
+ expect(AVAILABLE_COMMANDS).toContain('push-url');
117
+ expect(AVAILABLE_COMMANDS).toContain('replace');
118
+ expect(AVAILABLE_COMMANDS).toContain('replace-url');
119
+ expect(AVAILABLE_COMMANDS).toContain('morph');
120
+ expect(AVAILABLE_COMMANDS.length).toBeGreaterThan(30);
109
121
  });
110
122
 
111
123
  it('should list all available blocks', () => {
@@ -115,10 +127,20 @@ describe('Bundle Generator Validation', () => {
115
127
  it('should list full runtime only commands', () => {
116
128
  expect(FULL_RUNTIME_ONLY_COMMANDS).toContain('async');
117
129
  expect(FULL_RUNTIME_ONLY_COMMANDS).toContain('swap');
118
- expect(FULL_RUNTIME_ONLY_COMMANDS).toContain('morph');
119
- // break and continue are now available in lite bundles
130
+ expect(FULL_RUNTIME_ONLY_COMMANDS).toContain('tell');
131
+ expect(FULL_RUNTIME_ONLY_COMMANDS).toContain('install');
132
+ // These are now available in lite bundles (with morphlex/native support)
133
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('morph');
120
134
  expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('break');
121
135
  expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('continue');
136
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('js');
137
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('copy');
138
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('beep');
139
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('halt');
140
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('exit');
141
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('throw');
142
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('push');
143
+ expect(FULL_RUNTIME_ONLY_COMMANDS).not.toContain('replace');
122
144
  });
123
145
 
124
146
  it('isAvailableCommand should return true for valid commands', () => {