瀏覽代碼

feat: highlight searchWords support object props (#2600)

* feat: highlight searchWords support object props
* docs: update demo
pointhalo 10 月之前
父節點
當前提交
52b37b12e1

+ 26 - 0
content/show/highlight/index-en-US.md

@@ -71,6 +71,32 @@ import { Highlight } from '@douyinfe/semi-ui';
 };
 ```
 
+### Use Different Styles for Different Texts
+After v2.71.0, it supports using different highlight styles for different highlighted texts.
+The `searchWords` is a string array by default. When an array of objects is passed in, the highlighted text can be specified through `text`, and the `className` and `style` can be specified separately at the same time. 
+
+```jsx live=true dir="column"
+import React from 'react';
+import { Highlight } from '@douyinfe/semi-ui';
+
+() => {
+    return (
+        <h2>
+            <Highlight
+                component='span'
+                sourceString='From Semi Design,To Any Design. Quickly define your design system and apply it to design drafts and code'
+                searchWords={[
+                    { text: 'Semi', style: { backgroundColor: 'rgba(var(--semi-teal-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword1' },
+                    { text: 'Quickly', style: { backgroundColor: 'var(--semi-color-primary)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword2' },
+                    { text: 'code', style: { backgroundColor: 'rgba(var(--semi-violet-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword3' },
+                ]}
+                highlightStyle={{ borderRadius: 4 }}
+            />
+        </h2>
+    );
+};
+```
+
 
 ### Specify the highlight tag
 

+ 30 - 3
content/show/highlight/index.md

@@ -89,6 +89,32 @@ import { Highlight } from '@douyinfe/semi-ui';
 };
 ```
 
+### 不同文本使用差异化样式
+v2.71.0 后,支持针对不同的高亮文本使用不同的高亮样式
+searchWords 默认为字符串数组。当传入对象数组时,可以通过 text指定高亮文本,同时单独指定 className、style
+
+```jsx live=true dir="column"
+import React from 'react';
+import { Highlight } from '@douyinfe/semi-ui';
+
+() => {
+    return (
+        <h2>
+            <Highlight
+                component='span'
+                sourceString='从 Semi Design 到 Any Design  快速定义你的设计系统,并应用在设计稿和代码中'
+                searchWords={[
+                    { text: 'Semi', style: { backgroundColor: 'rgba(var(--semi-teal-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword1' },
+                    { text: '设计系统', style: { backgroundColor: 'var(--semi-color-primary)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword2' },
+                    { text: '设计稿和代码', style: { backgroundColor: 'rgba(var(--semi-violet-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword3' },
+                ]}
+                highlightStyle={{ borderRadius: 4 }}
+            />
+        </h2>
+    );
+};
+```
+
 
 ### 指定高亮标签
 
@@ -112,16 +138,17 @@ import { Highlight } from '@douyinfe/semi-ui';
 };
 ```
 
+
 ## API 参考
 
 ### Highlight
 
 | 属性         | 说明                                                     | 类型                             | 默认值     |
 | ------------ | -------------------------------------------------------- | -------------------------------- | ---------- |
-| searchWords  | 期望高亮显示的文本                                          | string[]                          | ''   |
+| searchWords  | 期望高亮显示的文本(对象数组在v2.71后支持)                     | string[]\|object[]                          | []   |
 | sourceString | 源文本                                      | string                           |           |
 | component   | 高亮标签                                              | string                           | `mark`          |
-| highlightClassName | 高亮标签的样式类名                                         | ReactNode                        | -          |
-| highlightStyle   | 高亮标签的内联样式                                                 | ReactNode                        | -          |
+| highlightClassName | 高亮标签的样式类名                                         | string                        | -          |
+| highlightStyle   | 高亮标签的内联样式                                           | CSSProperties                        | -          |
 | caseSensitive    | 是否大小写敏感                                            | false  | -          |
 | autoEscape       | 是否自动转义                                                | true                        | -          |

+ 211 - 0
packages/semi-foundation/highlight/foundation.ts

@@ -0,0 +1,211 @@
+// Modified version based on 'highlight-words-core'
+import { isString } from 'lodash';
+import BaseFoundation, { DefaultAdapter } from '../base/foundation';
+
+interface HighlightAdapter extends Partial<DefaultAdapter> {}
+
+interface ChunkQuery {
+    autoEscape?: boolean;
+    caseSensitive?: boolean;
+    searchWords: SearchWords;
+    sourceString: string
+}
+export interface Chunk {
+    start: number;
+    end: number;
+    highlight: boolean;
+    className: string;
+    style: Record<string, string>
+}
+
+export interface ComplexSearchWord {
+    text: string;
+    className?: string;
+    style?: Record<string, string>
+}
+
+export type SearchWord = string | ComplexSearchWord | undefined;
+export type SearchWords = SearchWord[];
+
+const escapeRegExpFn = (string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
+
+export default class HighlightFoundation extends BaseFoundation<HighlightAdapter> {
+
+    constructor(adapter?: HighlightAdapter) {
+        super({
+            ...adapter,
+        });
+    }
+
+    /**
+     * Creates an array of chunk objects representing both higlightable and non highlightable pieces of text that match each search word.
+     *
+        findAll ['z'], 'aaazaaazaaa'
+            result #=> [
+                { start: 0, end: 3, highlight: false }
+                { start: 3, end: 4, highlight: true }
+                { start: 4, end: 7, highlight: false }
+                { start: 7, end: 8, highlight: true }
+                { start: 8, end: 11, highlight: false }
+            ]
+
+        findAll ['do', 'dollar'], 'aaa do dollar aaa'
+            #=> chunks: [
+                    { start: 4, end: 6 },
+                    { start: 7, end: 9 },
+                    { start: 7, end: 13 },
+                ]
+            #=> chunksToHight: [
+                    { start: 4, end: 6 },
+                    { start: 7, end: 13 },
+                ]
+            #=> result: [
+                    { start: 0, end: 4, highlight: false },
+                    { start: 4, end: 6, highlight: true },
+                    { start: 6, end: 7, highlight: false },
+                    { start: 7, end: 13, highlight: true },
+                    { start: 13, end: 17, highlight: false },
+                ]
+
+    * @return Array of "chunks" (where a Chunk is { start:number, end:number, highlight:boolean })
+    */
+    findAll = ({
+        autoEscape = true,
+        caseSensitive = false,
+        searchWords,
+        sourceString
+    }: ChunkQuery) => {
+        if (isString(searchWords)) {
+            searchWords = [searchWords];
+        }
+
+        const chunks = this.findChunks({
+            autoEscape,
+            caseSensitive,
+            searchWords,
+            sourceString
+        });
+        const chunksToHighlight = this.combineChunks({ chunks });
+        const result = this.fillInChunks({
+            chunksToHighlight,
+            totalLength: sourceString ? sourceString.length : 0
+        });
+        return result;
+    };
+
+    /**
+        * Examine text for any matches.
+        * If we find matches, add them to the returned array as a "chunk" object ({start:number, end:number}).
+        * @return { start:number, end:number }[]
+    */
+    findChunks = ({
+        autoEscape,
+        caseSensitive,
+        searchWords,
+        sourceString
+    }: ChunkQuery): Chunk[] => (
+        searchWords
+            .map(searchWord => typeof searchWord === 'string' ? { text: searchWord } : searchWord)
+            .filter(searchWord => searchWord.text) // Remove empty words
+            .reduce((chunks, searchWord) => {
+                let searchText = searchWord.text;
+                if (autoEscape) {
+                    searchText = escapeRegExpFn(searchText);
+                }
+                const regex = new RegExp(searchText, caseSensitive ? 'g' : 'gi');
+
+                let match;
+                while ((match = regex.exec(sourceString))) {
+                    const start = match.index;
+                    const end = regex.lastIndex;
+                    if (end > start) {
+                        chunks.push({ 
+                            highlight: true, 
+                            start, 
+                            end, 
+                            className: searchWord.className,
+                            style: searchWord.style
+                        });
+                    }
+                    if (match.index === regex.lastIndex) {
+                        regex.lastIndex++;
+                    }
+                }
+                return chunks;
+            }, [])
+    );
+
+    /**
+   * Takes an array of {start:number, end:number} objects and combines chunks that overlap into single chunks.
+   * @return {start:number, end:number}[]
+   */
+    combineChunks = ({ chunks }: { chunks: Chunk[] }): Chunk[] => {
+        return chunks
+            .sort((first, second) => first.start - second.start)
+            .reduce((processedChunks, nextChunk) => {
+            // First chunk just goes straight in the array...
+                if (processedChunks.length === 0) {
+                    return [nextChunk];
+                } else {
+                // ... subsequent chunks get checked to see if they overlap...
+                    const prevChunk = processedChunks.pop();
+                    if (nextChunk.start <= prevChunk.end) {
+                    // It may be the case that prevChunk completely surrounds nextChunk, so take the
+                    // largest of the end indeces.
+                        const endIndex = Math.max(prevChunk.end, nextChunk.end);
+                        processedChunks.push({
+                            highlight: true,
+                            start: prevChunk.start,
+                            end: endIndex,
+                            className: prevChunk.className || nextChunk.className,
+                            style: { ...prevChunk.style, ...nextChunk.style }
+                        });
+                    } else {
+                        processedChunks.push(prevChunk, nextChunk);
+                    }
+                    return processedChunks;
+                }
+            }, []);
+    };
+
+    /**
+   * Given a set of chunks to highlight, create an additional set of chunks
+   * to represent the bits of text between the highlighted text.
+   * @param chunksToHighlight {start:number, end:number}[]
+   * @param totalLength number
+   * @return {start:number, end:number, highlight:boolean}[]
+   */
+    fillInChunks = ({ chunksToHighlight, totalLength }: { chunksToHighlight: Chunk[]; totalLength: number }): Chunk[] => {
+        const allChunks: Chunk[] = [];
+        const append = (start: number, end: number, highlight: boolean, className?: string, style?: Record<string, string>) => {
+            if (end - start > 0) {
+                allChunks.push({
+                    start,
+                    end,
+                    highlight,
+                    className,
+                    style
+                });
+            }
+        };
+
+        if (chunksToHighlight.length === 0) {
+            append(0, totalLength, false);
+        } else {
+            let lastIndex = 0;
+            chunksToHighlight.forEach(chunk => {
+                append(lastIndex, chunk.start, false);
+                append(chunk.start, chunk.end, true, chunk.className, chunk.style);
+                lastIndex = chunk.end;
+            });
+            append(lastIndex, totalLength, false);
+        }
+        return allChunks;
+    };
+
+}
+
+
+
+
+

+ 2 - 0
packages/semi-foundation/tree/tree.scss

@@ -137,6 +137,8 @@ $module: #{$prefix}-tree;
         &-highlight {
             font-weight: $font-tree_option_hightlight-fontWeight;
             color: $color-tree_option_hightlight-text;
+            // set inherit to override highlight component default bgc
+            background-color: inherit;
         }
 
         &-hidden {

+ 0 - 178
packages/semi-foundation/utils/getHighlight.ts

@@ -1,178 +0,0 @@
-// Modified version based on 'highlight-words-core'
-import { isString } from 'lodash';
-
-const escapeRegExpFn = (string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
-interface ChunkQuery {
-    autoEscape?: boolean;
-    caseSensitive?: boolean;
-    searchWords: string[];
-    sourceString: string
-}
-interface Chunk {
-    start: number;
-    end: number;
-    highlight: boolean
-}
-/**
-   * Examine text for any matches.
-   * If we find matches, add them to the returned array as a "chunk" object ({start:number, end:number}).
-   * @return { start:number, end:number }[]
-   */
-const findChunks = ({
-    autoEscape,
-    caseSensitive,
-    searchWords,
-    sourceString
-}: ChunkQuery): Chunk[] => (
-    searchWords
-        .filter(searchWord => searchWord) // Remove empty words
-        .reduce((chunks, searchWord) => {
-            if (autoEscape) {
-                searchWord = escapeRegExpFn(searchWord);
-            }
-            const regex = new RegExp(searchWord, caseSensitive ? 'g' : 'gi');
-
-            let match;
-            while ((match = regex.exec(sourceString))) {
-                const start = match.index;
-                const end = regex.lastIndex;
-                // We do not return zero-length matches
-                if (end > start) {
-                    chunks.push({ highlight: false, start, end });
-                }
-                // Prevent browsers like Firefox from getting stuck in an infinite loop
-                // See http://www.regexguru.com/2008/04/watch-out-for-zero-length-matches/
-                if (match.index === regex.lastIndex) {
-                    regex.lastIndex++;
-                }
-            }
-            return chunks;
-        }, [])
-);
-
-/**
-   * Takes an array of {start:number, end:number} objects and combines chunks that overlap into single chunks.
-   * @return {start:number, end:number}[]
-   */
-const combineChunks = ({ chunks }: { chunks: Chunk[] }) => {
-    chunks = chunks
-        .sort((first, second) => first.start - second.start)
-        .reduce((processedChunks, nextChunk) => {
-        // First chunk just goes straight in the array...
-            if (processedChunks.length === 0) {
-                return [nextChunk];
-            } else {
-                // ... subsequent chunks get checked to see if they overlap...
-                const prevChunk = processedChunks.pop();
-                if (nextChunk.start <= prevChunk.end) {
-                    // It may be the case that prevChunk completely surrounds nextChunk, so take the
-                    // largest of the end indeces.
-                    const endIndex = Math.max(prevChunk.end, nextChunk.end);
-                    processedChunks.push({
-                        highlight: false,
-                        start: prevChunk.start,
-                        end: endIndex
-                    });
-                } else {
-                    processedChunks.push(prevChunk, nextChunk);
-                }
-                return processedChunks;
-            }
-        }, []);
-
-    return chunks;
-};
-
-
-/**
-   * Given a set of chunks to highlight, create an additional set of chunks
-   * to represent the bits of text between the highlighted text.
-   * @param chunksToHighlight {start:number, end:number}[]
-   * @param totalLength number
-   * @return {start:number, end:number, highlight:boolean}[]
-   */
-const fillInChunks = ({ chunksToHighlight, totalLength }: { chunksToHighlight: Chunk[]; totalLength: number }) => {
-    const allChunks: Chunk[] = [];
-    const append = (start: number, end: number, highlight: boolean) => {
-        if (end - start > 0) {
-            allChunks.push({
-                start,
-                end,
-                highlight
-            });
-        }
-    };
-
-    if (chunksToHighlight.length === 0) {
-        append(0, totalLength, false);
-    } else {
-        let lastIndex = 0;
-        chunksToHighlight.forEach(chunk => {
-            append(lastIndex, chunk.start, false);
-            append(chunk.start, chunk.end, true);
-            lastIndex = chunk.end;
-        });
-        append(lastIndex, totalLength, false);
-    }
-    return allChunks;
-};
-
-
-/**
- * Creates an array of chunk objects representing both higlightable and non highlightable pieces of text that match each search word.
- *
-    findAll ['z'], 'aaazaaazaaa'
-        result #=> [
-            { start: 0, end: 3, highlight: false }
-            { start: 3, end: 4, highlight: true }
-            { start: 4, end: 7, highlight: false }
-            { start: 7, end: 8, highlight: true }
-            { start: 8, end: 11, highlight: false }
-        ]
-
-    findAll ['do', 'dollar'], 'aaa do dollar aaa'
-        #=> chunks: [
-                { start: 4, end: 6 },
-                { start: 7, end: 9 },
-                { start: 7, end: 13 },
-            ]
-        #=> chunksToHight: [
-                { start: 4, end: 6 },
-                { start: 7, end: 13 },
-            ]
-        #=> result: [
-                { start: 0, end: 4, highlight: false },
-                { start: 4, end: 6, highlight: true },
-                { start: 6, end: 7, highlight: false },
-                { start: 7, end: 13, highlight: true },
-                { start: 13, end: 17, highlight: false },
-            ]
-
- * @return Array of "chunks" (where a Chunk is { start:number, end:number, highlight:boolean })
- */
-
-const findAll = ({
-    autoEscape = true,
-    caseSensitive = false,
-    searchWords,
-    sourceString
-}: ChunkQuery) => {
-    if (isString(searchWords)) {
-        searchWords = [searchWords];
-    }
-
-    const chunks = findChunks({
-        autoEscape,
-        caseSensitive,
-        searchWords,
-        sourceString
-    });
-    const chunksToHighlight = combineChunks({ chunks });
-    const result = fillInChunks({
-        chunksToHighlight,
-        totalLength: sourceString ? sourceString.length : 0
-    });
-    return result;
-};
-
-export { findAll };

+ 0 - 62
packages/semi-ui/_utils/index.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 import { cloneDeepWith, set, get } from 'lodash';
 import warning from '@douyinfe/semi-foundation/utils/warning';
-import { findAll } from '@douyinfe/semi-foundation/utils/getHighlight';
 import { isHTMLElement } from '@douyinfe/semi-foundation/utils/dom';
 import semiGlobal from "./semi-global";
 /**
@@ -66,48 +65,6 @@ export function cloneDeep(value: any, customizer?: (value: any) => any) {
         return undefined;
     });
 }
-
-/**
- * [getHighLightTextHTML description]
- *
- * @param   {string} sourceString [source content text]
- * @param   {Array<string>} searchWords [keywords to be highlighted]
- * @param   {object} option
- * @param   {true}      option.highlightTag [The tag wrapped by the highlighted content, mark is used by default]
- * @param   {true}      option.highlightClassName
- * @param   {true}      option.highlightStyle
- * @param   {boolean}   option.caseSensitive
- *
- * @return  {Array<object>}
- */
-export const getHighLightTextHTML = ({
-    sourceString = '',
-    searchWords = [],
-    option = { autoEscape: true, caseSensitive: false }
-}: GetHighLightTextHTMLProps) => {
-    const chunks: HighLightTextHTMLChunk[] = findAll({ sourceString, searchWords, ...option });
-    const markEle = option.highlightTag || 'mark';
-    const highlightClassName = option.highlightClassName || '';
-    const highlightStyle = option.highlightStyle || {};
-    return chunks.map((chunk: HighLightTextHTMLChunk, index: number) => {
-        const { end, start, highlight } = chunk;
-        const text = sourceString.substr(start, end - start);
-        if (highlight) {
-            return React.createElement(
-                markEle,
-                {
-                    style: highlightStyle,
-                    className: highlightClassName,
-                    key: text + index
-                },
-                text
-            );
-        } else {
-            return text;
-        }
-    });
-};
-
 export interface RegisterMediaQueryOption {
     match?: (e: MediaQueryList | MediaQueryListEvent) => void;
     unmatch?: (e: MediaQueryList | MediaQueryListEvent) => void;
@@ -140,25 +97,6 @@ export const registerMediaQuery = (media: string, { match, unmatch, callInInit =
     }
     return () => undefined;
 };
-export interface GetHighLightTextHTMLProps {
-    sourceString?: string;
-    searchWords?: string[];
-    option: HighLightTextHTMLOption
-}
-
-export interface HighLightTextHTMLOption {
-    highlightTag?: string;
-    highlightClassName?: string;
-    highlightStyle?: React.CSSProperties;
-    caseSensitive: boolean;
-    autoEscape: boolean
-}
-
-export interface HighLightTextHTMLChunk {
-    start?: number;
-    end?: number;
-    highlight?: any
-}
 
 /**
  * Determine whether the incoming element is a built-in icon

+ 8 - 20
packages/semi-ui/autoComplete/option.tsx

@@ -5,7 +5,7 @@ import { isString } from 'lodash';
 import { cssClasses } from '@douyinfe/semi-foundation/autoComplete/constants';
 import LocaleConsumer from '../locale/localeConsumer';
 import { IconTick } from '@douyinfe/semi-icons';
-import { getHighLightTextHTML } from '../_utils/index';
+import Highlight from '../highlight';
 import { Locale } from '../locale/interface';
 import { BasicOptionProps } from '@douyinfe/semi-foundation/autoComplete/optionFoundation';
 
@@ -19,15 +19,6 @@ export interface OptionProps extends BasicOptionProps {
     className?: string;
     style?: React.CSSProperties
 }
-interface renderOptionContentArgument {
-    config: {
-        searchWords: any;
-        sourceString: React.ReactNode
-    };
-    children: React.ReactNode;
-    inputValue: string;
-    prefixCls: string
-}
 class Option extends PureComponent<OptionProps> {
     static isSelectOption = true;
 
@@ -62,9 +53,13 @@ class Option extends PureComponent<OptionProps> {
         }
     }
 
-    renderOptionContent({ config, children, inputValue, prefixCls }: renderOptionContentArgument) {
+    renderOptionContent({ children, inputValue, prefixCls }) {
         if (isString(children) && inputValue) {
-            return getHighLightTextHTML(config as any);
+            return (<Highlight 
+                searchWords={[inputValue]}
+                sourceString={children}
+                highlightClassName={`${prefixCls}-keyword`}
+            />);
         }
         return children;
     }
@@ -129,13 +124,6 @@ class Option extends PureComponent<OptionProps> {
             });
         }
 
-        const config = {
-            searchWords: inputValue,
-            sourceString: children,
-            option: {
-                highlightClassName: `${prefixCls}-keyword`
-            }
-        };
         return (
             // eslint-disable-next-line jsx-a11y/interactive-supports-focus,jsx-a11y/click-events-have-key-events
             <div
@@ -154,7 +142,7 @@ class Option extends PureComponent<OptionProps> {
                         <IconTick />
                     </div>
                 ) : null}
-                {isString(children) ? <div className={`${prefixCls}-text`}>{this.renderOptionContent({ children, config, inputValue, prefixCls })}</div> : children}
+                {isString(children) ? <div className={`${prefixCls}-text`}>{this.renderOptionContent({ children, inputValue, prefixCls })}</div> : children}
             </div>
         );
     }

+ 27 - 9
packages/semi-ui/highlight/_story/highlight.stories.jsx

@@ -1,8 +1,6 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-
-import { Skeleton, Avatar, Button, ButtonGroup, Spin, Highlight } from '../../index';
-
+import { Highlight } from '../../index';
 
 const searchWords = ['do', 'dollar'];
 const sourceString = 'aaa do dollar aaa';
@@ -11,7 +9,7 @@ export default {
   title: 'Highlight'
 }
 
-export const HighlightTag = () => (
+export const HighlightBase = () => (
   <h2>
     <Highlight
         component='span'
@@ -21,9 +19,6 @@ export const HighlightTag = () => (
   </h2>
 );
 
-HighlightTag.story = {
-  name: 'different tag',
-};
 
 export const HighlightStyle = () => (
   <h2>
@@ -36,6 +31,29 @@ export const HighlightStyle = () => (
   </h2>
 );
 
-HighlightStyle.story = {
-  name: 'custom style',
+export const HighlightClassName = () => (
+  <h2>
+    <Highlight
+        sourceString='semi design connect designOps & devOps'
+        searchWords={['semi', 'design']}
+        highlightClassName='test'
+        // highlightStyle={{ backgroundColor: 'var(--semi-color-warning)', borderRadius: 4 }}
+    />
+  </h2>
+);
+
+export const MutilpleSearchWords = () => {
+  return (
+      <Highlight
+          component='span'
+          sourceString='semi design connect designOps & devOps'
+          searchWords={[
+            { text: 'semi', style: { backgroundColor: 'rgba(var(--semi-teal-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword1' },
+            { text: 'connect', style: { backgroundColor: 'var(--semi-color-primary)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword2' },
+            { text: 'devOps', style: { backgroundColor: 'rgba(var(--semi-violet-5), 1)', color: 'rgba(var(--semi-white), 1)', padding: 4 }, className: 'keyword3' },
+          ]}
+          highlightStyle={{ borderRadius: 4 }}
+      />
+  )
 };
+

+ 49 - 3
packages/semi-ui/highlight/index.tsx

@@ -2,14 +2,32 @@ import React, { PureComponent } from 'react';
 import cls from 'classnames';
 import PropTypes, { string } from 'prop-types';
 import { cssClasses } from '@douyinfe/semi-foundation/highlight/constants';
-import { getHighLightTextHTML } from '../_utils/index';
+import HighlightFoundation from '@douyinfe/semi-foundation/highlight/foundation';
+import type { SearchWords, Chunk } from '@douyinfe/semi-foundation/highlight/foundation';
+
 import '@douyinfe/semi-foundation/highlight/highlight.scss';
 
+interface GetHighLightTextHTMLProps {
+    sourceString?: string;
+    searchWords?: SearchWords;
+    option: HighLightTextHTMLOption
+}
+
+interface HighLightTextHTMLOption {
+    highlightTag?: string;
+    highlightClassName?: string;
+    highlightStyle?: React.CSSProperties;
+    caseSensitive: boolean;
+    autoEscape: boolean
+}
+
+interface HighLightTextHTMLChunk extends Chunk { }
+
 export interface HighlightProps {
     autoEscape?: boolean;
     caseSensitive?: boolean;
     sourceString?: string;
-    searchWords?: Array<string>;
+    searchWords?: SearchWords;
     highlightStyle?: React.CSSProperties;
     highlightClassName?: string;
     component?: string
@@ -38,6 +56,34 @@ class Highlight extends PureComponent<HighlightProps> {
         sourceString: '',
     };
 
+    getHighLightTextHTML = ({
+        sourceString = '',
+        searchWords = [],
+        option = { autoEscape: true, caseSensitive: false }
+    }: GetHighLightTextHTMLProps) => {
+        const chunks: HighLightTextHTMLChunk[] = new HighlightFoundation().findAll({ sourceString, searchWords, ...option });
+        const markEle = option.highlightTag || 'mark';
+        const highlightClassName = option.highlightClassName || '';
+        const highlightStyle = option.highlightStyle || {};
+        return chunks.map((chunk: HighLightTextHTMLChunk, index: number) => {
+            const { end, start, highlight, style, className } = chunk;
+            const text = sourceString.substr(start, end - start);
+            if (highlight) {
+                return React.createElement(
+                    markEle,
+                    {
+                        style: { ...highlightStyle, ...style },
+                        className: `${highlightClassName} ${className || ''}`.trim(),
+                        key: text + index
+                    },
+                    text
+                );
+            } else {
+                return text;
+            }
+        });
+    };
+
     render() {
         const {
             searchWords,
@@ -62,7 +108,7 @@ class Highlight extends PureComponent<HighlightProps> {
         };
 
         return (
-            getHighLightTextHTML({ sourceString, searchWords, option })
+            this.getHighLightTextHTML({ sourceString, searchWords, option })
         );
     }
 }

+ 12 - 16
packages/semi-ui/select/option.tsx

@@ -5,7 +5,7 @@ import { isString } from 'lodash';
 import { cssClasses } from '@douyinfe/semi-foundation/select/constants';
 import LocaleConsumer from '../locale/localeConsumer';
 import { IconTick } from '@douyinfe/semi-icons';
-import { getHighLightTextHTML } from '../_utils/index';
+import Highlight, { HighlightProps } from '../highlight';
 import { Locale } from '../locale/interface';
 import getDataAttr from '@douyinfe/semi-foundation/utils/getDataAttr';
 import type { BasicOptionProps } from '@douyinfe/semi-foundation/select/optionFoundation';
@@ -20,15 +20,6 @@ export interface OptionProps extends BasicOptionProps {
     className?: string;
     style?: React.CSSProperties
 }
-interface renderOptionContentArgument {
-    config: {
-        searchWords: any;
-        sourceString: React.ReactNode
-    };
-    children: React.ReactNode;
-    inputValue: string;
-    prefixCls: string
-}
 class Option extends PureComponent<OptionProps> {
     static isSelectOption = true;
 
@@ -63,9 +54,15 @@ class Option extends PureComponent<OptionProps> {
         }
     }
 
-    renderOptionContent({ config, children, inputValue, prefixCls }: renderOptionContentArgument) {
+    renderOptionContent({ config, children, inputValue, prefixCls }) {
         if (isString(children) && inputValue) {
-            return getHighLightTextHTML(config as any);
+            return (
+                <Highlight
+                    searchWords={config.searchWords as HighlightProps['searchWords']}
+                    sourceString={config.sourceString as string}
+                    highlightClassName={config.highlightClassName as string}
+                />
+            );
         }
         return children;
     }
@@ -139,12 +136,11 @@ class Option extends PureComponent<OptionProps> {
         }
 
         const config = {
-            searchWords: inputValue,
+            searchWords: [inputValue],
             sourceString: children,
-            option: {
-                highlightClassName: `${prefixCls}-keyword`
-            }
+            highlightClassName: `${prefixCls}-keyword`
         };
+
         return (
             // eslint-disable-next-line jsx-a11y/interactive-supports-focus,jsx-a11y/click-events-have-key-events
             <div

+ 9 - 9
packages/semi-ui/tree/treeNode.tsx

@@ -9,7 +9,7 @@ import { Checkbox } from '../checkbox';
 import TreeContext, { TreeContextValue } from './treeContext';
 import Spin from '../spin';
 import { TreeNodeProps, TreeNodeState } from './interface';
-import { getHighLightTextHTML } from '../_utils/index';
+import Highlight from '../highlight';
 import Indent from './indent';
 
 const prefixcls = cssClasses.PREFIX_OPTION;
@@ -302,14 +302,14 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
         if (isFunction(renderLabel)) {
             return renderLabel(label, data, keyword);
         } else if (isString(label) && filtered && keyword) {
-            return getHighLightTextHTML({
-                sourceString: label,
-                searchWords: [keyword],
-                option: {
-                    highlightTag: 'span',
-                    highlightClassName: `${prefixcls}-highlight`,
-                },
-            } as any);
+            return (
+                <Highlight
+                    highlightClassName={`${prefixcls}-highlight`}
+                    component='span'
+                    sourceString={label}
+                    searchWords={[keyword]}
+                />
+            );
         } else {
             return label;
         }