@kenjura/ursa 0.51.0 → 0.52.0
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/CHANGELOG.md +5 -0
- package/meta/default-template.html +1 -13
- package/meta/default.css +8 -0
- package/meta/search.js +147 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,19 +6,6 @@
|
|
|
6
6
|
<title>${title}</title>
|
|
7
7
|
<link rel="stylesheet" href="/public/default.css" />
|
|
8
8
|
${styleLink}
|
|
9
|
-
<script>
|
|
10
|
-
// Search index loaded asynchronously from separate file to reduce page size
|
|
11
|
-
window.SEARCH_INDEX = ${searchIndex};
|
|
12
|
-
// Lazy load full search index if placeholder is empty
|
|
13
|
-
if (!window.SEARCH_INDEX || window.SEARCH_INDEX.length === 0) {
|
|
14
|
-
fetch('/public/search-index.json')
|
|
15
|
-
.then(r => r.json())
|
|
16
|
-
.then(data => { window.SEARCH_INDEX = data; })
|
|
17
|
-
.catch(() => { window.SEARCH_INDEX = []; });
|
|
18
|
-
}
|
|
19
|
-
</script>
|
|
20
|
-
<script src="/public/search.js"></script>
|
|
21
|
-
|
|
22
9
|
</head>
|
|
23
10
|
|
|
24
11
|
<body data-template-id="default">
|
|
@@ -47,6 +34,7 @@
|
|
|
47
34
|
<script src="/public/toc.js"></script>
|
|
48
35
|
<script src="/public/toc-generator.js"></script>
|
|
49
36
|
<script src="/public/menu.js"></script>
|
|
37
|
+
<script src="/public/search.js"></script>
|
|
50
38
|
<script src="/public/sectionify.js"></script>
|
|
51
39
|
<script src="/public/sticky.js"></script>
|
|
52
40
|
</body>
|
package/meta/default.css
CHANGED
|
@@ -142,6 +142,14 @@ nav#nav-global {
|
|
|
142
142
|
opacity: 0.7;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
.search-result-message {
|
|
146
|
+
padding: 16px;
|
|
147
|
+
text-align: center;
|
|
148
|
+
color: var(--text-color);
|
|
149
|
+
opacity: 0.7;
|
|
150
|
+
font-style: italic;
|
|
151
|
+
}
|
|
152
|
+
|
|
145
153
|
nav#nav-main {
|
|
146
154
|
position: fixed;
|
|
147
155
|
top: calc(var(--global-nav-height) + 0.5rem);
|
package/meta/search.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// Global search functionality with typeahead
|
|
2
|
+
// Supports lazy-loading of search index for large datasets
|
|
3
|
+
|
|
2
4
|
class GlobalSearch {
|
|
3
5
|
constructor() {
|
|
4
6
|
this.searchInput = document.getElementById('global-search');
|
|
5
7
|
this.searchResults = null;
|
|
6
|
-
this.searchIndex =
|
|
8
|
+
this.searchIndex = null; // Will be loaded asynchronously
|
|
9
|
+
this.indexLoading = false;
|
|
10
|
+
this.indexLoaded = false;
|
|
7
11
|
this.currentSelection = -1;
|
|
12
|
+
this._lastResults = null;
|
|
13
|
+
this.MIN_QUERY_LENGTH = 3;
|
|
8
14
|
|
|
9
15
|
if (!this.searchInput) return;
|
|
10
16
|
|
|
@@ -14,6 +20,8 @@ class GlobalSearch {
|
|
|
14
20
|
init() {
|
|
15
21
|
this.createResultsContainer();
|
|
16
22
|
this.bindEvents();
|
|
23
|
+
// Start loading the index immediately (but don't block)
|
|
24
|
+
this.loadSearchIndex();
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
createResultsContainer() {
|
|
@@ -23,6 +31,43 @@ class GlobalSearch {
|
|
|
23
31
|
this.searchInput.parentNode.appendChild(this.searchResults);
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Load search index from external JSON file
|
|
36
|
+
* This is done asynchronously to avoid blocking page render
|
|
37
|
+
*/
|
|
38
|
+
async loadSearchIndex() {
|
|
39
|
+
// Check if index was already embedded in page (legacy support)
|
|
40
|
+
if (window.SEARCH_INDEX && Array.isArray(window.SEARCH_INDEX) && window.SEARCH_INDEX.length > 0) {
|
|
41
|
+
this.searchIndex = window.SEARCH_INDEX;
|
|
42
|
+
this.indexLoaded = true;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.indexLoading = true;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch('/public/search-index.json');
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`HTTP ${response.status}`);
|
|
52
|
+
}
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
this.searchIndex = data;
|
|
55
|
+
this.indexLoaded = true;
|
|
56
|
+
window.SEARCH_INDEX = data; // Cache globally for potential reuse
|
|
57
|
+
|
|
58
|
+
// If user was waiting, trigger search now
|
|
59
|
+
if (this.searchInput.value.length >= this.MIN_QUERY_LENGTH) {
|
|
60
|
+
this.handleSearch(this.searchInput.value);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Failed to load search index:', error);
|
|
64
|
+
this.searchIndex = [];
|
|
65
|
+
this.indexLoaded = true; // Mark as loaded (with empty) to stop loading indicator
|
|
66
|
+
} finally {
|
|
67
|
+
this.indexLoading = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
26
71
|
bindEvents() {
|
|
27
72
|
// Input events
|
|
28
73
|
this.searchInput.addEventListener('input', (e) => {
|
|
@@ -57,55 +102,103 @@ class GlobalSearch {
|
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
handleSearch(query) {
|
|
60
|
-
|
|
105
|
+
const trimmedQuery = (query || '').trim();
|
|
106
|
+
|
|
107
|
+
// Clear results for empty query
|
|
108
|
+
if (!trimmedQuery) {
|
|
61
109
|
this.hideResults();
|
|
62
110
|
return;
|
|
63
111
|
}
|
|
64
112
|
|
|
65
|
-
|
|
66
|
-
this.
|
|
113
|
+
// Show minimum character message
|
|
114
|
+
if (trimmedQuery.length < this.MIN_QUERY_LENGTH) {
|
|
115
|
+
this.showMessage(`Type at least ${this.MIN_QUERY_LENGTH} characters to search`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Show loading indicator if index isn't ready
|
|
120
|
+
if (this.indexLoading || !this.indexLoaded) {
|
|
121
|
+
this.showMessage('Loading search index...');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const results = this.search(trimmedQuery);
|
|
126
|
+
this._lastResults = results;
|
|
127
|
+
this.displayResults(results, trimmedQuery);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Show a message in the results dropdown (for loading, errors, etc.)
|
|
132
|
+
*/
|
|
133
|
+
showMessage(message) {
|
|
134
|
+
this.searchResults.innerHTML = '';
|
|
135
|
+
this.currentSelection = -1;
|
|
136
|
+
|
|
137
|
+
const item = document.createElement('div');
|
|
138
|
+
item.className = 'search-result-message';
|
|
139
|
+
item.textContent = message;
|
|
140
|
+
this.searchResults.appendChild(item);
|
|
141
|
+
|
|
142
|
+
this.showResults();
|
|
67
143
|
}
|
|
68
144
|
|
|
69
145
|
search(query) {
|
|
146
|
+
if (!this.searchIndex || !Array.isArray(this.searchIndex)) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
70
150
|
const normalizedQuery = query.toLowerCase().trim();
|
|
151
|
+
const queryWords = normalizedQuery.split(/\s+/);
|
|
71
152
|
const results = [];
|
|
72
153
|
|
|
73
154
|
// Search through the index
|
|
74
155
|
this.searchIndex.forEach(item => {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
156
|
+
const titleLower = (item.title || '').toLowerCase();
|
|
157
|
+
const pathLower = (item.path || '').toLowerCase();
|
|
158
|
+
const contentLower = (item.content || '').toLowerCase();
|
|
78
159
|
|
|
79
|
-
if
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
160
|
+
// Check if all query words match somewhere
|
|
161
|
+
const allWordsMatch = queryWords.every(word =>
|
|
162
|
+
titleLower.includes(word) ||
|
|
163
|
+
pathLower.includes(word) ||
|
|
164
|
+
contentLower.includes(word)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!allWordsMatch) return;
|
|
168
|
+
|
|
169
|
+
let score = 0;
|
|
170
|
+
|
|
171
|
+
// Boost exact title matches
|
|
172
|
+
if (titleLower === normalizedQuery) score += 100;
|
|
173
|
+
else if (titleLower.startsWith(normalizedQuery)) score += 50;
|
|
174
|
+
else if (titleLower.includes(normalizedQuery)) score += 25;
|
|
175
|
+
|
|
176
|
+
// Boost path matches
|
|
177
|
+
if (pathLower.includes(normalizedQuery)) score += 10;
|
|
178
|
+
|
|
179
|
+
// Content matches get lower score
|
|
180
|
+
if (contentLower.includes(normalizedQuery)) score += 5;
|
|
181
|
+
|
|
182
|
+
// Bonus for each word match in title
|
|
183
|
+
queryWords.forEach(word => {
|
|
184
|
+
if (titleLower.includes(word)) score += 3;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
results.push({ ...item, score });
|
|
95
188
|
});
|
|
96
189
|
|
|
97
190
|
// Sort by score, then by title
|
|
98
191
|
return results
|
|
99
192
|
.sort((a, b) => {
|
|
100
193
|
if (b.score !== a.score) return b.score - a.score;
|
|
101
|
-
return a.title.localeCompare(b.title);
|
|
194
|
+
return (a.title || '').localeCompare(b.title || '');
|
|
102
195
|
})
|
|
103
196
|
.slice(0, 10); // Limit to 10 results
|
|
104
197
|
}
|
|
105
198
|
|
|
106
|
-
displayResults(results) {
|
|
199
|
+
displayResults(results, query) {
|
|
107
200
|
if (results.length === 0) {
|
|
108
|
-
this.
|
|
201
|
+
this.showMessage(`No results for "${query}"`);
|
|
109
202
|
return;
|
|
110
203
|
}
|
|
111
204
|
|
|
@@ -119,11 +212,11 @@ class GlobalSearch {
|
|
|
119
212
|
|
|
120
213
|
const title = document.createElement('div');
|
|
121
214
|
title.className = 'search-result-title';
|
|
122
|
-
title.textContent = result.title;
|
|
215
|
+
title.textContent = result.title || 'Untitled';
|
|
123
216
|
|
|
124
217
|
const path = document.createElement('div');
|
|
125
218
|
path.className = 'search-result-path';
|
|
126
|
-
path.textContent = result.path;
|
|
219
|
+
path.textContent = result.path || result.url || '';
|
|
127
220
|
|
|
128
221
|
item.appendChild(title);
|
|
129
222
|
item.appendChild(path);
|
|
@@ -133,6 +226,12 @@ class GlobalSearch {
|
|
|
133
226
|
this.navigateToResult(result);
|
|
134
227
|
});
|
|
135
228
|
|
|
229
|
+
// Mouse hover selection
|
|
230
|
+
item.addEventListener('mouseenter', () => {
|
|
231
|
+
this.currentSelection = index;
|
|
232
|
+
this.updateSelection();
|
|
233
|
+
});
|
|
234
|
+
|
|
136
235
|
this.searchResults.appendChild(item);
|
|
137
236
|
});
|
|
138
237
|
|
|
@@ -154,24 +253,24 @@ class GlobalSearch {
|
|
|
154
253
|
switch (e.key) {
|
|
155
254
|
case 'ArrowDown':
|
|
156
255
|
e.preventDefault();
|
|
157
|
-
|
|
158
|
-
|
|
256
|
+
if (items.length > 0) {
|
|
257
|
+
this.currentSelection = Math.min(this.currentSelection + 1, items.length - 1);
|
|
258
|
+
this.updateSelection();
|
|
259
|
+
}
|
|
159
260
|
break;
|
|
160
261
|
|
|
161
262
|
case 'ArrowUp':
|
|
162
263
|
e.preventDefault();
|
|
163
|
-
|
|
164
|
-
|
|
264
|
+
if (items.length > 0) {
|
|
265
|
+
this.currentSelection = Math.max(this.currentSelection - 1, 0);
|
|
266
|
+
this.updateSelection();
|
|
267
|
+
}
|
|
165
268
|
break;
|
|
166
269
|
|
|
167
270
|
case 'Enter':
|
|
168
271
|
e.preventDefault();
|
|
169
|
-
if (this.currentSelection >= 0 &&
|
|
170
|
-
|
|
171
|
-
const results = this.getLastSearchResults();
|
|
172
|
-
if (results && results[index]) {
|
|
173
|
-
this.navigateToResult(results[index]);
|
|
174
|
-
}
|
|
272
|
+
if (this.currentSelection >= 0 && this._lastResults && this._lastResults[this.currentSelection]) {
|
|
273
|
+
this.navigateToResult(this._lastResults[this.currentSelection]);
|
|
175
274
|
}
|
|
176
275
|
break;
|
|
177
276
|
|
|
@@ -187,22 +286,23 @@ class GlobalSearch {
|
|
|
187
286
|
items.forEach((item, index) => {
|
|
188
287
|
item.classList.toggle('selected', index === this.currentSelection);
|
|
189
288
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!this._lastResults) {
|
|
195
|
-
this._lastResults = this.search(this.searchInput.value);
|
|
289
|
+
|
|
290
|
+
// Scroll selected item into view
|
|
291
|
+
if (this.currentSelection >= 0 && items[this.currentSelection]) {
|
|
292
|
+
items[this.currentSelection].scrollIntoView({ block: 'nearest' });
|
|
196
293
|
}
|
|
197
|
-
return this._lastResults;
|
|
198
294
|
}
|
|
199
295
|
|
|
200
296
|
navigateToResult(result) {
|
|
201
|
-
|
|
297
|
+
if (result.url) {
|
|
298
|
+
window.location.href = result.url;
|
|
299
|
+
} else if (result.path) {
|
|
300
|
+
window.location.href = result.path;
|
|
301
|
+
}
|
|
202
302
|
}
|
|
203
303
|
}
|
|
204
304
|
|
|
205
305
|
// Initialize when DOM is loaded
|
|
206
306
|
document.addEventListener('DOMContentLoaded', () => {
|
|
207
|
-
new GlobalSearch();
|
|
307
|
+
window.globalSearch = new GlobalSearch();
|
|
208
308
|
});
|
package/package.json
CHANGED