@uniweb/core 0.1.10 → 0.1.12

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 CHANGED
@@ -126,7 +126,6 @@ page.label // Short navigation label (or null)
126
126
  page.order // Sort order
127
127
  page.children // Child pages (for nested hierarchy)
128
128
  page.website // Back-reference to parent Website
129
- page.site // Alias for page.website
130
129
 
131
130
  // Navigation visibility
132
131
  page.hidden // Hidden from all navigation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,6 +27,6 @@
27
27
  "node": ">=20.19"
28
28
  },
29
29
  "dependencies": {
30
- "@uniweb/semantic-parser": "1.0.7"
30
+ "@uniweb/semantic-parser": "1.0.9"
31
31
  }
32
32
  }
package/src/block.js CHANGED
@@ -26,9 +26,9 @@ export default class Block {
26
26
  this.rawContent = blockData.content || {}
27
27
  this.parsedContent = this.parseContent(blockData.content)
28
28
 
29
- const { main, items } = this.parsedContent
30
- this.main = main
31
- this.items = items
29
+ // Flat content structure - no more nested main/items
30
+ // parsedContent now has: title, pretitle, paragraphs, links, imgs, items, etc.
31
+ this.items = this.parsedContent.items || []
32
32
 
33
33
  // Block configuration
34
34
  const blockConfig = blockData.params || blockData.config || {}
@@ -81,11 +81,10 @@ export default class Block {
81
81
  // Simple key-value content (PoC style) - pass through directly
82
82
  // This allows components to receive content like { title, subtitle, items }
83
83
  if (content && typeof content === 'object' && !Array.isArray(content)) {
84
+ // Mark as PoC format so runtime can detect and pass through
84
85
  return {
85
- main: { header: {}, body: {} },
86
- items: [],
87
- // Store raw content for direct access
88
- raw: content
86
+ _isPoc: true,
87
+ _pocContent: content
89
88
  }
90
89
  }
91
90
 
@@ -99,24 +98,15 @@ export default class Block {
99
98
  /**
100
99
  * Extract structured content from ProseMirror document
101
100
  * Uses @uniweb/semantic-parser for intelligent content extraction
101
+ * Returns flat content structure
102
102
  */
103
103
  extractFromProseMirror(doc) {
104
104
  try {
105
- // Parse with semantic-parser
106
- const { groups, sequence, byType } = parseSemanticContent(doc)
105
+ // Parse with semantic-parser - returns flat structure
106
+ const parsed = parseSemanticContent(doc)
107
107
 
108
- // Transform groups structure to match expected format
109
- const main = groups.main || { header: {}, body: {} }
110
- const items = groups.items || []
111
-
112
- return {
113
- main,
114
- items,
115
- // Include additional data for advanced use cases
116
- sequence,
117
- byType,
118
- metadata: groups.metadata
119
- }
108
+ // Parsed content is now flat: { title, pretitle, paragraphs, links, items, sequence, ... }
109
+ return parsed
120
110
  } catch (err) {
121
111
  console.warn('[Block] Semantic parser error, using fallback:', err.message)
122
112
  return this.extractFromProseMirrorFallback(doc)
@@ -125,29 +115,39 @@ export default class Block {
125
115
 
126
116
  /**
127
117
  * Fallback extraction when semantic-parser fails
118
+ * Returns flat content structure matching new parser output
128
119
  */
129
120
  extractFromProseMirrorFallback(doc) {
130
- const main = { header: {}, body: {} }
131
- const items = []
121
+ const content = {
122
+ title: '',
123
+ pretitle: '',
124
+ subtitle: '',
125
+ paragraphs: [],
126
+ links: [],
127
+ imgs: [],
128
+ lists: [],
129
+ icons: [],
130
+ items: [],
131
+ sequence: []
132
+ }
132
133
 
133
- if (!doc.content) return { main, items }
134
+ if (!doc.content) return content
134
135
 
135
136
  for (const node of doc.content) {
136
137
  if (node.type === 'heading') {
137
138
  const text = this.extractText(node)
138
139
  if (node.attrs?.level === 1) {
139
- main.header.title = text
140
+ content.title = text
140
141
  } else if (node.attrs?.level === 2) {
141
- main.header.subtitle = text
142
+ content.subtitle = text
142
143
  }
143
144
  } else if (node.type === 'paragraph') {
144
145
  const text = this.extractText(node)
145
- if (!main.body.paragraphs) main.body.paragraphs = []
146
- main.body.paragraphs.push(text)
146
+ content.paragraphs.push(text)
147
147
  }
148
148
  }
149
149
 
150
- return { main, items }
150
+ return content
151
151
  }
152
152
 
153
153
  /**
@@ -175,43 +175,43 @@ export default class Block {
175
175
  return null
176
176
  }
177
177
 
178
- // Get component-level block configuration
179
- // Supports: Component.block (preferred), Component.blockDefaults (legacy)
180
- const blockConfig = this.Component.block || this.Component.blockDefaults || {}
178
+ // Get runtime metadata for this component (from meta.js, extracted at build time)
179
+ const meta = globalThis.uniweb?.getComponentMeta(this.type) || {}
181
180
 
182
181
  // Initialize state (dynamic, can change at runtime)
183
- const stateDefaults = blockConfig.state || this.Component.blockState
182
+ // Source: meta.js initialState field
183
+ const stateDefaults = meta.initialState
184
184
  this.startState = stateDefaults ? { ...stateDefaults } : null
185
185
  this.initState()
186
186
 
187
187
  // Initialize context (static, per component type)
188
- this.context = blockConfig.context ? { ...blockConfig.context } : null
188
+ // Source: meta.js context field
189
+ this.context = meta.context ? { ...meta.context } : null
189
190
 
190
191
  return this.Component
191
192
  }
192
193
 
193
194
  /**
194
195
  * Get structured block content for components
196
+ * Returns flat content structure
195
197
  */
196
198
  getBlockContent() {
197
- const mainHeader = this.main?.header || {}
198
- const mainBody = this.main?.body || {}
199
- const banner = this.main?.banner || null
199
+ const c = this.parsedContent || {}
200
200
 
201
201
  return {
202
- banner,
203
- pretitle: mainHeader.pretitle || '',
204
- title: mainHeader.title || '',
205
- subtitle: mainHeader.subtitle || '',
206
- description: mainHeader.description || '',
207
- paragraphs: mainBody.paragraphs || [],
208
- images: mainBody.imgs || mainBody.images || [],
209
- links: mainBody.links || [],
210
- icons: mainBody.icons || [],
211
- properties: mainBody.propertyBlocks?.[0] || {},
212
- videos: mainBody.videos || [],
213
- lists: mainBody.lists || [],
214
- buttons: mainBody.buttons || []
202
+ pretitle: c.pretitle || '',
203
+ title: c.title || '',
204
+ subtitle: c.subtitle || '',
205
+ description: c.subtitle2 || '',
206
+ paragraphs: c.paragraphs || [],
207
+ images: c.imgs || [],
208
+ links: c.links || [],
209
+ icons: c.icons || [],
210
+ properties: c.propertyBlocks?.[0] || c.properties || {},
211
+ videos: c.videos || [],
212
+ lists: c.lists || [],
213
+ buttons: c.buttons || [],
214
+ items: c.items || []
215
215
  }
216
216
  }
217
217
 
@@ -236,14 +236,15 @@ export default class Block {
236
236
  */
237
237
  getBlockLinks(options = {}) {
238
238
  const website = globalThis.uniweb?.activeWebsite
239
+ const c = this.parsedContent || {}
239
240
 
240
241
  if (options.nested) {
241
- const lists = this.main?.body?.lists || []
242
+ const lists = c.lists || []
242
243
  const links = lists[0]
243
244
  return Block.parseNestedLinks(links, website)
244
245
  }
245
246
 
246
- const links = this.main?.body?.links || []
247
+ const links = c.links || []
247
248
  return links.map((link) => ({
248
249
  route: website?.makeHref(link.href) || link.href,
249
250
  label: link.label
package/src/page.js CHANGED
@@ -51,7 +51,6 @@ export default class Page {
51
51
 
52
52
  // Back-reference to website
53
53
  this.website = website
54
- this.site = website // Alias
55
54
 
56
55
  // Scroll position memory (for navigation restoration)
57
56
  this.scrollY = 0
@@ -385,4 +384,80 @@ export default class Page {
385
384
  hasChildren() {
386
385
  return this.children.length > 0
387
386
  }
387
+
388
+ /**
389
+ * Check if page has body content (sections)
390
+ * @returns {boolean}
391
+ */
392
+ hasContent() {
393
+ return this.pageBlocks.body.length > 0
394
+ }
395
+
396
+ // ─────────────────────────────────────────────────────────────────
397
+ // Active Route Detection
398
+ // ─────────────────────────────────────────────────────────────────
399
+
400
+ /**
401
+ * Get the first navigable route for this page.
402
+ * If page has no content, recursively finds first child with content.
403
+ * Useful for category pages that are just navigation containers.
404
+ *
405
+ * @returns {string} The route to navigate to
406
+ *
407
+ * @example
408
+ * // For a "Docs" category page with no content but children:
409
+ * // page.route = '/docs'
410
+ * // page.hasContent() = false
411
+ * // First child with content: '/docs/getting-started'
412
+ * page.getNavigableRoute() // Returns '/docs/getting-started'
413
+ */
414
+ getNavigableRoute() {
415
+ if (this.hasContent()) return this.route
416
+ for (const child of this.children || []) {
417
+ const route = child.getNavigableRoute()
418
+ if (route) return route
419
+ }
420
+ return this.route // Fallback to own route
421
+ }
422
+
423
+ /**
424
+ * Get route without leading/trailing slashes.
425
+ * Useful for route comparisons.
426
+ *
427
+ * @returns {string} Normalized route (e.g., 'docs/getting-started')
428
+ */
429
+ getNormalizedRoute() {
430
+ return (this.route || '').replace(/^\//, '').replace(/\/$/, '')
431
+ }
432
+
433
+ /**
434
+ * Check if this page matches the given route exactly.
435
+ *
436
+ * @param {string} route - Normalized route (no leading/trailing slashes)
437
+ * @returns {boolean} True if this page's route matches
438
+ */
439
+ isActiveFor(route) {
440
+ return this.getNormalizedRoute() === route
441
+ }
442
+
443
+ /**
444
+ * Check if this page or any descendant matches the given route.
445
+ * Useful for highlighting parent nav items when a child page is active.
446
+ *
447
+ * @param {string} route - Normalized route (no leading/trailing slashes)
448
+ * @returns {boolean} True if this page or a descendant is active
449
+ *
450
+ * @example
451
+ * // Page route: '/docs'
452
+ * // Current route: 'docs/getting-started/installation'
453
+ * page.isActiveOrAncestor('docs/getting-started/installation') // true
454
+ */
455
+ isActiveOrAncestor(route) {
456
+ const pageRoute = this.getNormalizedRoute()
457
+ if (pageRoute === route) return true
458
+ // Check if route starts with this page's route followed by /
459
+ // Handle empty pageRoute (root) specially
460
+ if (pageRoute === '') return true // Root is ancestor of all
461
+ return route.startsWith(pageRoute + '/')
462
+ }
388
463
  }
package/src/uniweb.js CHANGED
@@ -14,7 +14,8 @@ export default class Uniweb {
14
14
  this.childBlockRenderer = null // Function to render child blocks
15
15
  this.routingComponents = {} // Link, SafeHtml, useNavigate, etc.
16
16
  this.foundation = null // The loaded foundation module
17
- this.foundationConfig = {} // Configuration from foundation
17
+ this.foundationConfig = {} // Configuration from foundation (capabilities)
18
+ this.meta = {} // Per-component runtime metadata (from meta.js)
18
19
  this.language = 'en'
19
20
 
20
21
  // Initialize analytics (disabled by default, configure via site config)
@@ -27,6 +28,29 @@ export default class Uniweb {
27
28
  */
28
29
  setFoundation(foundation) {
29
30
  this.foundation = foundation
31
+
32
+ // Store per-component metadata if present
33
+ if (foundation.meta) {
34
+ this.meta = foundation.meta
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get runtime metadata for a component
40
+ * @param {string} componentName
41
+ * @returns {Object|null} Meta with defaults, context, initialState, background, data
42
+ */
43
+ getComponentMeta(componentName) {
44
+ return this.meta[componentName] || null
45
+ }
46
+
47
+ /**
48
+ * Get default param values for a component
49
+ * @param {string} componentName
50
+ * @returns {Object} Default values (empty object if none)
51
+ */
52
+ getComponentDefaults(componentName) {
53
+ return this.meta[componentName]?.defaults || {}
30
54
  }
31
55
 
32
56
  /**
package/src/website.js CHANGED
@@ -489,11 +489,12 @@ export default class Website {
489
489
  const buildPageInfo = (page) => ({
490
490
  id: page.id,
491
491
  route: page.getNavRoute(), // Use canonical nav route (e.g., '/' for index pages)
492
+ navigableRoute: page.getNavigableRoute(), // First route with content (for links)
492
493
  title: page.title,
493
494
  label: page.getLabel(),
494
495
  description: page.description,
495
496
  order: page.order,
496
- hasContent: page.getBodyBlocks().length > 0,
497
+ hasContent: page.hasContent(),
497
498
  children: nested && page.hasChildren()
498
499
  ? page.children.filter(isPageVisible).map(buildPageInfo)
499
500
  : []
@@ -528,4 +529,35 @@ export default class Website {
528
529
  getAllPages(includeHidden = false) {
529
530
  return this.getPageHierarchy({ nested: false, includeHidden })
530
531
  }
532
+
533
+ // ─────────────────────────────────────────────────────────────────
534
+ // Active Route API (for navigation components)
535
+ // ─────────────────────────────────────────────────────────────────
536
+
537
+ /**
538
+ * Get the current active route, normalized (no leading/trailing slashes).
539
+ * Works in both SSR (from activePage) and client (from activePage).
540
+ *
541
+ * @returns {string} Normalized route (e.g., 'docs/getting-started')
542
+ *
543
+ * @example
544
+ * website.getActiveRoute() // 'docs/getting-started'
545
+ */
546
+ getActiveRoute() {
547
+ return this.activePage?.getNormalizedRoute() || ''
548
+ }
549
+
550
+ /**
551
+ * Get the first segment of the active route.
552
+ * Useful for root-level navigation highlighting.
553
+ *
554
+ * @returns {string} First segment (e.g., 'docs' for 'docs/getting-started')
555
+ *
556
+ * @example
557
+ * // Active route: 'docs/getting-started/installation'
558
+ * website.getActiveRootSegment() // 'docs'
559
+ */
560
+ getActiveRootSegment() {
561
+ return this.getActiveRoute().split('/')[0]
562
+ }
531
563
  }