diff --git a/src/components/Highlight.tsx b/src/components/Highlight.tsx
index 820a294..cb91e29 100644
--- a/src/components/Highlight.tsx
+++ b/src/components/Highlight.tsx
@@ -1,6 +1,9 @@
import * as React from 'react'
-export function Highlight(props: { text: string; match?: RegExp | string }) {
+export const Highlight = React.memo(function Highlight(props: {
+ text: string
+ match?: RegExp | string
+}) {
const { text, match } = props
const $match = React.useMemo(() => {
if (match) {
@@ -13,23 +16,32 @@ export function Highlight(props: { text: string; match?: RegExp | string }) {
return null
}, [match])
- if (!$match) return <>{text}>
+ const chunks = useChunks(text, $match)
+
+ return <>{chunks.map(([type, text], key) => React.createElement(type, { key }, text))}>
+})
+
+function useChunks(text: string, $match: RegExp | null) {
+ const contents: [string, string][] = []
+ if ($match === null) return [['span', text]]
const matchedPieces = Array.from(text.matchAll($match)).map(
([text, highlightText = text]) => highlightText,
)
const preservedPieces = text.split($match)
- const content = []
- let i = 0
- while (matchedPieces.length || preservedPieces.length) {
- if (preservedPieces.length) {
- content.push({preservedPieces.shift()})
- }
- if (matchedPieces.length) {
- content.push({matchedPieces.shift()})
- }
+ const push = (type: string, text: string) => {
+ if (!text) return
+ const last = contents[contents.length - 1]
+ if (last && last[0] === type) last[1] += text
+ else contents.push([type, text])
}
- return <>{content}>
+ let i = 0
+ while (i < Math.max(matchedPieces.length, preservedPieces.length)) {
+ push('span', preservedPieces[i])
+ push('mark', matchedPieces[i])
+ ++i
+ }
+ return contents
}
diff --git a/src/components/HighlightOnIndexes.test.tsx b/src/components/HighlightOnIndexes.test.tsx
new file mode 100644
index 0000000..1ffd64f
--- /dev/null
+++ b/src/components/HighlightOnIndexes.test.tsx
@@ -0,0 +1,27 @@
+import { render } from '@testing-library/react'
+import React from 'react'
+import { is } from 'utils/is'
+import { HighlightOnIndexes } from './HighlightOnIndexes'
+
+function test(title: string, text: string, indexes?: number[]) {
+ it(title, () => {
+ expect(render().container.textContent).toBe(
+ text,
+ )
+ })
+}
+
+const text = 'abcdef'
+for (let i = 0; i < Math.pow(2, text.length); i++) {
+ const bitwise = i.toString(2)
+ const indexes = bitwise
+ .split('')
+ .reverse()
+ .map((bit, index) => (bit === '1' ? index : false))
+ .filter(is.not.false)
+ test(
+ `renders properly when highlight ${bitwise.padStart(text.length, '0')}, indexes [${indexes}]`,
+ text,
+ indexes,
+ )
+}
diff --git a/src/components/HighlightOnIndexes.tsx b/src/components/HighlightOnIndexes.tsx
index f8a21f4..c31ac2b 100644
--- a/src/components/HighlightOnIndexes.tsx
+++ b/src/components/HighlightOnIndexes.tsx
@@ -1,17 +1,28 @@
-import * as React from 'react'
-
-export function HighlightOnIndexes(props: { text: string; indexes?: number[] }) {
- const { text, indexes } = props
-
- if (!indexes?.length) return <>{text}>
+import * as React from 'react';
+export function HighlightOnIndexes({ text, indexes = [] }: { text: string; indexes?: number[] }) {
return (
<>
- {text
- .split('')
- .map((char, i) =>
- indexes.includes(i) ? {char} : {char},
- )}
+ {[-1]
+ .concat(indexes)
+ .map((index, i, arr) => [
+ index === -1 ? '' : text.slice(index, index + 1),
+ text.slice(index + 1, arr[i + 1]),
+ ])
+ .reduce((arr, pair) => {
+ const last = arr[arr.length - 1]
+ if (last && !last[1]) {
+ last[0] += pair[0]
+ last[1] += pair[1]
+ } else {
+ arr.push(pair)
+ }
+ return arr
+ }, [] as string[][])
+ .map(([chunk, nextChunk], i) => [
+ chunk && {chunk},
+ nextChunk && {nextChunk},
+ ])}
>
)
}
diff --git a/src/utils/general.ts b/src/utils/general.ts
index 850244b..0b6925f 100644
--- a/src/utils/general.ts
+++ b/src/utils/general.ts
@@ -1,5 +1,6 @@
import { ReactElement } from 'react'
import * as ReactDOM from 'react-dom'
+import { is } from './is'
export function pick(source: T, keys: string[]): Partial {
if (keys && typeof keys === 'object') {
@@ -129,12 +130,20 @@ export async function JSONRequest(
).json()
}
-export function searchKeyToRegexp(searchKey: string) {
- if (!searchKey) return null
-
- return safeRegexp(searchKey, hasUpperCase(searchKey) ? 'g' : 'gi')
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function memoize(fn: (...args: Args) => R): (...args: Args) => R {
+ let lastArgs: Args | null = null
+ let lastR: R | null = null
+ return (...args) => {
+ if (lastArgs && is.shallowEqual.array(lastArgs, args)) return lastR as R
+ return (lastR = fn(...(lastArgs = args)))
+ }
}
+export const searchKeyToRegexp = memoize((searchKey: string) =>
+ searchKey ? safeRegexp(searchKey, hasUpperCase(searchKey) ? 'g' : 'gi') : null,
+)
+
export function hasUpperCase(input: string) {
return /[A-Z]/.test(input)
}