├── .editorconfig
├── .eslintrc.js
├── .github
└── workflows
│ ├── codeql-analysis.yml
│ └── gh-pages.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jsdoc
├── config.json
├── layout.tmpl
└── static
│ └── styles
│ └── overrides.css
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── QuickScore.js
├── config.js
├── index.d.ts
├── index.js
├── quick-score.js
└── range.js
├── test
├── QuickScore.test.js
├── index.test.js
├── quick-score.test.js
├── range.test.js
├── setup.js
├── tabs.js
├── transform-string.test.js
└── utils.js
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = tab
6 | indent_size = 4
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.yml]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.json]
15 | indent_style = space
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": "eslint:recommended",
7 | "parserOptions": {
8 | "ecmaVersion": 2015,
9 | "sourceType": "module"
10 | },
11 | "rules": {
12 | "accessor-pairs": "error",
13 | "array-bracket-newline": "error",
14 | "array-bracket-spacing": "error",
15 | "array-callback-return": "error",
16 | "array-element-newline": [
17 | "error",
18 | "consistent"
19 | ],
20 | "arrow-body-style": "error",
21 | "arrow-parens": ["error", "as-needed"],
22 | "arrow-spacing": "error",
23 | "block-scoped-var": "error",
24 | "block-spacing": "error",
25 | "brace-style": "off",
26 | "callback-return": "error",
27 | "camelcase": "error",
28 | "capitalized-comments": [
29 | "off",
30 | "never"
31 | ],
32 | "class-methods-use-this": "off",
33 | "comma-dangle": "error",
34 | "comma-spacing": [
35 | "error",
36 | {
37 | "after": true,
38 | "before": false
39 | }
40 | ],
41 | "comma-style": "error",
42 | "complexity": "off",
43 | "computed-property-spacing": "error",
44 | "consistent-return": "error",
45 | "consistent-this": "error",
46 | "curly": "error",
47 | "default-case": "error",
48 | "dot-location": "error",
49 | "dot-notation": "error",
50 | "eol-last": "error",
51 | "eqeqeq": "off",
52 | "func-call-spacing": "error",
53 | "func-name-matching": "error",
54 | "func-names": "off",
55 | "func-style": [
56 | "error",
57 | "declaration"
58 | ],
59 | "function-paren-newline": "off",
60 | "generator-star-spacing": "error",
61 | "global-require": "error",
62 | "guard-for-in": "error",
63 | "handle-callback-err": "error",
64 | "id-blacklist": "error",
65 | "id-length": "off",
66 | "id-match": "error",
67 | "implicit-arrow-linebreak": "error",
68 | "indent": "off",
69 | "indent-legacy": "off",
70 | "init-declarations": "error",
71 | "jsx-quotes": "error",
72 | "key-spacing": "error",
73 | "keyword-spacing": [
74 | "error",
75 | {
76 | "after": true,
77 | "before": true
78 | }
79 | ],
80 | "line-comment-position": "error",
81 | "linebreak-style": "off",
82 | "lines-around-comment": ["error",
83 | { "beforeBlockComment": false }
84 | ],
85 | "lines-around-directive": "error",
86 | "lines-between-class-members": [
87 | "error",
88 | "always"
89 | ],
90 | "max-classes-per-file": "off",
91 | "max-depth": "off",
92 | "max-len": "off",
93 | "max-lines": ["error", { "skipComments": true, "skipBlankLines": true }],
94 | "max-lines-per-function": "off",
95 | "max-nested-callbacks": "error",
96 | "max-params": "off",
97 | "max-statements": "off",
98 | "max-statements-per-line": "error",
99 | "multiline-comment-style": [
100 | "error",
101 | "separate-lines"
102 | ],
103 | "multiline-ternary": "off",
104 | "new-cap": "error",
105 | "new-parens": "error",
106 | "newline-after-var": [
107 | "error",
108 | "always"
109 | ],
110 | "newline-before-return": "error",
111 | "newline-per-chained-call": "error",
112 | "no-alert": "error",
113 | "no-array-constructor": "error",
114 | "no-async-promise-executor": "error",
115 | "no-await-in-loop": "error",
116 | "no-bitwise": "error",
117 | "no-buffer-constructor": "error",
118 | "no-caller": "error",
119 | "no-catch-shadow": "error",
120 | "no-confusing-arrow": [
121 | "error",
122 | {
123 | "allowParens": true
124 | }
125 | ],
126 | "no-continue": "off",
127 | "no-div-regex": "error",
128 | "no-duplicate-imports": [
129 | "error",
130 | {
131 | "includeExports": false
132 | }
133 | ],
134 | "no-else-return": "off",
135 | "no-empty-function": "error",
136 | "no-eq-null": "error",
137 | "no-eval": "error",
138 | "no-extend-native": "error",
139 | "no-extra-bind": "error",
140 | "no-extra-label": "error",
141 | "no-extra-parens": "off",
142 | "no-floating-decimal": "off",
143 | "no-global-assign": "error",
144 | "no-implicit-coercion": "error",
145 | "no-implicit-globals": "error",
146 | "no-implied-eval": "error",
147 | "no-inline-comments": "error",
148 | "no-invalid-this": "error",
149 | "no-iterator": "error",
150 | "no-label-var": "error",
151 | "no-labels": "error",
152 | "no-lone-blocks": "error",
153 | "no-lonely-if": "error",
154 | "no-loop-func": "error",
155 | "no-magic-numbers": "off",
156 | "no-misleading-character-class": "error",
157 | "no-mixed-operators": [
158 | "error",
159 | {
160 | "allowSamePrecedence": true
161 | }
162 | ],
163 | "no-mixed-requires": "error",
164 | "no-multi-assign": "error",
165 | "no-multi-spaces": "error",
166 | "no-multi-str": "error",
167 | "no-multiple-empty-lines": "error",
168 | "no-native-reassign": "error",
169 | "no-negated-condition": "off",
170 | "no-negated-in-lhs": "error",
171 | "no-nested-ternary": "error",
172 | "no-new": "error",
173 | "no-new-func": "error",
174 | "no-new-object": "error",
175 | "no-new-require": "error",
176 | "no-new-wrappers": "error",
177 | "no-octal-escape": "error",
178 | "no-param-reassign": "error",
179 | "no-path-concat": "error",
180 | "no-plusplus": "off",
181 | "no-process-env": "error",
182 | "no-process-exit": "error",
183 | "no-proto": "error",
184 | "no-prototype-builtins": "error",
185 | "no-restricted-globals": "error",
186 | "no-restricted-imports": "error",
187 | "no-restricted-modules": "error",
188 | "no-restricted-properties": "error",
189 | "no-restricted-syntax": "error",
190 | "no-return-assign": "error",
191 | "no-return-await": "error",
192 | "no-script-url": "error",
193 | "no-self-compare": "error",
194 | "no-sequences": "error",
195 | "no-shadow": "error",
196 | "no-shadow-restricted-names": "error",
197 | "no-spaced-func": "error",
198 | "no-sync": "error",
199 | "no-tabs": "off",
200 | "no-template-curly-in-string": "error",
201 | "no-ternary": "off",
202 | "no-throw-literal": "error",
203 | "no-trailing-spaces": "error",
204 | "no-undef-init": "error",
205 | "no-undefined": "off",
206 | "no-underscore-dangle": "error",
207 | "no-unmodified-loop-condition": "error",
208 | "no-unneeded-ternary": "error",
209 | "no-unused-expressions": "error",
210 | "no-unused-vars": ["error", {"args": "none"}],
211 | "no-use-before-define": "off",
212 | "no-useless-call": "error",
213 | "no-useless-computed-key": "error",
214 | "no-useless-concat": "error",
215 | "no-useless-constructor": "error",
216 | "no-useless-rename": "error",
217 | "no-useless-return": "error",
218 | "no-var": "error",
219 | "no-void": "error",
220 | "no-warning-comments": "error",
221 | "no-whitespace-before-property": "error",
222 | "no-with": "error",
223 | "nonblock-statement-body-position": "error",
224 | "object-curly-newline": "error",
225 | "object-curly-spacing": "off",
226 | "object-property-newline": [
227 | "error",
228 | { "allowAllPropertiesOnSameLine": true }
229 | ],
230 | "object-shorthand": [2, "properties"],
231 | "one-var": "off",
232 | "one-var-declaration-per-line": "error",
233 | "operator-assignment": [
234 | "error",
235 | "always"
236 | ],
237 | "operator-linebreak": [
238 | "error",
239 | "after",
240 | { "overrides": { "?": "ignore", ":": "ignore" } }
241 | ],
242 | "padded-blocks": "off",
243 | "padding-line-between-statements": "error",
244 | "prefer-arrow-callback": "error",
245 | "prefer-const": "error",
246 | "prefer-destructuring": "error",
247 | "prefer-numeric-literals": "error",
248 | "prefer-object-spread": "off",
249 | "prefer-promise-reject-errors": "error",
250 | "prefer-reflect": "off",
251 | "prefer-rest-params": "error",
252 | "prefer-spread": "error",
253 | "prefer-template": "off",
254 | "quote-props": "off",
255 | "quotes": [
256 | "error",
257 | "double"
258 | ],
259 | "radix": "error",
260 | "require-atomic-updates": "error",
261 | "require-await": "error",
262 | "require-jsdoc": "off",
263 | "require-unicode-regexp": "error",
264 | "rest-spread-spacing": "error",
265 | "semi": "error",
266 | "semi-spacing": [
267 | "error",
268 | {
269 | "after": true,
270 | "before": false
271 | }
272 | ],
273 | "semi-style": [
274 | "error",
275 | "last"
276 | ],
277 | "sort-imports": "off",
278 | "sort-keys": "off",
279 | "sort-vars": "off",
280 | "space-before-blocks": "error",
281 | "space-before-function-paren": "off",
282 | "space-in-parens": [
283 | "error",
284 | "never"
285 | ],
286 | "space-infix-ops": "error",
287 | "space-unary-ops": "error",
288 | "spaced-comment": "error",
289 | "strict": "error",
290 | "switch-colon-spacing": "error",
291 | "symbol-description": "error",
292 | "template-curly-spacing": "error",
293 | "template-tag-spacing": "error",
294 | "unicode-bom": [
295 | "error",
296 | "never"
297 | ],
298 | "valid-jsdoc": [2, {
299 | "prefer": {
300 | "return": "returns"
301 | },
302 | "requireReturn": false,
303 | "requireReturnDescription": false
304 | }],
305 | "vars-on-top": "error",
306 | "wrap-regex": "error",
307 | "yield-star-spacing": "error",
308 | "yoda": [
309 | "error",
310 | "never"
311 | ]
312 | }
313 | };
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master, main, develop, bug/**, chore/**, feature/**, release/** ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master, main, develop, bug/**, chore/**, feature/**, release/** ]
20 | schedule:
21 | - cron: '44 14 * * 2'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v2
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: GitHub Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-20.04
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v2
16 |
17 | - name: Set up node
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: '16'
21 | cache: 'npm'
22 |
23 | - name: Install
24 | run: npm ci
25 |
26 | - name: Build docs
27 | run: npm run build:docs
28 |
29 | - name: Deploy
30 | uses: peaceiris/actions-gh-pages@v3
31 | with:
32 | github_token: ${{ secrets.GITHUB_TOKEN }}
33 | publish_dir: ./docs
34 |
35 | - name: Run coverage
36 | run: npm run test:coverage
37 |
38 | - name: Upload coverage to Codecov
39 | uses: codecov/codecov-action@v2
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /coverage
3 | /dist
4 | /lib
5 | /docs
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.2.0](https://github.com/fwextensions/quick-score/releases/tag/v0.2.0) - 2022-6-03
4 |
5 | ### Fixed
6 |
7 | - Improve TypeScript compatibility by typing the `QuickScore` array params as `readonly`.
8 | - Type the `item` key in both `ScoredString` and `ScoredObject` as a generic so `ScoredResult` is more consistent.
9 | - Update 'devDependencies' to the latest major/minor versions, including Jest 28.
10 |
11 |
12 | ## [0.1.0](https://github.com/fwextensions/quick-score/releases/tag/v0.1.0) - 2022-04-19
13 |
14 | ### Added
15 |
16 | - Add TypeScript support via `index.d.ts` declaration file, and add `types` field to `package.json`.
17 | - Update JSDocs to include type information.
18 |
19 | ### Changed
20 |
21 | - Don't concat the parameters to `setItems()` and `setKeys()` with an empty array. Call `.slice()` instead to create a copy. This means that passing a single bare string instead of an array to the `QuickScore` constructor will no longer work.
22 | - Don't supply a default value for the `sortKey` parameter in `setKeys()`.
23 | - Don't supply default values for the `string` and `query` parameters in `quickScore()`.
24 |
25 | ### Fixed
26 |
27 | - Resolve #19 and #20.
28 | - Remove empty PR trigger in `gh-pages.yml`.
29 | - Improve API docs styling.
30 | - Update `devDependencies` to the latest minor versions.
31 |
32 |
33 | ## [0.0.14](https://github.com/fwextensions/quick-score/releases/tag/v0.0.14) - 2022-02-24
34 |
35 | ### Fixed
36 |
37 | - Update `devDependencies` to the latest versions.
38 | - Add GitHub action to push docs to GitHub Pages and code coverage to Codecov.
39 |
40 |
41 | ## [0.0.13](https://github.com/fwextensions/quick-score/releases/tag/v0.0.13) - 2021-10-05
42 |
43 | ### Fixed
44 |
45 | - Update `devDependencies` to the latest minor versions.
46 | - Run npm audit fix to remove vulnerabilities.
47 | - Update .travis.yml to enable partner queue builds.
48 | - Add GitHub code analysis workflow.
49 |
50 |
51 | ## [0.0.12](https://github.com/fwextensions/quick-score/releases/tag/v0.0.12) - 2021-04-24
52 |
53 | ### Fixed
54 |
55 | - Limit the number of loops inside the `quickScore()` function so that long, nearly-matching queries don't take too long before returning a 0 score. Added `config.maxIterations` to control the number of loops.
56 | - Update `devDependencies` to latest packages.
57 |
58 |
59 | ## [0.0.11](https://github.com/fwextensions/quick-score/releases/tag/v0.0.11) - 2021-03-26
60 |
61 | ### Added
62 |
63 | - Passing an empty array in the `keys` parameter will cause all of the keys on an item to be cached and searched, without having to specify each one.
64 | - Paths to nested keys in the `keys` array can be specified as arrays of strings, instead of a dot-delimited path in a single string. Wrapping a single string in an array will cause any dots it contains to not be treated as a path.
65 | - A new `sortKey` option can be used to specify on which key to sort identically-scored items, if a key other than the first one in `keys` is desired.
66 | - A new `scoreValue` field is returned in the results from `search()`, which provides the string pointed to be `scoreKey`. This makes it easier to access the string when it's nested.
67 |
68 |
69 | ## [0.0.10](https://github.com/fwextensions/quick-score/releases/tag/v0.0.10) - 2021-01-02
70 |
71 | ### Added
72 |
73 | - A new `transformString` option to the `QuickScore` constructor can be used to ignore diacritics and accents when searching.
74 |
75 |
76 | ### Fixed
77 |
78 | - Update `devDependencies` to latest packages, fixing a vulnerability in jest.
79 |
80 |
81 | ## [0.0.9](https://github.com/fwextensions/quick-score/releases/tag/v0.0.9) - 2020-07-25
82 |
83 | ### Fixed
84 |
85 | - Update `devDependencies` to latest packages.
86 |
87 |
88 | ## [0.0.8](https://github.com/fwextensions/quick-score/releases/tag/v0.0.8) - 2020-05-07
89 |
90 | ### Fixed
91 |
92 | - Use the correct unpkg.com CDN URL in the readme.
93 | - Highlight needing to access the methods through a global when loading the library via a `
32 |
35 | ```
36 |
37 |
38 | ## Usage
39 |
40 | ### Calling `quickScore()` directly
41 |
42 | You can import the [`quickScore()`](https://fwextensions.github.io/quick-score/global.html#quickScore) function from the ES6 module:
43 |
44 | ```js
45 | import {quickScore} from "quick-score";
46 | ```
47 |
48 | Or from a property of the CommonJS module:
49 |
50 | ```js
51 | const quickScore = require("quick-score").quickScore;
52 | ```
53 |
54 | Then call `quickScore()` with a `string` and a `query` to score against that string. It will return a floating point score between `0` and `1`. A higher score means that string is a better match for the query. A `1` means the query is the highest match for the string, though the two strings may still differ in case and whitespace characters.
55 |
56 | ```js
57 | quickScore("thought", "gh"); // 0.4142857142857143
58 | quickScore("GitHub", "gh"); // 0.9166666666666666
59 | ```
60 |
61 | Matching `gh` against `GitHub` returns a higher score than `thought`, because it matches the capital letters in `GitHub`, which are weighted more highly.
62 |
63 |
64 | ### Sorting lists of strings with a `QuickScore` instance
65 |
66 | A typical use-case for string scoring is auto-completion, where you want the user to get to the desired result by typing as few characters as possible. Instead of calling `quickScore()` directly for every item in a list and then sorting it based on the score, it's simpler to use an instance of the [`QuickScore`](https://fwextensions.github.io/quick-score/QuickScore.html) class:
67 |
68 | ```js
69 | import {QuickScore} from "quick-score";
70 |
71 | const qs = new QuickScore(["thought", "giraffe", "GitHub", "hello, Garth"]);
72 | const results = qs.search("gh");
73 |
74 | // results =>
75 | [
76 | {
77 | "item": "GitHub",
78 | "score": 0.9166666666666666,
79 | "matches": [[0, 1], [3, 4]]
80 | },
81 | {
82 | "item": "hello, Garth",
83 | "score": 0.6263888888888888,
84 | "matches": [[7, 8], [11, 12]]
85 | },
86 | // ...
87 | ]
88 | ```
89 |
90 | The `results` array in this example is a list of [ScoredString](https://fwextensions.github.io/quick-score/global.html#ScoredString) objects that represent the results of matching the query against each string that was passed to the constructor. It's sorted high to low on each item's score. Strings with identical scores are sorted alphabetically and case-insensitively. In the simple case of scoring bare strings, each `ScoredString` item has three properties:
91 |
92 | * `item`: the string that was scored
93 | * `score`: the floating point score of the string for the current query
94 | * `matches`: an array of arrays that specify the character ranges where the query matched the string
95 |
96 | This array could then be used to render a list of matching results as the user types a query.
97 |
98 |
99 | ### Sorting lists of objects
100 |
101 | Typically, you'll be sorting items more complex than a bare string. To tell QuickScore which of an object's keys to score a query against, pass an array of key names or dot-delimited paths as the second parameter to the `QuickScore()` constructor:
102 |
103 | ```js
104 | const bookmarks = [
105 | {
106 | "title": "lodash documentation",
107 | "url": "https://lodash.com/docs"
108 | },
109 | {
110 | "title": "Supplying Images - Google Chrome",
111 | "url": "developer.chrome.com/webstore/images"
112 | },
113 | // ...
114 | ];
115 | const qs = new QuickScore(bookmarks, ["title", "url"]);
116 | const results = qs.search("devel");
117 |
118 | // results =>
119 | [
120 | {
121 | "item": {
122 | "title": "Supplying Images - Google Chrome",
123 | "url": "developer.chrome.com/webstore/images"
124 | },
125 | "score": 0.9138888888888891,
126 | "scoreKey": "url",
127 | "scores": {
128 | "title": 0,
129 | "url": 0.9138888888888891
130 | },
131 | "matches": {
132 | "title": [],
133 | "url": [[0, 5]]
134 | }
135 | },
136 | // ...
137 | ]
138 | ```
139 |
140 | When matching against objects, each item in the results array is a [ScoredObject](https://fwextensions.github.io/quick-score/global.html#ScoredObject), with a few additional properties :
141 |
142 | * `item`: the object that was scored
143 | * `score`: the highest score from among the individual key scores
144 | * `scoreKey`: the name of the key with the highest score, which will be an empty string if they're all zero
145 | * `scoreValue`: the value of the key with the highest score, which makes it easier to access if it's a nested string
146 | * `scores`: a hash of the individual scores for each key
147 | * `matches`: a hash of arrays that specify the character ranges of the query match for each key
148 |
149 | When two items have the same score, they're sorted alphabetically and case-insensitively on the key specified by the `sortKey` option, which defaults to the first item in the keys array. In the example above, that would be `title`.
150 |
151 | Each `ScoredObject` item also has a `_` property, which caches transformed versions of the item's strings, and might contain additional internal metadata in the future. It can be ignored.
152 |
153 |
154 | ### TypeScript support
155 |
156 | Although the QuickScore codebase is currently written in JavaScript, the package comes with full TypeScript typings. The QuickScore class takes a generic type parameter based on the type of objects in the `items` array passed to the constructor. That way, you can access `.item` on the `ScoredObject` result and get back an object of the same type that you passed in.
157 |
158 |
159 | ### Ignoring diacritics and accents when scoring
160 |
161 | If the strings you're matching against contain diacritics on some of the letters, like `à` or `ç`, you may want to count a match even when the query string contains the unaccented forms of those letters. The QuickScore library doesn't contain support for this by default, since it's only needed with certain strings and the code to remove accents would triple its size. But it's easy to combine QuickScore with other libraries to ignore diacritics.
162 |
163 | One example is the [latinize](https://github.com/dundalek/latinize) [npm package](https://www.npmjs.com/package/latinize), which will strip accents from a string and can be used in a `transformString()` function that's passed as an option to the [QuickScore constructor](https://fwextensions.github.io/quick-score/QuickScore.html#QuickScore). This function takes a `string` parameter and returns a transformed version of that string:
164 |
165 | ```js
166 | // including latinize.js on the page creates a global latinize() function
167 | import {QuickScore} from "quick-score";
168 |
169 | const items = ["Café", "Cafeteria"];
170 | const qs = new QuickScore(items, { transformString: s => latinize(s).toLowerCase() });
171 | const results = qs.search("cafe");
172 |
173 | // results =>
174 | [
175 | {
176 | "item": "Café",
177 | "score": 1,
178 | "matches": [[0, 4]],
179 | "_": "cafe"
180 | },
181 | // ...
182 | ]
183 | ```
184 |
185 | `transformString()` will be called on each of the searchable keys in the `items` array as well as on the `query` parameter to the `search()` method. The default function calls `toLocaleLowerCase()` on each string, for a case-insensitive search. In the example above, the basic `toLowerCase()` call is sufficient, since `latinize()` will have already stripped any accents.
186 |
187 |
188 | ### Highlighting matched letters
189 |
190 | Many search interfaces highlight the letters in each item that match what the user has typed. The `matches` property of each item in the results array contains information that can be used to highlight those matching letters.
191 |
192 | The functional component below is an example of how an item could be highlighted using React. It surrounds each sequence of matching letters in a `` tag and then returns the full string in a ``. You could then style the `` tag to be bold or a different color to highlight the matches. (Something similar could be done by concatenating plain strings of HTML tags, though you'll need to be careful to escape the substrings.)
193 |
194 | ```jsx
195 | function MatchedString({ string, matches }) {
196 | const substrings = [];
197 | let previousEnd = 0;
198 |
199 | for (let [start, end] of matches) {
200 | const prefix = string.substring(previousEnd, start);
201 | const match = {string.substring(start, end)};
202 |
203 | substrings.push(prefix, match);
204 | previousEnd = end;
205 | }
206 |
207 | substrings.push(string.substring(previousEnd));
208 |
209 | return {React.Children.toArray(substrings)};
210 | }
211 | ```
212 |
213 | The [QuickScore demo](https://fwextensions.github.io/quick-score-demo/) uses this approach to highlight the query matches, via the [MatchedString](https://github.com/fwextensions/quick-score-demo/blob/master/src/js/MatchedString.js) component.
214 |
215 |
216 | ## API
217 |
218 | See the [API docs](https://fwextensions.github.io/quick-score/) for a full description of the [QuickScore class](https://fwextensions.github.io/quick-score/QuickScore.html) and the [quickScore function](https://fwextensions.github.io/quick-score/global.html#quickScore).
219 |
220 |
221 | ## License
222 |
223 | [MIT](./LICENSE) © [John Dunning](https://github.com/fwextensions)
224 |
225 |
226 | [build-badge]: https://github.com/fwextensions/quick-score/actions/workflows/gh-pages.yml/badge.svg?style=flat-square
227 | [build]: https://github.com/fwextensions/quick-score/actions/workflows/gh-pages.yml
228 | [coverage-badge]: https://img.shields.io/codecov/c/github/fwextensions/quick-score.svg?style=flat-square
229 | [coverage]: https://codecov.io/gh/fwextensions/quick-score
230 | [dependencies-badge]: https://img.shields.io/hackage-deps/v/quick-score?style=flat-square
231 | [dependencies]: https://www.npmjs.com/package/quick-score
232 | [license-badge]: https://img.shields.io/npm/l/quick-score.svg?style=flat-square
233 | [license]: https://github.com/fwextensions/quick-score/blob/master/LICENSE
234 | [size-badge]: https://img.shields.io/bundlephobia/minzip/quick-score.svg?style=flat-square
235 | [size]: https://www.npmjs.com/package/quick-score
236 | [package-badge]: https://img.shields.io/npm/v/quick-score.svg?style=flat-square
237 | [package]: https://www.npmjs.com/package/quick-score
238 |
--------------------------------------------------------------------------------
/jsdoc/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["plugins/markdown"],
3 | "source": {
4 | "include": "src"
5 | },
6 | "templates": {
7 | "default": {
8 | "layoutFile": "jsdoc/layout.tmpl",
9 | "staticFiles": {
10 | "include": [
11 | "jsdoc/static",
12 | "LICENSE"
13 | ]
14 | }
15 | }
16 | },
17 | "opts": {
18 | "destination": "docs",
19 | "readme": "README.md"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/jsdoc/layout.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | JSDoc:
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/jsdoc/static/styles/overrides.css:
--------------------------------------------------------------------------------
1 | h1, h2, h3, h4, h5 {
2 | font-weight: bold;
3 | letter-spacing: normal;
4 | }
5 |
6 | h1 {
7 | font-size: 3rem;
8 | }
9 |
10 | h2 {
11 | font-size: 2rem;
12 | }
13 |
14 | h3, h3.subsection-title {
15 | font-size: 1.75rem;
16 | letter-spacing: normal;
17 | }
18 |
19 | h4 {
20 | font-size: 1.5rem;
21 | }
22 |
23 | h4.name {
24 | font-size: 1.65rem;
25 | letter-spacing: -0.03rem;
26 | margin-bottom: .5rem;
27 | }
28 |
29 | h5, .container-overview .subsection-title {
30 | font-size: 1.15rem;
31 | color: #777 ;
32 | }
33 |
34 | h6 {
35 | font-size: 1rem;
36 | font-weight: normal;
37 | }
38 |
39 | ul {
40 | margin-block-start: .5rem;
41 | }
42 |
43 | .params code, .description code, .param-desc code {
44 | font-weight: bold;
45 | }
46 |
47 | .prettyprint {
48 | tab-size: 4;
49 | }
50 |
51 | .prettyprint.linenums li {
52 | line-height: 1.4em;
53 | }
54 |
55 | .prettyprint code {
56 | padding: 0 12px;
57 | }
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quick-score",
3 | "version": "0.2.0",
4 | "description": "A JavaScript string-scoring and fuzzy-matching library based on the Quicksilver algorithm, designed for smart auto-complete.",
5 | "keywords": [
6 | "string",
7 | "score",
8 | "sort",
9 | "search",
10 | "fuzzy",
11 | "filter",
12 | "quicksilver",
13 | "autocomplete",
14 | "auto-complete",
15 | "filter list",
16 | "intuitive sort",
17 | "smart sort"
18 | ],
19 | "license": "MIT",
20 | "author": "John Dunning (https://github.com/fwextensions)",
21 | "repository": "github:fwextensions/quick-score",
22 | "homepage": "https://fwextensions.github.io/quick-score-demo",
23 | "bugs": "https://github.com/fwextensions/quick-score/issues",
24 | "main": "lib/index.js",
25 | "module": "lib/index.esm.js",
26 | "types": "lib/index.d.ts",
27 | "files": [
28 | "dist",
29 | "lib"
30 | ],
31 | "scripts": {
32 | "prebuild": "npm run test:coverage",
33 | "build": "npm run build:lib && npm run build:docs",
34 | "build:lib": "rimraf dist lib && rollup -c",
35 | "build:dts": "rimraf dist-dts && tsc",
36 | "build:docs": "rimraf docs && jsdoc -c jsdoc/config.json",
37 | "prepare": "npm run build:lib",
38 | "pretest": "eslint src",
39 | "test": "jest",
40 | "test:watch": "npm test -- --watch",
41 | "test:coverage": "npm test -- --coverage"
42 | },
43 | "babel": {
44 | "env": {
45 | "test": {
46 | "presets": [
47 | "@babel/preset-env"
48 | ]
49 | }
50 | }
51 | },
52 | "jest": {
53 | "testEnvironment": "node",
54 | "collectCoverageFrom": [
55 | "src/*.js",
56 | "!src/index.js"
57 | ],
58 | "setupFilesAfterEnv": [
59 | "./test/setup.js"
60 | ]
61 | },
62 | "devDependencies": {
63 | "@babel/core": "^7.18.2",
64 | "@babel/preset-env": "^7.18.2",
65 | "eslint": "^8.16.0",
66 | "jest": "^28.1.0",
67 | "jsdoc": "^3.6.10",
68 | "rimraf": "^3.0.2",
69 | "rollup": "^2.75.5",
70 | "rollup-plugin-babel": "^4.4.0",
71 | "rollup-plugin-babel-minify": "^10.0.0",
72 | "rollup-plugin-dts": "^4.2.2",
73 | "rollup-plugin-node-resolve": "^5.2.0",
74 | "typescript": "^4.7.3"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 | import minify from "rollup-plugin-babel-minify";
3 | import dts from "rollup-plugin-dts";
4 |
5 |
6 | const Input = "src/index.js";
7 | const OutputESM = (dir, filename, minified) => ({
8 | file: `${dir}/${filename ? filename : "index"}.esm${minified ? ".min" : ""}.js`,
9 | format: "esm"
10 | });
11 | const OutputUMD = (dir, filename, minified) => ({
12 | file: `${dir}/${filename ? filename : "quick-score"}${minified ? ".min" : ""}.js`,
13 | format: "umd",
14 | exports: "named",
15 | name: "quickScore"
16 | });
17 | const BabelConfig = {
18 | exclude: "**/node_modules/**",
19 | // tell babel to not transform modules, so that rollup can do it
20 | presets: [
21 | ["@babel/preset-env", { modules: false }]
22 | ]
23 | };
24 |
25 |
26 | export default [
27 | {
28 | input: Input,
29 | output: OutputESM("lib")
30 | },
31 | {
32 | input: Input,
33 | output: OutputUMD("lib", "index")
34 | },
35 | {
36 | input: Input,
37 | output: OutputUMD("dist"),
38 | plugins: [
39 | babel(BabelConfig)
40 | ]
41 | },
42 | {
43 | input: Input,
44 | output: OutputUMD("dist", "", true),
45 | plugins: [
46 | babel(BabelConfig),
47 | minify({
48 | comments: false
49 | })
50 | ]
51 | },
52 | {
53 | input: Input,
54 | output: OutputESM("dist", "quick-score", true),
55 | plugins: [
56 | babel(BabelConfig),
57 | minify({
58 | comments: false
59 | })
60 | ]
61 | },
62 | {
63 | input: "./src/index.d.ts",
64 | output: [
65 | { file: "dist/index.d.ts", format: "es" },
66 | { file: "lib/index.d.ts", format: "es" }
67 | ],
68 | plugins: [dts()]
69 | }
70 | ];
71 |
--------------------------------------------------------------------------------
/src/QuickScore.js:
--------------------------------------------------------------------------------
1 | import {quickScore} from "./quick-score";
2 |
3 |
4 | /**
5 | * A class for scoring and sorting a list of items against a query string. Each
6 | * item receives a floating point score between `0` and `1`.
7 | */
8 | export class QuickScore {
9 | /**
10 | * @memberOf QuickScore.prototype
11 | * @member {Array