Browse Source

SONAR-11472 Add support highlighting exact matches in results

tags/7.6
Wouter Admiraal 5 years ago
parent
commit
77d4844e58

+ 26
- 3
server/sonar-docs/src/layouts/components/SearchEntryResult.js View File

@@ -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 (
<div className="note">
<SearchResultTokens tokens={cutWords(tokens)} />

+ 31
- 3
server/sonar-web/src/main/js/apps/documentation/components/SearchResultEntry.tsx View File

@@ -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 (
<div className="note">
<SearchResultTokens tokens={cutWords(tokens)} />

+ 1
- 1
server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx View File

@@ -89,7 +89,7 @@ export default class SearchResults extends React.PureComponent<Props> {
});
});

return { page, highlights, longestTerm, exactMatch };
return { exactMatch, highlights, longestTerm, page, query };
})
.filter(result => result.page) as SearchResult[];


+ 30
- 22
server/sonar-web/src/main/js/apps/documentation/components/__tests__/SearchResultEntry-test.tsx View File

@@ -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(
<SearchResultEntry active={true} result={{ page, highlights: {}, longestTerm: '' }} />
)
shallow(<SearchResultEntry active={true} result={mockSearchResult()} />)
).toMatchSnapshot();
});
});

describe('SearchResultText', () => {
it('should render with highlights', () => {
expect(
shallow(<SearchResultText result={mockSearchResult({ highlights: { text: [[12, 9]] } })} />)
).toMatchSnapshot();
});

it('should correctly extract exact matches', () => {
expect(
shallow(
<SearchResultText result={{ page, highlights: { text: [[12, 9]] }, longestTerm: '' }} />
<SearchResultText
result={mockSearchResult({ exactMatch: true, query: 'variable understood' })}
/>
)
).toMatchSnapshot();
});

it('should render without highlights', () => {
expect(
shallow(<SearchResultText result={{ page, highlights: {}, longestTerm: '' }} />)
).toMatchSnapshot();
expect(shallow(<SearchResultText result={mockSearchResult()} />)).toMatchSnapshot();
});
});

describe('SearchResultTitle', () => {
it('should render with highlights', () => {
expect(
shallow(
<SearchResultTitle result={{ page, highlights: { title: [[0, 6]] }, longestTerm: '' }} />
)
shallow(<SearchResultTitle result={mockSearchResult({ highlights: { title: [[0, 6]] } })} />)
).toMatchSnapshot();
});

it('should render not without highlights', () => {
expect(
shallow(<SearchResultTitle result={{ page, highlights: {}, longestTerm: '' }} />)
).toMatchSnapshot();
expect(shallow(<SearchResultTitle result={mockSearchResult()} />)).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
};
}

+ 27
- 0
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResultEntry-test.tsx.snap View File

@@ -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": "",
}
}
/>
</Link>
`;

exports[`SearchResultText should correctly extract exact matches 1`] = `
<div
className="note"
>
<SearchResultTokens
tokens={
Array [
Object {
"marked": false,
"text": "Foobar is a universal ",
},
Object {
"marked": true,
"text": "variable understood",
},
Object {
"marked": false,
"text": " to represent whatever is being discussed.",
},
]
}
/>
</div>
`;

exports[`SearchResultText should render with highlights 1`] = `
<div
className="note"

+ 2
- 0
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/SearchResults-test.tsx.snap View File

@@ -35,6 +35,7 @@ exports[`should search 1`] = `
"title": "Where does Foobar come from?",
"url": "/foobar",
},
"query": "simply text",
}
}
/>
@@ -71,6 +72,7 @@ exports[`should search 1`] = `
"title": "Where does it come from?",
"url": "/lorem/origin",
},
"query": "simply text",
}
}
/>

Loading…
Cancel
Save