@uniweb/projections 0.1.2 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/projections",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Projections of a Uniweb site's content — agent index, per-page markdown, search index. Pure JS, runs anywhere.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -31,8 +31,8 @@
31
31
  "node": ">=20.19"
32
32
  },
33
33
  "dependencies": {
34
- "@uniweb/core": "0.7.31",
35
- "@uniweb/content-writer": "0.2.9"
34
+ "@uniweb/content-writer": "0.2.9",
35
+ "@uniweb/core": "0.7.32"
36
36
  },
37
37
  "devDependencies": {
38
38
  "vitest": "^4.1.7",
@@ -5,6 +5,11 @@
5
5
  * for search indexing. Reuses patterns from i18n extraction.
6
6
  */
7
7
 
8
+ // Leaf subpath, not the bare `@uniweb/core` entry: this package's environment
9
+ // contract forbids the package root (it pulls semantic-parser + theming).
10
+ // Enforced by tests/environment.test.js.
11
+ import { sectionDomId } from '@uniweb/core/section-id'
12
+
8
13
  /**
9
14
  * Extract all searchable content from site
10
15
  * @param {Object} siteContent - Parsed site-content.json
@@ -103,12 +108,17 @@ function extractFromPage(page) {
103
108
 
104
109
  /**
105
110
  * Extract search entries from a section (and subsections)
111
+ *
106
112
  * @param {Object} section - Section data
107
113
  * @param {Object} page - Parent page
108
114
  * @param {Object} options - Extraction options
115
+ * @param {string} [ancestorAnchor] - The anchor of the nearest ancestor that
116
+ * is actually rendered with a wrapper. Absent at the top level, where the
117
+ * section renders itself; inherited by every nested level. See the note on
118
+ * anchors below.
109
119
  * @returns {Array<Object>} Array of search entries
110
120
  */
111
- function extractFromSection(section, page, options) {
121
+ function extractFromSection(section, page, options, ancestorAnchor) {
112
122
  const entries = []
113
123
  const {
114
124
  includeHeadings,
@@ -121,6 +131,27 @@ function extractFromSection(section, page, options) {
121
131
  const sectionId = section.id || 'unknown'
122
132
  const component = section.component || section.type || 'unknown'
123
133
 
134
+ // Where a reader gets taken for a hit in this section.
135
+ //
136
+ // A top-level section is wrapped by the renderer and carries its own id, so
137
+ // it is its own destination. A NESTED section usually is not: `<ChildBlocks>`
138
+ // renders children bare by default — no wrapper, and therefore no id — and
139
+ // whether a foundation opted into `wrapAs` is a runtime decision this
140
+ // build-time pass cannot see. Emitting the child's own id produced a
141
+ // fragment that existed nowhere, so a hit landed on the page without
142
+ // scrolling and did nothing at all when the reader was already on it.
143
+ //
144
+ // The nearest rendered ancestor is the honest answer: it always exists, and
145
+ // the child's content is physically inside it, so the reader lands where the
146
+ // matching text actually is. Precision is lost only in the rarer `wrapAs`
147
+ // case, where the alternative was being wrong in the common one.
148
+ //
149
+ // Consequence worth knowing: a parent and its children now share an anchor,
150
+ // so several results can point at one destination. They are still distinct
151
+ // content with their own titles and excerpts; which to surface is the
152
+ // consumer's ranking decision, not ours to collapse here.
153
+ const anchor = ancestorAnchor || sectionDomId(section)
154
+
124
155
  // Skip excluded components
125
156
  if (excludeComponents.includes(component)) {
126
157
  return entries
@@ -157,7 +188,9 @@ function extractFromSection(section, page, options) {
157
188
  type: 'section',
158
189
  route: page.route,
159
190
  sectionId,
160
- anchor: `Section${sectionId}`,
191
+ // Resolved above. Never spelled out here: the id format is the shared
192
+ // rule's to own, which is exactly how these two drifted apart before.
193
+ anchor,
161
194
  component,
162
195
  title: sectionTitle,
163
196
  pageTitle: page.title || '',
@@ -169,9 +202,14 @@ function extractFromSection(section, page, options) {
169
202
  })
170
203
  }
171
204
 
172
- // Recursively process subsections
205
+ // Recursively process subsections.
206
+ //
207
+ // `anchor` is passed down rather than the subsection resolving its own, so
208
+ // every level below the rendered one inherits the same destination — a
209
+ // grandchild is no more rendered than its parent, and inheriting only one
210
+ // level would put it back to naming an id that does not exist.
173
211
  for (const subsection of section.subsections || []) {
174
- const subEntries = extractFromSection(subsection, page, options)
212
+ const subEntries = extractFromSection(subsection, page, options, anchor)
175
213
  entries.push(...subEntries)
176
214
  }
177
215