react-highlight-me 1.2.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ type Props = {
4
4
  words?: string[] | string | RegExp | RegExp[];
5
5
  highlightStyle?: React.CSSProperties;
6
6
  caseSensitive?: boolean;
7
- isEscapePattern?: boolean;
8
7
  isWordBoundary?: boolean;
8
+ isDebug?: boolean;
9
9
  };
10
- declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive, isEscapePattern, isWordBoundary, }: Props) => React.ReactNode;
10
+ declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive, isWordBoundary, isDebug, }: Props) => React.JSX.Element;
11
11
  export default TextHighlighter;
package/dist/cjs/index.js CHANGED
@@ -1,77 +1,173 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- var react_1 = __importDefault(require("react"));
7
- var TextHighlighter = function (_a) {
8
- var _b = _a.children, children = _b === void 0 ? '' : _b, _c = _a.words, words = _c === void 0 ? [] : _c, _d = _a.highlightStyle, highlightStyle = _d === void 0 ? { backgroundColor: 'yellow', fontWeight: 'bold' } : _d, _e = _a.caseSensitive, caseSensitive = _e === void 0 ? false : _e, _f = _a.isEscapePattern, isEscapePattern = _f === void 0 ? true : _f, _g = _a.isWordBoundary, isWordBoundary = _g === void 0 ? true : _g;
9
- // Convert words to array if it's a string
10
- var wordsArray = Array.isArray(words) ? words : [words];
11
- // If no words to highlight, return original content
12
- if (!wordsArray.length || wordsArray.every(function (word) { return !word; })) {
13
- return children;
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
7
  }
15
- // Function to escape regex special characters
16
- var escapeRegex = function (term) {
17
- if (term instanceof RegExp) {
18
- return term.source;
19
- }
20
- if (isEscapePattern) {
21
- term = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const react_1 = __importStar(require("react"));
37
+ // Alternative approach: Use a single observer that never disconnects
38
+ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' }, caseSensitive = false, isWordBoundary = true, isDebug = false, }) => {
39
+ const containerRef = (0, react_1.useRef)(null);
40
+ const observerRef = (0, react_1.useRef)(null);
41
+ const lastHighlightSignature = (0, react_1.useRef)('');
42
+ const [isInitiallyReady, setIsInitiallyReady] = (0, react_1.useState)(false);
43
+ const propsRef = (0, react_1.useRef)({ words, highlightStyle, caseSensitive, isWordBoundary });
44
+ propsRef.current = { words, highlightStyle, caseSensitive, isWordBoundary };
45
+ const getTextSignature = (0, react_1.useCallback)((element) => {
46
+ var _a;
47
+ // Create a signature of all text content to detect real changes
48
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
49
+ const textParts = [];
50
+ let node;
51
+ while ((node = walker.nextNode())) {
52
+ if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute('data-highlighter'))) {
53
+ textParts.push(node.textContent || '');
54
+ }
22
55
  }
23
- if (isWordBoundary) {
24
- term = "\\b".concat(term, "\\b");
56
+ return textParts.join('|');
57
+ }, []);
58
+ const highlightTextInElement = (0, react_1.useCallback)((element) => {
59
+ const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
60
+ const wordsArray = Array.isArray(words) ? words : [words];
61
+ isDebug && console.log('Highlighting with words:', wordsArray);
62
+ // Remove existing highlights
63
+ const existingMarks = element.querySelectorAll('mark[data-highlighter="true"]');
64
+ existingMarks.forEach(mark => {
65
+ var _a;
66
+ const textContent = mark.textContent || '';
67
+ const textNode = document.createTextNode(textContent);
68
+ (_a = mark.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(textNode, mark);
69
+ });
70
+ element.normalize(); // Ensure text nodes are merged
71
+ if (!wordsArray.length || wordsArray.every(word => !word)) {
72
+ lastHighlightSignature.current = getTextSignature(element);
73
+ return;
25
74
  }
26
- return term;
27
- };
28
- // Create a regex pattern for all words
29
- var pattern = wordsArray
30
- .filter(function (word) { return word; })
31
- .map(function (word) { return escapeRegex(word); })
32
- .join('|');
33
- if (!pattern) {
34
- return children;
35
- }
36
- var regex = new RegExp("(".concat(pattern, ")"), caseSensitive ? 'g' : 'gi');
37
- // Function to highlight text content
38
- var highlightText = function (textContent) {
39
- if (!textContent || typeof textContent !== 'string') {
40
- return textContent;
75
+ // Apply highlighting (same logic as before)
76
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
77
+ const textNodes = [];
78
+ let node;
79
+ while ((node = walker.nextNode())) {
80
+ textNodes.push(node);
41
81
  }
42
- var parts = textContent.split(regex);
43
- return parts.map(function (part, index) {
44
- var shouldHighlight = wordsArray.some(function (word) {
82
+ textNodes.forEach(textNode => {
83
+ var _a;
84
+ const text = textNode.textContent || '';
85
+ if (!text.trim())
86
+ return;
87
+ const pattern = wordsArray
88
+ .filter(word => word)
89
+ .map(word => {
45
90
  if (word instanceof RegExp) {
46
- return word.test(part);
91
+ return word.source;
47
92
  }
48
- return caseSensitive
49
- ? part === word.trim()
50
- : part.toLowerCase() === word.trim().toLowerCase();
51
- });
52
- return shouldHighlight ? (react_1.default.createElement("mark", { key: index, style: highlightStyle }, part)) : (part);
93
+ let term = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94
+ if (isWordBoundary) {
95
+ term = `\\b${term}\\b`;
96
+ }
97
+ return term;
98
+ })
99
+ .join('|');
100
+ if (!pattern)
101
+ return;
102
+ const regex = new RegExp(`(${pattern})`, caseSensitive ? 'g' : 'gi');
103
+ const parts = text.split(regex);
104
+ if (parts.length > 1) {
105
+ const fragment = document.createDocumentFragment();
106
+ parts.forEach(part => {
107
+ if (!part)
108
+ return;
109
+ const shouldHighlight = wordsArray.some(word => {
110
+ if (word instanceof RegExp) {
111
+ const testRegex = new RegExp(word.source, caseSensitive ? word.flags : word.flags + 'i');
112
+ return testRegex.test(part);
113
+ }
114
+ return caseSensitive
115
+ ? part === word.trim()
116
+ : part.toLowerCase() === word.trim().toLowerCase();
117
+ });
118
+ if (shouldHighlight) {
119
+ const mark = document.createElement('mark');
120
+ mark.setAttribute('data-highlighter', 'true');
121
+ Object.assign(mark.style, highlightStyle);
122
+ mark.textContent = part;
123
+ fragment.appendChild(mark);
124
+ }
125
+ else {
126
+ fragment.appendChild(document.createTextNode(part));
127
+ }
128
+ });
129
+ (_a = textNode.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(fragment, textNode);
130
+ }
53
131
  });
54
- };
55
- // Function to recursively process React elements
56
- var processElement = function (element) {
57
- if (typeof element === 'string') {
58
- return highlightText(element);
59
- }
60
- if (typeof element === 'number') {
61
- return element;
62
- }
63
- if (!react_1.default.isValidElement(element)) {
64
- return element;
65
- }
66
- // Clone element and process its children
67
- var processedChildren = react_1.default.Children.map(element.props.children, function (child) {
68
- if (typeof child === 'string') {
69
- return highlightText(child);
132
+ // Update signature after highlighting
133
+ lastHighlightSignature.current = getTextSignature(element);
134
+ }, [getTextSignature]);
135
+ (0, react_1.useLayoutEffect)(() => {
136
+ if (!containerRef.current)
137
+ return;
138
+ isDebug && console.log('Setting up persistent MutationObserver');
139
+ highlightTextInElement(containerRef.current);
140
+ setIsInitiallyReady(true);
141
+ observerRef.current = new MutationObserver((mutations) => {
142
+ if (!containerRef.current)
143
+ return;
144
+ // Check if the text signature has actually changed
145
+ const currentSignature = getTextSignature(containerRef.current);
146
+ if (currentSignature !== lastHighlightSignature.current) {
147
+ isDebug && console.log('Text signature changed, re-highlighting');
148
+ isDebug && console.log('Old:', lastHighlightSignature.current);
149
+ isDebug && console.log('New:', currentSignature);
150
+ highlightTextInElement(containerRef.current);
151
+ }
152
+ else {
153
+ isDebug && console.log('Text signature unchanged, ignoring mutation');
70
154
  }
71
- return processElement(child);
72
155
  });
73
- return react_1.default.cloneElement(element, {}, processedChildren);
74
- };
75
- return processElement(children);
156
+ observerRef.current.observe(containerRef.current, {
157
+ childList: true,
158
+ subtree: true,
159
+ characterData: true
160
+ });
161
+ return () => {
162
+ if (observerRef.current) {
163
+ observerRef.current.disconnect();
164
+ }
165
+ };
166
+ }, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
167
+ return (react_1.default.createElement("div", { ref: containerRef, style: {
168
+ visibility: isInitiallyReady ? 'visible' : 'hidden',
169
+ minHeight: isInitiallyReady ? 'auto' : '1em'
170
+ } }, children));
76
171
  };
77
172
  exports.default = TextHighlighter;
173
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
@@ -4,8 +4,8 @@ type Props = {
4
4
  words?: string[] | string | RegExp | RegExp[];
5
5
  highlightStyle?: React.CSSProperties;
6
6
  caseSensitive?: boolean;
7
- isEscapePattern?: boolean;
8
7
  isWordBoundary?: boolean;
8
+ isDebug?: boolean;
9
9
  };
10
- declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive, isEscapePattern, isWordBoundary, }: Props) => React.ReactNode;
10
+ declare const TextHighlighter: ({ children, words, highlightStyle, caseSensitive, isWordBoundary, isDebug, }: Props) => React.JSX.Element;
11
11
  export default TextHighlighter;
package/dist/esm/index.js CHANGED
@@ -1,72 +1,138 @@
1
- import React from 'react';
2
- var TextHighlighter = function (_a) {
3
- var _b = _a.children, children = _b === void 0 ? '' : _b, _c = _a.words, words = _c === void 0 ? [] : _c, _d = _a.highlightStyle, highlightStyle = _d === void 0 ? { backgroundColor: 'yellow', fontWeight: 'bold' } : _d, _e = _a.caseSensitive, caseSensitive = _e === void 0 ? false : _e, _f = _a.isEscapePattern, isEscapePattern = _f === void 0 ? true : _f, _g = _a.isWordBoundary, isWordBoundary = _g === void 0 ? true : _g;
4
- // Convert words to array if it's a string
5
- var wordsArray = Array.isArray(words) ? words : [words];
6
- // If no words to highlight, return original content
7
- if (!wordsArray.length || wordsArray.every(function (word) { return !word; })) {
8
- return children;
9
- }
10
- // Function to escape regex special characters
11
- var escapeRegex = function (term) {
12
- if (term instanceof RegExp) {
13
- return term.source;
14
- }
15
- if (isEscapePattern) {
16
- term = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1
+ import React, { useLayoutEffect, useRef, useCallback, useState } from 'react';
2
+ // Alternative approach: Use a single observer that never disconnects
3
+ const TextHighlighter = ({ children, words = [], highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' }, caseSensitive = false, isWordBoundary = true, isDebug = false, }) => {
4
+ const containerRef = useRef(null);
5
+ const observerRef = useRef(null);
6
+ const lastHighlightSignature = useRef('');
7
+ const [isInitiallyReady, setIsInitiallyReady] = useState(false);
8
+ const propsRef = useRef({ words, highlightStyle, caseSensitive, isWordBoundary });
9
+ propsRef.current = { words, highlightStyle, caseSensitive, isWordBoundary };
10
+ const getTextSignature = useCallback((element) => {
11
+ var _a;
12
+ // Create a signature of all text content to detect real changes
13
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
14
+ const textParts = [];
15
+ let node;
16
+ while ((node = walker.nextNode())) {
17
+ if (!((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.hasAttribute('data-highlighter'))) {
18
+ textParts.push(node.textContent || '');
19
+ }
17
20
  }
18
- if (isWordBoundary) {
19
- term = "\\b".concat(term, "\\b");
21
+ return textParts.join('|');
22
+ }, []);
23
+ const highlightTextInElement = useCallback((element) => {
24
+ const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
25
+ const wordsArray = Array.isArray(words) ? words : [words];
26
+ isDebug && console.log('Highlighting with words:', wordsArray);
27
+ // Remove existing highlights
28
+ const existingMarks = element.querySelectorAll('mark[data-highlighter="true"]');
29
+ existingMarks.forEach(mark => {
30
+ var _a;
31
+ const textContent = mark.textContent || '';
32
+ const textNode = document.createTextNode(textContent);
33
+ (_a = mark.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(textNode, mark);
34
+ });
35
+ element.normalize(); // Ensure text nodes are merged
36
+ if (!wordsArray.length || wordsArray.every(word => !word)) {
37
+ lastHighlightSignature.current = getTextSignature(element);
38
+ return;
20
39
  }
21
- return term;
22
- };
23
- // Create a regex pattern for all words
24
- var pattern = wordsArray
25
- .filter(function (word) { return word; })
26
- .map(function (word) { return escapeRegex(word); })
27
- .join('|');
28
- if (!pattern) {
29
- return children;
30
- }
31
- var regex = new RegExp("(".concat(pattern, ")"), caseSensitive ? 'g' : 'gi');
32
- // Function to highlight text content
33
- var highlightText = function (textContent) {
34
- if (!textContent || typeof textContent !== 'string') {
35
- return textContent;
40
+ // Apply highlighting (same logic as before)
41
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
42
+ const textNodes = [];
43
+ let node;
44
+ while ((node = walker.nextNode())) {
45
+ textNodes.push(node);
36
46
  }
37
- var parts = textContent.split(regex);
38
- return parts.map(function (part, index) {
39
- var shouldHighlight = wordsArray.some(function (word) {
47
+ textNodes.forEach(textNode => {
48
+ var _a;
49
+ const text = textNode.textContent || '';
50
+ if (!text.trim())
51
+ return;
52
+ const pattern = wordsArray
53
+ .filter(word => word)
54
+ .map(word => {
40
55
  if (word instanceof RegExp) {
41
- return word.test(part);
56
+ return word.source;
42
57
  }
43
- return caseSensitive
44
- ? part === word.trim()
45
- : part.toLowerCase() === word.trim().toLowerCase();
46
- });
47
- return shouldHighlight ? (React.createElement("mark", { key: index, style: highlightStyle }, part)) : (part);
58
+ let term = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ if (isWordBoundary) {
60
+ term = `\\b${term}\\b`;
61
+ }
62
+ return term;
63
+ })
64
+ .join('|');
65
+ if (!pattern)
66
+ return;
67
+ const regex = new RegExp(`(${pattern})`, caseSensitive ? 'g' : 'gi');
68
+ const parts = text.split(regex);
69
+ if (parts.length > 1) {
70
+ const fragment = document.createDocumentFragment();
71
+ parts.forEach(part => {
72
+ if (!part)
73
+ return;
74
+ const shouldHighlight = wordsArray.some(word => {
75
+ if (word instanceof RegExp) {
76
+ const testRegex = new RegExp(word.source, caseSensitive ? word.flags : word.flags + 'i');
77
+ return testRegex.test(part);
78
+ }
79
+ return caseSensitive
80
+ ? part === word.trim()
81
+ : part.toLowerCase() === word.trim().toLowerCase();
82
+ });
83
+ if (shouldHighlight) {
84
+ const mark = document.createElement('mark');
85
+ mark.setAttribute('data-highlighter', 'true');
86
+ Object.assign(mark.style, highlightStyle);
87
+ mark.textContent = part;
88
+ fragment.appendChild(mark);
89
+ }
90
+ else {
91
+ fragment.appendChild(document.createTextNode(part));
92
+ }
93
+ });
94
+ (_a = textNode.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(fragment, textNode);
95
+ }
48
96
  });
49
- };
50
- // Function to recursively process React elements
51
- var processElement = function (element) {
52
- if (typeof element === 'string') {
53
- return highlightText(element);
54
- }
55
- if (typeof element === 'number') {
56
- return element;
57
- }
58
- if (!React.isValidElement(element)) {
59
- return element;
60
- }
61
- // Clone element and process its children
62
- var processedChildren = React.Children.map(element.props.children, function (child) {
63
- if (typeof child === 'string') {
64
- return highlightText(child);
97
+ // Update signature after highlighting
98
+ lastHighlightSignature.current = getTextSignature(element);
99
+ }, [getTextSignature]);
100
+ useLayoutEffect(() => {
101
+ if (!containerRef.current)
102
+ return;
103
+ isDebug && console.log('Setting up persistent MutationObserver');
104
+ highlightTextInElement(containerRef.current);
105
+ setIsInitiallyReady(true);
106
+ observerRef.current = new MutationObserver((mutations) => {
107
+ if (!containerRef.current)
108
+ return;
109
+ // Check if the text signature has actually changed
110
+ const currentSignature = getTextSignature(containerRef.current);
111
+ if (currentSignature !== lastHighlightSignature.current) {
112
+ isDebug && console.log('Text signature changed, re-highlighting');
113
+ isDebug && console.log('Old:', lastHighlightSignature.current);
114
+ isDebug && console.log('New:', currentSignature);
115
+ highlightTextInElement(containerRef.current);
116
+ }
117
+ else {
118
+ isDebug && console.log('Text signature unchanged, ignoring mutation');
65
119
  }
66
- return processElement(child);
67
120
  });
68
- return React.cloneElement(element, {}, processedChildren);
69
- };
70
- return processElement(children);
121
+ observerRef.current.observe(containerRef.current, {
122
+ childList: true,
123
+ subtree: true,
124
+ characterData: true
125
+ });
126
+ return () => {
127
+ if (observerRef.current) {
128
+ observerRef.current.disconnect();
129
+ }
130
+ };
131
+ }, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
132
+ return (React.createElement("div", { ref: containerRef, style: {
133
+ visibility: isInitiallyReady ? 'visible' : 'hidden',
134
+ minHeight: isInitiallyReady ? 'auto' : '1em'
135
+ } }, children));
71
136
  };
72
137
  export default TextHighlighter;
138
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,32 +1,44 @@
1
1
  {
2
2
  "name": "react-highlight-me",
3
- "version": "1.2.1",
3
+ "version": "2.0.1",
4
4
  "description": "Highlight words in React components or text",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./dist/esm/index.js",
10
- "require": "./dist/cjs/index.js"
11
- }
9
+ "import": {
10
+ "development": "./src/index.tsx",
11
+ "types": "./dist/esm/index.d.ts",
12
+ "default": "./dist/esm/index.js"
13
+ },
14
+ "require": {
15
+ "development": "./src/index.tsx",
16
+ "types": "./dist/cjs/index.d.ts",
17
+ "default": "./dist/cjs/index.js"
18
+ }
19
+ },
20
+ "./src/*": "./src/*"
12
21
  },
13
22
  "files": [
14
- "dist"
23
+ "dist",
24
+ "src"
15
25
  ],
16
- "scripts": {
17
- "test": "echo \"Error: no test specified\" && exit 1",
18
- "build": "npm run build:cjs && npm run build:esm",
19
- "build:cjs": "tsc -p tsconfig.cjs.json",
20
- "build:esm": "tsc -p tsconfig.esm.json",
21
- "clean": "rm -rf dist",
22
- "prepublishOnly": "npm run build"
23
- },
24
26
  "keywords": [],
25
27
  "author": "Andrey Hohutkin <Andrey.Hohutkin@gmail.com",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/AHgPuK/react-highlight-me"
31
+ },
26
32
  "license": "ISC",
27
- "packageManager": "pnpm@10.9.0",
28
33
  "devDependencies": {
29
34
  "@types/react": "^19.1.8",
30
35
  "typescript": "^5.3.3"
36
+ },
37
+ "scripts": {
38
+ "test": "echo \"Error: no test specified\" && exit 1",
39
+ "build": "npm run build:cjs && npm run build:esm",
40
+ "build:cjs": "tsc -p tsconfig.cjs.json",
41
+ "build:esm": "tsc -p tsconfig.esm.json",
42
+ "clean": "rm -rf dist"
31
43
  }
32
- }
44
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,190 @@
1
+ import React, { useLayoutEffect, useRef, useCallback, useState } from 'react';
2
+
3
+ type Props = {
4
+ children?: React.ReactNode;
5
+ words?: string[] | string | RegExp | RegExp[];
6
+ highlightStyle?: React.CSSProperties;
7
+ caseSensitive?: boolean;
8
+ isWordBoundary?: boolean;
9
+ isDebug?: boolean;
10
+ }
11
+ // Alternative approach: Use a single observer that never disconnects
12
+ const TextHighlighter = ({
13
+ children,
14
+ words = [],
15
+ highlightStyle = { backgroundColor: 'yellow', fontWeight: 'bold' },
16
+ caseSensitive = false,
17
+ isWordBoundary = true,
18
+ isDebug = false,
19
+ }: Props) => {
20
+ const containerRef = useRef<HTMLDivElement>(null);
21
+ const observerRef = useRef<MutationObserver | null>(null);
22
+ const lastHighlightSignature = useRef<string>('');
23
+ const [isInitiallyReady, setIsInitiallyReady] = useState(false);
24
+
25
+ const propsRef = useRef({ words, highlightStyle, caseSensitive, isWordBoundary });
26
+ propsRef.current = { words, highlightStyle, caseSensitive, isWordBoundary };
27
+
28
+ const getTextSignature = useCallback((element: HTMLElement): string => {
29
+ // Create a signature of all text content to detect real changes
30
+ const walker = document.createTreeWalker(
31
+ element,
32
+ NodeFilter.SHOW_TEXT,
33
+ null
34
+ );
35
+
36
+ const textParts: string[] = [];
37
+ let node;
38
+ while ((node = walker.nextNode())) {
39
+ if (!(node as Text).parentElement?.hasAttribute('data-highlighter')) {
40
+ textParts.push((node as Text).textContent || '');
41
+ }
42
+ }
43
+
44
+ return textParts.join('|');
45
+ }, []);
46
+
47
+ const highlightTextInElement = useCallback((element: HTMLElement) => {
48
+ const { words, highlightStyle, caseSensitive, isWordBoundary } = propsRef.current;
49
+ const wordsArray = Array.isArray(words) ? words : [words];
50
+
51
+ isDebug && console.log('Highlighting with words:', wordsArray);
52
+
53
+ // Remove existing highlights
54
+ const existingMarks = element.querySelectorAll('mark[data-highlighter="true"]');
55
+ existingMarks.forEach(mark => {
56
+ const textContent = mark.textContent || '';
57
+ const textNode = document.createTextNode(textContent);
58
+ mark.parentNode?.replaceChild(textNode, mark);
59
+ });
60
+
61
+ element.normalize(); // Ensure text nodes are merged
62
+
63
+ if (!wordsArray.length || wordsArray.every(word => !word)) {
64
+ lastHighlightSignature.current = getTextSignature(element);
65
+ return;
66
+ }
67
+
68
+ // Apply highlighting (same logic as before)
69
+ const walker = document.createTreeWalker(
70
+ element,
71
+ NodeFilter.SHOW_TEXT,
72
+ null
73
+ );
74
+
75
+ const textNodes: Text[] = [];
76
+ let node;
77
+ while ((node = walker.nextNode())) {
78
+ textNodes.push(node as Text);
79
+ }
80
+
81
+ textNodes.forEach(textNode => {
82
+ const text = textNode.textContent || '';
83
+ if (!text.trim()) return;
84
+
85
+ const pattern = wordsArray
86
+ .filter(word => word)
87
+ .map(word => {
88
+ if (word instanceof RegExp) {
89
+ return word.source;
90
+ }
91
+ let term = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ if (isWordBoundary) {
93
+ term = `\\b${term}\\b`;
94
+ }
95
+ return term;
96
+ })
97
+ .join('|');
98
+
99
+ if (!pattern) return;
100
+
101
+ const regex = new RegExp(`(${pattern})`, caseSensitive ? 'g' : 'gi');
102
+ const parts = text.split(regex);
103
+
104
+ if (parts.length > 1) {
105
+ const fragment = document.createDocumentFragment();
106
+
107
+ parts.forEach(part => {
108
+ if (!part) return;
109
+
110
+ const shouldHighlight = wordsArray.some(word => {
111
+ if (word instanceof RegExp) {
112
+ const testRegex = new RegExp(word.source, caseSensitive ? word.flags : word.flags + 'i');
113
+ return testRegex.test(part);
114
+ }
115
+ return caseSensitive
116
+ ? part === word.trim()
117
+ : part.toLowerCase() === word.trim().toLowerCase();
118
+ });
119
+
120
+ if (shouldHighlight) {
121
+ const mark = document.createElement('mark');
122
+ mark.setAttribute('data-highlighter', 'true');
123
+ Object.assign(mark.style, highlightStyle);
124
+ mark.textContent = part;
125
+ fragment.appendChild(mark);
126
+ } else {
127
+ fragment.appendChild(document.createTextNode(part));
128
+ }
129
+ });
130
+
131
+ textNode.parentNode?.replaceChild(fragment, textNode);
132
+ }
133
+ });
134
+
135
+ // Update signature after highlighting
136
+ lastHighlightSignature.current = getTextSignature(element);
137
+ }, [getTextSignature]);
138
+
139
+ useLayoutEffect(() => {
140
+ if (!containerRef.current) return;
141
+
142
+ isDebug && console.log('Setting up persistent MutationObserver');
143
+
144
+ highlightTextInElement(containerRef.current);
145
+ setIsInitiallyReady(true);
146
+
147
+ observerRef.current = new MutationObserver((mutations) => {
148
+ if (!containerRef.current) return;
149
+
150
+ // Check if the text signature has actually changed
151
+ const currentSignature = getTextSignature(containerRef.current);
152
+
153
+ if (currentSignature !== lastHighlightSignature.current) {
154
+ isDebug && console.log('Text signature changed, re-highlighting');
155
+ isDebug && console.log('Old:', lastHighlightSignature.current);
156
+ isDebug && console.log('New:', currentSignature);
157
+
158
+ highlightTextInElement(containerRef.current);
159
+ } else {
160
+ isDebug && console.log('Text signature unchanged, ignoring mutation');
161
+ }
162
+ });
163
+
164
+ observerRef.current.observe(containerRef.current, {
165
+ childList: true,
166
+ subtree: true,
167
+ characterData: true
168
+ });
169
+
170
+ return () => {
171
+ if (observerRef.current) {
172
+ observerRef.current.disconnect();
173
+ }
174
+ };
175
+ }, [words, highlightStyle, caseSensitive, isWordBoundary, highlightTextInElement, getTextSignature]);
176
+
177
+ return (
178
+ <div
179
+ ref={containerRef}
180
+ style={{
181
+ visibility: isInitiallyReady ? 'visible' : 'hidden',
182
+ minHeight: isInitiallyReady ? 'auto' : '1em'
183
+ }}
184
+ >
185
+ {children}
186
+ </div>
187
+ );
188
+ };
189
+
190
+ export default TextHighlighter;