@uniweb/runtime 0.1.1 → 0.2.1

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/src/core/block.js DELETED
@@ -1,311 +0,0 @@
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/core/input.js DELETED
@@ -1,15 +0,0 @@
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/core/page.js DELETED
@@ -1,75 +0,0 @@
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
- }
@@ -1,103 +0,0 @@
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
- }
@@ -1,157 +0,0 @@
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
- }