Highlight multiple words in search result snippet
This commit is contained in:
parent
83c1acedd8
commit
0afe124c68
3 changed files with 59 additions and 16 deletions
|
@ -727,6 +727,27 @@ describe('BodyRanges', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns proper bodyRange surrounding multiple keywords', () => {
|
||||||
|
const { cleanedSnippet, bodyRanges } = processBodyRangesForSearchResult({
|
||||||
|
snippet: "What's <<left>>going<<right>> <<left>>on<<right>>?",
|
||||||
|
body: "What's going on?",
|
||||||
|
bodyRanges: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(cleanedSnippet, "What's going on?");
|
||||||
|
assert.lengthOf(bodyRanges, 2);
|
||||||
|
assert.deepEqual(bodyRanges[0], {
|
||||||
|
start: 7,
|
||||||
|
length: 5,
|
||||||
|
displayStyle: DisplayStyle.SearchKeywordHighlight,
|
||||||
|
});
|
||||||
|
assert.deepEqual(bodyRanges[1], {
|
||||||
|
start: 13,
|
||||||
|
length: 2,
|
||||||
|
displayStyle: DisplayStyle.SearchKeywordHighlight,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('returns proper bodyRange surrounding keyword, with trailing ...', () => {
|
it('returns proper bodyRange surrounding keyword, with trailing ...', () => {
|
||||||
const { cleanedSnippet, bodyRanges } = processBodyRangesForSearchResult({
|
const { cleanedSnippet, bodyRanges } = processBodyRangesForSearchResult({
|
||||||
snippet: "What's <<left>>going<<right>> on<<truncation>>",
|
snippet: "What's <<left>>going<<right>> on<<truncation>>",
|
||||||
|
|
|
@ -7,9 +7,14 @@ import { escapeRegExp, isNumber, omit } from 'lodash';
|
||||||
|
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { assertDev } from '../util/assert';
|
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import {
|
||||||
|
SNIPPET_LEFT_PLACEHOLDER,
|
||||||
|
SNIPPET_RIGHT_PLACEHOLDER,
|
||||||
|
SNIPPET_TRUNCATION_PLACEHOLDER,
|
||||||
|
} from '../util/search';
|
||||||
|
import { assertDev } from '../util/assert';
|
||||||
|
|
||||||
// Cold storage of body ranges
|
// Cold storage of body ranges
|
||||||
|
|
||||||
|
@ -507,11 +512,8 @@ export function groupContiguousSpoilers(
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRUNCATION_CHAR = '...';
|
const TRUNCATION_CHAR = '...';
|
||||||
const LENGTH_OF_LEFT = '<<left>>'.length;
|
const TRUNCATION_START = new RegExp(`^${SNIPPET_TRUNCATION_PLACEHOLDER}`);
|
||||||
const TRUNCATION_PLACEHOLDER = '<<truncation>>';
|
const TRUNCATION_END = new RegExp(`${SNIPPET_TRUNCATION_PLACEHOLDER}$`);
|
||||||
const TRUNCATION_START = /^<<truncation>>/;
|
|
||||||
const TRUNCATION_END = /<<truncation>>$/;
|
|
||||||
|
|
||||||
// This function exists because bodyRanges tells us the character position
|
// This function exists because bodyRanges tells us the character position
|
||||||
// where the at-mention starts at according to the full body text. The snippet
|
// where the at-mention starts at according to the full body text. The snippet
|
||||||
// we get back is a portion of the text and we don't know where it starts. This
|
// we get back is a portion of the text and we don't know where it starts. This
|
||||||
|
@ -531,8 +533,8 @@ export function processBodyRangesForSearchResult({
|
||||||
} {
|
} {
|
||||||
// Find where the snippet starts in the full text
|
// Find where the snippet starts in the full text
|
||||||
const cleanedSnippet = snippet
|
const cleanedSnippet = snippet
|
||||||
.replace(/<<left>>/g, '')
|
.replace(new RegExp(SNIPPET_LEFT_PLACEHOLDER, 'g'), '')
|
||||||
.replace(/<<right>>/g, '');
|
.replace(new RegExp(SNIPPET_RIGHT_PLACEHOLDER, 'g'), '');
|
||||||
const withNoStartTruncation = cleanedSnippet.replace(TRUNCATION_START, '');
|
const withNoStartTruncation = cleanedSnippet.replace(TRUNCATION_START, '');
|
||||||
const withNoEndTruncation = withNoStartTruncation.replace(TRUNCATION_END, '');
|
const withNoEndTruncation = withNoStartTruncation.replace(TRUNCATION_END, '');
|
||||||
const finalSnippet = cleanedSnippet
|
const finalSnippet = cleanedSnippet
|
||||||
|
@ -574,21 +576,35 @@ export function processBodyRangesForSearchResult({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// To format the match identified by FTS, we create a synthetic BodyRange to mix in with
|
// To format the matches identified by FTS, we create synthetic BodyRanges to mix in
|
||||||
// all the other formatting embedded in this message.
|
// with all the other formatting embedded in this message.
|
||||||
const startOfKeywordMatch = snippet.match(/<<left>>/)?.index;
|
const highlightMatches = snippet.matchAll(
|
||||||
const endOfKeywordMatch = snippet.match(/<<right>>/)?.index;
|
new RegExp(
|
||||||
|
`${SNIPPET_LEFT_PLACEHOLDER}(.*?)${SNIPPET_RIGHT_PLACEHOLDER}`,
|
||||||
|
'dg'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (isNumber(startOfKeywordMatch) && isNumber(endOfKeywordMatch)) {
|
let placeholderCharsSkipped = 0;
|
||||||
|
for (const highlightMatch of highlightMatches) {
|
||||||
|
// TS < 5 does not have types for RegExpIndicesArray
|
||||||
|
const { indices } = highlightMatch as RegExpMatchArray & {
|
||||||
|
indices: Array<Array<number>>;
|
||||||
|
};
|
||||||
|
const [wholeMatchStartIdx] = indices[0];
|
||||||
|
const [matchedWordStartIdx, matchedWordEndIdx] = indices[1];
|
||||||
adjustedBodyRanges.push({
|
adjustedBodyRanges.push({
|
||||||
start:
|
start:
|
||||||
startOfKeywordMatch +
|
wholeMatchStartIdx +
|
||||||
|
-placeholderCharsSkipped +
|
||||||
(truncationDelta
|
(truncationDelta
|
||||||
? TRUNCATION_CHAR.length - TRUNCATION_PLACEHOLDER.length
|
? TRUNCATION_CHAR.length - SNIPPET_TRUNCATION_PLACEHOLDER.length
|
||||||
: 0),
|
: 0),
|
||||||
length: endOfKeywordMatch - (startOfKeywordMatch + LENGTH_OF_LEFT),
|
length: matchedWordEndIdx - matchedWordStartIdx,
|
||||||
displayStyle: DisplayStyle.SearchKeywordHighlight,
|
displayStyle: DisplayStyle.SearchKeywordHighlight,
|
||||||
});
|
});
|
||||||
|
placeholderCharsSkipped +=
|
||||||
|
SNIPPET_LEFT_PLACEHOLDER.length + SNIPPET_RIGHT_PLACEHOLDER.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
6
ts/util/search.ts
Normal file
6
ts/util/search.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export const SNIPPET_LEFT_PLACEHOLDER = '<<left>>';
|
||||||
|
export const SNIPPET_RIGHT_PLACEHOLDER = '<<right>>';
|
||||||
|
export const SNIPPET_TRUNCATION_PLACEHOLDER = '<<truncation>>';
|
Loading…
Add table
Add a link
Reference in a new issue