From 77d4844e588eac116cf2e5534c28ff7458f5e5f8 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Wed, 26 Dec 2018 14:54:08 +0100 Subject: [PATCH] SONAR-11472 Add support highlighting exact matches in results --- .../layouts/components/SearchEntryResult.js | 29 +++++++++-- .../components/SearchResultEntry.tsx | 34 ++++++++++-- .../components/SearchResults.tsx | 2 +- .../__tests__/SearchResultEntry-test.tsx | 52 +++++++++++-------- .../SearchResultEntry-test.tsx.snap | 27 ++++++++++ .../__snapshots__/SearchResults-test.tsx.snap | 2 + 6 files changed, 117 insertions(+), 29 deletions(-) diff --git a/server/sonar-docs/src/layouts/components/SearchEntryResult.js b/server/sonar-docs/src/layouts/components/SearchEntryResult.js index c1456691674..20d3a27e661 100644 --- a/server/sonar-docs/src/layouts/components/SearchEntryResult.js +++ b/server/sonar-docs/src/layouts/components/SearchEntryResult.js @@ -50,9 +50,32 @@ export function SearchResultTitle({ result }) { export function SearchResultText({ result }) { const textHighlights = result.highlights.text; - if (textHighlights && textHighlights.length > 0) { - const { text } = result.page; - const tokens = highlightMarks(text, textHighlights.map(h => ({ from: h[0], to: h[0] + h[1] }))); + const { text } = result.page; + let tokens = []; + + if (result.exactMatch) { + const pageText = result.page.text.toLowerCase(); + const highlights = []; + let start = 0; + let index; + let loopCount = 0; + + while ((index = pageText.indexOf(result.query, start)) > -1 && loopCount < 10) { + loopCount++; + highlights.push({ from: index, to: index + result.query.length }); + start = index + 1; + } + + if (highlights.length) { + tokens = highlightMarks(text, highlights); + } + } + + if (tokens.length === 0 && textHighlights && textHighlights.length > 0) { + tokens = highlightMarks(text, textHighlights.map(h => ({ from: h[0], to: h[0] + h[1] }))); + } + + if (tokens.length) { return (
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx index a47e71e0041..57083fa5021 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx @@ -27,6 +27,7 @@ export interface SearchResult { highlights: { [field: string]: [number, number][] }; longestTerm: string; page: DocumentationEntry; + query: string; } interface Props { @@ -69,9 +70,36 @@ export function SearchResultTitle({ result }: { result: SearchResult }) { export function SearchResultText({ result }: { result: SearchResult }) { const textHighlights = result.highlights.text; - if (textHighlights && textHighlights.length > 0) { - const { text } = result.page; - const tokens = highlightMarks(text, textHighlights.map(h => ({ from: h[0], to: h[0] + h[1] }))); + const { text } = result.page; + let tokens: { + text: string; + marked: boolean; + }[] = []; + + if (result.exactMatch) { + const pageText = result.page.text.toLowerCase(); + const highlights: { from: number; to: number }[] = []; + let start = 0; + let index = pageText.indexOf(result.query, start); + let loopCount = 0; + + while (index > -1 && loopCount < 10) { + loopCount++; + highlights.push({ from: index, to: index + result.query.length }); + start = index + 1; + index = pageText.indexOf(result.query, start); + } + + if (highlights.length) { + tokens = highlightMarks(text, highlights); + } + } + + if (tokens.length === 0 && textHighlights && textHighlights.length > 0) { + tokens = highlightMarks(text, textHighlights.map(h => ({ from: h[0], to: h[0] + h[1] }))); + } + + if (tokens.length) { return (
diff --git a/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx b/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx index 7a0be949f76..fc70715270b 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx @@ -89,7 +89,7 @@ export default class SearchResults extends React.PureComponent { }); }); - return { page, highlights, longestTerm, exactMatch }; + return { exactMatch, highlights, longestTerm, page, query }; }) .filter(result => result.page) as SearchResult[]; diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx index 0f28fc04b89..49ada2d46a1 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx +++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx @@ -25,54 +25,45 @@ import SearchResultEntry, { SearchResultTokens } from '../SearchResultEntry'; -const page = { - content: '', - relativeName: 'foo/bar', - url: '/foo/bar', - text: 'Foobar is a universal variable understood to represent whatever is being discussed.', - title: 'Foobar', - navTitle: undefined -}; - describe('SearchResultEntry', () => { it('should render', () => { expect( - shallow( - - ) + shallow() ).toMatchSnapshot(); }); }); describe('SearchResultText', () => { it('should render with highlights', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); + + it('should correctly extract exact matches', () => { expect( shallow( - + ) ).toMatchSnapshot(); }); it('should render without highlights', () => { - expect( - shallow() - ).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); describe('SearchResultTitle', () => { it('should render with highlights', () => { expect( - shallow( - - ) + shallow() ).toMatchSnapshot(); }); it('should render not without highlights', () => { - expect( - shallow() - ).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); @@ -94,3 +85,20 @@ describe('SearchResultTokens', () => { ).toMatchSnapshot(); }); }); + +function mockSearchResult(overrides = {}) { + return { + page: { + content: '', + relativeName: 'foo/bar', + url: '/foo/bar', + text: 'Foobar is a universal variable understood to represent whatever is being discussed.', + title: 'Foobar', + navTitle: undefined + }, + highlights: {}, + longestTerm: '', + query: '', + ...overrides + }; +} diff --git a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap index 40818733fe7..c45f65fc1a5 100644 --- a/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap @@ -20,6 +20,7 @@ exports[`SearchResultEntry should render 1`] = ` "title": "Foobar", "url": "/foo/bar", }, + "query": "", } } /> @@ -36,12 +37,38 @@ exports[`SearchResultEntry should render 1`] = ` "title": "Foobar", "url": "/foo/bar", }, + "query": "", } } /> `; +exports[`SearchResultText should correctly extract exact matches 1`] = ` +
+ +
+`; + exports[`SearchResultText should render with highlights 1`] = `
@@ -71,6 +72,7 @@ exports[`should search 1`] = ` "title": "Where does it come from?", "url": "/lorem/origin", }, + "query": "simply text", } } /> -- 2.39.5