@zenithbuild/core 0.3.3 → 0.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/core",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Core library for the Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "exports": {
27
27
  ".": "./index.ts",
28
28
  "./compiler": "./compiler/index.ts",
29
+ "./config": "./core/config/index.ts",
29
30
  "./core": "./core/index.ts",
30
31
  "./router": "./router/index.ts",
31
32
  "./runtime": "./runtime/index.ts"
@@ -58,7 +59,9 @@
58
59
  "typescript": "^5"
59
60
  },
60
61
  "dependencies": {
62
+ "@types/marked": "^6.0.0",
61
63
  "@types/parse5": "^7.0.0",
64
+ "marked": "^17.0.1",
62
65
  "parse5": "^8.0.0"
63
66
  }
64
67
  }
@@ -17,7 +17,7 @@
17
17
  * This is served as an external JS file, not inlined
18
18
  */
19
19
  export function generateBundleJS(): string {
20
- return `/*!
20
+ return `/*!
21
21
  * Zenith Runtime v0.1.0
22
22
  * Shared client-side runtime for hydration and reactivity
23
23
  */
@@ -296,6 +296,57 @@ export function generateBundleJS(): string {
296
296
  return expressionRegistry.get(id);
297
297
  }
298
298
 
299
+ function updateNode(node, exprId, pageState) {
300
+ const expr = getExpression(exprId);
301
+ if (!expr) return;
302
+
303
+ zenEffect(function() {
304
+ const result = expr(pageState);
305
+
306
+ if (node.hasAttribute('data-zen-text')) {
307
+ // Handle complex text/children results
308
+ if (result === null || result === undefined || result === false) {
309
+ node.textContent = '';
310
+ } else if (typeof result === 'string') {
311
+ if (result.trim().startsWith('<') && result.trim().endsWith('>')) {
312
+ node.innerHTML = result;
313
+ } else {
314
+ node.textContent = result;
315
+ }
316
+ } else if (result instanceof Node) {
317
+ node.innerHTML = '';
318
+ node.appendChild(result);
319
+ } else if (Array.isArray(result)) {
320
+ node.innerHTML = '';
321
+ const fragment = document.createDocumentFragment();
322
+ result.flat(Infinity).forEach(item => {
323
+ if (item instanceof Node) fragment.appendChild(item);
324
+ else if (item != null && item !== false) fragment.appendChild(document.createTextNode(String(item)));
325
+ });
326
+ node.appendChild(fragment);
327
+ } else {
328
+ node.textContent = String(result);
329
+ }
330
+ } else {
331
+ // Attribute update
332
+ const attrNames = ['class', 'style', 'src', 'href', 'disabled', 'checked'];
333
+ for (const attr of attrNames) {
334
+ if (node.hasAttribute('data-zen-attr-' + attr)) {
335
+ if (attr === 'class' || attr === 'className') {
336
+ node.className = String(result || '');
337
+ } else if (attr === 'disabled' || attr === 'checked') {
338
+ if (result) node.setAttribute(attr, '');
339
+ else node.removeAttribute(attr);
340
+ } else {
341
+ if (result != null && result !== false) node.setAttribute(attr, String(result));
342
+ else node.removeAttribute(attr);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ });
348
+ }
349
+
299
350
  /**
300
351
  * Hydrate a page with reactive bindings
301
352
  * Called after page HTML is in DOM
@@ -303,49 +354,209 @@ export function generateBundleJS(): string {
303
354
  function zenithHydrate(pageState, container) {
304
355
  container = container || document;
305
356
 
306
- // Find all data-zen-bind elements
307
- const bindings = container.querySelectorAll('[data-zen-bind]');
357
+ // Find all text expression placeholders
358
+ const textNodes = container.querySelectorAll('[data-zen-text]');
359
+ textNodes.forEach(el => updateNode(el, el.getAttribute('data-zen-text'), pageState));
308
360
 
309
- bindings.forEach(function(el) {
310
- const bindType = el.getAttribute('data-zen-bind');
311
- const exprId = el.getAttribute('data-zen-expr');
312
-
313
- if (bindType === 'text' && exprId) {
314
- const expr = getExpression(exprId);
315
- if (expr) {
316
- zenEffect(function() {
317
- el.textContent = expr(pageState);
318
- });
319
- }
320
- } else if (bindType === 'attr') {
321
- const attrName = el.getAttribute('data-zen-attr');
322
- const expr = getExpression(exprId);
323
- if (expr && attrName) {
324
- zenEffect(function() {
325
- el.setAttribute(attrName, expr(pageState));
326
- });
327
- }
328
- }
361
+ // Find all attribute expression placeholders
362
+ const attrNodes = container.querySelectorAll('[data-zen-attr-class], [data-zen-attr-style], [data-zen-attr-src], [data-zen-attr-href]');
363
+ attrNodes.forEach(el => {
364
+ const attrMatch = Array.from(el.attributes).find(a => a.name.startsWith('data-zen-attr-'));
365
+ if (attrMatch) updateNode(el, attrMatch.value, pageState);
329
366
  });
330
367
 
331
368
  // Wire up event handlers
332
- const handlers = container.querySelectorAll('[data-zen-event]');
333
- handlers.forEach(function(el) {
334
- const eventData = el.getAttribute('data-zen-event');
335
- if (eventData) {
336
- const parts = eventData.split(':');
337
- const eventType = parts[0];
338
- const handlerName = parts[1];
339
- if (handlerName && global[handlerName]) {
340
- el.addEventListener(eventType, global[handlerName]);
369
+ const eventTypes = ['click', 'change', 'input', 'submit', 'focus', 'blur', 'keyup', 'keydown'];
370
+ eventTypes.forEach(eventType => {
371
+ const elements = container.querySelectorAll('[data-zen-' + eventType + ']');
372
+ elements.forEach(el => {
373
+ const handlerName = el.getAttribute('data-zen-' + eventType);
374
+ if (handlerName && (global[handlerName] || getExpression(handlerName))) {
375
+ el.addEventListener(eventType, function(e) {
376
+ const handler = global[handlerName] || getExpression(handlerName);
377
+ if (typeof handler === 'function') handler(e, el);
378
+ });
341
379
  }
342
- }
380
+ });
343
381
  });
344
382
 
345
383
  // Trigger mount
346
384
  triggerMount();
347
385
  }
348
386
 
387
+ // ============================================
388
+ // zenith:content - Content Engine
389
+ // ============================================
390
+
391
+ const schemaRegistry = new Map();
392
+ const builtInEnhancers = {
393
+ readTime: (item) => {
394
+ const wordsPerMinute = 200;
395
+ const text = item.content || '';
396
+ const wordCount = text.split(/\\s+/).length;
397
+ const minutes = Math.ceil(wordCount / wordsPerMinute);
398
+ return Object.assign({}, item, { readTime: minutes + ' min' });
399
+ },
400
+ wordCount: (item) => {
401
+ const text = item.content || '';
402
+ const wordCount = text.split(/\\s+/).length;
403
+ return Object.assign({}, item, { wordCount: wordCount });
404
+ }
405
+ };
406
+
407
+ async function applyEnhancers(item, enhancers) {
408
+ let enrichedItem = Object.assign({}, item);
409
+ for (const enhancer of enhancers) {
410
+ if (typeof enhancer === 'string') {
411
+ const fn = builtInEnhancers[enhancer];
412
+ if (fn) enrichedItem = await fn(enrichedItem);
413
+ } else if (typeof enhancer === 'function') {
414
+ enrichedItem = await enhancer(enrichedItem);
415
+ }
416
+ }
417
+ return enrichedItem;
418
+ }
419
+
420
+ class ZenCollection {
421
+ constructor(items) {
422
+ this.items = [...items];
423
+ this.filters = [];
424
+ this.sortField = null;
425
+ this.sortOrder = 'desc';
426
+ this.limitCount = null;
427
+ this.selectedFields = null;
428
+ this.enhancers = [];
429
+ this._groupByFolder = false;
430
+ }
431
+ where(fn) { this.filters.push(fn); return this; }
432
+ sortBy(field, order = 'desc') { this.sortField = field; this.sortOrder = order; return this; }
433
+ limit(n) { this.limitCount = n; return this; }
434
+ fields(f) { this.selectedFields = f; return this; }
435
+ enhanceWith(e) { this.enhancers.push(e); return this; }
436
+ groupByFolder() { this._groupByFolder = true; return this; }
437
+ get() {
438
+ let results = [...this.items];
439
+ for (const filter of this.filters) results = results.filter(filter);
440
+ if (this.sortField) {
441
+ results.sort((a, b) => {
442
+ const valA = a[this.sortField];
443
+ const valB = b[this.sortField];
444
+ if (valA < valB) return this.sortOrder === 'asc' ? -1 : 1;
445
+ if (valA > valB) return this.sortOrder === 'asc' ? 1 : -1;
446
+ return 0;
447
+ });
448
+ }
449
+ if (this.limitCount !== null) results = results.slice(0, this.limitCount);
450
+
451
+ // Apply enhancers synchronously if possible
452
+ if (this.enhancers.length > 0) {
453
+ results = results.map(item => {
454
+ let enrichedItem = Object.assign({}, item);
455
+ for (const enhancer of this.enhancers) {
456
+ if (typeof enhancer === 'string') {
457
+ const fn = builtInEnhancers[enhancer];
458
+ if (fn) enrichedItem = fn(enrichedItem);
459
+ } else if (typeof enhancer === 'function') {
460
+ enrichedItem = enhancer(enrichedItem);
461
+ }
462
+ }
463
+ return enrichedItem;
464
+ });
465
+ }
466
+
467
+ if (this.selectedFields) {
468
+ results = results.map(item => {
469
+ const newItem = {};
470
+ this.selectedFields.forEach(f => { newItem[f] = item[f]; });
471
+ return newItem;
472
+ });
473
+ }
474
+
475
+ // Group by folder if requested
476
+ if (this._groupByFolder) {
477
+ const groups = {};
478
+ const groupOrder = [];
479
+ for (const item of results) {
480
+ // Extract folder from slug (e.g., "getting-started/installation" -> "getting-started")
481
+ const slug = item.slug || item.id || '';
482
+ const parts = slug.split('/');
483
+ const folder = parts.length > 1 ? parts[0] : 'root';
484
+
485
+ if (!groups[folder]) {
486
+ groups[folder] = {
487
+ id: folder,
488
+ title: folder.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
489
+ items: []
490
+ };
491
+ groupOrder.push(folder);
492
+ }
493
+ groups[folder].items.push(item);
494
+ }
495
+ return groupOrder.map(f => groups[f]);
496
+ }
497
+
498
+ return results;
499
+ }
500
+ }
501
+
502
+ function defineSchema(name, schema) { schemaRegistry.set(name, schema); }
503
+
504
+ function zenCollection(collectionName) {
505
+ const data = (global.__ZENITH_CONTENT__ && global.__ZENITH_CONTENT__[collectionName]) || [];
506
+ return new ZenCollection(data);
507
+ }
508
+
509
+ // Virtual DOM Helper for JSX-style expressions
510
+ function h(tag, props, children) {
511
+ const el = document.createElement(tag);
512
+ if (props) {
513
+ for (const [key, value] of Object.entries(props)) {
514
+ if (key.startsWith('on') && typeof value === 'function') {
515
+ el.addEventListener(key.slice(2).toLowerCase(), value);
516
+ } else if (key === 'class' || key === 'className') {
517
+ el.className = String(value || '');
518
+ } else if (key === 'style' && typeof value === 'object') {
519
+ Object.assign(el.style, value);
520
+ } else if (value != null && value !== false) {
521
+ el.setAttribute(key, String(value));
522
+ }
523
+ }
524
+ }
525
+ if (children != null && children !== false) {
526
+ // Flatten nested arrays (from .map() calls)
527
+ const childrenArray = Array.isArray(children) ? children.flat(Infinity) : [children];
528
+ for (const child of childrenArray) {
529
+ // Skip null, undefined, and false
530
+ if (child == null || child === false) continue;
531
+
532
+ if (typeof child === 'string') {
533
+ // Check if string looks like HTML
534
+ if (child.trim().startsWith('<') && child.trim().endsWith('>')) {
535
+ // Render as HTML
536
+ const wrapper = document.createElement('div');
537
+ wrapper.innerHTML = child;
538
+ while (wrapper.firstChild) {
539
+ el.appendChild(wrapper.firstChild);
540
+ }
541
+ } else {
542
+ el.appendChild(document.createTextNode(child));
543
+ }
544
+ } else if (typeof child === 'number') {
545
+ el.appendChild(document.createTextNode(String(child)));
546
+ } else if (child instanceof Node) {
547
+ el.appendChild(child);
548
+ } else if (Array.isArray(child)) {
549
+ // Handle nested arrays (shouldn't happen after flat() but just in case)
550
+ for (const c of child) {
551
+ if (c instanceof Node) el.appendChild(c);
552
+ else if (c != null && c !== false) el.appendChild(document.createTextNode(String(c)));
553
+ }
554
+ }
555
+ }
556
+ }
557
+ return el;
558
+ }
559
+
349
560
  // ============================================
350
561
  // Export to window.__zenith
351
562
  // ============================================
@@ -359,6 +570,11 @@ export function generateBundleJS(): string {
359
570
  ref: zenRef,
360
571
  batch: zenBatch,
361
572
  untrack: zenUntrack,
573
+ // zenith:content
574
+ defineSchema: defineSchema,
575
+ zenCollection: zenCollection,
576
+ // Virtual DOM helper for JSX
577
+ h: h,
362
578
  // Lifecycle
363
579
  onMount: zenOnMount,
364
580
  onUnmount: zenOnUnmount,
@@ -394,6 +610,51 @@ export function generateBundleJS(): string {
394
610
  global.onMount = zenOnMount;
395
611
  global.onUnmount = zenOnUnmount;
396
612
 
613
+ // ============================================
614
+ // HMR Client (Development Only)
615
+ // ============================================
616
+
617
+ if (typeof window !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1')) {
618
+ let socket;
619
+ function connectHMR() {
620
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
621
+ socket = new WebSocket(protocol + '//' + location.host + '/hmr');
622
+
623
+ socket.onmessage = function(event) {
624
+ try {
625
+ const data = JSON.parse(event.data);
626
+ if (data.type === 'reload') {
627
+ console.log('[Zenith] HMR: Reloading page...');
628
+ location.reload();
629
+ } else if (data.type === 'style-update') {
630
+ console.log('[Zenith] HMR: Updating style ' + data.url);
631
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
632
+ for (let i = 0; i < links.length; i++) {
633
+ const link = links[i];
634
+ const url = new URL(link.href);
635
+ if (url.pathname === data.url) {
636
+ link.href = data.url + '?t=' + Date.now();
637
+ break;
638
+ }
639
+ }
640
+ }
641
+ } catch (e) {
642
+ console.error('[Zenith] HMR Error:', e);
643
+ }
644
+ };
645
+
646
+ socket.onclose = function() {
647
+ console.log('[Zenith] HMR: Connection closed. Retrying in 2s...');
648
+ setTimeout(connectHMR, 2000);
649
+ };
650
+ }
651
+
652
+ // Connect unless explicitly disabled
653
+ if (!window.__ZENITH_NO_HMR__) {
654
+ connectHMR();
655
+ }
656
+ }
657
+
397
658
  })(typeof window !== 'undefined' ? window : this);
398
659
  `
399
660
  }
@@ -403,14 +664,14 @@ export function generateBundleJS(): string {
403
664
  * For production builds
404
665
  */
405
666
  export function generateMinifiedBundleJS(): string {
406
- // For now, return non-minified
407
- // TODO: Add minification via terser or similar
408
- return generateBundleJS()
667
+ // For now, return non-minified
668
+ // TODO: Add minification via terser or similar
669
+ return generateBundleJS()
409
670
  }
410
671
 
411
672
  /**
412
673
  * Get bundle version for cache busting
413
674
  */
414
675
  export function getBundleVersion(): string {
415
- return '0.1.0'
676
+ return '0.1.0'
416
677
  }
@@ -358,6 +358,23 @@ function updateTextBinding(node: Element, expressionId: string, state: any): voi
358
358
  const result = expression(state);
359
359
  if (result === null || result === undefined || result === false) {
360
360
  node.textContent = '';
361
+ } else if (typeof result === 'string') {
362
+ if (result.trim().startsWith('<') && result.trim().endsWith('>')) {
363
+ node.innerHTML = result;
364
+ } else {
365
+ node.textContent = result;
366
+ }
367
+ } else if (result instanceof Node) {
368
+ node.innerHTML = '';
369
+ node.appendChild(result);
370
+ } else if (Array.isArray(result)) {
371
+ node.innerHTML = '';
372
+ const fragment = document.createDocumentFragment();
373
+ result.flat(Infinity).forEach(item => {
374
+ if (item instanceof Node) fragment.appendChild(item);
375
+ else if (item != null && item !== false) fragment.appendChild(document.createTextNode(String(item)));
376
+ });
377
+ node.appendChild(fragment);
361
378
  } else {
362
379
  node.textContent = String(result);
363
380
  }