@uniweb/core 0.1.5 → 0.1.7
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 +1 -1
- package/src/block.js +76 -8
- package/src/page.js +74 -6
- package/src/website.js +113 -9
package/package.json
CHANGED
package/src/block.js
CHANGED
|
@@ -10,9 +10,14 @@ import { parseContent as parseSemanticContent } from '@uniweb/semantic-parser'
|
|
|
10
10
|
export default class Block {
|
|
11
11
|
constructor(blockData, id) {
|
|
12
12
|
this.id = id
|
|
13
|
-
|
|
13
|
+
// 'type' matches frontmatter convention; 'component' supported for backwards compatibility
|
|
14
|
+
this.type = blockData.type || blockData.component || 'Section'
|
|
14
15
|
this.Component = null
|
|
15
16
|
|
|
17
|
+
// Back-references (set by Page when creating blocks)
|
|
18
|
+
this.page = null
|
|
19
|
+
this.website = null
|
|
20
|
+
|
|
16
21
|
// Content structure
|
|
17
22
|
// The content can be:
|
|
18
23
|
// 1. Raw ProseMirror content (from content collection)
|
|
@@ -28,7 +33,7 @@ export default class Block {
|
|
|
28
33
|
// Block configuration
|
|
29
34
|
const blockConfig = blockData.params || blockData.config || {}
|
|
30
35
|
this.preset = blockData.preset
|
|
31
|
-
this.themeName =
|
|
36
|
+
this.themeName = blockConfig.theme || 'light'
|
|
32
37
|
this.standardOptions = blockConfig.standardOptions || {}
|
|
33
38
|
this.properties = blockConfig.properties || blockConfig
|
|
34
39
|
|
|
@@ -40,10 +45,13 @@ export default class Block {
|
|
|
40
45
|
// Input data
|
|
41
46
|
this.input = blockData.input || null
|
|
42
47
|
|
|
43
|
-
// State management
|
|
48
|
+
// State management (dynamic, can change at runtime)
|
|
44
49
|
this.startState = null
|
|
45
50
|
this.state = null
|
|
46
51
|
this.resetStateHook = null
|
|
52
|
+
|
|
53
|
+
// Context (static, defined per component type)
|
|
54
|
+
this.context = null
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -160,18 +168,25 @@ export default class Block {
|
|
|
160
168
|
initComponent() {
|
|
161
169
|
if (this.Component) return this.Component
|
|
162
170
|
|
|
163
|
-
this.Component = globalThis.uniweb?.getComponent(this.
|
|
171
|
+
this.Component = globalThis.uniweb?.getComponent(this.type)
|
|
164
172
|
|
|
165
173
|
if (!this.Component) {
|
|
166
|
-
console.warn(`[Block] Component not found: ${this.
|
|
174
|
+
console.warn(`[Block] Component not found: ${this.type}`)
|
|
167
175
|
return null
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
this.
|
|
178
|
+
// Get component-level block configuration
|
|
179
|
+
// Supports: Component.block (preferred), Component.blockDefaults (legacy)
|
|
180
|
+
const blockConfig = this.Component.block || this.Component.blockDefaults || {}
|
|
181
|
+
|
|
182
|
+
// Initialize state (dynamic, can change at runtime)
|
|
183
|
+
const stateDefaults = blockConfig.state || this.Component.blockState
|
|
184
|
+
this.startState = stateDefaults ? { ...stateDefaults } : null
|
|
173
185
|
this.initState()
|
|
174
186
|
|
|
187
|
+
// Initialize context (static, per component type)
|
|
188
|
+
this.context = blockConfig.context ? { ...blockConfig.context } : null
|
|
189
|
+
|
|
175
190
|
return this.Component
|
|
176
191
|
}
|
|
177
192
|
|
|
@@ -243,6 +258,59 @@ export default class Block {
|
|
|
243
258
|
if (this.resetStateHook) this.resetStateHook()
|
|
244
259
|
}
|
|
245
260
|
|
|
261
|
+
// ─────────────────────────────────────────────────────────────────
|
|
262
|
+
// Cross-Block Communication
|
|
263
|
+
// ─────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get this block's index within its page.
|
|
267
|
+
* Useful for finding neighboring blocks.
|
|
268
|
+
*
|
|
269
|
+
* @returns {number} The index, or -1 if not found
|
|
270
|
+
*/
|
|
271
|
+
getIndex() {
|
|
272
|
+
if (!this.page) return -1
|
|
273
|
+
return this.page.getBlockIndex(this)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get information about this block for cross-component communication.
|
|
278
|
+
* Other components (like NavBar) can use this to adapt their behavior.
|
|
279
|
+
*
|
|
280
|
+
* @returns {Object} Block info: { type, theme, state, context }
|
|
281
|
+
*/
|
|
282
|
+
getBlockInfo() {
|
|
283
|
+
return {
|
|
284
|
+
type: this.type,
|
|
285
|
+
theme: this.themeName,
|
|
286
|
+
state: this.state,
|
|
287
|
+
context: this.context
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get information about the next block in the page.
|
|
293
|
+
* Commonly used by headers/navbars to adapt to the first content section.
|
|
294
|
+
*
|
|
295
|
+
* @returns {Object|null} Next block's info or null
|
|
296
|
+
*/
|
|
297
|
+
getNextBlockInfo() {
|
|
298
|
+
const index = this.getIndex()
|
|
299
|
+
if (index < 0 || !this.page) return null
|
|
300
|
+
return this.page.getBlockInfo(index + 1)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get information about the previous block in the page.
|
|
305
|
+
*
|
|
306
|
+
* @returns {Object|null} Previous block's info or null
|
|
307
|
+
*/
|
|
308
|
+
getPrevBlockInfo() {
|
|
309
|
+
const index = this.getIndex()
|
|
310
|
+
if (index <= 0 || !this.page) return null
|
|
311
|
+
return this.page.getBlockInfo(index - 1)
|
|
312
|
+
}
|
|
313
|
+
|
|
246
314
|
/**
|
|
247
315
|
* React hook for block state management
|
|
248
316
|
* @param {Function} useState - React useState hook
|
package/src/page.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import Block from './block.js'
|
|
9
9
|
|
|
10
10
|
export default class Page {
|
|
11
|
-
constructor(pageData, id, pageHeader, pageFooter, pageLeft, pageRight) {
|
|
11
|
+
constructor(pageData, id, website, pageHeader, pageFooter, pageLeft, pageRight) {
|
|
12
12
|
this.id = id
|
|
13
13
|
this.route = pageData.route
|
|
14
14
|
this.title = pageData.title || ''
|
|
@@ -48,9 +48,9 @@ export default class Page {
|
|
|
48
48
|
// Child pages (for nested hierarchy) - populated by Website
|
|
49
49
|
this.children = []
|
|
50
50
|
|
|
51
|
-
// Back-reference to website
|
|
52
|
-
this.website =
|
|
53
|
-
this.site =
|
|
51
|
+
// Back-reference to website
|
|
52
|
+
this.website = website
|
|
53
|
+
this.site = website // Alias
|
|
54
54
|
|
|
55
55
|
// Scroll position memory (for navigation restoration)
|
|
56
56
|
this.scrollY = 0
|
|
@@ -99,18 +99,46 @@ export default class Page {
|
|
|
99
99
|
buildPageBlocks(body, header, footer, left, right) {
|
|
100
100
|
const buildBlocks = (sections, prefix) => {
|
|
101
101
|
if (!sections || sections.length === 0) return null
|
|
102
|
-
return sections.map((section, index) =>
|
|
102
|
+
return sections.map((section, index) => {
|
|
103
|
+
const block = new Block(section, `${prefix}-${index}`)
|
|
104
|
+
this.initBlockReferences(block)
|
|
105
|
+
return block
|
|
106
|
+
})
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
const bodyBlocks = (body || []).map((section, index) => {
|
|
110
|
+
const block = new Block(section, index)
|
|
111
|
+
this.initBlockReferences(block)
|
|
112
|
+
return block
|
|
113
|
+
})
|
|
114
|
+
|
|
105
115
|
return {
|
|
106
116
|
header: buildBlocks(header, 'header'),
|
|
107
|
-
body:
|
|
117
|
+
body: bodyBlocks,
|
|
108
118
|
footer: buildBlocks(footer, 'footer'),
|
|
109
119
|
left: buildBlocks(left, 'left'),
|
|
110
120
|
right: buildBlocks(right, 'right')
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Initialize block back-references to page and website.
|
|
126
|
+
* Also recursively sets references for child blocks.
|
|
127
|
+
*
|
|
128
|
+
* @param {Block} block - The block to initialize
|
|
129
|
+
*/
|
|
130
|
+
initBlockReferences(block) {
|
|
131
|
+
block.page = this
|
|
132
|
+
block.website = this.website
|
|
133
|
+
|
|
134
|
+
// Recursively set references for child blocks
|
|
135
|
+
if (block.childBlocks?.length) {
|
|
136
|
+
for (const childBlock of block.childBlocks) {
|
|
137
|
+
this.initBlockReferences(childBlock)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
/**
|
|
115
143
|
* Get all block groups (for Layout component)
|
|
116
144
|
* @returns {Object} { header, body, footer, left, right }
|
|
@@ -119,6 +147,46 @@ export default class Page {
|
|
|
119
147
|
return this.pageBlocks
|
|
120
148
|
}
|
|
121
149
|
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────
|
|
151
|
+
// Cross-Block Communication
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Find a block's index within the page's block list.
|
|
156
|
+
* Searches across all layout areas (header, body, footer, left, right).
|
|
157
|
+
*
|
|
158
|
+
* @param {Block} block - The block to find
|
|
159
|
+
* @returns {number} The index in the flat list, or -1 if not found
|
|
160
|
+
*/
|
|
161
|
+
getBlockIndex(block) {
|
|
162
|
+
const allBlocks = this.getPageBlocks()
|
|
163
|
+
return allBlocks.indexOf(block)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get information about a block at a specific index.
|
|
168
|
+
* Used for cross-component communication (e.g., NavBar checking Hero's theme).
|
|
169
|
+
*
|
|
170
|
+
* @param {number} index - The block index
|
|
171
|
+
* @returns {Object|null} Block info { theme, component, state } or null
|
|
172
|
+
*/
|
|
173
|
+
getBlockInfo(index) {
|
|
174
|
+
const allBlocks = this.getPageBlocks()
|
|
175
|
+
const block = allBlocks[index]
|
|
176
|
+
return block?.getBlockInfo() || null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the first body block's info.
|
|
181
|
+
* Common use case: NavBar checking if first section supports overlay.
|
|
182
|
+
*
|
|
183
|
+
* @returns {Object|null} First body block's info or null
|
|
184
|
+
*/
|
|
185
|
+
getFirstBodyBlockInfo() {
|
|
186
|
+
const bodyBlocks = this.pageBlocks.body
|
|
187
|
+
return bodyBlocks?.[0]?.getBlockInfo() || null
|
|
188
|
+
}
|
|
189
|
+
|
|
122
190
|
/**
|
|
123
191
|
* Get all blocks (header, body, footer) as flat array
|
|
124
192
|
* Respects page layout preferences (hasHeader, hasFooter, etc.)
|
package/src/website.js
CHANGED
|
@@ -46,14 +46,11 @@ export default class Website {
|
|
|
46
46
|
.filter((page) => !specialRoutes.includes(page.route))
|
|
47
47
|
.map(
|
|
48
48
|
(page, index) =>
|
|
49
|
-
new Page(page, index, this.headerPage, this.footerPage, this.leftPage, this.rightPage)
|
|
49
|
+
new Page(page, index, this, this.headerPage, this.footerPage, this.leftPage, this.rightPage)
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
page.website = this
|
|
55
|
-
page.site = this // Alias
|
|
56
|
-
}
|
|
52
|
+
// Build parent-child relationships based on route structure
|
|
53
|
+
this.buildPageHierarchy()
|
|
57
54
|
|
|
58
55
|
this.activePage =
|
|
59
56
|
this.pages.find((page) => page.route === '/' || page.route === '/index') ||
|
|
@@ -104,6 +101,53 @@ export default class Website {
|
|
|
104
101
|
}))
|
|
105
102
|
}
|
|
106
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Build parent-child relationships between pages based on route structure
|
|
106
|
+
* E.g., /getting-started/installation is a child of /getting-started
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
buildPageHierarchy() {
|
|
110
|
+
// Sort pages by route depth (parents before children)
|
|
111
|
+
const sortedPages = [...this.pages].sort((a, b) => {
|
|
112
|
+
const depthA = (a.route.match(/\//g) || []).length
|
|
113
|
+
const depthB = (b.route.match(/\//g) || []).length
|
|
114
|
+
return depthA - depthB
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Build a map of route to page for quick lookup
|
|
118
|
+
const pageMap = new Map()
|
|
119
|
+
for (const page of sortedPages) {
|
|
120
|
+
pageMap.set(page.route, page)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// For each page, find its parent and add it as a child
|
|
124
|
+
for (const page of sortedPages) {
|
|
125
|
+
const route = page.route
|
|
126
|
+
if (route === '/' || route === '') continue
|
|
127
|
+
|
|
128
|
+
// Find parent route by removing the last segment
|
|
129
|
+
// /getting-started/installation -> /getting-started
|
|
130
|
+
const segments = route.split('/').filter(Boolean)
|
|
131
|
+
if (segments.length <= 1) continue // Root-level pages have no parent
|
|
132
|
+
|
|
133
|
+
// Build parent route
|
|
134
|
+
const parentRoute = '/' + segments.slice(0, -1).join('/')
|
|
135
|
+
const parent = pageMap.get(parentRoute)
|
|
136
|
+
|
|
137
|
+
if (parent) {
|
|
138
|
+
parent.children.push(page)
|
|
139
|
+
page.parent = parent
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Sort children by order
|
|
144
|
+
for (const page of this.pages) {
|
|
145
|
+
if (page.children.length > 0) {
|
|
146
|
+
page.children.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
107
151
|
/**
|
|
108
152
|
* Get page by route
|
|
109
153
|
* @param {string} route
|
|
@@ -287,8 +331,60 @@ export default class Website {
|
|
|
287
331
|
return defaultVal
|
|
288
332
|
}
|
|
289
333
|
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────
|
|
335
|
+
// Search API
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check if search is enabled for this site
|
|
340
|
+
* @returns {boolean}
|
|
341
|
+
*/
|
|
342
|
+
isSearchEnabled() {
|
|
343
|
+
// Search is enabled by default unless explicitly disabled
|
|
344
|
+
return this.config?.search?.enabled !== false
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get search configuration
|
|
349
|
+
* @returns {Object} Search configuration
|
|
350
|
+
*/
|
|
351
|
+
getSearchConfig() {
|
|
352
|
+
const config = this.config?.search || {}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
enabled: this.isSearchEnabled(),
|
|
356
|
+
indexUrl: this.getSearchIndexUrl(),
|
|
357
|
+
locale: this.getActiveLocale(),
|
|
358
|
+
include: {
|
|
359
|
+
pages: config.include?.pages !== false,
|
|
360
|
+
sections: config.include?.sections !== false,
|
|
361
|
+
headings: config.include?.headings !== false,
|
|
362
|
+
paragraphs: config.include?.paragraphs !== false,
|
|
363
|
+
links: config.include?.links !== false,
|
|
364
|
+
lists: config.include?.lists !== false
|
|
365
|
+
},
|
|
366
|
+
exclude: {
|
|
367
|
+
routes: config.exclude?.routes || [],
|
|
368
|
+
components: config.exclude?.components || []
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get the URL for the search index file
|
|
375
|
+
* @returns {string} URL to fetch the search index
|
|
376
|
+
*/
|
|
377
|
+
getSearchIndexUrl() {
|
|
378
|
+
const locale = this.getActiveLocale()
|
|
379
|
+
const isDefault = locale === this.getDefaultLocale()
|
|
380
|
+
|
|
381
|
+
// Default locale uses root path, others use locale prefix
|
|
382
|
+
return isDefault ? '/search-index.json' : `/${locale}/search-index.json`
|
|
383
|
+
}
|
|
384
|
+
|
|
290
385
|
/**
|
|
291
386
|
* Get search data for all pages
|
|
387
|
+
* @deprecated Use getSearchConfig() and fetch the search index instead
|
|
292
388
|
*/
|
|
293
389
|
getSearchData() {
|
|
294
390
|
return this.pages.map((page) => ({
|
|
@@ -345,7 +441,7 @@ export default class Website {
|
|
|
345
441
|
} = options
|
|
346
442
|
|
|
347
443
|
// Filter pages based on navigation type and visibility
|
|
348
|
-
|
|
444
|
+
const isPageVisible = (page) => {
|
|
349
445
|
// Always exclude special pages (header/footer are already separated)
|
|
350
446
|
if (page.route.startsWith('/@')) return false
|
|
351
447
|
|
|
@@ -360,7 +456,15 @@ export default class Website {
|
|
|
360
456
|
if (customFilter && !customFilter(page)) return false
|
|
361
457
|
|
|
362
458
|
return true
|
|
363
|
-
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let filteredPages = this.pages.filter(isPageVisible)
|
|
462
|
+
|
|
463
|
+
// When nested, only include root-level pages at top level
|
|
464
|
+
// (children will be nested inside their parents)
|
|
465
|
+
if (nested) {
|
|
466
|
+
filteredPages = filteredPages.filter(page => !page.parent)
|
|
467
|
+
}
|
|
364
468
|
|
|
365
469
|
// Apply custom sort or default to order
|
|
366
470
|
if (customSort) {
|
|
@@ -378,7 +482,7 @@ export default class Website {
|
|
|
378
482
|
order: page.order,
|
|
379
483
|
hasContent: page.getBodyBlocks().length > 0,
|
|
380
484
|
children: nested && page.hasChildren()
|
|
381
|
-
? page.children.map(buildPageInfo)
|
|
485
|
+
? page.children.filter(isPageVisible).map(buildPageInfo)
|
|
382
486
|
: []
|
|
383
487
|
})
|
|
384
488
|
|