├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── bun.lockb ├── bunfig.toml ├── package.json ├── src ├── index.test.ts └── index.ts ├── tsconfig.json └── tsup.lib.js /.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 -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oramasearch/highlight/c59ed9eaf4d2f1fd5a537df2b7f7938bd45a49e2/bun.lockb -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | coverage = true -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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