@uniweb/core 0.1.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/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # @uniweb/core
2
+
3
+ Core classes for the Uniweb Component Web Platform.
4
+
5
+ ## Overview
6
+
7
+ This package provides the foundational classes that power Uniweb sites. It's a pure JavaScript library with no React dependencies, designed to be shared between the runtime and foundations.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @uniweb/core
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ import { createUniweb, getUniweb } from '@uniweb/core'
19
+
20
+ // Create the singleton instance (typically done by @uniweb/runtime)
21
+ const uniweb = createUniweb(siteConfig)
22
+
23
+ // Access the singleton from anywhere
24
+ const uniweb = getUniweb()
25
+
26
+ // Work with the active website
27
+ const website = uniweb.activeWebsite
28
+ const page = website.getPage('/about')
29
+ const language = website.getLanguage()
30
+ ```
31
+
32
+ ## API
33
+
34
+ ### Factory Functions
35
+
36
+ | Function | Description |
37
+ |----------|-------------|
38
+ | `createUniweb(config)` | Create and register the global Uniweb instance |
39
+ | `getUniweb()` | Get the current Uniweb instance |
40
+
41
+ ### Classes
42
+
43
+ #### Uniweb
44
+
45
+ The main runtime instance, available as `globalThis.uniweb`.
46
+
47
+ ```js
48
+ uniweb.getComponent(name) // Get component from foundation
49
+ uniweb.listComponents() // List available components
50
+ uniweb.activeWebsite // Current website instance
51
+ uniweb.setFoundation(module) // Set the foundation module
52
+ ```
53
+
54
+ #### Website
55
+
56
+ Manages pages, theme, and localization.
57
+
58
+ ```js
59
+ website.getPage(route) // Get page by route
60
+ website.setActivePage(route) // Navigate to page
61
+ website.localize(value) // Localize a multilingual value
62
+ website.getLanguage() // Get current language code
63
+ website.getLanguages() // Get available languages
64
+ website.makeHref(href) // Transform href for routing
65
+ ```
66
+
67
+ #### Page
68
+
69
+ Represents a page with its sections.
70
+
71
+ ```js
72
+ page.route // Page route path
73
+ page.title // Page title
74
+ page.sections // Array of section blocks
75
+ ```
76
+
77
+ #### Block
78
+
79
+ Represents a section/component on a page.
80
+
81
+ ```js
82
+ block.component // Component name
83
+ block.getBlockContent() // Get parsed content
84
+ block.getBlockProperties() // Get configuration properties
85
+ block.childBlockRenderer // Renderer for child blocks
86
+ ```
87
+
88
+ #### Input
89
+
90
+ Handles form input fields with validation.
91
+
92
+ ```js
93
+ input.value // Current value
94
+ input.validate() // Run validation
95
+ input.errors // Validation errors
96
+ ```
97
+
98
+ ## Architecture
99
+
100
+ ```
101
+ @uniweb/runtime (browser)
102
+
103
+ └── imports @uniweb/core
104
+
105
+ ├── createUniweb() - creates singleton
106
+ ├── Uniweb - main instance
107
+ ├── Website - page/locale management
108
+ ├── Page - page representation
109
+ ├── Block - section representation
110
+ └── Input - form handling
111
+
112
+ @uniweb/kit (foundation components)
113
+
114
+ └── imports @uniweb/core
115
+
116
+ └── getUniweb() - access singleton
117
+ ```
118
+
119
+ ## For Foundation Creators
120
+
121
+ Foundations typically don't import from `@uniweb/core` directly. Instead, use `@uniweb/kit` which provides React hooks and components that abstract the core:
122
+
123
+ ```js
124
+ // Prefer this (kit)
125
+ import { useWebsite } from '@uniweb/kit'
126
+ const { localize, website } = useWebsite()
127
+
128
+ // Instead of this (core)
129
+ import { getUniweb } from '@uniweb/core'
130
+ const website = getUniweb().activeWebsite
131
+ ```
132
+
133
+ Mark `@uniweb/core` as external in your foundation's Vite config:
134
+
135
+ ```js
136
+ // vite.config.js
137
+ export default {
138
+ build: {
139
+ rollupOptions: {
140
+ external: ['react', 'react-dom', 'react-router-dom', '@uniweb/core']
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ ## Related Packages
147
+
148
+ - [`@uniweb/runtime`](https://github.com/uniweb/runtime) - Browser runtime (creates the Uniweb instance)
149
+ - [`@uniweb/kit`](https://github.com/uniweb/kit) - Component library for foundations
150
+ - [`@uniweb/build`](https://github.com/uniweb/build) - Build tooling
151
+
152
+ ## License
153
+
154
+ Apache 2.0
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@uniweb/core",
3
+ "version": "0.1.2",
4
+ "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "keywords": [
13
+ "uniweb",
14
+ "core"
15
+ ],
16
+ "author": "Proximify",
17
+ "license": "Apache-2.0",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/uniweb/core.git"
21
+ },
22
+ "homepage": "https://github.com/uniweb/core#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/uniweb/core/issues"
25
+ },
26
+ "engines": {
27
+ "node": ">=20.19"
28
+ },
29
+ "dependencies": {
30
+ "@uniweb/semantic-parser": "1.0.1"
31
+ }
32
+ }
package/src/block.js ADDED
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Block
3
+ *
4
+ * Represents a section/block on a page. Contains content, properties,
5
+ * child blocks, and state management. Connects to foundation components.
6
+ */
7
+
8
+ import { parseContent as parseSemanticContent } from '@uniweb/semantic-parser'
9
+
10
+ export default class Block {
11
+ constructor(blockData, id) {
12
+ this.id = id
13
+ this.component = blockData.component || 'Section'
14
+ this.Component = null
15
+
16
+ // Content structure
17
+ // The content can be:
18
+ // 1. Raw ProseMirror content (from content collection)
19
+ // 2. Pre-parsed content with main/items structure
20
+ // For now, store raw and parse on demand
21
+ this.rawContent = blockData.content || {}
22
+ this.parsedContent = this.parseContent(blockData.content)
23
+
24
+ const { main, items } = this.parsedContent
25
+ this.main = main
26
+ this.items = items
27
+
28
+ // Block configuration
29
+ const blockConfig = blockData.params || blockData.config || {}
30
+ this.preset = blockData.preset
31
+ this.themeName = `context__${blockConfig.theme || 'light'}`
32
+ this.standardOptions = blockConfig.standardOptions || {}
33
+ this.properties = blockConfig.properties || blockConfig
34
+
35
+ // Child blocks (subsections)
36
+ this.childBlocks = blockData.subsections
37
+ ? blockData.subsections.map((block, i) => new Block(block, `${id}_${i}`))
38
+ : []
39
+
40
+ // Input data
41
+ this.input = blockData.input || null
42
+
43
+ // State management
44
+ this.startState = null
45
+ this.state = null
46
+ this.resetStateHook = null
47
+ }
48
+
49
+ /**
50
+ * Parse content into structured format using semantic-parser
51
+ * Supports multiple content formats:
52
+ * 1. Pre-parsed groups structure (from editor)
53
+ * 2. ProseMirror document (from markdown collection)
54
+ * 3. Simple key-value content (PoC style)
55
+ *
56
+ * Uses @uniweb/semantic-parser for rich content extraction including:
57
+ * - Pretitle detection (H3 before H1)
58
+ * - Banner/background image detection
59
+ * - Semantic grouping (main + items)
60
+ * - Lists, links, buttons, etc.
61
+ */
62
+ parseContent(content) {
63
+ // If content is already parsed with groups structure
64
+ if (content?.groups) {
65
+ return content.groups
66
+ }
67
+
68
+ // ProseMirror document - use semantic-parser
69
+ if (content?.type === 'doc') {
70
+ return this.extractFromProseMirror(content)
71
+ }
72
+
73
+ // Simple key-value content (PoC style) - pass through directly
74
+ // This allows components to receive content like { title, subtitle, items }
75
+ if (content && typeof content === 'object' && !Array.isArray(content)) {
76
+ return {
77
+ main: { header: {}, body: {} },
78
+ items: [],
79
+ // Store raw content for direct access
80
+ raw: content
81
+ }
82
+ }
83
+
84
+ // Fallback
85
+ return {
86
+ main: { header: {}, body: {} },
87
+ items: []
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Extract structured content from ProseMirror document
93
+ * Uses @uniweb/semantic-parser for intelligent content extraction
94
+ */
95
+ extractFromProseMirror(doc) {
96
+ try {
97
+ // Parse with semantic-parser
98
+ const { groups, sequence, byType } = parseSemanticContent(doc)
99
+
100
+ // Transform groups structure to match expected format
101
+ const main = groups.main || { header: {}, body: {} }
102
+ const items = groups.items || []
103
+
104
+ return {
105
+ main,
106
+ items,
107
+ // Include additional data for advanced use cases
108
+ sequence,
109
+ byType,
110
+ metadata: groups.metadata
111
+ }
112
+ } catch (err) {
113
+ console.warn('[Block] Semantic parser error, using fallback:', err.message)
114
+ return this.extractFromProseMirrorFallback(doc)
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Fallback extraction when semantic-parser fails
120
+ */
121
+ extractFromProseMirrorFallback(doc) {
122
+ const main = { header: {}, body: {} }
123
+ const items = []
124
+
125
+ if (!doc.content) return { main, items }
126
+
127
+ for (const node of doc.content) {
128
+ if (node.type === 'heading') {
129
+ const text = this.extractText(node)
130
+ if (node.attrs?.level === 1) {
131
+ main.header.title = text
132
+ } else if (node.attrs?.level === 2) {
133
+ main.header.subtitle = text
134
+ }
135
+ } else if (node.type === 'paragraph') {
136
+ const text = this.extractText(node)
137
+ if (!main.body.paragraphs) main.body.paragraphs = []
138
+ main.body.paragraphs.push(text)
139
+ }
140
+ }
141
+
142
+ return { main, items }
143
+ }
144
+
145
+ /**
146
+ * Extract text from a node
147
+ */
148
+ extractText(node) {
149
+ if (!node.content) return ''
150
+ return node.content
151
+ .filter((n) => n.type === 'text')
152
+ .map((n) => n.text)
153
+ .join('')
154
+ }
155
+
156
+ /**
157
+ * Initialize the component from the foundation
158
+ * @returns {React.ComponentType|null}
159
+ */
160
+ initComponent() {
161
+ if (this.Component) return this.Component
162
+
163
+ this.Component = globalThis.uniweb?.getComponent(this.component)
164
+
165
+ if (!this.Component) {
166
+ console.warn(`[Block] Component not found: ${this.component}`)
167
+ return null
168
+ }
169
+
170
+ // Initialize state from component defaults
171
+ const defaults = this.Component.blockDefaults || { state: this.Component.blockState }
172
+ this.startState = defaults.state ? { ...defaults.state } : null
173
+ this.initState()
174
+
175
+ return this.Component
176
+ }
177
+
178
+ /**
179
+ * Get structured block content for components
180
+ */
181
+ getBlockContent() {
182
+ const mainHeader = this.main?.header || {}
183
+ const mainBody = this.main?.body || {}
184
+ const banner = this.main?.banner || null
185
+
186
+ return {
187
+ banner,
188
+ pretitle: mainHeader.pretitle || '',
189
+ title: mainHeader.title || '',
190
+ subtitle: mainHeader.subtitle || '',
191
+ description: mainHeader.description || '',
192
+ paragraphs: mainBody.paragraphs || [],
193
+ images: mainBody.imgs || mainBody.images || [],
194
+ links: mainBody.links || [],
195
+ icons: mainBody.icons || [],
196
+ properties: mainBody.propertyBlocks?.[0] || {},
197
+ videos: mainBody.videos || [],
198
+ lists: mainBody.lists || [],
199
+ buttons: mainBody.buttons || []
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Get block properties
205
+ */
206
+ getBlockProperties() {
207
+ return this.properties
208
+ }
209
+
210
+ /**
211
+ * Get child block renderer from runtime
212
+ */
213
+ getChildBlockRenderer() {
214
+ return globalThis.uniweb?.childBlockRenderer
215
+ }
216
+
217
+ /**
218
+ * Get links from block content
219
+ * @param {Object} options
220
+ * @returns {Array}
221
+ */
222
+ getBlockLinks(options = {}) {
223
+ const website = globalThis.uniweb?.activeWebsite
224
+
225
+ if (options.nested) {
226
+ const lists = this.main?.body?.lists || []
227
+ const links = lists[0]
228
+ return Block.parseNestedLinks(links, website)
229
+ }
230
+
231
+ const links = this.main?.body?.links || []
232
+ return links.map((link) => ({
233
+ route: website?.makeHref(link.href) || link.href,
234
+ label: link.label
235
+ }))
236
+ }
237
+
238
+ /**
239
+ * Initialize block state
240
+ */
241
+ initState() {
242
+ this.state = this.startState
243
+ if (this.resetStateHook) this.resetStateHook()
244
+ }
245
+
246
+ /**
247
+ * React hook for block state management
248
+ * @param {Function} useState - React useState hook
249
+ * @param {any} initState - Initial state
250
+ * @returns {[any, Function]}
251
+ */
252
+ useBlockState(useState, initState) {
253
+ if (initState !== undefined && this.startState === null) {
254
+ this.startState = initState
255
+ this.state = initState
256
+ } else {
257
+ initState = this.startState
258
+ }
259
+
260
+ const [state, setState] = useState(initState)
261
+
262
+ this.resetStateHook = () => setState(initState)
263
+
264
+ return [state, (newState) => setState((this.state = newState))]
265
+ }
266
+
267
+ /**
268
+ * Parse nested links structure
269
+ */
270
+ static parseNestedLinks(list, website) {
271
+ const parsed = []
272
+
273
+ if (!list?.length) return parsed
274
+
275
+ for (const listItem of list) {
276
+ const { links = [], lists = [], paragraphs = [] } = listItem
277
+
278
+ const link = links[0]
279
+ const nestedList = lists[0]
280
+ const text = paragraphs[0]
281
+
282
+ let label = ''
283
+ let href = ''
284
+ let subLinks = []
285
+ let hasData = true
286
+
287
+ if (link) {
288
+ label = link.label
289
+ href = link.href
290
+ if (nestedList) {
291
+ subLinks = Block.parseNestedLinks(nestedList, website)
292
+ }
293
+ } else {
294
+ label = text
295
+ hasData = false
296
+ if (nestedList) {
297
+ subLinks = Block.parseNestedLinks(nestedList, website)
298
+ }
299
+ }
300
+
301
+ parsed.push({
302
+ label,
303
+ route: website?.makeHref(href) || href,
304
+ child_items: subLinks,
305
+ hasData
306
+ })
307
+ }
308
+
309
+ return parsed
310
+ }
311
+ }
package/src/index.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @uniweb/core
3
+ *
4
+ * Core classes for the Uniweb platform.
5
+ * Pure JavaScript - no React or framework dependencies.
6
+ */
7
+
8
+ import Uniweb from './uniweb.js'
9
+
10
+ // Core classes
11
+ export { Uniweb }
12
+ export { default as Website } from './website.js'
13
+ export { default as Page } from './page.js'
14
+ export { default as Block } from './block.js'
15
+ export { default as Input } from './input.js'
16
+
17
+ /**
18
+ * The singleton Uniweb instance.
19
+ * Created by the runtime during initialization.
20
+ * Access via globalThis.uniweb or import { getUniweb } from '@uniweb/core'
21
+ */
22
+ export function getUniweb() {
23
+ return globalThis.uniweb
24
+ }
25
+
26
+ /**
27
+ * Create and register the Uniweb singleton.
28
+ * Called by @uniweb/runtime during site initialization.
29
+ *
30
+ * @param {Object} configData - Website configuration (pages, theme, config)
31
+ * @returns {Uniweb} The created instance
32
+ */
33
+ export function createUniweb(configData) {
34
+ const instance = new Uniweb(configData)
35
+ globalThis.uniweb = instance
36
+ return instance
37
+ }
package/src/input.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Input
3
+ *
4
+ * Handles input/form data within blocks.
5
+ */
6
+
7
+ export default class Input {
8
+ constructor(inputData) {
9
+ this.data = inputData || {}
10
+ }
11
+
12
+ getData() {
13
+ return this.data
14
+ }
15
+ }
package/src/page.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Page
3
+ *
4
+ * Represents a single page with header, body sections, and footer.
5
+ */
6
+
7
+ import Block from './block.js'
8
+
9
+ export default class Page {
10
+ constructor(pageData, id, pageHeader, pageFooter) {
11
+ this.id = id
12
+ this.route = pageData.route
13
+ this.title = pageData.title || ''
14
+ this.description = pageData.description || ''
15
+
16
+ this.pageBlocks = this.buildPageBlocks(
17
+ pageData.sections,
18
+ pageHeader?.sections,
19
+ pageFooter?.sections
20
+ )
21
+ }
22
+
23
+ /**
24
+ * Build the page block structure
25
+ */
26
+ buildPageBlocks(body, header, footer) {
27
+ const headerSection = header?.[0]
28
+ const footerSection = footer?.[0]
29
+ const bodySections = body || []
30
+
31
+ return {
32
+ header: headerSection ? new Block(headerSection, 'header') : null,
33
+ body: bodySections.map((section, index) => new Block(section, index)),
34
+ footer: footerSection ? new Block(footerSection, 'footer') : null,
35
+ leftPanel: null,
36
+ rightPanel: null
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get all blocks (header, body, footer) as flat array
42
+ * @returns {Block[]}
43
+ */
44
+ getPageBlocks() {
45
+ return [
46
+ this.pageBlocks.header,
47
+ ...this.pageBlocks.body,
48
+ this.pageBlocks.footer
49
+ ].filter(Boolean)
50
+ }
51
+
52
+ /**
53
+ * Get just body blocks
54
+ * @returns {Block[]}
55
+ */
56
+ getBodyBlocks() {
57
+ return this.pageBlocks.body
58
+ }
59
+
60
+ /**
61
+ * Get header block
62
+ * @returns {Block|null}
63
+ */
64
+ getHeader() {
65
+ return this.pageBlocks.header
66
+ }
67
+
68
+ /**
69
+ * Get footer block
70
+ * @returns {Block|null}
71
+ */
72
+ getFooter() {
73
+ return this.pageBlocks.footer
74
+ }
75
+ }
package/src/uniweb.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Uniweb Core Runtime
3
+ *
4
+ * The main runtime instance that manages the website, foundation components,
5
+ * and provides utilities to components.
6
+ */
7
+
8
+ import Website from './website.js'
9
+
10
+ export default class Uniweb {
11
+ constructor(configData) {
12
+ this.activeWebsite = new Website(configData)
13
+ this.childBlockRenderer = null // Function to render child blocks
14
+ this.routingComponents = {} // Link, SafeHtml, useNavigate, etc.
15
+ this.foundation = null // The loaded foundation module
16
+ this.foundationConfig = {} // Configuration from foundation
17
+ this.language = 'en'
18
+ }
19
+
20
+ /**
21
+ * Set the foundation module after loading
22
+ * @param {Object} foundation - The loaded ESM foundation module
23
+ */
24
+ setFoundation(foundation) {
25
+ this.foundation = foundation
26
+ }
27
+
28
+ /**
29
+ * Get a component from the foundation by name
30
+ * @param {string} name - Component name
31
+ * @returns {React.ComponentType|undefined}
32
+ */
33
+ getComponent(name) {
34
+ if (!this.foundation) {
35
+ console.warn('[Runtime] No foundation loaded')
36
+ return undefined
37
+ }
38
+
39
+ // Use foundation's getComponent interface
40
+ if (typeof this.foundation.getComponent === 'function') {
41
+ return this.foundation.getComponent(name)
42
+ }
43
+
44
+ // Fallback: direct component access
45
+ return this.foundation[name]
46
+ }
47
+
48
+ /**
49
+ * List available components from the foundation
50
+ * @returns {string[]}
51
+ */
52
+ listComponents() {
53
+ if (!this.foundation) return []
54
+
55
+ if (typeof this.foundation.listComponents === 'function') {
56
+ return this.foundation.listComponents()
57
+ }
58
+
59
+ return []
60
+ }
61
+
62
+ /**
63
+ * Get component schema
64
+ * @param {string} name - Component name
65
+ * @returns {Object|undefined}
66
+ */
67
+ getSchema(name) {
68
+ if (!this.foundation) return undefined
69
+
70
+ if (typeof this.foundation.getSchema === 'function') {
71
+ return this.foundation.getSchema(name)
72
+ }
73
+
74
+ return undefined
75
+ }
76
+
77
+ /**
78
+ * Set foundation configuration
79
+ * @param {Object} config
80
+ */
81
+ setFoundationConfig(config) {
82
+ this.foundationConfig = config
83
+ }
84
+
85
+ // Legacy compatibility - maps to new method names
86
+ getRemoteComponent(name) {
87
+ return this.getComponent(name)
88
+ }
89
+
90
+ setRemoteComponents(components) {
91
+ // Legacy: components was an object map
92
+ // Convert to foundation-like interface
93
+ this.foundation = {
94
+ getComponent: (name) => components[name],
95
+ listComponents: () => Object.keys(components),
96
+ components
97
+ }
98
+ }
99
+
100
+ setRemoteConfig(config) {
101
+ this.setFoundationConfig(config)
102
+ }
103
+ }
package/src/website.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Website
3
+ *
4
+ * Manages pages, themes, and localization for a website instance.
5
+ */
6
+
7
+ import Page from './page.js'
8
+
9
+ export default class Website {
10
+ constructor(websiteData) {
11
+ const { pages = [], theme = {}, config = {} } = websiteData
12
+
13
+ // Extract special pages (header, footer) and regular pages
14
+ this.headerPage = pages.find((p) => p.route === '/@header')
15
+ this.footerPage = pages.find((p) => p.route === '/@footer')
16
+
17
+ this.pages = pages
18
+ .filter((page) => page.route !== '/@header' && page.route !== '/@footer')
19
+ .map(
20
+ (page, index) =>
21
+ new Page(page, index, this.headerPage, this.footerPage)
22
+ )
23
+
24
+ this.activePage =
25
+ this.pages.find((page) => page.route === '/' || page.route === '/index') ||
26
+ this.pages[0]
27
+
28
+ this.pageRoutes = this.pages.map((page) => page.route)
29
+ this.themeData = theme
30
+ this.config = config
31
+ this.activeLang = config.defaultLanguage || 'en'
32
+ this.langs = config.languages || [
33
+ { label: 'English', value: 'en' },
34
+ { label: 'français', value: 'fr' }
35
+ ]
36
+ }
37
+
38
+ /**
39
+ * Get page by route
40
+ * @param {string} route
41
+ * @returns {Page|undefined}
42
+ */
43
+ getPage(route) {
44
+ return this.pages.find((page) => page.route === route)
45
+ }
46
+
47
+ /**
48
+ * Set active page by route
49
+ * @param {string} route
50
+ */
51
+ setActivePage(route) {
52
+ const page = this.getPage(route)
53
+ if (page) {
54
+ this.activePage = page
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get remote layout component from foundation config
60
+ */
61
+ getRemoteLayout() {
62
+ return globalThis.uniweb?.foundationConfig?.Layout || null
63
+ }
64
+
65
+ /**
66
+ * Get remote props from foundation config
67
+ */
68
+ getRemoteProps() {
69
+ return globalThis.uniweb?.foundationConfig?.props || null
70
+ }
71
+
72
+ /**
73
+ * Get routing components (Link, useNavigate, etc.)
74
+ */
75
+ getRoutingComponents() {
76
+ return globalThis.uniweb?.routingComponents || {}
77
+ }
78
+
79
+ /**
80
+ * Make href (for link transformation)
81
+ * @param {string} href
82
+ * @returns {string}
83
+ */
84
+ makeHref(href) {
85
+ // Could add basename handling here
86
+ return href
87
+ }
88
+
89
+ /**
90
+ * Get available languages
91
+ */
92
+ getLanguages() {
93
+ return this.langs
94
+ }
95
+
96
+ /**
97
+ * Get current language
98
+ */
99
+ getLanguage() {
100
+ return this.activeLang
101
+ }
102
+
103
+ /**
104
+ * Localize a value
105
+ * @param {any} val - Value to localize (object with lang keys, or string)
106
+ * @param {string} defaultVal - Default value if not found
107
+ * @param {string} givenLang - Override language
108
+ * @param {boolean} fallbackDefaultLangVal - Fall back to default language
109
+ * @returns {string}
110
+ */
111
+ localize(val, defaultVal = '', givenLang = '', fallbackDefaultLangVal = false) {
112
+ const lang = givenLang || this.activeLang
113
+ const defaultLang = this.langs[0]?.value || 'en'
114
+
115
+ if (typeof val === 'object' && !Array.isArray(val)) {
116
+ return fallbackDefaultLangVal
117
+ ? val?.[lang] || val?.[defaultLang] || defaultVal
118
+ : val?.[lang] || defaultVal
119
+ }
120
+
121
+ if (typeof val === 'string') {
122
+ if (!val.startsWith('{') && !val.startsWith('"')) return val
123
+
124
+ try {
125
+ const obj = JSON.parse(val)
126
+ if (typeof obj === 'object') {
127
+ return fallbackDefaultLangVal
128
+ ? obj?.[lang] || obj?.[defaultLang] || defaultVal
129
+ : obj?.[lang] || defaultVal
130
+ }
131
+ return obj
132
+ } catch {
133
+ return val
134
+ }
135
+ }
136
+
137
+ return defaultVal
138
+ }
139
+
140
+ /**
141
+ * Get search data for all pages
142
+ */
143
+ getSearchData() {
144
+ return this.pages.map((page) => ({
145
+ id: page.id,
146
+ title: page.title,
147
+ href: page.route,
148
+ route: page.route,
149
+ description: page.description,
150
+ content: page
151
+ .getPageBlocks()
152
+ .map((b) => b.title)
153
+ .filter(Boolean)
154
+ .join('\n')
155
+ }))
156
+ }
157
+ }