text-highlight-js 1.0.0
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.
- package/dist/cjs/index.js +368 -0
- package/dist/esm/index.d.ts +108 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +364 -0
- package/package.json +49 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Enterprise-grade text highlighting utility
|
|
4
|
+
* Provides safe, performant text highlighting with HTML preservation
|
|
5
|
+
* @module TextHighlightHelper
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.hasMatch = exports.countMatches = exports.stripHtmlTags = exports.highlightHtmlContent = exports.highlightText = void 0;
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// const DEFAULT_HIGHLIGHT_CLASS = 'text-highlight';
|
|
14
|
+
const MAX_SEARCH_TERMS = 50; // Prevent performance issues with too many terms
|
|
15
|
+
const MAX_TEXT_LENGTH = 1000000; // 1MB text limit
|
|
16
|
+
const MAX_SEARCH_TERM_LENGTH = 1000;
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Private Utilities
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Validates and sanitizes input text
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
const validateText = (text) => {
|
|
25
|
+
if (text === null || text === undefined) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
if (typeof text !== "string") {
|
|
29
|
+
console.warn("[TextHighlight] Invalid text type:", typeof text);
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
33
|
+
console.warn(`[TextHighlight] Text exceeds maximum length (${MAX_TEXT_LENGTH}). Truncating.`);
|
|
34
|
+
return text.substring(0, MAX_TEXT_LENGTH);
|
|
35
|
+
}
|
|
36
|
+
return text;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Normalizes search terms to array and validates
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
const normalizeSearchTerms = (searchTerm) => {
|
|
43
|
+
if (!searchTerm) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const terms = Array.isArray(searchTerm) ? searchTerm : [searchTerm];
|
|
47
|
+
const validTerms = terms
|
|
48
|
+
.filter((term) => {
|
|
49
|
+
if (typeof term !== "string") {
|
|
50
|
+
console.warn("[TextHighlight] Invalid search term type:", typeof term);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (term.length > MAX_SEARCH_TERM_LENGTH) {
|
|
54
|
+
console.warn(`[TextHighlight] Search term exceeds maximum length (${MAX_SEARCH_TERM_LENGTH})`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
})
|
|
59
|
+
.map((term) => term.trim())
|
|
60
|
+
.filter((term) => term.length > 0);
|
|
61
|
+
if (validTerms.length > MAX_SEARCH_TERMS) {
|
|
62
|
+
console.warn(`[TextHighlight] Too many search terms (${validTerms.length}). Using first ${MAX_SEARCH_TERMS}.`);
|
|
63
|
+
return validTerms.slice(0, MAX_SEARCH_TERMS);
|
|
64
|
+
}
|
|
65
|
+
return validTerms;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Escapes special regex characters in the search string
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
const escapeRegExp = (text) => {
|
|
72
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Creates a regex pattern from search terms
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
const createSearchPattern = (terms, options = {}) => {
|
|
79
|
+
if (terms.length === 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const escapedTerms = [...terms]
|
|
84
|
+
.sort((a, b) => b.length - a.length)
|
|
85
|
+
.map(escapeRegExp);
|
|
86
|
+
const pattern = options.wholeWord
|
|
87
|
+
? `\\b(${escapedTerms.join("|")})\\b`
|
|
88
|
+
: `(${escapedTerms.join("|")})`;
|
|
89
|
+
const flags = options.caseSensitive ? "g" : "gi";
|
|
90
|
+
return new RegExp(pattern, flags);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error("[TextHighlight] Failed to create search pattern:", error);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Checks if a string matches any search term
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
const matchesSearchTerm = (text, normalizedTermSet, caseSensitive = false) => {
|
|
102
|
+
const compareText = caseSensitive ? text : text.toLowerCase();
|
|
103
|
+
return normalizedTermSet.has(compareText);
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Creates a highlight mark element (styled via CSS class)
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
const createHighlightMark = (content, options = {}) => {
|
|
110
|
+
const mark = document.createElement("mark");
|
|
111
|
+
if (options.className) {
|
|
112
|
+
mark.className = options.className;
|
|
113
|
+
}
|
|
114
|
+
mark.textContent = content;
|
|
115
|
+
return mark;
|
|
116
|
+
};
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Public API
|
|
119
|
+
// ============================================================================
|
|
120
|
+
/**
|
|
121
|
+
* Highlights search terms in plain text content
|
|
122
|
+
* Returns structured segments indicating which parts should be highlighted.
|
|
123
|
+
* This function is framework-agnostic and does not return JSX.
|
|
124
|
+
*
|
|
125
|
+
* @param text - The text to highlight
|
|
126
|
+
* @param searchTerm - Single search term or array of terms
|
|
127
|
+
* @param options - Highlighting configuration options
|
|
128
|
+
* @returns Array of segments with highlight metadata
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* highlightText('Hello World', 'world')
|
|
132
|
+
* // → [{ text: 'Hello ', highlighted: false }, { text: 'World', highlighted: true }]
|
|
133
|
+
*/
|
|
134
|
+
const highlightText = (text, searchTerm, options = {}) => {
|
|
135
|
+
const validatedText = validateText(text);
|
|
136
|
+
if (!validatedText.trim()) {
|
|
137
|
+
return [{ text: validatedText, highlighted: false }];
|
|
138
|
+
}
|
|
139
|
+
const searchTerms = normalizeSearchTerms(searchTerm);
|
|
140
|
+
if (searchTerms.length === 0) {
|
|
141
|
+
return [{ text: validatedText, highlighted: false }];
|
|
142
|
+
}
|
|
143
|
+
const normalizedTermSet = new Set(options.caseSensitive
|
|
144
|
+
? searchTerms
|
|
145
|
+
: searchTerms.map((t) => t.toLowerCase()));
|
|
146
|
+
const pattern = createSearchPattern(searchTerms, options);
|
|
147
|
+
if (!pattern) {
|
|
148
|
+
return [{ text: validatedText, highlighted: false }];
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const parts = validatedText.split(pattern);
|
|
152
|
+
let highlightCount = 0;
|
|
153
|
+
const maxHighlights = typeof options.maxHighlights === "number"
|
|
154
|
+
? options.maxHighlights
|
|
155
|
+
: Infinity;
|
|
156
|
+
const segments = [];
|
|
157
|
+
parts.forEach((part) => {
|
|
158
|
+
if (!part) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const isMatch = matchesSearchTerm(part, normalizedTermSet, options.caseSensitive);
|
|
162
|
+
if (isMatch && highlightCount < maxHighlights) {
|
|
163
|
+
highlightCount++;
|
|
164
|
+
segments.push({ text: part, highlighted: true });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
segments.push({ text: part, highlighted: false });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return segments;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error("[TextHighlight] Error highlighting text:", error);
|
|
174
|
+
return [{ text: validatedText, highlighted: false }];
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
exports.highlightText = highlightText;
|
|
178
|
+
/**
|
|
179
|
+
* Highlights search terms in HTML content while preserving HTML structure
|
|
180
|
+
* Only highlights text nodes, leaving HTML tags and attributes intact
|
|
181
|
+
*
|
|
182
|
+
* @param htmlContent - HTML string to highlight
|
|
183
|
+
* @param searchTerm - Single search term or array of terms
|
|
184
|
+
* @param options - Highlighting configuration options
|
|
185
|
+
* @returns HTML string with highlighted text
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```tsx
|
|
189
|
+
* highlightHtmlContent('<p>Hello World</p>', 'world')
|
|
190
|
+
* highlightHtmlContent('<div>React <br/> TypeScript</div>', ['react', 'script'])
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
const highlightHtmlContent = (htmlContent, searchTerm, options = {}) => {
|
|
194
|
+
const validatedHtml = validateText(htmlContent);
|
|
195
|
+
if (!validatedHtml) {
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
if (typeof document === "undefined") {
|
|
199
|
+
console.warn("[TextHighlight] DOM not available (SSR mode)");
|
|
200
|
+
return validatedHtml;
|
|
201
|
+
}
|
|
202
|
+
const searchTerms = normalizeSearchTerms(searchTerm);
|
|
203
|
+
if (searchTerms.length === 0) {
|
|
204
|
+
return validatedHtml;
|
|
205
|
+
}
|
|
206
|
+
const normalizedTermSet = new Set(options.caseSensitive
|
|
207
|
+
? searchTerms
|
|
208
|
+
: searchTerms.map((t) => t.toLowerCase()));
|
|
209
|
+
const pattern = createSearchPattern(searchTerms, options);
|
|
210
|
+
if (!pattern) {
|
|
211
|
+
return validatedHtml;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
// Create temporary DOM element to parse HTML safely
|
|
215
|
+
const tempDiv = document.createElement("div");
|
|
216
|
+
tempDiv.innerHTML = validatedHtml;
|
|
217
|
+
let highlightCount = 0;
|
|
218
|
+
const maxHighlights = typeof options.maxHighlights === "number"
|
|
219
|
+
? options.maxHighlights
|
|
220
|
+
: Infinity;
|
|
221
|
+
/**
|
|
222
|
+
* Recursively highlights text nodes in DOM tree
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
const highlightTextNodes = (node) => {
|
|
226
|
+
var _a;
|
|
227
|
+
if (highlightCount >= maxHighlights) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
231
|
+
// Text node - apply highlighting
|
|
232
|
+
const textContent = node.textContent || "";
|
|
233
|
+
// Reset regex lastIndex for accurate testing
|
|
234
|
+
pattern.lastIndex = 0;
|
|
235
|
+
if (pattern.test(textContent)) {
|
|
236
|
+
// Reset again for split operation
|
|
237
|
+
pattern.lastIndex = 0;
|
|
238
|
+
const fragment = document.createDocumentFragment();
|
|
239
|
+
const parts = textContent.split(pattern);
|
|
240
|
+
parts.forEach((part) => {
|
|
241
|
+
if (!part) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const isMatch = matchesSearchTerm(part, normalizedTermSet, options.caseSensitive);
|
|
245
|
+
if (isMatch && highlightCount < maxHighlights) {
|
|
246
|
+
highlightCount++;
|
|
247
|
+
fragment.appendChild(createHighlightMark(part, options));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
fragment.appendChild(document.createTextNode(part));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
// Replace original text node with highlighted fragment
|
|
254
|
+
if (node.parentNode) {
|
|
255
|
+
node.parentNode.replaceChild(fragment, node);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
260
|
+
// Element node - recursively process child nodes
|
|
261
|
+
// Skip script, style, and mark elements
|
|
262
|
+
const element = node;
|
|
263
|
+
const tagName = (_a = element.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
264
|
+
if (tagName === "script" || tagName === "style" || tagName === "mark") {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Convert to array to avoid live collection issues during modification
|
|
268
|
+
const childNodes = Array.from(node.childNodes);
|
|
269
|
+
childNodes.forEach(highlightTextNodes);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
highlightTextNodes(tempDiv);
|
|
273
|
+
return tempDiv.innerHTML;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.error("[TextHighlight] Error highlighting HTML content:", error);
|
|
277
|
+
return validatedHtml;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
exports.highlightHtmlContent = highlightHtmlContent;
|
|
281
|
+
/**
|
|
282
|
+
* Strips HTML tags from content to get plain text
|
|
283
|
+
* Useful for extracting searchable text from HTML
|
|
284
|
+
*
|
|
285
|
+
* @param html - HTML string to strip
|
|
286
|
+
* @returns Plain text content
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```tsx
|
|
290
|
+
* stripHtmlTags('<p>Hello <strong>World</strong></p>') // Returns: "Hello World"
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
const stripHtmlTags = (html) => {
|
|
294
|
+
const validatedHtml = validateText(html);
|
|
295
|
+
if (!validatedHtml) {
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
298
|
+
if (typeof document === "undefined") {
|
|
299
|
+
console.warn("[TextHighlight] DOM not available (SSR mode)");
|
|
300
|
+
return validatedHtml;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const tempDiv = document.createElement("div");
|
|
304
|
+
tempDiv.innerHTML = validatedHtml;
|
|
305
|
+
return tempDiv.textContent || tempDiv.innerText || "";
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.error("[TextHighlight] Error stripping HTML tags:", error);
|
|
309
|
+
return validatedHtml;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
exports.stripHtmlTags = stripHtmlTags;
|
|
313
|
+
/**
|
|
314
|
+
* Counts the number of matches for search terms in text
|
|
315
|
+
* Useful for displaying search result counts
|
|
316
|
+
*
|
|
317
|
+
* @param text - Text to search
|
|
318
|
+
* @param searchTerm - Single search term or array of terms
|
|
319
|
+
* @param options - Search configuration options
|
|
320
|
+
* @returns Number of matches found
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```tsx
|
|
324
|
+
* countMatches('Hello World Hello', 'hello') // Returns: 2
|
|
325
|
+
* countMatches('React TypeScript', ['react', 'script']) // Returns: 2
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
const countMatches = (text, searchTerm, options = {}) => {
|
|
329
|
+
const validatedText = validateText(text);
|
|
330
|
+
if (!validatedText) {
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
const searchTerms = normalizeSearchTerms(searchTerm);
|
|
334
|
+
if (searchTerms.length === 0) {
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
const pattern = createSearchPattern(searchTerms, options);
|
|
338
|
+
if (!pattern) {
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const matches = validatedText.match(pattern);
|
|
343
|
+
return matches ? matches.length : 0;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
console.error("[TextHighlight] Error counting matches:", error);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
exports.countMatches = countMatches;
|
|
351
|
+
/**
|
|
352
|
+
* Checks if text contains any of the search terms
|
|
353
|
+
*
|
|
354
|
+
* @param text - Text to search
|
|
355
|
+
* @param searchTerm - Single search term or array of terms
|
|
356
|
+
* @param options - Search configuration options
|
|
357
|
+
* @returns True if any match found
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```tsx
|
|
361
|
+
* hasMatch('Hello World', 'world') // Returns: true
|
|
362
|
+
* hasMatch('React', ['angular', 'vue']) // Returns: false
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
const hasMatch = (text, searchTerm, options = {}) => {
|
|
366
|
+
return countMatches(text, searchTerm, options) > 0;
|
|
367
|
+
};
|
|
368
|
+
exports.hasMatch = hasMatch;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enterprise-grade text highlighting utility
|
|
3
|
+
* Provides safe, performant text highlighting with HTML preservation
|
|
4
|
+
* @module TextHighlightHelper
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Configuration options for text highlighting
|
|
9
|
+
*/
|
|
10
|
+
export interface HighlightOptions {
|
|
11
|
+
/** CSS class name for highlighted elements (style via your own CSS) */
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Case-sensitive search */
|
|
14
|
+
caseSensitive?: boolean;
|
|
15
|
+
/** Match whole words only */
|
|
16
|
+
wholeWord?: boolean;
|
|
17
|
+
/** Maximum number of highlights to apply (for performance) */
|
|
18
|
+
maxHighlights?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Search term type - can be string or array of strings
|
|
22
|
+
*/
|
|
23
|
+
export type SearchTerm = string | string[];
|
|
24
|
+
/**
|
|
25
|
+
* Result segment for highlighted text.
|
|
26
|
+
* Framework-agnostic structure that callers can render in any UI library.
|
|
27
|
+
*/
|
|
28
|
+
export interface HighlightSegment {
|
|
29
|
+
text: string;
|
|
30
|
+
highlighted: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Highlights search terms in plain text content
|
|
34
|
+
* Returns structured segments indicating which parts should be highlighted.
|
|
35
|
+
* This function is framework-agnostic and does not return JSX.
|
|
36
|
+
*
|
|
37
|
+
* @param text - The text to highlight
|
|
38
|
+
* @param searchTerm - Single search term or array of terms
|
|
39
|
+
* @param options - Highlighting configuration options
|
|
40
|
+
* @returns Array of segments with highlight metadata
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* highlightText('Hello World', 'world')
|
|
44
|
+
* // → [{ text: 'Hello ', highlighted: false }, { text: 'World', highlighted: true }]
|
|
45
|
+
*/
|
|
46
|
+
declare const highlightText: (text: string, searchTerm: SearchTerm, options?: HighlightOptions) => HighlightSegment[];
|
|
47
|
+
/**
|
|
48
|
+
* Highlights search terms in HTML content while preserving HTML structure
|
|
49
|
+
* Only highlights text nodes, leaving HTML tags and attributes intact
|
|
50
|
+
*
|
|
51
|
+
* @param htmlContent - HTML string to highlight
|
|
52
|
+
* @param searchTerm - Single search term or array of terms
|
|
53
|
+
* @param options - Highlighting configuration options
|
|
54
|
+
* @returns HTML string with highlighted text
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* highlightHtmlContent('<p>Hello World</p>', 'world')
|
|
59
|
+
* highlightHtmlContent('<div>React <br/> TypeScript</div>', ['react', 'script'])
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare const highlightHtmlContent: (htmlContent: string, searchTerm: SearchTerm, options?: HighlightOptions) => string;
|
|
63
|
+
/**
|
|
64
|
+
* Strips HTML tags from content to get plain text
|
|
65
|
+
* Useful for extracting searchable text from HTML
|
|
66
|
+
*
|
|
67
|
+
* @param html - HTML string to strip
|
|
68
|
+
* @returns Plain text content
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* stripHtmlTags('<p>Hello <strong>World</strong></p>') // Returns: "Hello World"
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
declare const stripHtmlTags: (html: string) => string;
|
|
76
|
+
/**
|
|
77
|
+
* Counts the number of matches for search terms in text
|
|
78
|
+
* Useful for displaying search result counts
|
|
79
|
+
*
|
|
80
|
+
* @param text - Text to search
|
|
81
|
+
* @param searchTerm - Single search term or array of terms
|
|
82
|
+
* @param options - Search configuration options
|
|
83
|
+
* @returns Number of matches found
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* countMatches('Hello World Hello', 'hello') // Returns: 2
|
|
88
|
+
* countMatches('React TypeScript', ['react', 'script']) // Returns: 2
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare const countMatches: (text: string, searchTerm: SearchTerm, options?: HighlightOptions) => number;
|
|
92
|
+
/**
|
|
93
|
+
* Checks if text contains any of the search terms
|
|
94
|
+
*
|
|
95
|
+
* @param text - Text to search
|
|
96
|
+
* @param searchTerm - Single search term or array of terms
|
|
97
|
+
* @param options - Search configuration options
|
|
98
|
+
* @returns True if any match found
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* hasMatch('Hello World', 'world') // Returns: true
|
|
103
|
+
* hasMatch('React', ['angular', 'vue']) // Returns: false
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare const hasMatch: (text: string, searchTerm: SearchTerm, options?: HighlightOptions) => boolean;
|
|
107
|
+
export { highlightText, highlightHtmlContent, stripHtmlTags, countMatches, hasMatch, };
|
|
108
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;AAE3C;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;CACtB;AA0ID;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,aAAa,GACjB,MAAM,MAAM,EACZ,YAAY,UAAU,EACtB,UAAS,gBAAqB,KAC7B,gBAAgB,EAyDlB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,QAAA,MAAM,oBAAoB,GACxB,aAAa,MAAM,EACnB,YAAY,UAAU,EACtB,UAAS,gBAAqB,KAC7B,MA6GF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAmBrC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,QAAA,MAAM,YAAY,GAChB,MAAM,MAAM,EACZ,YAAY,UAAU,EACtB,UAAS,gBAAqB,KAC7B,MAuBF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,QAAQ,GACZ,MAAM,MAAM,EACZ,YAAY,UAAU,EACtB,UAAS,gBAAqB,KAC7B,OAEF,CAAC;AAMF,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,aAAa,EACb,YAAY,EACZ,QAAQ,GACT,CAAC"}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enterprise-grade text highlighting utility
|
|
3
|
+
* Provides safe, performant text highlighting with HTML preservation
|
|
4
|
+
* @module TextHighlightHelper
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Constants
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// const DEFAULT_HIGHLIGHT_CLASS = 'text-highlight';
|
|
11
|
+
const MAX_SEARCH_TERMS = 50; // Prevent performance issues with too many terms
|
|
12
|
+
const MAX_TEXT_LENGTH = 1000000; // 1MB text limit
|
|
13
|
+
const MAX_SEARCH_TERM_LENGTH = 1000;
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Private Utilities
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Validates and sanitizes input text
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
const validateText = (text) => {
|
|
22
|
+
if (text === null || text === undefined) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
if (typeof text !== "string") {
|
|
26
|
+
console.warn("[TextHighlight] Invalid text type:", typeof text);
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
30
|
+
console.warn(`[TextHighlight] Text exceeds maximum length (${MAX_TEXT_LENGTH}). Truncating.`);
|
|
31
|
+
return text.substring(0, MAX_TEXT_LENGTH);
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Normalizes search terms to array and validates
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
const normalizeSearchTerms = (searchTerm) => {
|
|
40
|
+
if (!searchTerm) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const terms = Array.isArray(searchTerm) ? searchTerm : [searchTerm];
|
|
44
|
+
const validTerms = terms
|
|
45
|
+
.filter((term) => {
|
|
46
|
+
if (typeof term !== "string") {
|
|
47
|
+
console.warn("[TextHighlight] Invalid search term type:", typeof term);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (term.length > MAX_SEARCH_TERM_LENGTH) {
|
|
51
|
+
console.warn(`[TextHighlight] Search term exceeds maximum length (${MAX_SEARCH_TERM_LENGTH})`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
})
|
|
56
|
+
.map((term) => term.trim())
|
|
57
|
+
.filter((term) => term.length > 0);
|
|
58
|
+
if (validTerms.length > MAX_SEARCH_TERMS) {
|
|
59
|
+
console.warn(`[TextHighlight] Too many search terms (${validTerms.length}). Using first ${MAX_SEARCH_TERMS}.`);
|
|
60
|
+
return validTerms.slice(0, MAX_SEARCH_TERMS);
|
|
61
|
+
}
|
|
62
|
+
return validTerms;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Escapes special regex characters in the search string
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
const escapeRegExp = (text) => {
|
|
69
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Creates a regex pattern from search terms
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
const createSearchPattern = (terms, options = {}) => {
|
|
76
|
+
if (terms.length === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const escapedTerms = [...terms]
|
|
81
|
+
.sort((a, b) => b.length - a.length)
|
|
82
|
+
.map(escapeRegExp);
|
|
83
|
+
const pattern = options.wholeWord
|
|
84
|
+
? `\\b(${escapedTerms.join("|")})\\b`
|
|
85
|
+
: `(${escapedTerms.join("|")})`;
|
|
86
|
+
const flags = options.caseSensitive ? "g" : "gi";
|
|
87
|
+
return new RegExp(pattern, flags);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error("[TextHighlight] Failed to create search pattern:", error);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Checks if a string matches any search term
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
const matchesSearchTerm = (text, normalizedTermSet, caseSensitive = false) => {
|
|
99
|
+
const compareText = caseSensitive ? text : text.toLowerCase();
|
|
100
|
+
return normalizedTermSet.has(compareText);
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Creates a highlight mark element (styled via CSS class)
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
const createHighlightMark = (content, options = {}) => {
|
|
107
|
+
const mark = document.createElement("mark");
|
|
108
|
+
if (options.className) {
|
|
109
|
+
mark.className = options.className;
|
|
110
|
+
}
|
|
111
|
+
mark.textContent = content;
|
|
112
|
+
return mark;
|
|
113
|
+
};
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Public API
|
|
116
|
+
// ============================================================================
|
|
117
|
+
/**
|
|
118
|
+
* Highlights search terms in plain text content
|
|
119
|
+
* Returns structured segments indicating which parts should be highlighted.
|
|
120
|
+
* This function is framework-agnostic and does not return JSX.
|
|
121
|
+
*
|
|
122
|
+
* @param text - The text to highlight
|
|
123
|
+
* @param searchTerm - Single search term or array of terms
|
|
124
|
+
* @param options - Highlighting configuration options
|
|
125
|
+
* @returns Array of segments with highlight metadata
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* highlightText('Hello World', 'world')
|
|
129
|
+
* // → [{ text: 'Hello ', highlighted: false }, { text: 'World', highlighted: true }]
|
|
130
|
+
*/
|
|
131
|
+
const highlightText = (text, searchTerm, options = {}) => {
|
|
132
|
+
const validatedText = validateText(text);
|
|
133
|
+
if (!validatedText.trim()) {
|
|
134
|
+
return [{ text: validatedText, highlighted: false }];
|
|
135
|
+
}
|
|
136
|
+
const searchTerms = normalizeSearchTerms(searchTerm);
|
|
137
|
+
if (searchTerms.length === 0) {
|
|
138
|
+
return [{ text: validatedText, highlighted: false }];
|
|
139
|
+
}
|
|
140
|
+
const normalizedTermSet = new Set(options.caseSensitive
|
|
141
|
+
? searchTerms
|
|
142
|
+
: searchTerms.map((t) => t.toLowerCase()));
|
|
143
|
+
const pattern = createSearchPattern(searchTerms, options);
|
|
144
|
+
if (!pattern) {
|
|
145
|
+
return [{ text: validatedText, highlighted: false }];
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const parts = validatedText.split(pattern);
|
|
149
|
+
let highlightCount = 0;
|
|
150
|
+
const maxHighlights = typeof options.maxHighlights === "number"
|
|
151
|
+
? options.maxHighlights
|
|
152
|
+
: Infinity;
|
|
153
|
+
const segments = [];
|
|
154
|
+
parts.forEach((part) => {
|
|
155
|
+
if (!part) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const isMatch = matchesSearchTerm(part, normalizedTermSet, options.caseSensitive);
|
|
159
|
+
if (isMatch && highlightCount < maxHighlights) {
|
|
160
|
+
highlightCount++;
|
|
161
|
+
segments.push({ text: part, highlighted: true });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
segments.push({ text: part, highlighted: false });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return segments;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error("[TextHighlight] Error highlighting text:", error);
|
|
171
|
+
return [{ text: validatedText, highlighted: false }];
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Highlights search terms in HTML content while preserving HTML structure
|
|
176
|
+
* Only highlights text nodes, leaving HTML tags and attributes intact
|
|
177
|
+
*
|
|
178
|
+
* @param htmlContent - HTML string to highlight
|
|
179
|
+
* @param searchTerm - Single search term or array of terms
|
|
180
|
+
* @param options - Highlighting configuration options
|
|
181
|
+
* @returns HTML string with highlighted text
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```tsx
|
|
185
|
+
* highlightHtmlContent('<p>Hello World</p>', 'world')
|
|
186
|
+
* highlightHtmlContent('<div>React <br/> TypeScript</div>', ['react', 'script'])
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
const highlightHtmlContent = (htmlContent, searchTerm, options = {}) => {
|
|
190
|
+
const validatedHtml = validateText(htmlContent);
|
|
191
|
+
if (!validatedHtml) {
|
|
192
|
+
return "";
|
|
193
|
+
}
|
|
194
|
+
if (typeof document === "undefined") {
|
|
195
|
+
console.warn("[TextHighlight] DOM not available (SSR mode)");
|
|
196
|
+
return validatedHtml;
|
|
197
|
+
}
|
|
198
|
+
const searchTerms = normalizeSearchTerms(searchTerm);
|
|
199
|
+
if (searchTerms.length === 0) {
|
|
200
|
+
return validatedHtml;
|
|
201
|
+
}
|
|
202
|
+
const normalizedTermSet = new Set(options.caseSensitive
|
|
203
|
+
? searchTerms
|
|
204
|
+
: searchTerms.map((t) => t.toLowerCase()));
|
|
205
|
+
const pattern = createSearchPattern(searchTerms, options);
|
|
206
|
+
if (!pattern) {
|
|
207
|
+
return validatedHtml;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
// Create temporary DOM element to parse HTML safely
|
|
211
|
+
const tempDiv = document.createElement("div");
|
|
212
|
+
tempDiv.innerHTML = validatedHtml;
|
|
213
|
+
let highlightCount = 0;
|
|
214
|
+
const maxHighlights = typeof options.maxHighlights === "number"
|
|
215
|
+
? options.maxHighlights
|
|
216
|
+
: Infinity;
|
|
217
|
+
/**
|
|
218
|
+
* Recursively highlights text nodes in DOM tree
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
const highlightTextNodes = (node) => {
|
|
222
|
+
var _a;
|
|
223
|
+
if (highlightCount >= maxHighlights) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
227
|
+
// Text node - apply highlighting
|
|
228
|
+
const textContent = node.textContent || "";
|
|
229
|
+
// Reset regex lastIndex for accurate testing
|
|
230
|
+
pattern.lastIndex = 0;
|
|
231
|
+
if (pattern.test(textContent)) {
|
|
232
|
+
// Reset again for split operation
|
|
233
|
+
pattern.lastIndex = 0;
|
|
234
|
+
const fragment = document.createDocumentFragment();
|
|
235
|
+
const parts = textContent.split(pattern);
|
|
236
|
+
parts.forEach((part) => {
|
|
237
|
+
if (!part) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const isMatch = matchesSearchTerm(part, normalizedTermSet, options.caseSensitive);
|
|
241
|
+
if (isMatch && highlightCount < maxHighlights) {
|
|
242
|
+
highlightCount++;
|
|
243
|
+
fragment.appendChild(createHighlightMark(part, options));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
fragment.appendChild(document.createTextNode(part));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// Replace original text node with highlighted fragment
|
|
250
|
+
if (node.parentNode) {
|
|
251
|
+
node.parentNode.replaceChild(fragment, node);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
256
|
+
// Element node - recursively process child nodes
|
|
257
|
+
// Skip script, style, and mark elements
|
|
258
|
+
const element = node;
|
|
259
|
+
const tagName = (_a = element.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
260
|
+
if (tagName === "script" || tagName === "style" || tagName === "mark") {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Convert to array to avoid live collection issues during modification
|
|
264
|
+
const childNodes = Array.from(node.childNodes);
|
|
265
|
+
childNodes.forEach(highlightTextNodes);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
highlightTextNodes(tempDiv);
|
|
269
|
+
return tempDiv.innerHTML;
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error("[TextHighlight] Error highlighting HTML content:", error);
|
|
273
|
+
return validatedHtml;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Strips HTML tags from content to get plain text
|
|
278
|
+
* Useful for extracting searchable text from HTML
|
|
279
|
+
*
|
|
280
|
+
* @param html - HTML string to strip
|
|
281
|
+
* @returns Plain text content
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```tsx
|
|
285
|
+
* stripHtmlTags('<p>Hello <strong>World</strong></p>') // Returns: "Hello World"
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
const stripHtmlTags = (html) => {
|
|
289
|
+
const validatedHtml = validateText(html);
|
|
290
|
+
if (!validatedHtml) {
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
if (typeof document === "undefined") {
|
|
294
|
+
console.warn("[TextHighlight] DOM not available (SSR mode)");
|
|
295
|
+
return validatedHtml;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const tempDiv = document.createElement("div");
|
|
299
|
+
tempDiv.innerHTML = validatedHtml;
|
|
300
|
+
return tempDiv.textContent || tempDiv.innerText || "";
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
console.error("[TextHighlight] Error stripping HTML tags:", error);
|
|
304
|
+
return validatedHtml;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Counts the number of matches for search terms in text
|
|
309
|
+
* Useful for displaying search result counts
|
|
310
|
+
*
|
|
311
|
+
* @param text - Text to search
|
|
312
|
+
* @param searchTerm - Single search term or array of terms
|
|
313
|
+
* @param options - Search configuration options
|
|
314
|
+
* @returns Number of matches found
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```tsx
|
|
318
|
+
* countMatches('Hello World Hello', 'hello') // Returns: 2
|
|
319
|
+
* countMatches('React TypeScript', ['react', 'script']) // Returns: 2
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
const countMatches = (text, searchTerm, options = {}) => {
|
|
323
|
+
const validatedText = validateText(text);
|
|
324
|
+
if (!validatedText) {
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
const searchTerms = normalizeSearchTerms(searchTerm);
|
|
328
|
+
if (searchTerms.length === 0) {
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
const pattern = createSearchPattern(searchTerms, options);
|
|
332
|
+
if (!pattern) {
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const matches = validatedText.match(pattern);
|
|
337
|
+
return matches ? matches.length : 0;
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
console.error("[TextHighlight] Error counting matches:", error);
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Checks if text contains any of the search terms
|
|
346
|
+
*
|
|
347
|
+
* @param text - Text to search
|
|
348
|
+
* @param searchTerm - Single search term or array of terms
|
|
349
|
+
* @param options - Search configuration options
|
|
350
|
+
* @returns True if any match found
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```tsx
|
|
354
|
+
* hasMatch('Hello World', 'world') // Returns: true
|
|
355
|
+
* hasMatch('React', ['angular', 'vue']) // Returns: false
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
const hasMatch = (text, searchTerm, options = {}) => {
|
|
359
|
+
return countMatches(text, searchTerm, options) > 0;
|
|
360
|
+
};
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// Exports
|
|
363
|
+
// ============================================================================
|
|
364
|
+
export { highlightText, highlightHtmlContent, stripHtmlTags, countMatches, hasMatch, };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "text-highlight-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight text highlighting utility with ESM and CJS support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"text",
|
|
7
|
+
"highlight",
|
|
8
|
+
"search",
|
|
9
|
+
"html",
|
|
10
|
+
"utility"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Anuj Gupta",
|
|
14
|
+
|
|
15
|
+
"main": "./dist/cjs/index.js",
|
|
16
|
+
"module": "./dist/esm/index.js",
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/esm/index.d.ts",
|
|
22
|
+
"import": "./dist/esm/index.js",
|
|
23
|
+
"require": "./dist/cjs/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
|
|
33
|
+
"scripts": {
|
|
34
|
+
"clean": "rimraf dist",
|
|
35
|
+
"build:esm": "tsc",
|
|
36
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
37
|
+
"build": "npm run clean && npm run build:esm && npm run build:cjs",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"rimraf": "^5.0.5"
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=14"
|
|
48
|
+
}
|
|
49
|
+
}
|