diff --git a/package.json b/package.json
index 743fd58..afb366e 100644
--- a/package.json
+++ b/package.json
@@ -13,19 +13,13 @@
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"browser": "./dist/index.global.js"
- },
- "./react": {
- "require": "./dist/react/index.cjs",
- "import": "./dist/react/index.js",
- "types": "./dist/react/index.d.ts"
}
},
"scripts": {
"test": "bun test",
"lint": "ts-standard --fix ./src/**/*.ts",
- "build": "npm run build:lib && npm run build:react",
- "build:lib": "tsup --config tsup.lib.js",
- "build:react": "tsup --config tsup.react.js"
+ "build": "npm run build:lib",
+ "build:lib": "tsup --config tsup.lib.js"
},
"keywords": [
"full-text search",
diff --git a/src/index.test.ts b/src/index.test.ts
index 7a72256..6afcf53 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -1,7 +1,7 @@
import { describe, it, beforeEach, afterEach } from 'bun:test'
import assert from 'node:assert'
import sinon from 'sinon'
-import { highlight } from './index.js'
+import { Highlight } from './index.js'
describe('default configuration', () => {
it('should correctly highlight a text', () => {
@@ -13,8 +13,10 @@ describe('default configuration', () => {
const searchTerm2 = 'yesterday I was in trouble'
const expectedResult2 = 'Yesterday all my troubles seemed so far away, now it looks as though they\'re here to stay oh, I believe in yesterday'
- assert.strictEqual(highlight(text1, searchTerm1).toString(), expectedResult1)
- assert.strictEqual(highlight(text2, searchTerm2).toString(), expectedResult2)
+ const highlighter = new Highlight()
+
+ assert.strictEqual(highlighter.highlight(text1, searchTerm1).HTML, expectedResult1)
+ assert.strictEqual(highlighter.highlight(text2, searchTerm2).HTML, expectedResult2)
})
it('should return the correct positions', () => {
@@ -22,7 +24,9 @@ describe('default configuration', () => {
const searchTerm = 'fox'
const expectedPositions = [{ start: 16, end: 18 }]
- assert.deepStrictEqual(highlight(text, searchTerm).positions, expectedPositions)
+ const highlighter = new Highlight()
+
+ assert.deepStrictEqual(highlighter.highlight(text, searchTerm).positions, expectedPositions)
})
it('should return multiple positions', () => {
@@ -30,7 +34,9 @@ describe('default configuration', () => {
const searchTerm = 'the'
const expectedPositions = [{ start: 0, end: 2 }, { start: 31, end: 33 }]
- assert.deepStrictEqual(highlight(text, searchTerm).positions, expectedPositions)
+ const highlighter = new Highlight()
+
+ assert.deepStrictEqual(highlighter.highlight(text, searchTerm).positions, expectedPositions)
})
})
@@ -44,8 +50,10 @@ describe('custom configuration', () => {
const searchTerm2 = 'yesterday I was in trouble'
const expectedResult2 = 'Yesterday all my troubles seemed so far away, now it looks as though they\'re here to stay oh, I believe in yesterday'
- assert.strictEqual(highlight(text1, searchTerm1, { caseSensitive: true }).toString(), expectedResult1)
- assert.strictEqual(highlight(text2, searchTerm2, { caseSensitive: true }).toString(), expectedResult2)
+ const highlighter = new Highlight({ caseSensitive: true })
+
+ assert.strictEqual(highlighter.highlight(text1, searchTerm1).HTML, expectedResult1)
+ assert.strictEqual(highlighter.highlight(text2, searchTerm2).HTML, expectedResult2)
})
it('should correctly set a custom CSS class', () => {
@@ -53,7 +61,9 @@ describe('custom configuration', () => {
const searchTerm = 'fox'
const expectedResult = 'The quick brown fox jumps over the lazy dog'
- assert.strictEqual(highlight(text, searchTerm, { CSSClass: 'custom-class' }).toString(), expectedResult)
+ const highlighter = new Highlight({ CSSClass: 'custom-class' })
+
+ assert.strictEqual(highlighter.highlight(text, searchTerm).HTML, expectedResult)
})
it('should correctly use a custom HTML tag', () => {
@@ -61,7 +71,9 @@ describe('custom configuration', () => {
const searchTerm = 'fox'
const expectedResult = 'The quick brown
fox
jumps over the lazy dog'
- assert.strictEqual(highlight(text, searchTerm, { HTMLTag: 'div' }).toString(), expectedResult)
+ const highlighter = new Highlight({ HTMLTag: 'div' })
+
+ assert.strictEqual(highlighter.highlight(text, searchTerm).HTML, expectedResult)
})
it('should correctly highlight whole words only', () => {
@@ -69,7 +81,9 @@ describe('custom configuration', () => {
const searchTerm = 'fox jump'
const expectedResult = 'The quick brown fox jumps over the lazy dog'
- assert.strictEqual(highlight(text, searchTerm, { wholeWords: true }).toString(), expectedResult)
+ const highlighter = new Highlight({ wholeWords: true })
+
+ assert.strictEqual(highlighter.highlight(text, searchTerm).HTML, expectedResult)
})
})
@@ -94,10 +108,26 @@ describe('highlight function - infinite loop protection', () => {
return null
})
- const result = highlight(text, searchTerm)
+ const highlighter = new Highlight()
+ const result = highlighter.highlight(text, searchTerm)
- assert.strictEqual(result.toString(), text)
+ assert.strictEqual(result.HTML, text)
assert(regexExecStub.called)
})
})
+
+describe('trim method', () => {
+ it('should correctly trim the text', () => {
+ const text = 'The quick brown fox jumps over the lazy dog'
+ const searchTerm = 'fox'
+ const highlighter = new Highlight()
+
+ assert.strictEqual(highlighter.highlight(text, searchTerm).trim(10), '...rown fox j...')
+ assert.strictEqual(highlighter.highlight(text, searchTerm).trim(5), '...n fox...')
+ assert.strictEqual(highlighter.highlight(text, 'the').trim(5), 'The q...')
+ assert.strictEqual(highlighter.highlight(text, 'dog').trim(5), '...y dog')
+ assert.strictEqual(highlighter.highlight(text, 'dog').trim(5, false), 'y dog')
+ assert.strictEqual(highlighter.highlight(text, 'the').trim(5, false), 'The q')
+ })
+})
diff --git a/src/index.ts b/src/index.ts
index f88d97e..8729227 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,3 @@
-interface Highlight {
- positions: Array<{ start: number, end: number }>
- toString: () => string
-}
-
export interface HighlightOptions {
caseSensitive?: boolean
wholeWords?: boolean
@@ -10,6 +5,8 @@ export interface HighlightOptions {
CSSClass?: string
}
+type Positions = Array<{ start: number, end: number }>
+
const defaultOptions: HighlightOptions = {
caseSensitive: false,
wholeWords: false,
@@ -17,43 +14,80 @@ const defaultOptions: HighlightOptions = {
CSSClass: 'orama-highlight'
}
-export function highlight (text: string, searchTerm: string, options: HighlightOptions = defaultOptions): Highlight {
- const caseSensitive = options.caseSensitive ?? defaultOptions.caseSensitive
- const wholeWords = options.wholeWords ?? defaultOptions.wholeWords
- const HTMLTag = options.HTMLTag ?? defaultOptions.HTMLTag
- const CSSClass = options.CSSClass ?? defaultOptions.CSSClass
- const regexFlags = caseSensitive ? 'g' : 'gi'
- const boundary = wholeWords ? '\\b' : ''
- const searchTerms = (caseSensitive ? searchTerm : searchTerm.toLowerCase()).trim().split(/\s+/).join('|')
- const regex = new RegExp(`${boundary}${searchTerms}${boundary}`, regexFlags)
- const positions: Array<{ start: number, end: number }> = []
- const highlightedParts: string[] = []
-
- let match
- let lastEnd = 0
- let previousLastIndex = -1
-
- while ((match = regex.exec(text)) !== null) {
- if (regex.lastIndex === previousLastIndex) {
- break
+export class Highlight {
+ private readonly options: HighlightOptions
+ private _positions: Positions = []
+ private _HTML: string = ''
+ private _searchTerm: string = ''
+ private _originalText: string = ''
+
+ constructor (options: HighlightOptions = defaultOptions) {
+ this.options = { ...defaultOptions, ...options }
+ }
+
+ public highlight (text: string, searchTerm: string): Highlight {
+ this._searchTerm = searchTerm
+ this._originalText = text
+
+ const caseSensitive = this.options.caseSensitive ?? defaultOptions.caseSensitive
+ const wholeWords = this.options.wholeWords ?? defaultOptions.wholeWords
+ const HTMLTag = this.options.HTMLTag ?? defaultOptions.HTMLTag
+ const CSSClass = this.options.CSSClass ?? defaultOptions.CSSClass
+ const regexFlags = caseSensitive ? 'g' : 'gi'
+ const boundary = wholeWords ? '\\b' : ''
+ const searchTerms = (caseSensitive ? searchTerm : searchTerm.toLowerCase()).trim().split(/\s+/).join('|')
+ const regex = new RegExp(`${boundary}${searchTerms}${boundary}`, regexFlags)
+ const positions: Array<{ start: number, end: number }> = []
+ const highlightedParts: string[] = []
+
+ let match
+ let lastEnd = 0
+ let previousLastIndex = -1
+
+ while ((match = regex.exec(text)) !== null) {
+ if (regex.lastIndex === previousLastIndex) {
+ break
+ }
+ previousLastIndex = regex.lastIndex
+
+ const start = match.index
+ const end = start + match[0].length - 1
+
+ positions.push({ start, end })
+
+ highlightedParts.push(text.slice(lastEnd, start))
+ highlightedParts.push(`<${HTMLTag} class="${CSSClass}">${match[0]}${HTMLTag}>`)
+
+ lastEnd = end + 1
}
- previousLastIndex = regex.lastIndex
- const start = match.index
- const end = start + match[0].length - 1
+ highlightedParts.push(text.slice(lastEnd))
- positions.push({ start, end })
+ this._positions = positions
+ this._HTML = highlightedParts.join('')
- highlightedParts.push(text.slice(lastEnd, start))
- highlightedParts.push(`<${HTMLTag} class="${CSSClass}">${match[0]}${HTMLTag}>`)
+ return this
+ }
- lastEnd = end + 1
+ public trim (trimLength: number, ellipsis: boolean = true): string {
+ if (this._positions.length === 0 || this._originalText.length <= trimLength) {
+ return this._HTML
+ }
+
+ const firstMatch = this._positions[0].start
+ const start = Math.max(firstMatch - Math.floor(trimLength / 2), 0)
+ const end = Math.min(start + trimLength, this._originalText.length)
+ const trimmedContent = `${start === 0 || !ellipsis ? '' : '...'}${this._originalText.slice(start, end)}${end < this._originalText.length && ellipsis ? '...' : ''}`
+
+ this.highlight(trimmedContent, this._searchTerm)
+ return this._HTML
}
- highlightedParts.push(text.slice(lastEnd))
+ get positions (): Positions {
+ return this._positions
+ }
- return {
- positions,
- toString: () => highlightedParts.join('')
+ get HTML (): string {
+ return this._HTML
}
}
diff --git a/src/react/index.tsx b/src/react/index.tsx
deleted file mode 100644
index f84b5d3..0000000
--- a/src/react/index.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { FC } from 'react'
-import type { HighlightOptions } from '../index.js'
-import { highlight } from '../index.js'
-
-interface HighlightProps {
- text: string
- searchTerm: string
- options?: HighlightOptions
-}
-
-export const Highlight: FC = ({ text, searchTerm, options }) => {
- const { toString } = highlight(text, searchTerm, options)
- return toString()
-}
\ No newline at end of file
diff --git a/tsup.react.js b/tsup.react.js
deleted file mode 100644
index 5afdb4f..0000000
--- a/tsup.react.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { defineConfig } from 'tsup'
-
-const entry = new URL('src/react/index.tsx', import.meta.url).pathname
-const outDir = new URL('dist/react', import.meta.url).pathname
-
-export default defineConfig({
- entry: [entry],
- splitting: false,
- sourcemap: true,
- minify: true,
- external: ['react'],
- format: ['cjs', 'esm'],
- dts: true,
- clean: true,
- bundle: true,
- outDir
-})