@uniweb/core 0.7.30 → 0.7.31

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/block.js +99 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.7.30",
3
+ "version": "0.7.31",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -31,8 +31,8 @@
31
31
  "vitest": "^4.1.7"
32
32
  },
33
33
  "dependencies": {
34
- "@uniweb/semantic-parser": "1.1.19",
35
- "@uniweb/theming": "0.1.14"
34
+ "@uniweb/theming": "0.1.14",
35
+ "@uniweb/semantic-parser": "1.1.20"
36
36
  },
37
37
  "scripts": {
38
38
  "test": "vitest run"
package/src/block.js CHANGED
@@ -8,6 +8,70 @@
8
8
  import { parseContent as parseSemanticContent } from '@uniweb/semantic-parser'
9
9
  import { normalizeTokenValue } from '@uniweb/theming'
10
10
 
11
+ /**
12
+ * Lift container fences out of a content document.
13
+ *
14
+ * A ` ```@Component{params} ` fence parses to an `inset_block` node carrying a
15
+ * body of real block content. This rewrites each one to the `inset_placeholder`
16
+ * a leaf inset leaves behind, so a container resolves through exactly the same
17
+ * path: `getInset(refId)` → a Block → the foundation's component. Kit and the
18
+ * SSR renderer already handle placeholders, so neither needs to know containers
19
+ * exist.
20
+ *
21
+ * PURE with respect to the input. `blockData.content` is shared with the sync /
22
+ * pull machinery, which must keep seeing `inset_block` — that is the canonical
23
+ * stored shape. Nodes on the path to a container are cloned; everything else is
24
+ * passed through by reference, so a document with no containers costs one array
25
+ * walk and allocates nothing.
26
+ *
27
+ * Containers are NOT recursed into here. A container's body becomes its child
28
+ * Block's content, and that Block's constructor lifts its own containers — so
29
+ * nesting resolves one level at a time, each at the level that owns it.
30
+ *
31
+ * @param {Object} content - ProseMirror document (never mutated)
32
+ * @returns {{ content: Object, refs: Array<{refId, type, params, content}> }}
33
+ */
34
+ function liftContainers(content) {
35
+ if (!content || !Array.isArray(content.content)) return { content, refs: [] }
36
+
37
+ const refs = []
38
+
39
+ const visit = (nodes) => {
40
+ let changed = false
41
+ const out = nodes.map((node) => {
42
+ if (!node) return node
43
+
44
+ if (node.type === 'inset_block') {
45
+ const { component, ...params } = node.attrs || {}
46
+ const refId = `container_${refs.length}`
47
+ refs.push({
48
+ refId,
49
+ type: component,
50
+ params,
51
+ content: { type: 'doc', content: node.content || [] },
52
+ })
53
+ changed = true
54
+ return { type: 'inset_placeholder', attrs: { refId, embedKind: 'block' } }
55
+ }
56
+
57
+ if (Array.isArray(node.content)) {
58
+ const inner = visit(node.content)
59
+ if (inner !== node.content) {
60
+ changed = true
61
+ return { ...node, content: inner }
62
+ }
63
+ }
64
+ return node
65
+ })
66
+ return changed ? out : nodes
67
+ }
68
+
69
+ const next = visit(content.content)
70
+ return next === content.content
71
+ ? { content, refs }
72
+ : { content: { ...content, content: next }, refs }
73
+ }
74
+
11
75
  export default class Block {
12
76
  constructor(blockData, id, page) {
13
77
  this.id = id
@@ -22,8 +86,17 @@ export default class Block {
22
86
  // 1. Raw ProseMirror content (from content collection)
23
87
  // 2. Pre-parsed content with main/items structure
24
88
  // For now, store raw and parse on demand
25
- this.rawContent = blockData.content || {}
26
- this.parsedContent = this.parseContent(blockData.content)
89
+ //
90
+ // Container fences (```@Component around a body) arrive as `inset_block`
91
+ // nodes and are lifted out HERE, into the same placeholder + refId shape
92
+ // the build gives leaf insets. Doing it at render-graph construction
93
+ // rather than at build time is deliberate: `inset_block` is the canonical
94
+ // STORED shape, so the content that syncs and round-trips must keep
95
+ // carrying it. `blockData.content` is left untouched — the lift produces a
96
+ // new tree and only this Block's view of it changes.
97
+ const lifted = liftContainers(blockData.content)
98
+ this.rawContent = lifted.content || {}
99
+ this.parsedContent = this.parseContent(lifted.content)
27
100
 
28
101
  // Merge fetched data from prerender (if present)
29
102
  // Prerender stores fetched data in blockData.parsedContent.data
@@ -114,6 +187,30 @@ export default class Block {
114
187
  }
115
188
  }
116
189
 
190
+ // Containers, appended AFTER the leaf insets so `block.insets[0]` keeps
191
+ // meaning what it meant to every foundation already using <Visual>.
192
+ // Unlike a leaf inset, a container's body becomes the child Block's
193
+ // content, so the foundation's component receives a fully parsed
194
+ // `content` — title, paragraphs, items, sequence — exactly as a section
195
+ // does. Nested containers resolve for free: the child Block runs this
196
+ // same constructor over its own body.
197
+ for (let i = 0; i < lifted.refs.length; i++) {
198
+ const ref = lifted.refs[i]
199
+ this.insets.push(
200
+ new Block(
201
+ {
202
+ type: ref.type,
203
+ params: ref.params || {},
204
+ content: ref.content,
205
+ stableId: ref.refId,
206
+ refId: ref.refId,
207
+ },
208
+ `${id}_container_${i}`,
209
+ this.page
210
+ )
211
+ )
212
+ }
213
+
117
214
  // Fetch configuration (from section frontmatter)
118
215
  // Supports local files (path) or remote URLs (url)
119
216
  this.fetch = blockData.fetch || null