@uniweb/core 0.7.30 → 0.7.32
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 +5 -4
- package/src/block.js +99 -2
- package/src/section-id.js +67 -0
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.32",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
|
-
"./locale-config": "./src/locale-config.js"
|
|
8
|
+
"./locale-config": "./src/locale-config.js",
|
|
9
|
+
"./section-id": "./src/section-id.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"src"
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
"vitest": "^4.1.7"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@uniweb/
|
|
35
|
-
"@uniweb/
|
|
35
|
+
"@uniweb/theming": "0.1.14",
|
|
36
|
+
"@uniweb/semantic-parser": "1.1.20"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"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
|
-
|
|
26
|
-
|
|
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
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DOM id of a rendered section — one rule, shared by everything that
|
|
3
|
+
* needs to name a section.
|
|
4
|
+
*
|
|
5
|
+
* A section's id is written by the renderer and read by anything that links
|
|
6
|
+
* to a section: the search index's anchors, a table of contents, a deep link
|
|
7
|
+
* someone pastes. Those only agree if they derive the id the same way, and
|
|
8
|
+
* they cannot check each other — a mismatch produces no error anywhere. The
|
|
9
|
+
* link simply lands on the page and does not scroll, and if the target is the
|
|
10
|
+
* page you are already on, nothing visibly happens at all.
|
|
11
|
+
*
|
|
12
|
+
* That is not hypothetical. The renderers moved from the positional `id` to
|
|
13
|
+
* `stableId` (so an id survives reordering) and the search extractor was not
|
|
14
|
+
* updated, so it kept emitting `Section1` while the DOM said
|
|
15
|
+
* `section-what-is-uniweb`. Every section-level search result on every site
|
|
16
|
+
* pointed at a fragment that did not exist, and no test failed.
|
|
17
|
+
*
|
|
18
|
+
* So this is deliberately the ONLY place the rule is written down, and it
|
|
19
|
+
* captures both halves — which identity to use AND how to spell it. A helper
|
|
20
|
+
* that only formatted a value the caller picked would have prevented the
|
|
21
|
+
* spelling half of that bug and none of the identity half.
|
|
22
|
+
*
|
|
23
|
+
* **Zero imports, and it must stay that way.** `@uniweb/projections` consumes
|
|
24
|
+
* this through the leaf subpath `@uniweb/core/section-id` because its
|
|
25
|
+
* environment contract forbids the bare `@uniweb/core` entry (that pulls in
|
|
26
|
+
* semantic-parser and theming). Adding an import here would break that
|
|
27
|
+
* package's `tests/environment.test.js`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Prefix for every section wrapper id. */
|
|
31
|
+
const PREFIX = 'section-'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The DOM id for a section, from either a Block or the raw section data.
|
|
35
|
+
*
|
|
36
|
+
* Accepts both shapes on purpose: the runtime holds `Block` instances while
|
|
37
|
+
* build-time consumers hold plain objects off the wire. Both carry the same
|
|
38
|
+
* two fields, so one function serves both rather than each growing its own.
|
|
39
|
+
*
|
|
40
|
+
* `stableId` is preferred because it is derived from the section's filename
|
|
41
|
+
* (or an authored `id:`) and therefore survives reordering; the positional
|
|
42
|
+
* `id` is the fallback for content that has no stable identity.
|
|
43
|
+
*
|
|
44
|
+
* @param {{stableId?: string, id?: string|number}} section - A Block, or a
|
|
45
|
+
* section object from site content.
|
|
46
|
+
* @returns {string} e.g. `section-hero`. Returns `section-unknown` rather
|
|
47
|
+
* than an id ending in `undefined` when a section carries no identity at
|
|
48
|
+
* all — a wrong-but-obvious anchor beats a malformed one.
|
|
49
|
+
*/
|
|
50
|
+
export function sectionDomId(section) {
|
|
51
|
+
if (!section) return `${PREFIX}unknown`
|
|
52
|
+
const id = section.stableId || section.id
|
|
53
|
+
return `${PREFIX}${id === undefined || id === null || id === '' ? 'unknown' : id}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The same id as a URL fragment, for building a link to a section.
|
|
58
|
+
*
|
|
59
|
+
* Exists so a caller composing an href never has to remember whether the
|
|
60
|
+
* `#` is already included — the concatenation is the part people get wrong.
|
|
61
|
+
*
|
|
62
|
+
* @param {{stableId?: string, id?: string|number}} section
|
|
63
|
+
* @returns {string} e.g. `#section-hero`
|
|
64
|
+
*/
|
|
65
|
+
export function sectionHash(section) {
|
|
66
|
+
return `#${sectionDomId(section)}`
|
|
67
|
+
}
|