diff --git a/src/compile.test.ts b/src/compile.test.ts index 48de1e2..800b144 100644 --- a/src/compile.test.ts +++ b/src/compile.test.ts @@ -1,30 +1,34 @@ import compile from './compile'; +test('empty content should works', () => { + expect(compile('', 'party')).toMatchObject([]); + expect(compile('', ['party'])).toMatchObject([]); +}); + +test('empty keywords should works', () => { + const content = 'Welcome everyone to my party'; + expect(compile(content, '')).toMatchObject([content]); + expect(compile(content, [''])).toMatchObject([content]); + expect(compile(content, [])).toMatchObject([content]); +}); + +test('empty content and empty keywords should works', () => { + expect(compile('', '')).toMatchObject([]); + expect(compile('', [''])).toMatchObject([]); + expect(compile('', [])).toMatchObject([]); +}); + test('no matched word should works', () => { const content = 'Welcome everyone to come and join my birthday party.'; - expect(compile(content, 'tom')).toMatchObject([ - { - text: content, - matched: false, - }, - ]); + expect(compile(content, 'tom')).toMatchObject([content]); }); test('a single matched word should works', () => { const content = 'Welcome everyone to come and join my birthday party.'; expect(compile(content, 'party')).toMatchObject([ - { - text: 'Welcome everyone to come and join my birthday ', - matched: false, - }, - { - text: 'party', - matched: true, - }, - { - text: '.', - matched: false, - }, + 'Welcome everyone to come and join my birthday ', + { text: 'party', matched: true }, + '.', ]); }); @@ -32,71 +36,42 @@ test(`multiple matched words should works`, () => { const content = 'hi, party time. Welcome everyone to come and join my birthday party.'; expect(compile(content, 'party')).toMatchObject([ - { - text: 'hi, ', - matched: false, - }, - { - text: 'party', - matched: true, - }, - { - text: ' time. Welcome everyone to come and join my birthday ', - matched: false, - }, - { - text: 'party', - matched: true, - }, - { - text: '.', - matched: false, - }, + 'hi, ', + { text: 'party', matched: true }, + ' time. Welcome everyone to come and join my birthday ', + { text: 'party', matched: true }, + '.', ]); }); test('matched word at begin should works', () => { const content = 'party.Welcome everyone.'; expect(compile(content, 'party')).toMatchObject([ - { - text: 'party', - matched: true, - }, - { - text: '.Welcome everyone.', - matched: false, - }, + { text: 'party', matched: true }, + '.Welcome everyone.', ]); }); test('matched word at end should works', () => { const content = 'Welcome everyone to my party'; expect(compile(content, 'party')).toMatchObject([ - { - text: 'Welcome everyone to my ', - matched: false, - }, - { - text: 'party', - matched: true, - }, + 'Welcome everyone to my ', + { text: 'party', matched: true }, ]); }); -test('empty content should works', () => { - expect(compile('', 'party')).toMatchObject([]); -}); - -test('empty keywords should works', () => { +test('multiple keywords as props should works', () => { const content = 'Welcome everyone to my party'; - expect(compile(content, '')).toMatchObject([ - { - text: content, - matched: false, - }, + + expect(compile(content, ['party', 'to'])).toMatchObject([ + 'Welcome everyone ', + { text: 'to', matched: true }, + ' my ', + { text: 'party', matched: true }, ]); -}); -test('empty content and empty keywords should works', () => { - expect(compile('', '')).toMatchObject([]); + expect(compile(content, ['party', 'other'])).toMatchObject([ + 'Welcome everyone to my ', + { text: 'party', matched: true }, + ]); }); diff --git a/src/compile.ts b/src/compile.ts index 763dc14..803942d 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -3,12 +3,22 @@ type Node = { matched: boolean; }; -export default function compile(content: string, keywords: string) { +export default function compile(content: string, words: string | string[]) { + let keywords = typeof words === 'string' ? [words] : words; + keywords = keywords.filter((w) => w); + keywords = Array.from(new Set(keywords)); + + return innerCompile(content, keywords).map((node) => { + return node.matched ? node : node.text; + }); +} + +const innerCompile = (content: string, keywords: string[]) => { if (!content) { return []; } - if (!keywords) { + if (keywords.length === 0) { return [ { text: content, @@ -17,21 +27,42 @@ export default function compile(content: string, keywords: string) { ]; } - return content - .split(keywords) - .reduce((acc: Node[], cur, idx) => { - return [ - ...acc, - { - text: keywords, - matched: true, - }, - { - text: cur, - matched: false, - }, - ]; + let nodes: Node[] = [ + { + text: content, + matched: false, + }, + ]; + for (let keyword of keywords) { + nodes = nodes.reduce((acc, cur) => { + if (cur.matched) { + return [...acc, cur]; + } else { + const list = split(cur.text, keyword); + return [...acc, ...list]; + } + }, []); + } + return nodes; +}; + +const split = (content: string, keyword: string) => { + const nodes = content.split(keyword).map((text) => { + return { + text: text, + matched: false, + }; + }); + return join(nodes, { + text: keyword, + matched: true, + }).filter((node) => node.text); +}; + +const join = (arr: Node[], separator: Node) => { + return arr + .reduce((acc: Node[], cur) => { + return [...acc, separator, cur]; }, []) - .slice(1) - .filter((node) => node.text.length); -} + .slice(1); +}; diff --git a/src/index.test.tsx b/src/index.test.tsx index 8000ce6..789ba09 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -4,13 +4,19 @@ import HighlightWords from './index'; test(`component could be updated and unmounted without errors`, () => { const text = 'Welcome everyone to come and join my birthday party.'; const { unmount, rerender } = render( - + {word}} + > {text} , ); expect(() => { rerender( - + {word}} + > {text} , ); @@ -18,10 +24,13 @@ test(`component could be updated and unmounted without errors`, () => { }).not.toThrow(); }); -test(`a single matched word should works`, () => { +test(`single matched should works`, () => { const text = 'Welcome everyone to come and join my birthday party.'; const { container } = render( - + {word}} + > {text} , ); @@ -31,11 +40,14 @@ test(`a single matched word should works`, () => { ); }); -test(`multiple matched words should works`, () => { +test(`multiple matched should works`, () => { const text = 'hi, party time. Welcome everyone to come and join my birthday party.'; const { container } = render( - + {word}} + > {text} , ); @@ -45,10 +57,13 @@ test(`multiple matched words should works`, () => { ); }); -test('empty keywords should works', () => { +test('empty words should works', () => { const text = 'Welcome everyone to come and join my birthday party.'; const { container } = render( - + {word}} + > {text} , ); @@ -58,13 +73,19 @@ test('empty keywords should works', () => { test(`component should update when props changes`, () => { const { container, rerender } = render( - + {word}} + > hello, everyone. , ); rerender( - + {word}} + > Welcome everyone to come and join my birthday party. , ); @@ -73,3 +94,14 @@ test(`component should update when props changes`, () => { `Welcome everyone to come and join my birthday party.`, ); }); + +test('no render props should works', () => { + const text = 'Welcome everyone to come and join my birthday party.'; + const { container } = render( + {text}, + ); + + expect(container.innerHTML).toEqual( + `Welcome everyone to come and join my birthday party.`, + ); +}); diff --git a/src/index.tsx b/src/index.tsx index 21e8160..817c564 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,31 +1,32 @@ -import { CSSProperties, FC, Fragment, useMemo } from 'react'; +import { FC, Fragment, ReactNode, useCallback, useMemo } from 'react'; import compile from './compile'; -const defaultStyle = { - color: '#00BC70', -}; - -type KeywordsHighlightProps = { - keywords: string; - style?: CSSProperties; +type HighlightWordsProps = { + words: string; children: string; + render?: (keyword: string) => ReactNode; }; -const KeywordsHighlight: FC = ({ - keywords, - style = defaultStyle, +const HighlightWords: FC = ({ + words, children, + render, }) => { - const nodes = useMemo(() => { - return compile(children, keywords); - }, [children, keywords]); + const nodes = useMemo(() => compile(children, words), [children, words]); + + const realRender = useCallback( + (word: string) => { + return render ? render(word) : {word}; + }, + [render], + ); return ( {nodes.map((node, idx) => { return ( - {node.matched ? {node.text} : node.text} + {typeof node === 'string' ? node : realRender(node.text)} ); })} @@ -33,4 +34,4 @@ const KeywordsHighlight: FC = ({ ); }; -export default KeywordsHighlight; +export default HighlightWords;