mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: optimize highlight render performance
This commit is contained in:
parent
caaff93ad7
commit
b0283f1f5e
4 changed files with 86 additions and 27 deletions
|
|
@ -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(<span key={i++}>{preservedPieces.shift()}</span>)
|
||||
}
|
||||
if (matchedPieces.length) {
|
||||
content.push(<mark key={i++}>{matchedPieces.shift()}</mark>)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
27
src/components/HighlightOnIndexes.test.tsx
Normal file
27
src/components/HighlightOnIndexes.test.tsx
Normal file
|
|
@ -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(<HighlightOnIndexes text={text} indexes={indexes} />).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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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) ? <mark key={i}>{char}</mark> : <span key={i}>{char}</span>,
|
||||
)}
|
||||
{[-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 && <mark key={i * 2}>{chunk}</mark>,
|
||||
nextChunk && <span key={i * 2 + 1}>{nextChunk}</span>,
|
||||
])}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ReactElement } from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import { is } from './is'
|
||||
|
||||
export function pick<T>(source: T, keys: string[]): Partial<T> {
|
||||
if (keys && typeof keys === 'object') {
|
||||
|
|
@ -129,12 +130,20 @@ export async function JSONRequest<D>(
|
|||
).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<Args extends any[], R>(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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue