Skip to content

Commit 68175f3

Browse files
authored
Merge pull request #8 from oramasearch/feat/trim
BREAKING: Adds trim function (implements #2)
2 parents 0121351 + 14ec880 commit 68175f3

File tree

5 files changed

+113
-86
lines changed

5 files changed

+113
-86
lines changed

package.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,13 @@
1313
"import": "./dist/index.js",
1414
"types": "./dist/index.d.ts",
1515
"browser": "./dist/index.global.js"
16-
},
17-
"./react": {
18-
"require": "./dist/react/index.cjs",
19-
"import": "./dist/react/index.js",
20-
"types": "./dist/react/index.d.ts"
2116
}
2217
},
2318
"scripts": {
2419
"test": "bun test",
2520
"lint": "ts-standard --fix ./src/**/*.ts",
26-
"build": "npm run build:lib && npm run build:react",
27-
"build:lib": "tsup --config tsup.lib.js",
28-
"build:react": "tsup --config tsup.react.js"
21+
"build": "npm run build:lib",
22+
"build:lib": "tsup --config tsup.lib.js"
2923
},
3024
"keywords": [
3125
"full-text search",

src/index.test.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, beforeEach, afterEach } from 'bun:test'
22
import assert from 'node:assert'
33
import sinon from 'sinon'
4-
import { highlight } from './index.js'
4+
import { Highlight } from './index.js'
55

66
describe('default configuration', () => {
77
it('should correctly highlight a text', () => {
@@ -13,24 +13,30 @@ describe('default configuration', () => {
1313
const searchTerm2 = 'yesterday I was in trouble'
1414
const expectedResult2 = '<mark class="orama-highlight">Yesterday</mark> all my <mark class="orama-highlight">trouble</mark>s seemed so far away, now <mark class="orama-highlight">i</mark>t looks as though they\'re here to stay oh, <mark class="orama-highlight">I</mark> bel<mark class="orama-highlight">i</mark>eve <mark class="orama-highlight">i</mark>n <mark class="orama-highlight">yesterday</mark>'
1515

16-
assert.strictEqual(highlight(text1, searchTerm1).toString(), expectedResult1)
17-
assert.strictEqual(highlight(text2, searchTerm2).toString(), expectedResult2)
16+
const highlighter = new Highlight()
17+
18+
assert.strictEqual(highlighter.highlight(text1, searchTerm1).HTML, expectedResult1)
19+
assert.strictEqual(highlighter.highlight(text2, searchTerm2).HTML, expectedResult2)
1820
})
1921

2022
it('should return the correct positions', () => {
2123
const text = 'The quick brown fox jumps over the lazy dog'
2224
const searchTerm = 'fox'
2325
const expectedPositions = [{ start: 16, end: 18 }]
2426

25-
assert.deepStrictEqual(highlight(text, searchTerm).positions, expectedPositions)
27+
const highlighter = new Highlight()
28+
29+
assert.deepStrictEqual(highlighter.highlight(text, searchTerm).positions, expectedPositions)
2630
})
2731

2832
it('should return multiple positions', () => {
2933
const text = 'The quick brown fox jumps over the lazy dog'
3034
const searchTerm = 'the'
3135
const expectedPositions = [{ start: 0, end: 2 }, { start: 31, end: 33 }]
3236

33-
assert.deepStrictEqual(highlight(text, searchTerm).positions, expectedPositions)
37+
const highlighter = new Highlight()
38+
39+
assert.deepStrictEqual(highlighter.highlight(text, searchTerm).positions, expectedPositions)
3440
})
3541
})
3642

@@ -44,32 +50,40 @@ describe('custom configuration', () => {
4450
const searchTerm2 = 'yesterday I was in trouble'
4551
const expectedResult2 = 'Yesterday all my <mark class="orama-highlight">trouble</mark>s seemed so far away, now it looks as though they\'re here to stay oh, <mark class="orama-highlight">I</mark> believe <mark class="orama-highlight">in</mark> <mark class="orama-highlight">yesterday</mark>'
4652

47-
assert.strictEqual(highlight(text1, searchTerm1, { caseSensitive: true }).toString(), expectedResult1)
48-
assert.strictEqual(highlight(text2, searchTerm2, { caseSensitive: true }).toString(), expectedResult2)
53+
const highlighter = new Highlight({ caseSensitive: true })
54+
55+
assert.strictEqual(highlighter.highlight(text1, searchTerm1).HTML, expectedResult1)
56+
assert.strictEqual(highlighter.highlight(text2, searchTerm2).HTML, expectedResult2)
4957
})
5058

5159
it('should correctly set a custom CSS class', () => {
5260
const text = 'The quick brown fox jumps over the lazy dog'
5361
const searchTerm = 'fox'
5462
const expectedResult = 'The quick brown <mark class="custom-class">fox</mark> jumps over the lazy dog'
5563

56-
assert.strictEqual(highlight(text, searchTerm, { CSSClass: 'custom-class' }).toString(), expectedResult)
64+
const highlighter = new Highlight({ CSSClass: 'custom-class' })
65+
66+
assert.strictEqual(highlighter.highlight(text, searchTerm).HTML, expectedResult)
5767
})
5868

5969
it('should correctly use a custom HTML tag', () => {
6070
const text = 'The quick brown fox jumps over the lazy dog'
6171
const searchTerm = 'fox'
6272
const expectedResult = 'The quick brown <div class="orama-highlight">fox</div> jumps over the lazy dog'
6373

64-
assert.strictEqual(highlight(text, searchTerm, { HTMLTag: 'div' }).toString(), expectedResult)
74+
const highlighter = new Highlight({ HTMLTag: 'div' })
75+
76+
assert.strictEqual(highlighter.highlight(text, searchTerm).HTML, expectedResult)
6577
})
6678

6779
it('should correctly highlight whole words only', () => {
6880
const text = 'The quick brown fox jumps over the lazy dog'
6981
const searchTerm = 'fox jump'
7082
const expectedResult = 'The quick brown <mark class="orama-highlight">fox</mark> jumps over the lazy dog'
7183

72-
assert.strictEqual(highlight(text, searchTerm, { wholeWords: true }).toString(), expectedResult)
84+
const highlighter = new Highlight({ wholeWords: true })
85+
86+
assert.strictEqual(highlighter.highlight(text, searchTerm).HTML, expectedResult)
7387
})
7488
})
7589

@@ -94,10 +108,26 @@ describe('highlight function - infinite loop protection', () => {
94108
return null
95109
})
96110

97-
const result = highlight(text, searchTerm)
111+
const highlighter = new Highlight()
112+
const result = highlighter.highlight(text, searchTerm)
98113

99-
assert.strictEqual(result.toString(), text)
114+
assert.strictEqual(result.HTML, text)
100115

101116
assert(regexExecStub.called)
102117
})
103118
})
119+
120+
describe('trim method', () => {
121+
it('should correctly trim the text', () => {
122+
const text = 'The quick brown fox jumps over the lazy dog'
123+
const searchTerm = 'fox'
124+
const highlighter = new Highlight()
125+
126+
assert.strictEqual(highlighter.highlight(text, searchTerm).trim(10), '...rown <mark class="orama-highlight">fox</mark> j...')
127+
assert.strictEqual(highlighter.highlight(text, searchTerm).trim(5), '...n <mark class="orama-highlight">fox</mark>...')
128+
assert.strictEqual(highlighter.highlight(text, 'the').trim(5), '<mark class="orama-highlight">The</mark> q...')
129+
assert.strictEqual(highlighter.highlight(text, 'dog').trim(5), '...y <mark class="orama-highlight">dog</mark>')
130+
assert.strictEqual(highlighter.highlight(text, 'dog').trim(5, false), 'y <mark class="orama-highlight">dog</mark>')
131+
assert.strictEqual(highlighter.highlight(text, 'the').trim(5, false), '<mark class="orama-highlight">The</mark> q')
132+
})
133+
})

src/index.ts

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,93 @@
1-
interface Highlight {
2-
positions: Array<{ start: number, end: number }>
3-
toString: () => string
4-
}
5-
61
export interface HighlightOptions {
72
caseSensitive?: boolean
83
wholeWords?: boolean
94
HTMLTag?: string
105
CSSClass?: string
116
}
127

8+
type Positions = Array<{ start: number, end: number }>
9+
1310
const defaultOptions: HighlightOptions = {
1411
caseSensitive: false,
1512
wholeWords: false,
1613
HTMLTag: 'mark',
1714
CSSClass: 'orama-highlight'
1815
}
1916

20-
export function highlight (text: string, searchTerm: string, options: HighlightOptions = defaultOptions): Highlight {
21-
const caseSensitive = options.caseSensitive ?? defaultOptions.caseSensitive
22-
const wholeWords = options.wholeWords ?? defaultOptions.wholeWords
23-
const HTMLTag = options.HTMLTag ?? defaultOptions.HTMLTag
24-
const CSSClass = options.CSSClass ?? defaultOptions.CSSClass
25-
const regexFlags = caseSensitive ? 'g' : 'gi'
26-
const boundary = wholeWords ? '\\b' : ''
27-
const searchTerms = (caseSensitive ? searchTerm : searchTerm.toLowerCase()).trim().split(/\s+/).join('|')
28-
const regex = new RegExp(`${boundary}${searchTerms}${boundary}`, regexFlags)
29-
const positions: Array<{ start: number, end: number }> = []
30-
const highlightedParts: string[] = []
31-
32-
let match
33-
let lastEnd = 0
34-
let previousLastIndex = -1
35-
36-
while ((match = regex.exec(text)) !== null) {
37-
if (regex.lastIndex === previousLastIndex) {
38-
break
17+
export class Highlight {
18+
private readonly options: HighlightOptions
19+
private _positions: Positions = []
20+
private _HTML: string = ''
21+
private _searchTerm: string = ''
22+
private _originalText: string = ''
23+
24+
constructor (options: HighlightOptions = defaultOptions) {
25+
this.options = { ...defaultOptions, ...options }
26+
}
27+
28+
public highlight (text: string, searchTerm: string): Highlight {
29+
this._searchTerm = searchTerm
30+
this._originalText = text
31+
32+
const caseSensitive = this.options.caseSensitive ?? defaultOptions.caseSensitive
33+
const wholeWords = this.options.wholeWords ?? defaultOptions.wholeWords
34+
const HTMLTag = this.options.HTMLTag ?? defaultOptions.HTMLTag
35+
const CSSClass = this.options.CSSClass ?? defaultOptions.CSSClass
36+
const regexFlags = caseSensitive ? 'g' : 'gi'
37+
const boundary = wholeWords ? '\\b' : ''
38+
const searchTerms = (caseSensitive ? searchTerm : searchTerm.toLowerCase()).trim().split(/\s+/).join('|')
39+
const regex = new RegExp(`${boundary}${searchTerms}${boundary}`, regexFlags)
40+
const positions: Array<{ start: number, end: number }> = []
41+
const highlightedParts: string[] = []
42+
43+
let match
44+
let lastEnd = 0
45+
let previousLastIndex = -1
46+
47+
while ((match = regex.exec(text)) !== null) {
48+
if (regex.lastIndex === previousLastIndex) {
49+
break
50+
}
51+
previousLastIndex = regex.lastIndex
52+
53+
const start = match.index
54+
const end = start + match[0].length - 1
55+
56+
positions.push({ start, end })
57+
58+
highlightedParts.push(text.slice(lastEnd, start))
59+
highlightedParts.push(`<${HTMLTag} class="${CSSClass}">${match[0]}</${HTMLTag}>`)
60+
61+
lastEnd = end + 1
3962
}
40-
previousLastIndex = regex.lastIndex
4163

42-
const start = match.index
43-
const end = start + match[0].length - 1
64+
highlightedParts.push(text.slice(lastEnd))
4465

45-
positions.push({ start, end })
66+
this._positions = positions
67+
this._HTML = highlightedParts.join('')
4668

47-
highlightedParts.push(text.slice(lastEnd, start))
48-
highlightedParts.push(`<${HTMLTag} class="${CSSClass}">${match[0]}</${HTMLTag}>`)
69+
return this
70+
}
4971

50-
lastEnd = end + 1
72+
public trim (trimLength: number, ellipsis: boolean = true): string {
73+
if (this._positions.length === 0 || this._originalText.length <= trimLength) {
74+
return this._HTML
75+
}
76+
77+
const firstMatch = this._positions[0].start
78+
const start = Math.max(firstMatch - Math.floor(trimLength / 2), 0)
79+
const end = Math.min(start + trimLength, this._originalText.length)
80+
const trimmedContent = `${start === 0 || !ellipsis ? '' : '...'}${this._originalText.slice(start, end)}${end < this._originalText.length && ellipsis ? '...' : ''}`
81+
82+
this.highlight(trimmedContent, this._searchTerm)
83+
return this._HTML
5184
}
5285

53-
highlightedParts.push(text.slice(lastEnd))
86+
get positions (): Positions {
87+
return this._positions
88+
}
5489

55-
return {
56-
positions,
57-
toString: () => highlightedParts.join('')
90+
get HTML (): string {
91+
return this._HTML
5892
}
5993
}

src/react/index.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.

tsup.react.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)