From a88fac6a7da27510afa88f885472bbbc50741f49 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Wed, 6 Nov 2024 19:22:13 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=8F=20Replace=20token-limit=20with=20c?= =?UTF-8?q?har-limit=20in=20search=20listings=20(#479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/purple-bugs-relate.md | 5 + .../site/src/components/Navigation/Search.tsx | 92 ++++++++++++++----- 2 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 .changeset/purple-bugs-relate.md diff --git a/.changeset/purple-bugs-relate.md b/.changeset/purple-bugs-relate.md new file mode 100644 index 00000000..f32cf46c --- /dev/null +++ b/.changeset/purple-bugs-relate.md @@ -0,0 +1,5 @@ +--- +'@myst-theme/site': patch +--- + +Replace token-length limit with character-length limit, hide ellipsis from markup diff --git a/packages/site/src/components/Navigation/Search.tsx b/packages/site/src/components/Navigation/Search.tsx index dba7c498..5dbb876e 100644 --- a/packages/site/src/components/Navigation/Search.tsx +++ b/packages/site/src/components/Navigation/Search.tsx @@ -1,4 +1,12 @@ -import { useEffect, useState, useMemo, useCallback, useRef, forwardRef } from 'react'; +import { + createElement, + useEffect, + useState, + useMemo, + useCallback, + useRef, + forwardRef, +} from 'react'; import type { KeyboardEventHandler, Dispatch, SetStateAction, FormEvent, MouseEvent } from 'react'; import { useFetcher } from '@remix-run/react'; import { @@ -42,10 +50,21 @@ function matchAll(text: string, pattern: RegExp) { * Highlight a text string with an array of match words * * @param text - text to highlight - * @param result - search result to use for highlighting - * @param limit - limit to the number of tokens after first match + * @param matches - regular expression patterns to match against + * @param limit - limit to the number of characters after first match + * @param classname - CSS classname to use */ -function MarkedText({ text, matches, limit }: { text: string; matches: string[]; limit?: number }) { +function MarkedText({ + text, + matches, + limit, + className, +}: { + text: string; + matches: string[]; + limit?: number; + className?: string; +}) { // Split by delimeter, but _keep_ delimeter! const splits = matchAll(text, SPACE_OR_PUNCTUATION); const tokens: string[] = []; @@ -79,23 +98,38 @@ function MarkedText({ text, matches, limit }: { text: string; matches: string[]; lastIndex = tokens.length; } else { firstIndex = tokens.findIndex((token) => pattern.test(token)); - lastIndex = firstIndex + limit; + let numChars = 0; + for ( + lastIndex = firstIndex + 1; + lastIndex < tokens.length - 1 && numChars + tokens[lastIndex].length <= limit; + lastIndex++ + ) { + numChars += tokens[lastIndex].length; + } } if (tokens.length === 0) { - return <>{...tokens}; + return {...tokens}; } else { const firstRenderer = renderToken(tokens[firstIndex]); const remainingTokens = tokens.slice(firstIndex + 1, lastIndex); const remainingRenderers = remainingTokens.map((token) => renderToken(token)); return ( - <> - {hasLimit && '... '} + {firstRenderer} {...remainingRenderers} - {hasLimit && ' ...'} - + ); } } @@ -178,8 +212,10 @@ function SearchShortcut() { function SearchResultItem({ result, closeSearch, + charLimit, }: { result: RankedSearchResult; + charLimit?: number; closeSearch?: () => void; }) { const { hierarchy, type, url, queries } = result; @@ -187,14 +223,13 @@ function SearchResultItem({ const Link = useLinkProvider(); // Render the icon - const iconRenderer = - type === 'lvl1' ? ( - - ) : type === 'content' ? ( - - ) : ( - - ); + const iconProps = useMemo(() => { + return { className: 'inline-block w-6 mx-2 shrink-0' }; + }, []); + const iconRenderer = createElement( + type === 'lvl1' ? DocumentIcon : type === 'content' ? Bars3BottomLeftIcon : HashtagIcon, + iconProps, + ); // Generic "this document matched" const title = result.type === 'content' ? result['content'] : hierarchy[type as HeadingLevel]!; @@ -202,7 +237,12 @@ function SearchResultItem({ // Render the title, i.e. content or heading const titleRenderer = ( - + ); // Render the subtitle i.e. file name @@ -211,7 +251,7 @@ function SearchResultItem({ subtitleRenderer = undefined; } else { const subtitle = result.hierarchy.lvl1!; - subtitleRenderer = ; + subtitleRenderer = ; } const enterIconRenderer = ( @@ -228,8 +268,8 @@ function SearchResultItem({
{iconRenderer}
- {titleRenderer} - {subtitleRenderer && {subtitleRenderer}} + {titleRenderer} + {subtitleRenderer}
{enterIconRenderer}
@@ -242,6 +282,7 @@ interface SearchResultsProps { searchListID: string; searchLabelID: string; selectedIndex: number; + charLimit?: number; onHoverSelect: (index: number) => void; className?: string; closeSearch?: () => void; @@ -251,6 +292,7 @@ function SearchResults({ searchResults, searchListID, searchLabelID, + charLimit, className, selectedIndex, onHoverSelect, @@ -325,7 +367,7 @@ function SearchResults({ // Trigger selection on movement, so that scrolling doesn't trigger handler onMouseMove={handleMouseMove} > - + ))} @@ -541,11 +583,12 @@ const SearchPlaceholderButton = forwardRef< export interface SearchProps { debounceTime?: number; + charLimit?: number; } /** * Component that implements a basic search interface */ -export function Search({ debounceTime = 500 }: SearchProps) { +export function Search({ debounceTime = 500, charLimit = 64 }: SearchProps) { const [open, setOpen] = useState(false); const [searchResults, setSearchResults] = useState(); const [selectedIndex, setSelectedIndex] = useState(0); @@ -624,6 +667,7 @@ export function Search({ debounceTime = 500 }: SearchProps) { selectedIndex={selectedIndex} onHoverSelect={setSelectedIndex} closeSearch={triggerClose} + charLimit={charLimit} /> )}