@kenjura/ursa 0.50.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 +10 -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/src/jobs/generate.js +47 -9
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
package/src/jobs/generate.js
CHANGED
|
@@ -309,6 +309,15 @@ export async function generate({
|
|
|
309
309
|
(filename) => isDirectory(filename)
|
|
310
310
|
)).filter((filename) => !filename.match(hiddenOrSystemDirs) && !isFolderHidden(filename, source));
|
|
311
311
|
|
|
312
|
+
// Build set of existing HTML files in source directory (these should not be overwritten)
|
|
313
|
+
const htmlExtensions = /\.html$/;
|
|
314
|
+
const existingHtmlFiles = new Set(
|
|
315
|
+
allSourceFilenames
|
|
316
|
+
.filter(f => f.match(htmlExtensions) && !f.match(hiddenOrSystemDirs))
|
|
317
|
+
.map(f => f.replace(source, '')) // Store relative paths for easy lookup
|
|
318
|
+
);
|
|
319
|
+
progress.log(`Found ${existingHtmlFiles.size} existing HTML files in source`);
|
|
320
|
+
|
|
312
321
|
// Build set of valid internal paths for link validation (must be before menu)
|
|
313
322
|
// Pass directories to ensure folder links are valid (auto-index generates index.html for all folders)
|
|
314
323
|
const validPaths = buildValidPaths(allSourceFilenamesThatAreArticles, source, allSourceFilenamesThatAreDirectories);
|
|
@@ -391,6 +400,14 @@ export async function generate({
|
|
|
391
400
|
content: '' // Content excerpts built lazily to save memory
|
|
392
401
|
});
|
|
393
402
|
|
|
403
|
+
// Check if a corresponding .html file already exists in source directory
|
|
404
|
+
const outputHtmlRelative = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
|
|
405
|
+
if (existingHtmlFiles.has(outputHtmlRelative)) {
|
|
406
|
+
progress.log(`⚠️ Warning: Skipping ${shortFile} - would overwrite existing ${outputHtmlRelative} in source`);
|
|
407
|
+
skippedCount++;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
394
411
|
// Check if file needs regeneration
|
|
395
412
|
const needsRegen = _clean || needsRegeneration(file, rawBody, hashCache);
|
|
396
413
|
|
|
@@ -600,16 +617,23 @@ export async function generate({
|
|
|
600
617
|
// Clear directory index cache to free memory before processing static files
|
|
601
618
|
dirIndexCache.clear();
|
|
602
619
|
|
|
603
|
-
// copy all static files (
|
|
620
|
+
// copy all static files (images and existing HTML files) with batched concurrency
|
|
604
621
|
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|svg|ico)/; // static asset extensions
|
|
605
622
|
const allSourceFilenamesThatAreImages = allSourceFilenames.filter(
|
|
606
623
|
(filename) => filename.match(imageExtensions)
|
|
607
624
|
);
|
|
608
|
-
|
|
625
|
+
|
|
626
|
+
// Also copy existing HTML files from source to output (they're treated as static)
|
|
627
|
+
const allSourceFilenamesThatAreHtml = allSourceFilenames.filter(
|
|
628
|
+
(filename) => filename.match(/\.html$/) && !filename.match(hiddenOrSystemDirs)
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const allStaticFiles = [...allSourceFilenamesThatAreImages, ...allSourceFilenamesThatAreHtml];
|
|
632
|
+
const totalStatic = allStaticFiles.length;
|
|
609
633
|
let processedStatic = 0;
|
|
610
634
|
let copiedStatic = 0;
|
|
611
|
-
progress.log(`Processing ${totalStatic} static files...`);
|
|
612
|
-
await processBatched(
|
|
635
|
+
progress.log(`Processing ${totalStatic} static files (${allSourceFilenamesThatAreImages.length} images, ${allSourceFilenamesThatAreHtml.length} HTML)...`);
|
|
636
|
+
await processBatched(allStaticFiles, async (file) => {
|
|
613
637
|
try {
|
|
614
638
|
processedStatic++;
|
|
615
639
|
const shortFile = file.replace(source, '');
|
|
@@ -639,7 +663,7 @@ export async function generate({
|
|
|
639
663
|
|
|
640
664
|
// Automatic index generation for folders without index.html
|
|
641
665
|
progress.log(`Checking for missing index files...`);
|
|
642
|
-
await generateAutoIndices(output, allSourceFilenamesThatAreDirectories, source, templates, menu, footer, allSourceFilenamesThatAreArticles, copiedCssFiles);
|
|
666
|
+
await generateAutoIndices(output, allSourceFilenamesThatAreDirectories, source, templates, menu, footer, allSourceFilenamesThatAreArticles, copiedCssFiles, existingHtmlFiles);
|
|
643
667
|
|
|
644
668
|
// Save the hash cache to .ursa folder in source directory
|
|
645
669
|
if (hashCache.size > 0) {
|
|
@@ -706,8 +730,9 @@ export async function generate({
|
|
|
706
730
|
* @param {string} footer - Footer HTML
|
|
707
731
|
* @param {string[]} generatedArticles - List of source article paths that were generated
|
|
708
732
|
* @param {Set<string>} copiedCssFiles - Set of CSS files already copied to output
|
|
733
|
+
* @param {Set<string>} existingHtmlFiles - Set of existing HTML files in source (relative paths)
|
|
709
734
|
*/
|
|
710
|
-
async function generateAutoIndices(output, directories, source, templates, menu, footer, generatedArticles, copiedCssFiles) {
|
|
735
|
+
async function generateAutoIndices(output, directories, source, templates, menu, footer, generatedArticles, copiedCssFiles, existingHtmlFiles) {
|
|
711
736
|
// Alternate index file names to look for (in priority order)
|
|
712
737
|
const INDEX_ALTERNATES = ['_index.html', 'home.html', '_home.html'];
|
|
713
738
|
|
|
@@ -736,6 +761,7 @@ async function generateAutoIndices(output, directories, source, templates, menu,
|
|
|
736
761
|
|
|
737
762
|
let generatedCount = 0;
|
|
738
763
|
let renamedCount = 0;
|
|
764
|
+
let skippedHtmlCount = 0;
|
|
739
765
|
|
|
740
766
|
for (const dir of outputDirs) {
|
|
741
767
|
const indexPath = join(dir, 'index.html');
|
|
@@ -745,7 +771,15 @@ async function generateAutoIndices(output, directories, source, templates, menu,
|
|
|
745
771
|
continue;
|
|
746
772
|
}
|
|
747
773
|
|
|
748
|
-
//
|
|
774
|
+
// Check if there's an existing index.html in the source directory (don't overwrite it)
|
|
775
|
+
const sourceDir = dir.replace(outputNorm, sourceNorm);
|
|
776
|
+
const relativeIndexPath = join(sourceDir, 'index.html').replace(sourceNorm + '/', '');
|
|
777
|
+
if (existingHtmlFiles && existingHtmlFiles.has(relativeIndexPath)) {
|
|
778
|
+
skippedHtmlCount++;
|
|
779
|
+
continue; // Don't overwrite existing source HTML
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Skip if index.html already exists in output (e.g., created by previous run)
|
|
749
783
|
if (existsSync(indexPath)) {
|
|
750
784
|
continue;
|
|
751
785
|
}
|
|
@@ -860,8 +894,12 @@ async function generateAutoIndices(output, directories, source, templates, menu,
|
|
|
860
894
|
}
|
|
861
895
|
}
|
|
862
896
|
|
|
863
|
-
if (generatedCount > 0 || renamedCount > 0) {
|
|
864
|
-
|
|
897
|
+
if (generatedCount > 0 || renamedCount > 0 || skippedHtmlCount > 0) {
|
|
898
|
+
let summary = `${generatedCount} generated, ${renamedCount} promoted`;
|
|
899
|
+
if (skippedHtmlCount > 0) {
|
|
900
|
+
summary += `, ${skippedHtmlCount} skipped (existing HTML)`;
|
|
901
|
+
}
|
|
902
|
+
progress.done('Auto-index', summary);
|
|
865
903
|
} else {
|
|
866
904
|
progress.log(`Auto-index: All folders already have index.html`);
|
|
867
905
|
}
|