Skip to content

Commit

Permalink
optimize
Browse files Browse the repository at this point in the history
  • Loading branch information
shhhplus committed May 18, 2023
1 parent 1ab4dce commit 2cf1219
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 112 deletions.
109 changes: 42 additions & 67 deletions src/compile.test.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,77 @@
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 },
'.',
]);
});

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 },
]);
});
69 changes: 50 additions & 19 deletions src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Node[]>((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);
};
52 changes: 42 additions & 10 deletions src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,33 @@ 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(
<HighlightWords keywords="birthday" style={{ color: 'greenyellow' }}>
<HighlightWords
words="birthday"
render={(word) => <span style={{ color: 'greenyellow' }}>{word}</span>}
>
{text}
</HighlightWords>,
);
expect(() => {
rerender(
<HighlightWords keywords="party" style={{ color: 'red' }}>
<HighlightWords
words="party"
render={(word) => <span style={{ color: 'red' }}>{word}</span>}
>
{text}
</HighlightWords>,
);
unmount();
}).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(
<HighlightWords keywords="birthday" style={{ color: 'red' }}>
<HighlightWords
words="birthday"
render={(word) => <span style={{ color: 'red' }}>{word}</span>}
>
{text}
</HighlightWords>,
);
Expand All @@ -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(
<HighlightWords keywords="party" style={{ color: 'red' }}>
<HighlightWords
words="party"
render={(word) => <span style={{ color: 'red' }}>{word}</span>}
>
{text}
</HighlightWords>,
);
Expand All @@ -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(
<HighlightWords keywords="" style={{ color: 'red' }}>
<HighlightWords
words=""
render={(word) => <span style={{ color: 'red' }}>{word}</span>}
>
{text}
</HighlightWords>,
);
Expand All @@ -58,13 +73,19 @@ test('empty keywords should works', () => {

test(`component should update when props changes`, () => {
const { container, rerender } = render(
<HighlightWords keywords="nothing" style={{ color: 'green' }}>
<HighlightWords
words="nothing"
render={(word) => <span style={{ color: 'green' }}>{word}</span>}
>
hello, everyone.
</HighlightWords>,
);

rerender(
<HighlightWords keywords="birthday" style={{ color: 'red' }}>
<HighlightWords
words="birthday"
render={(word) => <span style={{ color: 'red' }}>{word}</span>}
>
Welcome everyone to come and join my birthday party.
</HighlightWords>,
);
Expand All @@ -73,3 +94,14 @@ test(`component should update when props changes`, () => {
`Welcome everyone to come and join my <span style="color: red;">birthday</span> party.`,
);
});

test('no render props should works', () => {
const text = 'Welcome everyone to come and join my birthday party.';
const { container } = render(
<HighlightWords words="birthday">{text}</HighlightWords>,
);

expect(container.innerHTML).toEqual(
`Welcome everyone to come and join my <mark>birthday</mark> party.`,
);
});
33 changes: 17 additions & 16 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
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<KeywordsHighlightProps> = ({
keywords,
style = defaultStyle,
const HighlightWords: FC<HighlightWordsProps> = ({
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) : <mark>{word}</mark>;
},
[render],
);

return (
<Fragment>
{nodes.map((node, idx) => {
return (
<Fragment key={idx}>
{node.matched ? <span style={style}>{node.text}</span> : node.text}
{typeof node === 'string' ? node : realRender(node.text)}
</Fragment>
);
})}
</Fragment>
);
};

export default KeywordsHighlight;
export default HighlightWords;

0 comments on commit 2cf1219

Please sign in to comment.