├── bunfig.toml
├── bun.lockb
├── tsconfig.json
├── .github
└── workflows
│ └── test.yml
├── tsup.lib.js
├── LICENSE.md
├── package.json
├── README.md
├── .gitignore
└── src
├── index.ts
└── index.test.ts
/bunfig.toml:
--------------------------------------------------------------------------------
1 | [test]
2 | coverage = true
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oramasearch/highlight/HEAD/bun.lockb
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "types": ["bun-types"],
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: oven-sh/setup-bun@v1
15 | with:
16 | bun-version: latest
17 | - run: bun install
18 | - run: bun run build
19 | - run: bun test
--------------------------------------------------------------------------------
/tsup.lib.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | const entry = new URL('src/index.ts', import.meta.url).pathname
4 | const outDir = new URL('dist', import.meta.url).pathname
5 |
6 | export default defineConfig({
7 | entry: [entry],
8 | splitting: false,
9 | sourcemap: true,
10 | minify: true,
11 | format: ['cjs', 'esm', 'iife'],
12 | globalName: 'OramaHighlight',
13 | dts: true,
14 | clean: true,
15 | bundle: true,
16 | outDir
17 | })
18 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2023 OramaSearch Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@orama/highlight",
3 | "version": "0.1.9",
4 | "description": "Highlight any text in any JavaScript lib (browser, server, React, Vue, you name it!)",
5 | "main": "dist/index.js",
6 | "type": "module",
7 | "files": [
8 | "dist"
9 | ],
10 | "exports": {
11 | ".": {
12 | "require": "./dist/index.cjs",
13 | "import": "./dist/index.js",
14 | "types": "./dist/index.d.ts",
15 | "browser": "./dist/index.global.js"
16 | }
17 | },
18 | "scripts": {
19 | "test": "bun test",
20 | "lint": "ts-standard --fix ./src/**/*.ts",
21 | "build": "npm run build:lib",
22 | "build:lib": "tsup --config tsup.lib.js"
23 | },
24 | "keywords": [
25 | "full-text search",
26 | "search",
27 | "highlight"
28 | ],
29 | "author": {
30 | "name": "Michele Riva",
31 | "email": "michele.riva@oramasearch.com"
32 | },
33 | "license": "Apache-2.0",
34 | "devDependencies": {
35 | "@types/react": "^18.2.25",
36 | "@types/sinon": "^10.0.20",
37 | "bun-types": "^1.0.4-canary.20231004T140131",
38 | "react": "^18.2.0",
39 | "sinon": "^17.0.0",
40 | "ts-standard": "^12.0.2",
41 | "tsup": "^7.2.0",
42 | "typescript": "^5.2.2"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Highlight
2 |
3 | [](https://github.com/oramasearch/highlight/actions/workflows/test.yml)
4 |
5 | Orama Highlight allows you to easily highlight substrings in a given input.
6 |
7 | # Installation
8 |
9 | ```bash
10 | npm i @orama/highlight
11 | bun i @orama/highlight
12 | ```
13 |
14 | # Usage
15 |
16 | ```js
17 | import { Highlight } from '@orama/highlight'
18 |
19 | const inputString = 'The quick brown fox jumps over the lazy dog'
20 | const toHighlight = 'brown fox jump'
21 |
22 | const highlighter = new Highlight()
23 | const highlighted = highlighter.highlight(inputString, toHighlight)
24 |
25 | console.log(highlighted.positions)
26 | // [
27 | // {
28 | // start: 10,
29 | // end: 14
30 | // }, {
31 | // start: 16,
32 | // end: 18
33 | // }, {
34 | // start: 20,
35 | // end: 23
36 | // }
37 | // ]
38 |
39 | console.log(highlighted.HTML)
40 | // "The quick brown fox jumps over the lazy dog"
41 |
42 | console.log(highlighted.trim(10))
43 | // "...uick brown..."
44 | ```
45 |
46 | You can always customize the library behavior by passing some options to the class constructor:
47 |
48 | ```js
49 | const highlighted = new Highlight({
50 | caseSensitive: true, // Only highlight words that respect the second parameter's casing. Default is false
51 | wholeWords: true, // Only highlight entire words, no prefixes
52 | HTMLTag: 'div', // Default is "mark"
53 | CSSClass: 'my-custom-class' // default is 'orama-highlight'
54 | })
55 | ```
56 |
57 | # License
58 | [Apache 2.0](/LICENSE.md)
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/node,osx,windows,linux
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,osx,windows,linux
3 |
4 | ### Linux ###
5 | *~
6 |
7 | # temporary files which can be created if a process still has a handle open of a deleted file
8 | .fuse_hidden*
9 |
10 | # KDE directory preferences
11 | .directory
12 |
13 | # Linux trash folder which might appear on any partition or disk
14 | .Trash-*
15 |
16 | # .nfs files are created when an open file is removed but is still being accessed
17 | .nfs*
18 |
19 | ### Node ###
20 | # Logs
21 | logs
22 | *.log
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | lerna-debug.log*
27 | .pnpm-debug.log*
28 |
29 | # Diagnostic reports (https://nodejs.org/api/report.html)
30 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
31 |
32 | # Runtime data
33 | pids
34 | *.pid
35 | *.seed
36 | *.pid.lock
37 |
38 | # Directory for instrumented libs generated by jscoverage/JSCover
39 | lib-cov
40 |
41 | # Coverage directory used by tools like istanbul
42 | coverage
43 | *.lcov
44 |
45 | # nyc test coverage
46 | .nyc_output
47 |
48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
49 | .grunt
50 |
51 | # Bower dependency directory (https://bower.io/)
52 | bower_components
53 |
54 | # node-waf configuration
55 | .lock-wscript
56 |
57 | # Compiled binary addons (https://nodejs.org/api/addons.html)
58 | build/Release
59 |
60 | # Dependency directories
61 | node_modules/
62 | jspm_packages/
63 |
64 | # Snowpack dependency directory (https://snowpack.dev/)
65 | web_modules/
66 |
67 | # TypeScript cache
68 | *.tsbuildinfo
69 |
70 | # Optional npm cache directory
71 | .npm
72 |
73 | # Optional eslint cache
74 | .eslintcache
75 |
76 | # Optional stylelint cache
77 | .stylelintcache
78 |
79 | # Microbundle cache
80 | .rpt2_cache/
81 | .rts2_cache_cjs/
82 | .rts2_cache_es/
83 | .rts2_cache_umd/
84 |
85 | # Optional REPL history
86 | .node_repl_history
87 |
88 | # Output of 'npm pack'
89 | *.tgz
90 |
91 | # Yarn Integrity file
92 | .yarn-integrity
93 |
94 | # dotenv environment variable files
95 | .env
96 | .env.development.local
97 | .env.test.local
98 | .env.production.local
99 | .env.local
100 |
101 | # parcel-bundler cache (https://parceljs.org/)
102 | .cache
103 | .parcel-cache
104 |
105 | # Next.js build output
106 | .next
107 | out
108 |
109 | # Nuxt.js build / generate output
110 | .nuxt
111 | dist
112 |
113 | # Gatsby files
114 | .cache/
115 | # Comment in the public line in if your project uses Gatsby and not Next.js
116 | # https://nextjs.org/blog/next-9-1#public-directory-support
117 | # public
118 |
119 | # vuepress build output
120 | .vuepress/dist
121 |
122 | # vuepress v2.x temp and cache directory
123 | .temp
124 |
125 | # Docusaurus cache and generated files
126 | .docusaurus
127 |
128 | # Serverless directories
129 | .serverless/
130 |
131 | # FuseBox cache
132 | .fusebox/
133 |
134 | # DynamoDB Local files
135 | .dynamodb/
136 |
137 | # TernJS port file
138 | .tern-port
139 |
140 | # Stores VSCode versions used for testing VSCode extensions
141 | .vscode-test
142 |
143 | # yarn v2
144 | .yarn/cache
145 | .yarn/unplugged
146 | .yarn/build-state.yml
147 | .yarn/install-state.gz
148 | .pnp.*
149 |
150 | ### Node Patch ###
151 | # Serverless Webpack directories
152 | .webpack/
153 |
154 | # Optional stylelint cache
155 |
156 | # SvelteKit build / generate output
157 | .svelte-kit
158 |
159 | ### OSX ###
160 | # General
161 | .DS_Store
162 | .AppleDouble
163 | .LSOverride
164 |
165 | # Icon must end with two \r
166 | Icon
167 |
168 |
169 | # Thumbnails
170 | ._*
171 |
172 | # Files that might appear in the root of a volume
173 | .DocumentRevisions-V100
174 | .fseventsd
175 | .Spotlight-V100
176 | .TemporaryItems
177 | .Trashes
178 | .VolumeIcon.icns
179 | .com.apple.timemachine.donotpresent
180 |
181 | # Directories potentially created on remote AFP share
182 | .AppleDB
183 | .AppleDesktop
184 | Network Trash Folder
185 | Temporary Items
186 | .apdisk
187 |
188 | ### Windows ###
189 | # Windows thumbnail cache files
190 | Thumbs.db
191 | Thumbs.db:encryptable
192 | ehthumbs.db
193 | ehthumbs_vista.db
194 |
195 | # Dump file
196 | *.stackdump
197 |
198 | # Folder config file
199 | [Dd]esktop.ini
200 |
201 | # Recycle Bin used on file shares
202 | $RECYCLE.BIN/
203 |
204 | # Windows Installer files
205 | *.cab
206 | *.msi
207 | *.msix
208 | *.msm
209 | *.msp
210 |
211 | # Windows shortcuts
212 | *.lnk
213 |
214 | # IDEA
215 | .idea
216 |
217 | # End of https://www.toptal.com/developers/gitignore/api/node,osx,windows,linux
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export const highlightStrategy = {
2 | WHOLE_WORD_MATCH: "wholeWordMatch",
3 | PARTIAL_MATCH: "partialMatch",
4 | PARTIAL_MATCH_FULL_WORD: "partialMatchFullWord",
5 | } as const;
6 |
7 | export type HighlightStrategy =
8 | (typeof highlightStrategy)[keyof typeof highlightStrategy];
9 |
10 | export interface HighlightOptions {
11 | caseSensitive?: boolean;
12 | strategy?: HighlightStrategy;
13 | HTMLTag?: string;
14 | CSSClass?: string;
15 | }
16 | export type Position = { start: number; end: number };
17 |
18 | type Positions = Position[];
19 |
20 | const defaultOptions: Required = {
21 | caseSensitive: false,
22 | strategy: highlightStrategy.PARTIAL_MATCH,
23 | HTMLTag: "mark",
24 | CSSClass: "orama-highlight",
25 | };
26 |
27 | export class Highlight {
28 | private readonly options: HighlightOptions;
29 | private _positions: Positions = [];
30 | private _HTML: string = "";
31 | private _searchTerm: string = "";
32 | private _originalText: string = "";
33 |
34 | constructor(options: HighlightOptions = defaultOptions) {
35 | this.options = { ...defaultOptions, ...options };
36 | }
37 |
38 | public highlight(text: string, searchTerm: string): Highlight {
39 | this._searchTerm = searchTerm ?? "";
40 | this._originalText = text ?? "";
41 |
42 | if (!this._searchTerm || !this._originalText) {
43 | this._positions = [];
44 | this._HTML = this._originalText;
45 | return this;
46 | }
47 |
48 | const HTMLTag = this.options.HTMLTag ?? defaultOptions.HTMLTag;
49 | const CSSClass = this.options.CSSClass ?? defaultOptions.CSSClass;
50 |
51 | const caseSensitive =
52 | this.options.caseSensitive ?? defaultOptions.caseSensitive;
53 | const strategy = this.options.strategy ?? defaultOptions.strategy;
54 | const regexFlags = caseSensitive ? "g" : "gi";
55 | const searchTerms = this.escapeRegExp(
56 | caseSensitive ? this._searchTerm : this._searchTerm.toLowerCase()
57 | )
58 | .trim()
59 | .split(/\s+/)
60 | .join("|");
61 |
62 | let regex: RegExp;
63 | if (strategy === highlightStrategy.WHOLE_WORD_MATCH) {
64 | regex = new RegExp(`\\b${searchTerms}\\b`, regexFlags);
65 | } else if (strategy === highlightStrategy.PARTIAL_MATCH) {
66 | regex = new RegExp(searchTerms, regexFlags);
67 | } else if (strategy === highlightStrategy.PARTIAL_MATCH_FULL_WORD) {
68 | regex = new RegExp(`\\b[^\\s]*(${searchTerms})[^\\s]*\\b`, regexFlags);
69 | } else {
70 | throw new Error("Invalid highlighter strategy");
71 | }
72 |
73 | const positions: Array<{ start: number; end: number }> = [];
74 | const highlightedParts: string[] = [];
75 |
76 | let match;
77 | let lastEnd = 0;
78 | let previousLastIndex = -1;
79 |
80 | while ((match = regex.exec(this._originalText)) !== null) {
81 | if (regex.lastIndex === previousLastIndex) {
82 | break;
83 | }
84 | previousLastIndex = regex.lastIndex;
85 |
86 | const start = match.index;
87 | const end = start + match[0].length - 1;
88 |
89 | positions.push({ start, end });
90 |
91 | highlightedParts.push(this._originalText.slice(lastEnd, start));
92 | highlightedParts.push(
93 | `<${HTMLTag} class="${CSSClass}">${match[0]}${HTMLTag}>`
94 | );
95 |
96 | lastEnd = end + 1;
97 | }
98 |
99 | highlightedParts.push(this._originalText.slice(lastEnd));
100 |
101 | this._positions = positions;
102 | this._HTML = highlightedParts.join("");
103 |
104 | return this;
105 | }
106 |
107 | public trim(trimLength: number, ellipsis: boolean = true): string {
108 | if (this._positions.length === 0) {
109 | return `${this._HTML.substring(0, trimLength)}${ellipsis ? `...` : ""}`;
110 | }
111 |
112 | if (this._originalText.length <= trimLength) {
113 | return this._HTML;
114 | }
115 |
116 | const firstMatch = this._positions[0].start;
117 | const start = Math.max(firstMatch - Math.floor(trimLength / 2), 0);
118 | const end = Math.min(start + trimLength, this._originalText.length);
119 | const trimmedContent = `${
120 | start === 0 || !ellipsis ? "" : "..."
121 | }${this._originalText.slice(start, end)}${
122 | end < this._originalText.length && ellipsis ? "..." : ""
123 | }`;
124 |
125 | this.highlight(trimmedContent, this._searchTerm);
126 | return this._HTML;
127 | }
128 |
129 | get positions(): Positions {
130 | return this._positions;
131 | }
132 |
133 | get HTML(): string {
134 | return this._HTML;
135 | }
136 |
137 | private escapeRegExp(string: string): string {
138 | return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { afterEach, beforeEach, describe, it } from "bun:test";
2 | import assert from "node:assert";
3 | import sinon from "sinon";
4 | import { Highlight, highlightStrategy } from "./index.js";
5 |
6 | describe("default configuration", () => {
7 | it("should correctly highlight a text", () => {
8 | const text1 = "The quick brown fox jumps over the lazy dog";
9 | const searchTerm1 = "fox";
10 | const expectedResult1 =
11 | 'The quick brown fox jumps over the lazy dog';
12 |
13 | const text2 =
14 | "Yesterday all my troubles seemed so far away, now it looks as though they're here to stay oh, I believe in yesterday";
15 | const searchTerm2 = "yesterday I was in trouble";
16 | const expectedResult2 =
17 | 'Yesterday all my troubles seemed so far away, now it looks as though they\'re here to stay oh, I believe in yesterday';
18 |
19 | const highlighter = new Highlight();
20 |
21 | assert.strictEqual(
22 | highlighter.highlight(text1, searchTerm1).HTML,
23 | expectedResult1
24 | );
25 | assert.strictEqual(
26 | highlighter.highlight(text2, searchTerm2).HTML,
27 | expectedResult2
28 | );
29 | });
30 |
31 | it("should return the correct positions", () => {
32 | const text = "The quick brown fox jumps over the lazy dog";
33 | const searchTerm = "fox";
34 | const expectedPositions = [{ start: 16, end: 18 }];
35 |
36 | const highlighter = new Highlight();
37 |
38 | assert.deepStrictEqual(
39 | highlighter.highlight(text, searchTerm).positions,
40 | expectedPositions
41 | );
42 | });
43 |
44 | it("should return multiple positions", () => {
45 | const text = "The quick brown fox jumps over the lazy dog";
46 | const searchTerm = "the";
47 | const expectedPositions = [
48 | { start: 0, end: 2 },
49 | { start: 31, end: 33 },
50 | ];
51 |
52 | const highlighter = new Highlight();
53 |
54 | assert.deepStrictEqual(
55 | highlighter.highlight(text, searchTerm).positions,
56 | expectedPositions
57 | );
58 | });
59 | });
60 |
61 | describe("custom configuration", () => {
62 | it("should correctly highlight a text (case sensitive)", () => {
63 | const text1 = "The quick brown fox jumps over the lazy dog";
64 | const searchTerm1 = "Fox";
65 | const expectedResult1 = "The quick brown fox jumps over the lazy dog";
66 |
67 | const text2 =
68 | "Yesterday all my troubles seemed so far away, now it looks as though they're here to stay oh, I believe in yesterday";
69 | const searchTerm2 = "yesterday I was in trouble";
70 | const expectedResult2 =
71 | 'Yesterday all my troubles seemed so far away, now it looks as though they\'re here to stay oh, I believe in yesterday';
72 |
73 | const highlighter = new Highlight({ caseSensitive: true });
74 |
75 | assert.strictEqual(
76 | highlighter.highlight(text1, searchTerm1).HTML,
77 | expectedResult1
78 | );
79 | assert.strictEqual(
80 | highlighter.highlight(text2, searchTerm2).HTML,
81 | expectedResult2
82 | );
83 | });
84 |
85 | it("should correctly set a custom CSS class", () => {
86 | const text = "The quick brown fox jumps over the lazy dog";
87 | const searchTerm = "fox";
88 | const expectedResult =
89 | 'The quick brown fox jumps over the lazy dog';
90 |
91 | const highlighter = new Highlight({ CSSClass: "custom-class" });
92 |
93 | assert.strictEqual(
94 | highlighter.highlight(text, searchTerm).HTML,
95 | expectedResult
96 | );
97 | });
98 |
99 | it("should correctly use a custom HTML tag", () => {
100 | const text = "The quick brown fox jumps over the lazy dog";
101 | const searchTerm = "fox";
102 | const expectedResult =
103 | 'The quick brown fox
jumps over the lazy dog';
104 |
105 | const highlighter = new Highlight({ HTMLTag: "div" });
106 |
107 | assert.strictEqual(
108 | highlighter.highlight(text, searchTerm).HTML,
109 | expectedResult
110 | );
111 | });
112 |
113 | it("should correctly highlight whole word matches only", () => {
114 | const text = "The quick brown fox jumps over the lazy dog";
115 | const searchTerm = "fox jump";
116 | const expectedResult =
117 | 'The quick brown fox jumps over the lazy dog';
118 |
119 | const highlighter = new Highlight({ strategy: highlightStrategy.WHOLE_WORD_MATCH });
120 |
121 | assert.strictEqual(
122 | highlighter.highlight(text, searchTerm).HTML,
123 | expectedResult
124 | );
125 | });
126 |
127 | it("should correctly highlight whole words on partial matches only", () => {
128 | const text = "The quick brown fox jumps over the lazy dog";
129 | const searchTerm = "fo umps ve";
130 | const expectedResult =
131 | 'The quick brown fox jumps over the lazy dog';
132 |
133 | const highlighter = new Highlight({
134 | strategy: highlightStrategy.PARTIAL_MATCH_FULL_WORD,
135 | });
136 |
137 | assert.strictEqual(
138 | highlighter.highlight(text, searchTerm).HTML,
139 | expectedResult
140 | );
141 | });
142 |
143 | it("should not highlight anything on 0 matches on a partial match, full word highlight strategy when the search term is nothing", () => {
144 | const text = "The quick brown fox jumps over the lazy dog";
145 | const searchTerm = "";
146 | const expectedResult = "The quick brown fox jumps over the lazy dog";
147 |
148 | const highlighter = new Highlight({
149 | strategy: highlightStrategy.PARTIAL_MATCH_FULL_WORD,
150 | });
151 |
152 | assert.strictEqual(
153 | highlighter.highlight(text, searchTerm).HTML,
154 | expectedResult
155 | );
156 | });
157 | });
158 |
159 | describe("highlight function - infinite loop protection", () => {
160 | let regexExecStub: sinon.SinonStub;
161 |
162 | beforeEach(() => {
163 | regexExecStub = sinon.stub(RegExp.prototype, "exec");
164 | });
165 |
166 | afterEach(() => {
167 | regexExecStub.restore();
168 | });
169 |
170 | it("should exit the loop if regex.lastIndex does not advance", () => {
171 | const text = "The quick brown fox jumps over the lazy dog";
172 | const searchTerm = "fox";
173 |
174 | regexExecStub.callsFake(function () {
175 | // @ts-expect-error
176 | this.lastIndex = 0;
177 | return null;
178 | });
179 |
180 | const highlighter = new Highlight();
181 | const result = highlighter.highlight(text, searchTerm);
182 |
183 | assert.strictEqual(result.HTML, text);
184 |
185 | assert(regexExecStub.called);
186 | });
187 | });
188 |
189 | describe("trim method", () => {
190 | it("should correctly trim the text", () => {
191 | const text = "The quick brown fox jumps over the lazy dog";
192 | const searchTerm = "fox";
193 | const highlighter = new Highlight();
194 |
195 | assert.strictEqual(
196 | highlighter.highlight(text, searchTerm).trim(10),
197 | '...rown fox j...'
198 | );
199 | assert.strictEqual(
200 | highlighter.highlight(text, searchTerm).trim(5),
201 | '...n fox...'
202 | );
203 | assert.strictEqual(
204 | highlighter.highlight(text, "the").trim(5),
205 | 'The q...'
206 | );
207 | assert.strictEqual(
208 | highlighter.highlight(text, "dog").trim(5),
209 | '...y dog'
210 | );
211 | assert.strictEqual(
212 | highlighter.highlight(text, "dog").trim(5, false),
213 | 'y dog'
214 | );
215 | assert.strictEqual(
216 | highlighter.highlight(text, "the").trim(5, false),
217 | 'The q'
218 | );
219 | });
220 | it("should correctly trim the text when no match is found", () => {
221 | const text = "The quick brown dog jumps over the lazy dog in a forrest";
222 | const searchTerm = "fox";
223 | const highlighter = new Highlight();
224 |
225 | assert.strictEqual(
226 | highlighter.highlight(text, searchTerm).trim(10),
227 | "The quick ..."
228 | );
229 | assert.strictEqual(
230 | highlighter.highlight(text, searchTerm).trim(10, false),
231 | "The quick "
232 | );
233 | });
234 | });
235 |
236 | describe("special characters", () => {
237 | it("should correctly highlight a text with special characters", () => {
238 | const text = "C++ is a hell of a language";
239 | const searchTerm = "C++";
240 | const expectedResult =
241 | 'C++ is a hell of a language';
242 |
243 | const highlighter = new Highlight();
244 |
245 | assert.strictEqual(
246 | highlighter.highlight(text, searchTerm).HTML,
247 | expectedResult
248 | );
249 | });
250 | });
251 |
252 | describe("null example", () => {
253 | // even though it is not expected we should make sure it won't break
254 | it("should not break when text is null", () => {
255 | const searchTerm = "C";
256 | const highlighter = new Highlight();
257 |
258 | // @ts-expect-error
259 | assert.strictEqual(highlighter.highlight(null, searchTerm).HTML, "");
260 | });
261 | it("should not break when text and search term is null", () => {
262 | const highlighter = new Highlight();
263 |
264 | assert.strictEqual(
265 | // @ts-expect-error
266 | highlighter.highlight(null, null).HTML,
267 | ""
268 | );
269 | });
270 | });
271 |
--------------------------------------------------------------------------------