Skip to content

Commit ab96d74

Browse files
authored
feat: improve indexing and embedded search (#2734)
1 parent 4c19380 commit ab96d74

2 files changed

Lines changed: 333 additions & 19 deletions

File tree

src/plugins/search/search.js

Lines changed: 154 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import {
44
removeAtag,
55
escapeHtml,
66
} from '../../core/render/utils.js';
7+
import {
8+
getPath,
9+
getParentPath,
10+
isAbsolutePath,
11+
} from '../../core/router/util.js';
712
import { markdownToTxt } from './markdown-to-txt.js';
813
import Dexie from 'dexie';
914

@@ -16,10 +21,29 @@ db.version(1).stores({
1621
});
1722

1823
async function saveData(maxAge, expireKey) {
19-
INDEXES = Object.values(INDEXES).flatMap(innerData =>
20-
Object.values(innerData),
21-
);
22-
await /** @type {any} */ (db).search.bulkPut(INDEXES);
24+
const records = [];
25+
26+
Object.values(INDEXES).forEach(entry => {
27+
if (!entry || typeof entry !== 'object') {
28+
return;
29+
}
30+
31+
// Entry may already be a flat record read from IndexedDB.
32+
if ('slug' in entry) {
33+
records.push(entry);
34+
return;
35+
}
36+
37+
// Entry may be a per-path map of slug -> record produced by genIndex().
38+
Object.values(entry).forEach(item => {
39+
if (item && typeof item === 'object' && 'slug' in item) {
40+
records.push(item);
41+
}
42+
});
43+
});
44+
45+
INDEXES = records;
46+
await /** @type {any} */ (db).search.bulkPut(records);
2347
await /** @type {any} */ (db).expires.put({
2448
key: expireKey,
2549
value: Date.now() + maxAge,
@@ -96,6 +120,111 @@ function getListData(token) {
96120
return token.text;
97121
}
98122

123+
function extractFragmentContent(text, fragment, fullLine) {
124+
if (!fragment) {
125+
return text;
126+
}
127+
128+
let fragmentRegex = `(?:###|\\/\\/\\/)\\s*\\[${fragment}\\]`;
129+
if (fullLine) {
130+
fragmentRegex = `.*${fragmentRegex}.*\n`;
131+
}
132+
133+
const pattern = new RegExp(
134+
`(?:${fragmentRegex})([\\s\\S]*?)(?:${fragmentRegex})`,
135+
);
136+
const match = text.match(pattern);
137+
return ((match || [])[1] || '').trim();
138+
}
139+
140+
function collectEmbedRequests(raw = '', path, vm) {
141+
const tokens = window.marked.lexer(raw);
142+
const requests = [];
143+
144+
const maybePushEmbed = inlineToken => {
145+
if (
146+
!inlineToken ||
147+
(inlineToken.type !== 'link' && inlineToken.type !== 'image')
148+
) {
149+
return;
150+
}
151+
152+
const { config } = getAndRemoveConfig(inlineToken.title || '');
153+
if (!config.include || !inlineToken.href) {
154+
return;
155+
}
156+
157+
const href = isAbsolutePath(inlineToken.href)
158+
? inlineToken.href
159+
: getPath(vm.router.getBasePath(), getParentPath(path), inlineToken.href);
160+
161+
let type = 'code';
162+
if (/\.(md|markdown)/.test(href)) {
163+
type = 'markdown';
164+
} else if (/\.mmd/.test(href)) {
165+
type = 'mermaid';
166+
}
167+
168+
requests.push({
169+
url: href,
170+
type,
171+
fragment: config.fragment,
172+
omitFragmentLine: config.omitFragmentLine,
173+
});
174+
};
175+
176+
tokens.forEach(token => {
177+
if (token.type === 'paragraph') {
178+
(token.tokens || []).forEach(maybePushEmbed);
179+
} else if (token.type === 'table') {
180+
(token.header || []).forEach(cell => {
181+
(cell.tokens || []).forEach(maybePushEmbed);
182+
});
183+
(token.rows || []).forEach(row => {
184+
row.forEach(cell => {
185+
(cell.tokens || []).forEach(maybePushEmbed);
186+
});
187+
});
188+
}
189+
});
190+
191+
return requests;
192+
}
193+
194+
async function getEmbeddedContent(raw = '', path, vm) {
195+
const requests = collectEmbedRequests(raw, path, vm);
196+
if (!requests.length) {
197+
return '';
198+
}
199+
200+
const results = await Promise.all(
201+
requests.map(
202+
request =>
203+
new Promise(resolve => {
204+
Docsify.get(request.url, false, vm.config.requestHeaders).then(
205+
text => {
206+
let content = text || '';
207+
if (request.fragment) {
208+
content = extractFragmentContent(
209+
content,
210+
request.fragment,
211+
request.omitFragmentLine,
212+
);
213+
}
214+
215+
resolve(
216+
request.type === 'markdown' ? content : markdownToTxt(content),
217+
);
218+
},
219+
() => resolve(''),
220+
);
221+
}),
222+
),
223+
);
224+
225+
return results.filter(Boolean).join('\n');
226+
}
227+
99228
export function genIndex(path, content = '', router, depth, indexKey) {
100229
const tokens = window.marked.lexer(content);
101230
const slugify = window.Docsify.slugify;
@@ -205,29 +334,24 @@ export function search(query) {
205334
),
206335
'gi',
207336
);
208-
let indexTitle = -1;
209-
let indexContent = -1;
210337
handlePostTitle = postTitle
211338
? escapeHtml(ignoreDiacriticalMarks(postTitle))
212339
: postTitle;
213340
handlePostContent = postContent
214341
? escapeHtml(ignoreDiacriticalMarks(postContent))
215342
: postContent;
216343

217-
indexTitle = postTitle ? handlePostTitle.search(regEx) : -1;
218-
indexContent = postContent ? handlePostContent.search(regEx) : -1;
344+
const indexTitle = postTitle ? handlePostTitle.search(regEx) : -1;
345+
let indexContent = postContent ? handlePostContent.search(regEx) : -1;
219346

220347
if (indexTitle >= 0 || indexContent >= 0) {
221348
matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0;
222349
if (indexContent < 0) {
223350
indexContent = 0;
224351
}
225352

226-
let start = 0;
227-
let end = 0;
228-
229-
start = indexContent < 11 ? 0 : indexContent - 10;
230-
end = start === 0 ? 100 : indexContent + keyword.length + 90;
353+
const start = indexContent < 11 ? 0 : indexContent - 10;
354+
let end = start === 0 ? 100 : indexContent + keyword.length + 90;
231355

232356
if (handlePostContent && end > handlePostContent.length) {
233357
end = handlePostContent.length;
@@ -306,26 +430,38 @@ export async function init(config, vm) {
306430
const len = paths.length;
307431
let count = 0;
308432

433+
const markComplete = async () => {
434+
if (len === ++count) {
435+
await saveData(config.maxAge, expireKey);
436+
}
437+
};
438+
309439
paths.forEach(path => {
310440
const pathExists = Array.isArray(INDEXES)
311441
? INDEXES.some(obj => obj.path === path)
312442
: false;
313443
if (pathExists) {
314-
return count++;
444+
void markComplete();
445+
return;
315446
}
316447

317448
Docsify.get(vm.router.getFile(path), false, vm.config.requestHeaders).then(
318449
async result => {
450+
const embeddedContent = await getEmbeddedContent(result, path, vm);
451+
const contentToIndex = embeddedContent
452+
? `${result}\n${embeddedContent}`
453+
: result;
319454
INDEXES[path] = genIndex(
320455
path,
321-
result,
456+
contentToIndex,
322457
vm.router,
323458
config.depth,
324459
indexKey,
325460
);
326-
if (len === ++count) {
327-
await saveData(config.maxAge, expireKey);
328-
}
461+
return markComplete();
462+
},
463+
() => {
464+
return markComplete();
329465
},
330466
);
331467
});

0 commit comments

Comments
 (0)