@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 +0 -1
- package/package.json +2 -2
- package/src/block.js +53 -52
- package/src/page.js +76 -1
- package/src/uniweb.js +25 -1
- package/src/website.js +33 -1
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.
|
|
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.
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
105
|
+
// Parse with semantic-parser - returns flat structure
|
|
106
|
+
const parsed = parseSemanticContent(doc)
|
|
107
107
|
|
|
108
|
-
//
|
|
109
|
-
|
|
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
|
|
131
|
-
|
|
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
|
|
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
|
-
|
|
140
|
+
content.title = text
|
|
140
141
|
} else if (node.attrs?.level === 2) {
|
|
141
|
-
|
|
142
|
+
content.subtitle = text
|
|
142
143
|
}
|
|
143
144
|
} else if (node.type === 'paragraph') {
|
|
144
145
|
const text = this.extractText(node)
|
|
145
|
-
|
|
146
|
-
main.body.paragraphs.push(text)
|
|
146
|
+
content.paragraphs.push(text)
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
return
|
|
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
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
198
|
-
const mainBody = this.main?.body || {}
|
|
199
|
-
const banner = this.main?.banner || null
|
|
199
|
+
const c = this.parsedContent || {}
|
|
200
200
|
|
|
201
201
|
return {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
}
|