From 3b22b908c46a72f0c633603f39c8eac77b87aca9 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Wed, 21 Feb 2024 18:03:06 +0100 Subject: [PATCH] Components for displaying shortened text with highlights --- .changelog/1397.trivial.md | 1 + .../AdaptiveHighlightedText.tsx | 66 +++++++++++++++++ .../HighlightedTrimmedText.tsx | 43 +++++++++++ .../HighlightedText/text-cutting.ts | 74 +++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 .changelog/1397.trivial.md create mode 100644 src/app/components/HighlightedText/AdaptiveHighlightedText.tsx create mode 100644 src/app/components/HighlightedText/HighlightedTrimmedText.tsx create mode 100644 src/app/components/HighlightedText/text-cutting.ts diff --git a/.changelog/1397.trivial.md b/.changelog/1397.trivial.md new file mode 100644 index 0000000000..e1da642203 --- /dev/null +++ b/.changelog/1397.trivial.md @@ -0,0 +1 @@ +Use adaptive method when trimming texts with highlights diff --git a/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx b/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx new file mode 100644 index 0000000000..d79746e4df --- /dev/null +++ b/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx @@ -0,0 +1,66 @@ +import { FC, ReactNode } from 'react' +import InfoIcon from '@mui/icons-material/Info' +import { HighlightedText, HighlightOptions } from './index' +import { AdaptiveDynamicTrimmer } from '../AdaptiveTrimmer/AdaptiveDynamicTrimmer' +import { HighlightedTrimmedText } from './HighlightedTrimmedText' + +type AdaptiveHighlightedTextProps = { + /** + * The text to display + */ + text: string | undefined + + /** + * The pattern to search for (and highlight) + */ + pattern: string | undefined + + /** + * Options for highlighting (case sensitivity, styling, etc.) + * + * (This is optional, sensible defaults are provided.) + */ + options?: HighlightOptions + + /** + * Extra content to put into the tooltip + */ + extraTooltip?: ReactNode +} + +/** + * Display a text with a part highlighted, adaptively trimmed to the maximum length around the highlight + */ +export const AdaptiveHighlightedText: FC = ({ + text, + pattern, + options, + extraTooltip, +}) => { + const fullContent = + + return text ? ( + ({ + content: fullContent, + length: text.length, + })} + getShortenedContent={wantedLength => ( + + )} + extraTooltip={ + extraTooltip ? ( + <> + + {extraTooltip} + + ) : undefined + } + /> + ) : undefined +} diff --git a/src/app/components/HighlightedText/HighlightedTrimmedText.tsx b/src/app/components/HighlightedText/HighlightedTrimmedText.tsx new file mode 100644 index 0000000000..02ea8114d1 --- /dev/null +++ b/src/app/components/HighlightedText/HighlightedTrimmedText.tsx @@ -0,0 +1,43 @@ +import { FC } from 'react' + +import { HighlightedText, HighlightOptions } from './index' +import { cutAroundMatch } from './text-cutting' + +type HighlightedTrimmedTextProps = { + /** + * The text to display + */ + text: string | undefined + + /** + * The pattern to search for (and highlight) + */ + pattern: string | undefined + + /** + * Options for highlighting (case sensitivity, styling, etc.) + * + * (This is optional, sensible defaults are provided.) + */ + options?: HighlightOptions + + /** + * What should be the length of the fragment delivered, which + * has the pattern inside it? + */ + fragmentLength: number +} + +/** + * Display a text with a part highlighted, trimmed to a specific length around the highlight + */ +export const HighlightedTrimmedText: FC = props => { + const { text, pattern, fragmentLength, options } = props + return ( + + ) +} diff --git a/src/app/components/HighlightedText/text-cutting.ts b/src/app/components/HighlightedText/text-cutting.ts new file mode 100644 index 0000000000..cb7555fa69 --- /dev/null +++ b/src/app/components/HighlightedText/text-cutting.ts @@ -0,0 +1,74 @@ +import { findTextMatch, NormalizerOptions } from './text-matching' + +export interface CutAroundOptions extends NormalizerOptions { + /** + * What should be the length of the fragment delivered, which + * has the pattern inside it? + * + * The default value is 80. + */ + fragmentLength?: number +} + +/** + * Return a part of the corpus that contains the match to the pattern, if any + * + * If the corpus is undefined or empty, undefined is returned. + * + * If either the pattern is undefined or empty, or there is no match, + * an adequately sized part from the beginning of the corpus is returned. + * + * If there is a match, but the corpus is at most as long as the desired fragment length, + * the whole corpus is returned. + * + * If there is a match, and the corpus is longer than the desired fragment length, + * then a part of a corpus is returned, so that the match is within the returned part, + * around the middle. + */ +export function cutAroundMatch( + corpus: string | undefined, + pattern: string | undefined, + options: CutAroundOptions = {}, +): { + hasMatch: boolean + part: string | undefined +} { + const { fragmentLength = 80, ...matchOptions } = options + + if (!corpus) { + // there is nothing to see here + return { + hasMatch: false, + part: undefined, + } + } + + // do we have a match? + const match = pattern ? findTextMatch(corpus, [pattern], matchOptions) : undefined + + if (corpus.length <= fragmentLength) { + // the whole corpus fits into the max size, no need to cut. + return { + hasMatch: !!match, + part: corpus, + } + } + + // how much extra space do we have? + const buffer = fragmentLength - (pattern || '').length + + const matchStart = match?.startPos ?? 0 + + // We will start before the start of the match, by buffer / 2 chars + const startPos = Math.max(Math.min(matchStart - Math.floor(buffer / 2), corpus.length - fragmentLength), 0) + const endPos = Math.min(startPos + fragmentLength, corpus.length) + + // compile the result + const part = + (startPos ? '…' : '') + corpus.substring(startPos, endPos) + (endPos < corpus.length - 1 ? '…' : '') + + return { + hasMatch: true, + part, + } +}