@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 +3 -3
- package/src/search/extract.js +42 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/projections",
|
|
3
|
-
"version": "0.1.
|
|
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/
|
|
35
|
-
"@uniweb/
|
|
34
|
+
"@uniweb/content-writer": "0.2.9",
|
|
35
|
+
"@uniweb/core": "0.7.32"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"vitest": "^4.1.7",
|
package/src/search/extract.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|