Skip to content

Commit 34fa6a3

Browse files
committed
fix: implement embedded content retrieval and indexing in search.js
1 parent bc7ae63 commit 34fa6a3

2 files changed

Lines changed: 164 additions & 12 deletions

File tree

src/plugins/search/search.js

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
removeAtag,
55
escapeHtml,
66
} from '../../core/render/utils.js';
7+
import { getPath, getParentPath, isAbsolutePath } from '../../core/router/util.js';
78
import { markdownToTxt } from './markdown-to-txt.js';
89
import Dexie from 'dexie';
910

@@ -115,6 +116,104 @@ function getListData(token) {
115116
return token.text;
116117
}
117118

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

236-
indexTitle = postTitle ? handlePostTitle.search(regEx) : -1;
237-
indexContent = postContent ? handlePostContent.search(regEx) : -1;
333+
const indexTitle = postTitle ? handlePostTitle.search(regEx) : -1;
334+
let indexContent = postContent ? handlePostContent.search(regEx) : -1;
238335

239336
if (indexTitle >= 0 || indexContent >= 0) {
240337
matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0;
241338
if (indexContent < 0) {
242339
indexContent = 0;
243340
}
244341

245-
let start = 0;
246-
let end = 0;
247-
248-
start = indexContent < 11 ? 0 : indexContent - 10;
249-
end = start === 0 ? 100 : indexContent + keyword.length + 90;
342+
const start = indexContent < 11 ? 0 : indexContent - 10;
343+
let end = start === 0 ? 100 : indexContent + keyword.length + 90;
250344

251345
if (handlePostContent && end > handlePostContent.length) {
252346
end = handlePostContent.length;
@@ -341,10 +435,14 @@ export async function init(config, vm) {
341435
}
342436

343437
Docsify.get(vm.router.getFile(path), false, vm.config.requestHeaders).then(
344-
result => {
438+
async result => {
439+
const embeddedContent = await getEmbeddedContent(result, path, vm);
440+
const contentToIndex = embeddedContent
441+
? `${result}\n${embeddedContent}`
442+
: result;
345443
INDEXES[path] = genIndex(
346444
path,
347-
result,
445+
contentToIndex,
348446
vm.router,
349447
config.depth,
350448
indexKey,

test/e2e/search.test.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,64 @@ console.log('Hello World');
401401
await docsifyInit(docsifyInitConfig);
402402
await searchFieldElm.fill('filename');
403403
expect(await resultsHeadingElm.textContent()).toContain(
404-
'...filename _media/example.js :include :type=code :fragment=demo...',
404+
'filename _media/example.js :include :type=code :fragment=demo',
405405
);
406406
});
407407

408+
test('search should index embedded include content', async ({ page }) => {
409+
const docsifyInitConfig = {
410+
markdown: {
411+
homepage: `
412+
# Include Search
413+
414+
![snippet](snippet.js ':include :type=code')
415+
`,
416+
},
417+
routes: {
418+
'/snippet.js': `
419+
const embeddedSearchKeyword = 'ok';
420+
`,
421+
},
422+
scriptURLs: ['/dist/plugins/search.js'],
423+
};
424+
425+
const searchFieldElm = page.locator('input[type=search]');
426+
const resultsHeadingElm = page.locator('.results-panel .title');
427+
428+
await docsifyInit(docsifyInitConfig);
429+
await searchFieldElm.fill('embeddedSearchKeyword');
430+
await expect(resultsHeadingElm).toHaveText('Include Search');
431+
});
432+
433+
test('search should index embedded include content from relative path', async ({
434+
page,
435+
}) => {
436+
const docsifyInitConfig = {
437+
markdown: {
438+
homepage: '# Home',
439+
sidebar: '- [Guide Intro](guide/intro)',
440+
},
441+
routes: {
442+
'/guide/intro.md': `
443+
# Relative Include Search
444+
445+
![snippet](./snippets/demo.js ':include :type=code')
446+
`,
447+
'/guide/snippets/demo.js': `
448+
const embeddedRelativeKeyword = 'ok';
449+
`,
450+
},
451+
scriptURLs: ['/dist/plugins/search.js'],
452+
};
453+
454+
const searchFieldElm = page.locator('input[type=search]');
455+
const resultsHeadingElm = page.locator('.results-panel .title');
456+
457+
await docsifyInit(docsifyInitConfig);
458+
await searchFieldElm.fill('embeddedRelativeKeyword');
459+
await expect(resultsHeadingElm).toHaveText('Relative Include Search');
460+
});
461+
408462
test('search result should remove checkbox markdown and keep related values', async ({
409463
page,
410464
}) => {

0 commit comments

Comments
 (0)