@zenithbuild/core 0.3.3 → 0.4.2

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.2",
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,323 @@ 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
+ // ============================================
510
+ // useZenOrder - Documentation ordering & navigation
511
+ // ============================================
512
+
513
+ function slugify(text) {
514
+ return String(text || '')
515
+ .toLowerCase()
516
+ .replace(/[^\\w\\s-]/g, '')
517
+ .replace(/\\s+/g, '-')
518
+ .replace(/-+/g, '-')
519
+ .trim();
520
+ }
521
+
522
+ function getDocSlug(doc) {
523
+ const slugOrId = String(doc.slug || doc.id || '');
524
+ const parts = slugOrId.split('/');
525
+ const filename = parts[parts.length - 1];
526
+ return filename ? slugify(filename) : slugify(doc.title || 'untitled');
527
+ }
528
+
529
+ function processRawSections(rawSections) {
530
+ const sections = (rawSections || []).map(function(rawSection) {
531
+ const sectionSlug = slugify(rawSection.title || rawSection.id || 'section');
532
+ const items = (rawSection.items || []).map(function(item) {
533
+ return Object.assign({}, item, {
534
+ slug: getDocSlug(item),
535
+ sectionSlug: sectionSlug,
536
+ isIntro: item.intro === true || (item.tags && item.tags.includes && item.tags.includes('intro'))
537
+ });
538
+ });
539
+
540
+ // Sort items: intro first, then order, then alphabetical
541
+ items.sort(function(a, b) {
542
+ if (a.isIntro && !b.isIntro) return -1;
543
+ if (!a.isIntro && b.isIntro) return 1;
544
+ if (a.order !== undefined && b.order !== undefined) return a.order - b.order;
545
+ if (a.order !== undefined) return -1;
546
+ if (b.order !== undefined) return 1;
547
+ return (a.title || '').localeCompare(b.title || '');
548
+ });
549
+
550
+ return {
551
+ id: rawSection.id || sectionSlug,
552
+ title: rawSection.title || 'Untitled',
553
+ slug: sectionSlug,
554
+ order: rawSection.order !== undefined ? rawSection.order : (rawSection.meta && rawSection.meta.order),
555
+ hasIntro: items.some(function(i) { return i.isIntro; }),
556
+ items: items
557
+ };
558
+ });
559
+
560
+ // Sort sections: order → hasIntro → alphabetical
561
+ sections.sort(function(a, b) {
562
+ if (a.order !== undefined && b.order !== undefined) return a.order - b.order;
563
+ if (a.order !== undefined) return -1;
564
+ if (b.order !== undefined) return 1;
565
+ if (a.hasIntro && !b.hasIntro) return -1;
566
+ if (!a.hasIntro && b.hasIntro) return 1;
567
+ return a.title.localeCompare(b.title);
568
+ });
569
+
570
+ return sections;
571
+ }
572
+
573
+ function createZenOrder(rawSections) {
574
+ const sections = processRawSections(rawSections);
575
+
576
+ return {
577
+ sections: sections,
578
+ selectedSection: sections[0] || null,
579
+ selectedDoc: sections[0] && sections[0].items[0] || null,
580
+
581
+ getSectionBySlug: function(sectionSlug) {
582
+ return sections.find(function(s) { return s.slug === sectionSlug; }) || null;
583
+ },
584
+
585
+ getDocBySlug: function(sectionSlug, docSlug) {
586
+ var section = sections.find(function(s) { return s.slug === sectionSlug; });
587
+ if (!section) return null;
588
+ return section.items.find(function(d) { return d.slug === docSlug; }) || null;
589
+ },
590
+
591
+ getNextDoc: function(currentDoc) {
592
+ if (!currentDoc) return null;
593
+ var currentSection = sections.find(function(s) { return s.slug === currentDoc.sectionSlug; });
594
+ if (!currentSection) return null;
595
+ var idx = currentSection.items.findIndex(function(d) { return d.slug === currentDoc.slug; });
596
+ if (idx < currentSection.items.length - 1) return currentSection.items[idx + 1];
597
+ var secIdx = sections.findIndex(function(s) { return s.slug === currentSection.slug; });
598
+ if (secIdx < sections.length - 1) return sections[secIdx + 1].items[0] || null;
599
+ return null;
600
+ },
601
+
602
+ getPrevDoc: function(currentDoc) {
603
+ if (!currentDoc) return null;
604
+ var currentSection = sections.find(function(s) { return s.slug === currentDoc.sectionSlug; });
605
+ if (!currentSection) return null;
606
+ var idx = currentSection.items.findIndex(function(d) { return d.slug === currentDoc.slug; });
607
+ if (idx > 0) return currentSection.items[idx - 1];
608
+ var secIdx = sections.findIndex(function(s) { return s.slug === currentSection.slug; });
609
+ if (secIdx > 0) {
610
+ var prev = sections[secIdx - 1];
611
+ return prev.items[prev.items.length - 1] || null;
612
+ }
613
+ return null;
614
+ },
615
+
616
+ buildDocUrl: function(sectionSlug, docSlug) {
617
+ if (!docSlug || docSlug === 'index') return '/documentation/' + sectionSlug;
618
+ return '/documentation/' + sectionSlug + '/' + docSlug;
619
+ }
620
+ };
621
+ }
622
+
623
+ // Virtual DOM Helper for JSX-style expressions
624
+ function h(tag, props, children) {
625
+ const el = document.createElement(tag);
626
+ if (props) {
627
+ for (const [key, value] of Object.entries(props)) {
628
+ if (key.startsWith('on') && typeof value === 'function') {
629
+ el.addEventListener(key.slice(2).toLowerCase(), value);
630
+ } else if (key === 'class' || key === 'className') {
631
+ el.className = String(value || '');
632
+ } else if (key === 'style' && typeof value === 'object') {
633
+ Object.assign(el.style, value);
634
+ } else if (value != null && value !== false) {
635
+ el.setAttribute(key, String(value));
636
+ }
637
+ }
638
+ }
639
+ if (children != null && children !== false) {
640
+ // Flatten nested arrays (from .map() calls)
641
+ const childrenArray = Array.isArray(children) ? children.flat(Infinity) : [children];
642
+ for (const child of childrenArray) {
643
+ // Skip null, undefined, and false
644
+ if (child == null || child === false) continue;
645
+
646
+ if (typeof child === 'string') {
647
+ // Check if string looks like HTML
648
+ if (child.trim().startsWith('<') && child.trim().endsWith('>')) {
649
+ // Render as HTML
650
+ const wrapper = document.createElement('div');
651
+ wrapper.innerHTML = child;
652
+ while (wrapper.firstChild) {
653
+ el.appendChild(wrapper.firstChild);
654
+ }
655
+ } else {
656
+ el.appendChild(document.createTextNode(child));
657
+ }
658
+ } else if (typeof child === 'number') {
659
+ el.appendChild(document.createTextNode(String(child)));
660
+ } else if (child instanceof Node) {
661
+ el.appendChild(child);
662
+ } else if (Array.isArray(child)) {
663
+ // Handle nested arrays (shouldn't happen after flat() but just in case)
664
+ for (const c of child) {
665
+ if (c instanceof Node) el.appendChild(c);
666
+ else if (c != null && c !== false) el.appendChild(document.createTextNode(String(c)));
667
+ }
668
+ }
669
+ }
670
+ }
671
+ return el;
672
+ }
673
+
349
674
  // ============================================
350
675
  // Export to window.__zenith
351
676
  // ============================================
@@ -359,6 +684,15 @@ export function generateBundleJS(): string {
359
684
  ref: zenRef,
360
685
  batch: zenBatch,
361
686
  untrack: zenUntrack,
687
+ // zenith:content
688
+ defineSchema: defineSchema,
689
+ zenCollection: zenCollection,
690
+ // useZenOrder hook
691
+ createZenOrder: createZenOrder,
692
+ processRawSections: processRawSections,
693
+ slugify: slugify,
694
+ // Virtual DOM helper for JSX
695
+ h: h,
362
696
  // Lifecycle
363
697
  onMount: zenOnMount,
364
698
  onUnmount: zenOnUnmount,
@@ -394,6 +728,56 @@ export function generateBundleJS(): string {
394
728
  global.onMount = zenOnMount;
395
729
  global.onUnmount = zenOnUnmount;
396
730
 
731
+ // useZenOrder hook exports
732
+ global.createZenOrder = createZenOrder;
733
+ global.processRawSections = processRawSections;
734
+ global.slugify = slugify;
735
+
736
+ // ============================================
737
+ // HMR Client (Development Only)
738
+ // ============================================
739
+
740
+ if (typeof window !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1')) {
741
+ let socket;
742
+ function connectHMR() {
743
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
744
+ socket = new WebSocket(protocol + '//' + location.host + '/hmr');
745
+
746
+ socket.onmessage = function(event) {
747
+ try {
748
+ const data = JSON.parse(event.data);
749
+ if (data.type === 'reload') {
750
+ console.log('[Zenith] HMR: Reloading page...');
751
+ location.reload();
752
+ } else if (data.type === 'style-update') {
753
+ console.log('[Zenith] HMR: Updating style ' + data.url);
754
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
755
+ for (let i = 0; i < links.length; i++) {
756
+ const link = links[i];
757
+ const url = new URL(link.href);
758
+ if (url.pathname === data.url) {
759
+ link.href = data.url + '?t=' + Date.now();
760
+ break;
761
+ }
762
+ }
763
+ }
764
+ } catch (e) {
765
+ console.error('[Zenith] HMR Error:', e);
766
+ }
767
+ };
768
+
769
+ socket.onclose = function() {
770
+ console.log('[Zenith] HMR: Connection closed. Retrying in 2s...');
771
+ setTimeout(connectHMR, 2000);
772
+ };
773
+ }
774
+
775
+ // Connect unless explicitly disabled
776
+ if (!window.__ZENITH_NO_HMR__) {
777
+ connectHMR();
778
+ }
779
+ }
780
+
397
781
  })(typeof window !== 'undefined' ? window : this);
398
782
  `
399
783
  }
@@ -403,14 +787,14 @@ export function generateBundleJS(): string {
403
787
  * For production builds
404
788
  */
405
789
  export function generateMinifiedBundleJS(): string {
406
- // For now, return non-minified
407
- // TODO: Add minification via terser or similar
408
- return generateBundleJS()
790
+ // For now, return non-minified
791
+ // TODO: Add minification via terser or similar
792
+ return generateBundleJS()
409
793
  }
410
794
 
411
795
  /**
412
796
  * Get bundle version for cache busting
413
797
  */
414
798
  export function getBundleVersion(): string {
415
- return '0.1.0'
799
+ return '0.1.0'
416
800
  }
@@ -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
  }