├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── slate.sh
└── source
├── fonts
├── slate.eot
├── slate.svg
├── slate.ttf
├── slate.woff
└── slate.woff2
├── images
├── favicon.ico
├── logo.png
└── navbar.png
├── includes
├── _code_challenges.md
├── _errors.md
├── _users.md
└── _webhooks.md
├── index.html.md
├── javascripts
├── all.js
├── all_nosearch.js
├── app
│ ├── _copy.js
│ ├── _lang.js
│ ├── _search.js
│ └── _toc.js
└── lib
│ ├── _energize.js
│ ├── _imagesloaded.min.js
│ ├── _jquery.highlight.js
│ ├── _jquery.js
│ └── _lunr.js
├── layouts
└── layout.erb
└── stylesheets
├── _icon-font.scss
├── _normalize.scss
├── _rtl.scss
├── _variables.scss
├── print.css.scss
└── screen.css.scss
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | # Create the build directory before binding so that directory is not owned by root.
12 | - run: mkdir -p build
13 | - run: ./slate.sh
14 | - run: echo "dev.codewars.com" > ./build/CNAME
15 | - uses: peaceiris/actions-gh-pages@v3
16 | with:
17 | github_token: ${{ secrets.GITHUB_TOKEN }}
18 | publish_dir: ./build
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | coverage
6 | InstalledFiles
7 | lib/bundler/man
8 | pkg
9 | rdoc
10 | spec/reports
11 | test/tmp
12 | test/version_tmp
13 | tmp
14 | *.DS_STORE
15 | build/
16 | .cache
17 |
18 | # YARD artifacts
19 | .yardoc
20 | _yardoc
21 | doc/
22 | .idea/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Codewars API Docs
2 |
--------------------------------------------------------------------------------
/slate.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Exit on error and error on unbound variable.
3 | set -eu
4 |
5 | function usage() {
6 | if [ -n "$1" ]; then
7 | echo -e "$1\n";
8 | fi
9 |
10 | echo "Usage: $0 [--serve] [--port port]";
11 | echo " -s, --serve Start dev server. Build and exit without this.";
12 | echo " -p, --port The port for the dev server. Defaults to 4567";
13 | echo "";
14 | echo "Examples:";
15 | echo " $0";
16 | echo " $0 --serve --port 8888";
17 | exit 1;
18 | }
19 |
20 | PORT=4567
21 | while [[ "$#" > 0 ]]; do case $1 in
22 | -s|--serve) SERVE="1"; shift;;
23 | -p|--port) PORT="$2"; shift; shift;;
24 | *) usage "Unknown parameter: $1"; shift; shift;;
25 | esac; done
26 |
27 | if [ -z "${SERVE:-}" ]; then
28 | echo "Building...";
29 | docker run \
30 | --rm \
31 | -v $(pwd)/build:/srv/slate/build \
32 | -v $(pwd)/source:/srv/slate/source \
33 | slatedocs/slate:v2.9.2
34 | else
35 | echo "Starting dev server...";
36 | docker run \
37 | --rm \
38 | -v $(pwd)/build:/srv/slate/build \
39 | -v $(pwd)/source:/srv/slate/source \
40 | -p $PORT:4567 \
41 | slatedocs/slate:v2.9.2 \
42 | serve
43 | fi
44 |
--------------------------------------------------------------------------------
/source/fonts/slate.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codewars/codewars-api-docs/f3478173877bd12ccc415e61e8b02afaba309ea4/source/fonts/slate.eot
--------------------------------------------------------------------------------
/source/fonts/slate.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/source/fonts/slate.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codewars/codewars-api-docs/f3478173877bd12ccc415e61e8b02afaba309ea4/source/fonts/slate.ttf
--------------------------------------------------------------------------------
/source/fonts/slate.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codewars/codewars-api-docs/f3478173877bd12ccc415e61e8b02afaba309ea4/source/fonts/slate.woff
--------------------------------------------------------------------------------
/source/fonts/slate.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codewars/codewars-api-docs/f3478173877bd12ccc415e61e8b02afaba309ea4/source/fonts/slate.woff2
--------------------------------------------------------------------------------
/source/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codewars/codewars-api-docs/f3478173877bd12ccc415e61e8b02afaba309ea4/source/images/favicon.ico
--------------------------------------------------------------------------------
/source/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codewars/codewars-api-docs/f3478173877bd12ccc415e61e8b02afaba309ea4/source/images/logo.png
--------------------------------------------------------------------------------
/source/images/navbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codewars/codewars-api-docs/f3478173877bd12ccc415e61e8b02afaba309ea4/source/images/navbar.png
--------------------------------------------------------------------------------
/source/includes/_code_challenges.md:
--------------------------------------------------------------------------------
1 | # Code Challenges API
2 |
3 | ## Get Code Challenge
4 |
5 | Return a code challenge information.
6 |
7 | ```bash
8 | curl https://www.codewars.com/api/v1/code-challenges/valid-braces
9 | ```
10 |
11 | > Response
12 |
13 | ```json
14 | {
15 | "id": "5277c8a221e209d3f6000b56",
16 | "name": "Valid Braces",
17 | "slug": "valid-braces",
18 | "url": "http://www.codewars.com/kata/valid-braces",
19 | "category": "algorithms",
20 | "description": "Write a function called `validBraces` that takes a string ...",
21 | "tags": ["Algorithms", "Validation", "Logic", "Utilities"],
22 | "languages": ["javascript", "coffeescript"],
23 | "rank": {
24 | "id": -4,
25 | "name": "4 kyu",
26 | "color": "blue"
27 | },
28 | "createdBy": {
29 | "username": "xDranik",
30 | "url": "http://www.codewars.com/users/xDranik"
31 | },
32 | "approvedBy": {
33 | "username": "xDranik",
34 | "url": "http://www.codewars.com/users/xDranik"
35 | },
36 | "totalAttempts": 4911,
37 | "totalCompleted": 919,
38 | "totalStars": 12,
39 | "voteScore": 512,
40 | "publishedAt": "2013-11-05T00:07:31Z",
41 | "approvedAt": "2013-12-20T14:53:06Z"
42 | }
43 | ```
44 |
45 |
46 | ### HTTP Request
47 |
48 | `https://www.codewars.com/api/v1/code-challenges/{challenge}`
49 |
50 | ### Path Parameters
51 |
52 | | Parameter | Description |
53 | | ----------- | -------------- |
54 | | `challenge` | ID or slug |
55 |
56 |
57 | ### CodeChallenge Object
58 |
59 | | Field | Type | Description |
60 | | -------------------- | ------------- | --------------------------------------- |
61 | | `id` | `string` | ID of the kata. |
62 | | `name` | `string` | Name of the kata. |
63 | | `slug` | `string` | Slug of the kata. |
64 | | `url` | `string` | URL of the kata. |
65 | | `category` | `string` | Category of the kata. |
66 | | `description` | `string` | Description of the kata in Markdown. |
67 | | `tags` | `string[]` | Array of tags associated with the kata. |
68 | | `languages` | `string[]` | Array of language names the kata is available in. |
69 | | `rank` | `object?` | Object describing the rank of the kata if approved. |
70 | | `createdBy` | `object` | The author of the kata. |
71 | | `publishedAt` | `string` | Date and time when the kata was first published. |
72 | | `approvedBy` | `object?` | The approver of the kata. |
73 | | `approvedAt` | `string` | Date and time when the kata was approved. |
74 | | `totalCompleted` | `number` | Total number of completions. |
75 | | `totalAttempts` | `number` | Total number of attempts. |
76 | | `totalStars` | `number` | The number of bookmarks. |
77 | | `voteScore` | `number` | The sum of all votes casted. |
78 | | `contributorsWanted` | `boolean` | Whether to allow contributions. |
79 | | `unresolved` | `object` | Object with fields `issues` and `suggestions` for the number of unresolved issues and suggestions respectively. |
80 |
81 | ### User Object
82 |
83 | | Field | Type | Description |
84 | | -------------------- | ------------- | --------------------------------------- |
85 | | `username` | `string` | Username of the user. |
86 | | `url` | `string` | URL of the user's profile. |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/source/includes/_errors.md:
--------------------------------------------------------------------------------
1 | # Errors
2 |
3 | Codewars uses conventional HTTP response codes to indicate success or failure of an API request.
4 | In general, codes in the 2xx range indicate success, codes in the 4xx range indicate an error that resulted from
5 | the provided information (e.g. a required parameter was missing, a charge failed, etc.), and codes in the
6 | 5xx range indicate an error with Codewars' servers.
7 |
8 |
9 | Error Code | Meaning
10 | ---------- | -------
11 | 400 | Bad Request -- Something went wrong
12 | 401 | Unauthorized -- Your API key is wrong
13 | 403 | Forbidden -- You do not have permission to access this resource
14 | 404 | Not Found -- The specified resource could not be found
15 | 405 | Method Not Allowed -- You tried to access a resource with an invalid method
16 | 406 | Not Acceptable -- You requested a format that isn't json
17 | 422 | Unprocessable Entity -- Your input failed validation.
18 | 429 | Too Many Requests -- You're making too many API requests.
19 | 500 | Internal Server Error -- We had a problem with our server. Try again later.
20 | 503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later.
21 |
--------------------------------------------------------------------------------
/source/includes/_users.md:
--------------------------------------------------------------------------------
1 | # Users API
2 |
3 | ## Get User
4 |
5 | Returns a user information.
6 |
7 | ```bash
8 | curl https://www.codewars.com/api/v1/users/some_user
9 | ```
10 |
11 | > Response
12 |
13 | ```json
14 | {
15 | "username": "some_user",
16 | "name": "Some Person",
17 | "honor": 544,
18 | "clan": "some clan",
19 | "leaderboardPosition": 134,
20 | "skills": [
21 | "ruby",
22 | "c#",
23 | ".net",
24 | "javascript",
25 | "coffeescript",
26 | "nodejs",
27 | "rails"
28 | ],
29 | "ranks": {
30 | "overall": {
31 | "rank": -3,
32 | "name": "3 kyu",
33 | "color": "blue",
34 | "score": 2116
35 | },
36 | "languages": {
37 | "javascript": {
38 | "rank": -3,
39 | "name": "3 kyu",
40 | "color": "blue",
41 | "score": 1819
42 | },
43 | "ruby": {
44 | "rank": -4,
45 | "name": "4 kyu",
46 | "color": "blue",
47 | "score": 1005
48 | },
49 | "coffeescript": {
50 | "rank": -4,
51 | "name": "4 kyu",
52 | "color": "blue",
53 | "score": 870
54 | }
55 | }
56 | },
57 | "codeChallenges": {
58 | "totalAuthored": 3,
59 | "totalCompleted": 230
60 | }
61 | }
62 | ```
63 |
64 | ### HTTP Request
65 |
66 | `https://www.codewars.com/api/v1/users/{user}`
67 |
68 | ### Path Parameters
69 |
70 | | Parameter | Description |
71 | | --------- | -------------- |
72 | | `user` | Username or ID |
73 |
74 | ### User Object
75 |
76 | | Field | Type | Description |
77 | | --------------------- | ---------------------- | ----------------------------------------------- |
78 | | `username` | `string` | Username of the user. |
79 | | `name` | `string` | Name of the user. |
80 | | `honor` | `number` | Total honor points earned by the user. |
81 | | `clan` | `string` | Name of the clan. |
82 | | `leaderboardPosition` | `number` | The user's position on the overall leaderboard. |
83 | | `skills` | `string[]` | Array of skills entered by the user. |
84 | | `ranks` | `object` | Ranks object with overall and language ranks. |
85 | | `codeChallenges` | `object` | Object with fields `totalAuthored` and `totalCompleted` for the number of authored and completed kata respectively. |
86 |
87 | ### Ranks Object
88 |
89 | | Field | Type | Description |
90 | | --------------------- | ---------------------- | --------------------------------------------- |
91 | | `overall` | `object` | Overall rank. |
92 | | `languages` | `object` | Ranks for each language trained. |
93 |
94 | ### Rank Object
95 |
96 | | Field | Type | Description |
97 | | --------------------- | ---------------------- | -------------------------------------------------------------- |
98 | | `rank` | `number` | Rank in integer. `[-8, -1]` maps to kyu, `[1, 8]` maps to dan. |
99 | | `name` | `string` | Either `{-rank} kyu` or `{rank} dan`. |
100 | | `color` | `string` | The color of the rank. Possible colors are `white` (7-8 kyu), `yellow` (5-6 kyu), `blue` (3-4 kyu), `purple` (1-2 kyu), `black` (1-4 dan), and `red` (5-8 dan). |
101 | | `score` | `number` | The total score earned. This is the number that determines the rank. |
102 |
103 |
104 | ## List Completed Challenges
105 |
106 | Lists challenges completed by a user, 200 items per page. Use `page` parameter (zero based) to paginate.
107 |
108 | ```bash
109 | curl http://www.codewars.com/api/v1/users/some_user/code-challenges/completed?page=0
110 | ```
111 |
112 | > Response
113 |
114 | ```json
115 | {
116 | "totalPages": 1,
117 | "totalItems": 1,
118 | "data": [
119 | {
120 | "id": "514b92a657cdc65150000006",
121 | "name": "Multiples of 3 and 5",
122 | "slug": "multiples-of-3-and-5",
123 | "completedAt": "2017-04-06T16:32:09Z",
124 | "completedLanguages": [
125 | "javascript",
126 | "coffeescript",
127 | "ruby",
128 | "javascript",
129 | "ruby",
130 | "javascript",
131 | "ruby",
132 | "coffeescript",
133 | "javascript",
134 | "ruby",
135 | "coffeescript"
136 | ]
137 | }
138 | ]
139 | }
140 | ```
141 |
142 |
143 | ### HTTP Request
144 |
145 | `https://www.codewars.com/api/v1/users/{user}/code-challenges/completed?page={page}`
146 |
147 | ### Path Parameters
148 |
149 | | Parameter | Description |
150 | | --------- | -------------- |
151 | | `user` | Username or ID |
152 |
153 | ### Query Parameters
154 |
155 | | Parameter | Description | Default |
156 | | --------- | ------------------------------------------------------ | ------- |
157 | | `page` | The page offset. Each page contains at most 200 items. | 0 |
158 |
159 | ### CompletedChallenge Object
160 |
161 | | Field | Type | Description |
162 | | -------------------- | ----------- | --------------------------------------- |
163 | | `id` | `string` | ID of the kata. |
164 | | `name` | `string` | Name of the kata. |
165 | | `slug` | `string` | Slug of the kata. |
166 | | `completedAt` | `string` | Date and time of the completion. |
167 | | `completedLanguages` | `string[]` | Array of languages a user completed in. |
168 |
169 |
170 |
171 | ## List Authored Challenges
172 |
173 | List challenges authored by the user.
174 |
175 | ```bash
176 | curl http://www.codewars.com/api/v1/users/some_user/code-challenges/authored
177 | ```
178 |
179 | > Response
180 |
181 | ```json
182 | {
183 | "data": [
184 | {
185 | "id": "5571d9fc11526780a000011a",
186 | "name": "The builder of things",
187 | "description": "For this kata you will be using some meta-programming ...",
188 | "rank": -3,
189 | "rankName": "3 kyu",
190 | "tags": [
191 | "Algorithms",
192 | "Metaprogramming",
193 | "Programming Paradigms",
194 | "Advanced Language Features",
195 | "Fundamentals",
196 | "Domain Specific Languages",
197 | "Declarative Programming"
198 | ],
199 | "languages": ["ruby", "javascript", "python", "coffeescript"]
200 | },
201 | {
202 | "id": "51ba717bb08c1cd60f00002f",
203 | "name": "Range Extraction",
204 | "description": "A format for expressing an ordered list of integers ...",
205 | "rank": -4,
206 | "rankName": "4 kyu",
207 | "tags": [
208 | "Algorithms",
209 | "String Formatting",
210 | "Formatting",
211 | "Logic",
212 | "Strings"
213 | ],
214 | "languages": [
215 | "javascript",
216 | "coffeescript",
217 | "ruby",
218 | "go",
219 | "python",
220 | "java",
221 | "haskell",
222 | "csharp",
223 | "cpp"
224 | ]
225 | }
226 | ]
227 | }
228 | ```
229 |
230 | ### HTTP Request
231 |
232 | `https://www.codewars.com/api/v1/users/{user}/code-challenges/authored`
233 |
234 | ### Path Parameters
235 |
236 | | Parameter | Description |
237 | | --------- | -------------- |
238 | | `user` | Username or ID |
239 |
240 | ### AuthoredChallenge Object
241 |
242 | | Field | Type | Description |
243 | | -------------------- | ------------- | --------------------------------------- |
244 | | `id` | `string` | ID of the kata. |
245 | | `name` | `string` | Name of the kata. |
246 | | `description` | `string` | Description of the kata in Markdown. |
247 | | `rank` | `number?` | Rank of the kata if approved. |
248 | | `rankName` | `string?` | Rank name of the kata if approved. |
249 | | `tags` | `string[]` | Array of tags associated with the kata. |
250 | | `languages` | `string[]` | Array of language names the kata is available in. |
251 |
252 |
--------------------------------------------------------------------------------
/source/includes/_webhooks.md:
--------------------------------------------------------------------------------
1 | # Webhooks
2 |
3 | Webhooks allows you to receive notifications when events occur.
4 |
5 | Like API v1, webhooks feature was never actively developed and poorly documented.
6 | It's not very usable at the moment.
7 |
8 | ## Structure
9 |
10 | > Sample Event (Code Challenge Created)
11 |
12 | ```
13 | User-Agent: Codewars Hookbot
14 | Content-Type: application/json
15 | X-Webhook-Event: code_challenge
16 | X-Webhook-Secret: some-shared-secret
17 | ```
18 |
19 | ```json
20 | {
21 | "action": "created",
22 | "code_challenge": {
23 | "id": "50654ddff44f800200000001",
24 | "created_by_id": "508f2708b3be0c0200000002"
25 | }
26 | }
27 | ```
28 |
29 |
30 | When an event occurs in the Codewars system, any relevant webhooks will be triggered to the specified URL. Typically,
31 | events are categorized into **event** and **actions**. Typically an event references *what type of model*
32 | (i.e. `code_challenge`) and the action references what happened to it (i.e. `created`).
33 |
34 |
35 |
38 |
39 | Webhooks typically contain a very small payload (often times, it only contains the `id` of some object). You may need
40 | to query the rest of the API to get more information about a particular object.
41 |
42 | ## Register your Webhook
43 |
44 | > Webhook Ping Event
45 |
46 | ```
47 | User-Agent: Codewars Hookbot
48 | Content-Type: application/json
49 | X-Webhook-Event: webhook
50 | ```
51 | ```json
52 | {
53 | "action": "updated",
54 | "webhook": {
55 | "id": "53aa3f265b97485984000001"
56 | }
57 | }
58 | ```
59 |
60 |
61 | Visit your [Codewars account page](https://www.codewars.com/users/edit) and add a webhook. A webhook takes the
62 | following input:
63 |
64 | | Input | Meaning |
65 | | ----- | ------- |
66 | | Payload URL | The server endpoint that will relieve the webhook payload (e.g, `https://example.com/my/endpoint`) |
67 | | Secret | An optional secret shared between you and our webhook service. Ensures only Codewars is sending you the webhook |
68 |
69 | Once you create or update your webhook, your endpoint will receive a webhook updated event.
70 |
71 | ## Code Challenges
72 |
73 | > Sample Webhook Payload
74 |
75 | ```
76 | User-Agent: Codewars Hookbot
77 | Content-Type: application/json
78 | X-Webhook-Event: code_challenge
79 | ```
80 |
81 | ```json
82 | {
83 | "action": "",
84 | "code_challenge": {
85 | "id": "53aa3f265b97485984000001",
86 | "created_by_id": "53af25145b97487568000001"
87 | }
88 | }
89 | ```
90 | > The solution_finalized also includes the following json:
91 |
92 | ```json
93 | {
94 | "solution": {
95 | "id": "53aa3f265b97485984000001",
96 | "user_id": "53417de006654f4171000587"
97 | }
98 | }
99 | ```
100 |
101 | The following actions are supported:
102 |
103 | | Action | Meaning |
104 | | -------------------- | ------- |
105 | | `created` | Code challenge was created |
106 | | `approved` | A code challenge was successfully approved (no longer in beta state) |
107 | | `voted` | Someone voted on the code challenge. Does not specify what type of vote. |
108 | | `solution_finalized` | Someone submitted a solution to the code challenge |
109 |
110 |
111 | ## User
112 |
113 |
114 | > Webhook Headers
115 |
116 | ```
117 | User-Agent: Codewars Hookbot
118 | Content-Type: application/json
119 | X-Webhook-Event: user
120 | ```
121 |
122 | > `rank_earned` Event
123 |
124 | ```json
125 | {
126 | "action": "rank_earned",
127 | "user": {
128 | "id": "53aa3f265b97485984000001",
129 | "rank": -5
130 | },
131 | "language": null
132 | }
133 | ```
134 |
135 | > `honor_changed` Event
136 |
137 | ```json
138 | {
139 | "action": "honor_changed",
140 | "user": {
141 | "id": "53aa3f265b97485984000001",
142 | "honor": 420,
143 | "honor_delta": 3
144 | }
145 | }
146 | ```
147 |
148 | The following actions are supported:
149 |
150 | | Action | Meaning |
151 | | --------------- | ------- |
152 | | `rank_earned` | The user's rank has been upgraded. Could be a global rank, or a language rank |
153 | | `honor_changed` | The user's honor has changed (usually in a positive direction) |
154 |
--------------------------------------------------------------------------------
/source/index.html.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Codewars API Reference
3 |
4 | language_tabs:
5 | - shell
6 |
7 | toc_footers:
8 | - codewars.com
9 | - github.com/codewars
10 |
11 | includes:
12 | - users
13 | - code_challenges
14 | - webhooks
15 | - errors
16 |
17 | search: true
18 | ---
19 |
20 | # Introduction
21 |
22 | Codewars API v1 is minimal and inconsistent. It was never actively developed.
23 |
24 | API v2 is planned, but there's no ETA at the moment.
25 |
26 | ## Authentication
27 |
28 | Not required. API v1 endpoints are all public.
29 |
30 | ## Content-Type
31 |
32 | API v1 endpoints responds with JSON (`application/json`).
33 |
--------------------------------------------------------------------------------
/source/javascripts/all.js:
--------------------------------------------------------------------------------
1 | //= require ./all_nosearch
2 | //= require ./app/_search
3 |
--------------------------------------------------------------------------------
/source/javascripts/all_nosearch.js:
--------------------------------------------------------------------------------
1 | //= require ./lib/_energize
2 | //= require ./app/_copy
3 | //= require ./app/_toc
4 | //= require ./app/_lang
5 |
6 | function adjustLanguageSelectorWidth() {
7 | const elem = $('.dark-box > .lang-selector');
8 | elem.width(elem.parent().width());
9 | }
10 |
11 | $(function() {
12 | loadToc($('#toc'), '.toc-link', '.toc-list-h2', 10);
13 | setupLanguages($('body').data('languages'));
14 | $('.content').imagesLoaded( function() {
15 | window.recacheHeights();
16 | window.refreshToc();
17 | });
18 |
19 | $(window).resize(function() {
20 | adjustLanguageSelectorWidth();
21 | });
22 | adjustLanguageSelectorWidth();
23 | });
24 |
25 | window.onpopstate = function() {
26 | activateLanguage(getLanguageFromQueryString());
27 | };
28 |
--------------------------------------------------------------------------------
/source/javascripts/app/_copy.js:
--------------------------------------------------------------------------------
1 | function copyToClipboard(container) {
2 | const el = document.createElement('textarea');
3 | el.value = container.textContent.replace(/\n$/, '');
4 | document.body.appendChild(el);
5 | el.select();
6 | document.execCommand('copy');
7 | document.body.removeChild(el);
8 | }
9 |
10 | function setupCodeCopy() {
11 | $('pre.highlight').prepend('');
12 | $('.copy-clipboard').on('click', function() {
13 | copyToClipboard(this.parentNode.children[1]);
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/source/javascripts/app/_lang.js:
--------------------------------------------------------------------------------
1 | //= require ../lib/_jquery
2 |
3 | /*
4 | Copyright 2008-2013 Concur Technologies, Inc.
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may
7 | not use this file except in compliance with the License. You may obtain
8 | a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | License for the specific language governing permissions and limitations
16 | under the License.
17 | */
18 | ;(function () {
19 | 'use strict';
20 |
21 | var languages = [];
22 |
23 | window.setupLanguages = setupLanguages;
24 | window.activateLanguage = activateLanguage;
25 | window.getLanguageFromQueryString = getLanguageFromQueryString;
26 |
27 | function activateLanguage(language) {
28 | if (!language) return;
29 | if (language === "") return;
30 |
31 | $(".lang-selector a").removeClass('active');
32 | $(".lang-selector a[data-language-name='" + language + "']").addClass('active');
33 | for (var i=0; i < languages.length; i++) {
34 | $(".highlight.tab-" + languages[i]).hide();
35 | $(".lang-specific." + languages[i]).hide();
36 | }
37 | $(".highlight.tab-" + language).show();
38 | $(".lang-specific." + language).show();
39 |
40 | window.recacheHeights();
41 |
42 | // scroll to the new location of the position
43 | if ($(window.location.hash).get(0)) {
44 | $(window.location.hash).get(0).scrollIntoView(true);
45 | }
46 | }
47 |
48 | // parseURL and stringifyURL are from https://github.com/sindresorhus/query-string
49 | // MIT licensed
50 | // https://github.com/sindresorhus/query-string/blob/7bee64c16f2da1a326579e96977b9227bf6da9e6/license
51 | function parseURL(str) {
52 | if (typeof str !== 'string') {
53 | return {};
54 | }
55 |
56 | str = str.trim().replace(/^(\?|#|&)/, '');
57 |
58 | if (!str) {
59 | return {};
60 | }
61 |
62 | return str.split('&').reduce(function (ret, param) {
63 | var parts = param.replace(/\+/g, ' ').split('=');
64 | var key = parts[0];
65 | var val = parts[1];
66 |
67 | key = decodeURIComponent(key);
68 | // missing `=` should be `null`:
69 | // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
70 | val = val === undefined ? null : decodeURIComponent(val);
71 |
72 | if (!ret.hasOwnProperty(key)) {
73 | ret[key] = val;
74 | } else if (Array.isArray(ret[key])) {
75 | ret[key].push(val);
76 | } else {
77 | ret[key] = [ret[key], val];
78 | }
79 |
80 | return ret;
81 | }, {});
82 | };
83 |
84 | function stringifyURL(obj) {
85 | return obj ? Object.keys(obj).sort().map(function (key) {
86 | var val = obj[key];
87 |
88 | if (Array.isArray(val)) {
89 | return val.sort().map(function (val2) {
90 | return encodeURIComponent(key) + '=' + encodeURIComponent(val2);
91 | }).join('&');
92 | }
93 |
94 | return encodeURIComponent(key) + '=' + encodeURIComponent(val);
95 | }).join('&') : '';
96 | };
97 |
98 | // gets the language set in the query string
99 | function getLanguageFromQueryString() {
100 | if (location.search.length >= 1) {
101 | var language = parseURL(location.search).language;
102 | if (language) {
103 | return language;
104 | } else if (jQuery.inArray(location.search.substr(1), languages) != -1) {
105 | return location.search.substr(1);
106 | }
107 | }
108 |
109 | return false;
110 | }
111 |
112 | // returns a new query string with the new language in it
113 | function generateNewQueryString(language) {
114 | var url = parseURL(location.search);
115 | if (url.language) {
116 | url.language = language;
117 | return stringifyURL(url);
118 | }
119 | return language;
120 | }
121 |
122 | // if a button is clicked, add the state to the history
123 | function pushURL(language) {
124 | if (!history) { return; }
125 | var hash = window.location.hash;
126 | if (hash) {
127 | hash = hash.replace(/^#+/, '');
128 | }
129 | history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash);
130 |
131 | // save language as next default
132 | if (localStorage) {
133 | localStorage.setItem("language", language);
134 | }
135 | }
136 |
137 | function setupLanguages(l) {
138 | var defaultLanguage = null;
139 | if (localStorage) {
140 | defaultLanguage = localStorage.getItem("language");
141 | }
142 |
143 | languages = l;
144 |
145 | var presetLanguage = getLanguageFromQueryString();
146 | if (presetLanguage) {
147 | // the language is in the URL, so use that language!
148 | activateLanguage(presetLanguage);
149 |
150 | if (localStorage) {
151 | localStorage.setItem("language", presetLanguage);
152 | }
153 | } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) {
154 | // the language was the last selected one saved in localstorage, so use that language!
155 | activateLanguage(defaultLanguage);
156 | } else {
157 | // no language selected, so use the default
158 | activateLanguage(languages[0]);
159 | }
160 | }
161 |
162 | // if we click on a language tab, activate that language
163 | $(function() {
164 | $(".lang-selector a").on("click", function() {
165 | var language = $(this).data("language-name");
166 | pushURL(language);
167 | activateLanguage(language);
168 | return false;
169 | });
170 | });
171 | })();
172 |
--------------------------------------------------------------------------------
/source/javascripts/app/_search.js:
--------------------------------------------------------------------------------
1 | //= require ../lib/_lunr
2 | //= require ../lib/_jquery
3 | //= require ../lib/_jquery.highlight
4 | ;(function () {
5 | 'use strict';
6 |
7 | var content, searchResults;
8 | var highlightOpts = { element: 'span', className: 'search-highlight' };
9 | var searchDelay = 0;
10 | var timeoutHandle = 0;
11 | var index;
12 |
13 | function populate() {
14 | index = lunr(function(){
15 |
16 | this.ref('id');
17 | this.field('title', { boost: 10 });
18 | this.field('body');
19 | this.pipeline.add(lunr.trimmer, lunr.stopWordFilter);
20 | var lunrConfig = this;
21 |
22 | $('h1, h2').each(function() {
23 | var title = $(this);
24 | var body = title.nextUntil('h1, h2');
25 | lunrConfig.add({
26 | id: title.prop('id'),
27 | title: title.text(),
28 | body: body.text()
29 | });
30 | });
31 |
32 | });
33 | determineSearchDelay();
34 | }
35 |
36 | $(populate);
37 | $(bind);
38 |
39 | function determineSearchDelay() {
40 | if (index.tokenSet.toArray().length>5000) {
41 | searchDelay = 300;
42 | }
43 | }
44 |
45 | function bind() {
46 | content = $('.content');
47 | searchResults = $('.search-results');
48 |
49 | $('#input-search').on('keyup',function(e) {
50 | var wait = function() {
51 | return function(executingFunction, waitTime){
52 | clearTimeout(timeoutHandle);
53 | timeoutHandle = setTimeout(executingFunction, waitTime);
54 | };
55 | }();
56 | wait(function(){
57 | search(e);
58 | }, searchDelay);
59 | });
60 | }
61 |
62 | function search(event) {
63 |
64 | var searchInput = $('#input-search')[0];
65 |
66 | unhighlight();
67 | searchResults.addClass('visible');
68 |
69 | // ESC clears the field
70 | if (event.keyCode === 27) searchInput.value = '';
71 |
72 | if (searchInput.value) {
73 | var results = index.search(searchInput.value).filter(function(r) {
74 | return r.score > 0.0001;
75 | });
76 |
77 | if (results.length) {
78 | searchResults.empty();
79 | $.each(results, function (index, result) {
80 | var elem = document.getElementById(result.ref);
81 | searchResults.append("
");
82 | });
83 | highlight.call(searchInput);
84 | } else {
85 | searchResults.html('');
86 | $('.search-results li').text('No Results Found for "' + searchInput.value + '"');
87 | }
88 | } else {
89 | unhighlight();
90 | searchResults.removeClass('visible');
91 | }
92 | }
93 |
94 | function highlight() {
95 | if (this.value) content.highlight(this.value, highlightOpts);
96 | }
97 |
98 | function unhighlight() {
99 | content.unhighlight(highlightOpts);
100 | }
101 | })();
102 |
103 |
--------------------------------------------------------------------------------
/source/javascripts/app/_toc.js:
--------------------------------------------------------------------------------
1 | //= require ../lib/_jquery
2 | //= require ../lib/_imagesloaded.min
3 | ;(function () {
4 | 'use strict';
5 |
6 | var htmlPattern = /<[^>]*>/g;
7 | var loaded = false;
8 |
9 | var debounce = function(func, waitTime) {
10 | var timeout = false;
11 | return function() {
12 | if (timeout === false) {
13 | setTimeout(function() {
14 | func();
15 | timeout = false;
16 | }, waitTime);
17 | timeout = true;
18 | }
19 | };
20 | };
21 |
22 | var closeToc = function() {
23 | $(".toc-wrapper").removeClass('open');
24 | $("#nav-button").removeClass('open');
25 | };
26 |
27 | function loadToc($toc, tocLinkSelector, tocListSelector, scrollOffset) {
28 | var headerHeights = {};
29 | var pageHeight = 0;
30 | var windowHeight = 0;
31 | var originalTitle = document.title;
32 |
33 | var recacheHeights = function() {
34 | headerHeights = {};
35 | pageHeight = $(document).height();
36 | windowHeight = $(window).height();
37 |
38 | $toc.find(tocLinkSelector).each(function() {
39 | var targetId = $(this).attr('href');
40 | if (targetId[0] === "#") {
41 | headerHeights[targetId] = $("#" + $.escapeSelector(targetId.substring(1))).offset().top;
42 | }
43 | });
44 | };
45 |
46 | var refreshToc = function() {
47 | var currentTop = $(document).scrollTop() + scrollOffset;
48 |
49 | if (currentTop + windowHeight >= pageHeight) {
50 | // at bottom of page, so just select last header by making currentTop very large
51 | // this fixes the problem where the last header won't ever show as active if its content
52 | // is shorter than the window height
53 | currentTop = pageHeight + 1000;
54 | }
55 |
56 | var best = null;
57 | for (var name in headerHeights) {
58 | if ((headerHeights[name] < currentTop && headerHeights[name] > headerHeights[best]) || best === null) {
59 | best = name;
60 | }
61 | }
62 |
63 | // Catch the initial load case
64 | if (currentTop == scrollOffset && !loaded) {
65 | best = window.location.hash;
66 | loaded = true;
67 | }
68 |
69 | var $best = $toc.find("[href='" + best + "']").first();
70 | if (!$best.hasClass("active")) {
71 | // .active is applied to the ToC link we're currently on, and its parent
s selected by tocListSelector
72 | // .active-expanded is applied to the ToC links that are parents of this one
73 | $toc.find(".active").removeClass("active");
74 | $toc.find(".active-parent").removeClass("active-parent");
75 | $best.addClass("active");
76 | $best.parents(tocListSelector).addClass("active").siblings(tocLinkSelector).addClass('active-parent');
77 | $best.siblings(tocListSelector).addClass("active");
78 | $toc.find(tocListSelector).filter(":not(.active)").slideUp(150);
79 | $toc.find(tocListSelector).filter(".active").slideDown(150);
80 | if (window.history.replaceState) {
81 | window.history.replaceState(null, "", best);
82 | }
83 | var thisTitle = $best.data("title");
84 | if (thisTitle !== undefined && thisTitle.length > 0) {
85 | document.title = thisTitle.replace(htmlPattern, "") + " – " + originalTitle;
86 | } else {
87 | document.title = originalTitle;
88 | }
89 | }
90 | };
91 |
92 | var makeToc = function() {
93 | recacheHeights();
94 | refreshToc();
95 |
96 | $("#nav-button").click(function() {
97 | $(".toc-wrapper").toggleClass('open');
98 | $("#nav-button").toggleClass('open');
99 | return false;
100 | });
101 | $(".page-wrapper").click(closeToc);
102 | $(".toc-link").click(closeToc);
103 |
104 | // reload immediately after scrolling on toc click
105 | $toc.find(tocLinkSelector).click(function() {
106 | setTimeout(function() {
107 | refreshToc();
108 | }, 0);
109 | });
110 |
111 | $(window).scroll(debounce(refreshToc, 200));
112 | $(window).resize(debounce(recacheHeights, 200));
113 | };
114 |
115 | makeToc();
116 |
117 | window.recacheHeights = recacheHeights;
118 | window.refreshToc = refreshToc;
119 | }
120 |
121 | window.loadToc = loadToc;
122 | })();
123 |
--------------------------------------------------------------------------------
/source/javascripts/lib/_energize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * energize.js v0.1.0
3 | *
4 | * Speeds up click events on mobile devices.
5 | * https://github.com/davidcalhoun/energize.js
6 | */
7 |
8 | (function() { // Sandbox
9 | /**
10 | * Don't add to non-touch devices, which don't need to be sped up
11 | */
12 | if(!('ontouchstart' in window)) return;
13 |
14 | var lastClick = {},
15 | isThresholdReached, touchstart, touchmove, touchend,
16 | click, closest;
17 |
18 | /**
19 | * isThresholdReached
20 | *
21 | * Compare touchstart with touchend xy coordinates,
22 | * and only fire simulated click event if the coordinates
23 | * are nearby. (don't want clicking to be confused with a swipe)
24 | */
25 | isThresholdReached = function(startXY, xy) {
26 | return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5;
27 | };
28 |
29 | /**
30 | * touchstart
31 | *
32 | * Save xy coordinates when the user starts touching the screen
33 | */
34 | touchstart = function(e) {
35 | this.startXY = [e.touches[0].clientX, e.touches[0].clientY];
36 | this.threshold = false;
37 | };
38 |
39 | /**
40 | * touchmove
41 | *
42 | * Check if the user is scrolling past the threshold.
43 | * Have to check here because touchend will not always fire
44 | * on some tested devices (Kindle Fire?)
45 | */
46 | touchmove = function(e) {
47 | // NOOP if the threshold has already been reached
48 | if(this.threshold) return false;
49 |
50 | this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]);
51 | };
52 |
53 | /**
54 | * touchend
55 | *
56 | * If the user didn't scroll past the threshold between
57 | * touchstart and touchend, fire a simulated click.
58 | *
59 | * (This will fire before a native click)
60 | */
61 | touchend = function(e) {
62 | // Don't fire a click if the user scrolled past the threshold
63 | if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) {
64 | return;
65 | }
66 |
67 | /**
68 | * Create and fire a click event on the target element
69 | * https://developer.mozilla.org/en/DOM/event.initMouseEvent
70 | */
71 | var touch = e.changedTouches[0],
72 | evt = document.createEvent('MouseEvents');
73 | evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
74 | evt.simulated = true; // distinguish from a normal (nonsimulated) click
75 | e.target.dispatchEvent(evt);
76 | };
77 |
78 | /**
79 | * click
80 | *
81 | * Because we've already fired a click event in touchend,
82 | * we need to listed for all native click events here
83 | * and suppress them as necessary.
84 | */
85 | click = function(e) {
86 | /**
87 | * Prevent ghost clicks by only allowing clicks we created
88 | * in the click event we fired (look for e.simulated)
89 | */
90 | var time = Date.now(),
91 | timeDiff = time - lastClick.time,
92 | x = e.clientX,
93 | y = e.clientY,
94 | xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)],
95 | target = closest(e.target, 'A') || e.target, // needed for standalone apps
96 | nodeName = target.nodeName,
97 | isLink = nodeName === 'A',
98 | standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href");
99 |
100 | lastClick.time = time;
101 | lastClick.x = x;
102 | lastClick.y = y;
103 |
104 | /**
105 | * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire),
106 | * so we have to add more logic to determine the time of the last click. Not perfect...
107 | *
108 | * Older, simpler check: if((!e.simulated) || standAlone)
109 | */
110 | if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) {
111 | e.preventDefault();
112 | e.stopPropagation();
113 | if(!standAlone) return false;
114 | }
115 |
116 | /**
117 | * Special logic for standalone web apps
118 | * See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window
119 | */
120 | if(standAlone) {
121 | window.location = target.getAttribute("href");
122 | }
123 |
124 | /**
125 | * Add an energize-focus class to the targeted link (mimics :focus behavior)
126 | * TODO: test and/or remove? Does this work?
127 | */
128 | if(!target || !target.classList) return;
129 | target.classList.add("energize-focus");
130 | window.setTimeout(function(){
131 | target.classList.remove("energize-focus");
132 | }, 150);
133 | };
134 |
135 | /**
136 | * closest
137 | * @param {HTMLElement} node current node to start searching from.
138 | * @param {string} tagName the (uppercase) name of the tag you're looking for.
139 | *
140 | * Find the closest ancestor tag of a given node.
141 | *
142 | * Starts at node and goes up the DOM tree looking for a
143 | * matching nodeName, continuing until hitting document.body
144 | */
145 | closest = function(node, tagName){
146 | var curNode = node;
147 |
148 | while(curNode !== document.body) { // go up the dom until we find the tag we're after
149 | if(!curNode || curNode.nodeName === tagName) { return curNode; } // found
150 | curNode = curNode.parentNode; // not found, so keep going up
151 | }
152 |
153 | return null; // not found
154 | };
155 |
156 | /**
157 | * Add all delegated event listeners
158 | *
159 | * All the events we care about bubble up to document,
160 | * so we can take advantage of event delegation.
161 | *
162 | * Note: no need to wait for DOMContentLoaded here
163 | */
164 | document.addEventListener('touchstart', touchstart, false);
165 | document.addEventListener('touchmove', touchmove, false);
166 | document.addEventListener('touchend', touchend, false);
167 | document.addEventListener('click', click, true); // TODO: why does this use capture?
168 |
169 | })();
--------------------------------------------------------------------------------
/source/javascripts/lib/_imagesloaded.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * imagesLoaded PACKAGED v4.1.4
3 | * JavaScript is all like "You images are done yet or what?"
4 | * MIT License
5 | */
6 |
7 | !function(e,t){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",t):"object"==typeof module&&module.exports?module.exports=t():e.EvEmitter=t()}("undefined"!=typeof window?window:this,function(){function e(){}var t=e.prototype;return t.on=function(e,t){if(e&&t){var i=this._events=this._events||{},n=i[e]=i[e]||[];return n.indexOf(t)==-1&&n.push(t),this}},t.once=function(e,t){if(e&&t){this.on(e,t);var i=this._onceEvents=this._onceEvents||{},n=i[e]=i[e]||{};return n[t]=!0,this}},t.off=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){var n=i.indexOf(t);return n!=-1&&i.splice(n,1),this}},t.emitEvent=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){i=i.slice(0),t=t||[];for(var n=this._onceEvents&&this._onceEvents[e],o=0;o (default options)
16 | * $('#content').highlight('lorem');
17 | *
18 | * // search for and highlight more terms at once
19 | * // so you can save some time on traversing DOM
20 | * $('#content').highlight(['lorem', 'ipsum']);
21 | * $('#content').highlight('lorem ipsum');
22 | *
23 | * // search only for entire word 'lorem'
24 | * $('#content').highlight('lorem', { wordsOnly: true });
25 | *
26 | * // don't ignore case during search of term 'lorem'
27 | * $('#content').highlight('lorem', { caseSensitive: true });
28 | *
29 | * // wrap every occurrance of term 'ipsum' in content
30 | * // with
31 | * $('#content').highlight('ipsum', { element: 'em', className: 'important' });
32 | *
33 | * // remove default highlight
34 | * $('#content').unhighlight();
35 | *
36 | * // remove custom highlight
37 | * $('#content').unhighlight({ element: 'em', className: 'important' });
38 | *
39 | *
40 | * Copyright (c) 2009 Bartek Szopka
41 | *
42 | * Licensed under MIT license.
43 | *
44 | */
45 |
46 | jQuery.extend({
47 | highlight: function (node, re, nodeName, className) {
48 | if (node.nodeType === 3) {
49 | var match = node.data.match(re);
50 | if (match) {
51 | var highlight = document.createElement(nodeName || 'span');
52 | highlight.className = className || 'highlight';
53 | var wordNode = node.splitText(match.index);
54 | wordNode.splitText(match[0].length);
55 | var wordClone = wordNode.cloneNode(true);
56 | highlight.appendChild(wordClone);
57 | wordNode.parentNode.replaceChild(highlight, wordNode);
58 | return 1; //skip added node in parent
59 | }
60 | } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
61 | !/(script|style)/i.test(node.tagName) && // ignore script and style nodes
62 | !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted
63 | for (var i = 0; i < node.childNodes.length; i++) {
64 | i += jQuery.highlight(node.childNodes[i], re, nodeName, className);
65 | }
66 | }
67 | return 0;
68 | }
69 | });
70 |
71 | jQuery.fn.unhighlight = function (options) {
72 | var settings = { className: 'highlight', element: 'span' };
73 | jQuery.extend(settings, options);
74 |
75 | return this.find(settings.element + "." + settings.className).each(function () {
76 | var parent = this.parentNode;
77 | parent.replaceChild(this.firstChild, this);
78 | parent.normalize();
79 | }).end();
80 | };
81 |
82 | jQuery.fn.highlight = function (words, options) {
83 | var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false };
84 | jQuery.extend(settings, options);
85 |
86 | if (words.constructor === String) {
87 | words = [words];
88 | }
89 | words = jQuery.grep(words, function(word, i){
90 | return word != '';
91 | });
92 | words = jQuery.map(words, function(word, i) {
93 | return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
94 | });
95 | if (words.length == 0) { return this; };
96 |
97 | var flag = settings.caseSensitive ? "" : "i";
98 | var pattern = "(" + words.join("|") + ")";
99 | if (settings.wordsOnly) {
100 | pattern = "\\b" + pattern + "\\b";
101 | }
102 | var re = new RegExp(pattern, flag);
103 |
104 | return this.each(function () {
105 | jQuery.highlight(this, re, settings.element, settings.className);
106 | });
107 | };
108 |
109 |
--------------------------------------------------------------------------------
/source/javascripts/lib/_lunr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9
3 | * Copyright (C) 2020 Oliver Nightingale
4 | * @license MIT
5 | */
6 |
7 | ;(function(){
8 |
9 | /**
10 | * A convenience function for configuring and constructing
11 | * a new lunr Index.
12 | *
13 | * A lunr.Builder instance is created and the pipeline setup
14 | * with a trimmer, stop word filter and stemmer.
15 | *
16 | * This builder object is yielded to the configuration function
17 | * that is passed as a parameter, allowing the list of fields
18 | * and other builder parameters to be customised.
19 | *
20 | * All documents _must_ be added within the passed config function.
21 | *
22 | * @example
23 | * var idx = lunr(function () {
24 | * this.field('title')
25 | * this.field('body')
26 | * this.ref('id')
27 | *
28 | * documents.forEach(function (doc) {
29 | * this.add(doc)
30 | * }, this)
31 | * })
32 | *
33 | * @see {@link lunr.Builder}
34 | * @see {@link lunr.Pipeline}
35 | * @see {@link lunr.trimmer}
36 | * @see {@link lunr.stopWordFilter}
37 | * @see {@link lunr.stemmer}
38 | * @namespace {function} lunr
39 | */
40 | var lunr = function (config) {
41 | var builder = new lunr.Builder
42 |
43 | builder.pipeline.add(
44 | lunr.trimmer,
45 | lunr.stopWordFilter,
46 | lunr.stemmer
47 | )
48 |
49 | builder.searchPipeline.add(
50 | lunr.stemmer
51 | )
52 |
53 | config.call(builder, builder)
54 | return builder.build()
55 | }
56 |
57 | lunr.version = "2.3.9"
58 | /*!
59 | * lunr.utils
60 | * Copyright (C) 2020 Oliver Nightingale
61 | */
62 |
63 | /**
64 | * A namespace containing utils for the rest of the lunr library
65 | * @namespace lunr.utils
66 | */
67 | lunr.utils = {}
68 |
69 | /**
70 | * Print a warning message to the console.
71 | *
72 | * @param {String} message The message to be printed.
73 | * @memberOf lunr.utils
74 | * @function
75 | */
76 | lunr.utils.warn = (function (global) {
77 | /* eslint-disable no-console */
78 | return function (message) {
79 | if (global.console && console.warn) {
80 | console.warn(message)
81 | }
82 | }
83 | /* eslint-enable no-console */
84 | })(this)
85 |
86 | /**
87 | * Convert an object to a string.
88 | *
89 | * In the case of `null` and `undefined` the function returns
90 | * the empty string, in all other cases the result of calling
91 | * `toString` on the passed object is returned.
92 | *
93 | * @param {Any} obj The object to convert to a string.
94 | * @return {String} string representation of the passed object.
95 | * @memberOf lunr.utils
96 | */
97 | lunr.utils.asString = function (obj) {
98 | if (obj === void 0 || obj === null) {
99 | return ""
100 | } else {
101 | return obj.toString()
102 | }
103 | }
104 |
105 | /**
106 | * Clones an object.
107 | *
108 | * Will create a copy of an existing object such that any mutations
109 | * on the copy cannot affect the original.
110 | *
111 | * Only shallow objects are supported, passing a nested object to this
112 | * function will cause a TypeError.
113 | *
114 | * Objects with primitives, and arrays of primitives are supported.
115 | *
116 | * @param {Object} obj The object to clone.
117 | * @return {Object} a clone of the passed object.
118 | * @throws {TypeError} when a nested object is passed.
119 | * @memberOf Utils
120 | */
121 | lunr.utils.clone = function (obj) {
122 | if (obj === null || obj === undefined) {
123 | return obj
124 | }
125 |
126 | var clone = Object.create(null),
127 | keys = Object.keys(obj)
128 |
129 | for (var i = 0; i < keys.length; i++) {
130 | var key = keys[i],
131 | val = obj[key]
132 |
133 | if (Array.isArray(val)) {
134 | clone[key] = val.slice()
135 | continue
136 | }
137 |
138 | if (typeof val === 'string' ||
139 | typeof val === 'number' ||
140 | typeof val === 'boolean') {
141 | clone[key] = val
142 | continue
143 | }
144 |
145 | throw new TypeError("clone is not deep and does not support nested objects")
146 | }
147 |
148 | return clone
149 | }
150 | lunr.FieldRef = function (docRef, fieldName, stringValue) {
151 | this.docRef = docRef
152 | this.fieldName = fieldName
153 | this._stringValue = stringValue
154 | }
155 |
156 | lunr.FieldRef.joiner = "/"
157 |
158 | lunr.FieldRef.fromString = function (s) {
159 | var n = s.indexOf(lunr.FieldRef.joiner)
160 |
161 | if (n === -1) {
162 | throw "malformed field ref string"
163 | }
164 |
165 | var fieldRef = s.slice(0, n),
166 | docRef = s.slice(n + 1)
167 |
168 | return new lunr.FieldRef (docRef, fieldRef, s)
169 | }
170 |
171 | lunr.FieldRef.prototype.toString = function () {
172 | if (this._stringValue == undefined) {
173 | this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef
174 | }
175 |
176 | return this._stringValue
177 | }
178 | /*!
179 | * lunr.Set
180 | * Copyright (C) 2020 Oliver Nightingale
181 | */
182 |
183 | /**
184 | * A lunr set.
185 | *
186 | * @constructor
187 | */
188 | lunr.Set = function (elements) {
189 | this.elements = Object.create(null)
190 |
191 | if (elements) {
192 | this.length = elements.length
193 |
194 | for (var i = 0; i < this.length; i++) {
195 | this.elements[elements[i]] = true
196 | }
197 | } else {
198 | this.length = 0
199 | }
200 | }
201 |
202 | /**
203 | * A complete set that contains all elements.
204 | *
205 | * @static
206 | * @readonly
207 | * @type {lunr.Set}
208 | */
209 | lunr.Set.complete = {
210 | intersect: function (other) {
211 | return other
212 | },
213 |
214 | union: function () {
215 | return this
216 | },
217 |
218 | contains: function () {
219 | return true
220 | }
221 | }
222 |
223 | /**
224 | * An empty set that contains no elements.
225 | *
226 | * @static
227 | * @readonly
228 | * @type {lunr.Set}
229 | */
230 | lunr.Set.empty = {
231 | intersect: function () {
232 | return this
233 | },
234 |
235 | union: function (other) {
236 | return other
237 | },
238 |
239 | contains: function () {
240 | return false
241 | }
242 | }
243 |
244 | /**
245 | * Returns true if this set contains the specified object.
246 | *
247 | * @param {object} object - Object whose presence in this set is to be tested.
248 | * @returns {boolean} - True if this set contains the specified object.
249 | */
250 | lunr.Set.prototype.contains = function (object) {
251 | return !!this.elements[object]
252 | }
253 |
254 | /**
255 | * Returns a new set containing only the elements that are present in both
256 | * this set and the specified set.
257 | *
258 | * @param {lunr.Set} other - set to intersect with this set.
259 | * @returns {lunr.Set} a new set that is the intersection of this and the specified set.
260 | */
261 |
262 | lunr.Set.prototype.intersect = function (other) {
263 | var a, b, elements, intersection = []
264 |
265 | if (other === lunr.Set.complete) {
266 | return this
267 | }
268 |
269 | if (other === lunr.Set.empty) {
270 | return other
271 | }
272 |
273 | if (this.length < other.length) {
274 | a = this
275 | b = other
276 | } else {
277 | a = other
278 | b = this
279 | }
280 |
281 | elements = Object.keys(a.elements)
282 |
283 | for (var i = 0; i < elements.length; i++) {
284 | var element = elements[i]
285 | if (element in b.elements) {
286 | intersection.push(element)
287 | }
288 | }
289 |
290 | return new lunr.Set (intersection)
291 | }
292 |
293 | /**
294 | * Returns a new set combining the elements of this and the specified set.
295 | *
296 | * @param {lunr.Set} other - set to union with this set.
297 | * @return {lunr.Set} a new set that is the union of this and the specified set.
298 | */
299 |
300 | lunr.Set.prototype.union = function (other) {
301 | if (other === lunr.Set.complete) {
302 | return lunr.Set.complete
303 | }
304 |
305 | if (other === lunr.Set.empty) {
306 | return this
307 | }
308 |
309 | return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements)))
310 | }
311 | /**
312 | * A function to calculate the inverse document frequency for
313 | * a posting. This is shared between the builder and the index
314 | *
315 | * @private
316 | * @param {object} posting - The posting for a given term
317 | * @param {number} documentCount - The total number of documents.
318 | */
319 | lunr.idf = function (posting, documentCount) {
320 | var documentsWithTerm = 0
321 |
322 | for (var fieldName in posting) {
323 | if (fieldName == '_index') continue // Ignore the term index, its not a field
324 | documentsWithTerm += Object.keys(posting[fieldName]).length
325 | }
326 |
327 | var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5)
328 |
329 | return Math.log(1 + Math.abs(x))
330 | }
331 |
332 | /**
333 | * A token wraps a string representation of a token
334 | * as it is passed through the text processing pipeline.
335 | *
336 | * @constructor
337 | * @param {string} [str=''] - The string token being wrapped.
338 | * @param {object} [metadata={}] - Metadata associated with this token.
339 | */
340 | lunr.Token = function (str, metadata) {
341 | this.str = str || ""
342 | this.metadata = metadata || {}
343 | }
344 |
345 | /**
346 | * Returns the token string that is being wrapped by this object.
347 | *
348 | * @returns {string}
349 | */
350 | lunr.Token.prototype.toString = function () {
351 | return this.str
352 | }
353 |
354 | /**
355 | * A token update function is used when updating or optionally
356 | * when cloning a token.
357 | *
358 | * @callback lunr.Token~updateFunction
359 | * @param {string} str - The string representation of the token.
360 | * @param {Object} metadata - All metadata associated with this token.
361 | */
362 |
363 | /**
364 | * Applies the given function to the wrapped string token.
365 | *
366 | * @example
367 | * token.update(function (str, metadata) {
368 | * return str.toUpperCase()
369 | * })
370 | *
371 | * @param {lunr.Token~updateFunction} fn - A function to apply to the token string.
372 | * @returns {lunr.Token}
373 | */
374 | lunr.Token.prototype.update = function (fn) {
375 | this.str = fn(this.str, this.metadata)
376 | return this
377 | }
378 |
379 | /**
380 | * Creates a clone of this token. Optionally a function can be
381 | * applied to the cloned token.
382 | *
383 | * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token.
384 | * @returns {lunr.Token}
385 | */
386 | lunr.Token.prototype.clone = function (fn) {
387 | fn = fn || function (s) { return s }
388 | return new lunr.Token (fn(this.str, this.metadata), this.metadata)
389 | }
390 | /*!
391 | * lunr.tokenizer
392 | * Copyright (C) 2020 Oliver Nightingale
393 | */
394 |
395 | /**
396 | * A function for splitting a string into tokens ready to be inserted into
397 | * the search index. Uses `lunr.tokenizer.separator` to split strings, change
398 | * the value of this property to change how strings are split into tokens.
399 | *
400 | * This tokenizer will convert its parameter to a string by calling `toString` and
401 | * then will split this string on the character in `lunr.tokenizer.separator`.
402 | * Arrays will have their elements converted to strings and wrapped in a lunr.Token.
403 | *
404 | * Optional metadata can be passed to the tokenizer, this metadata will be cloned and
405 | * added as metadata to every token that is created from the object to be tokenized.
406 | *
407 | * @static
408 | * @param {?(string|object|object[])} obj - The object to convert into tokens
409 | * @param {?object} metadata - Optional metadata to associate with every token
410 | * @returns {lunr.Token[]}
411 | * @see {@link lunr.Pipeline}
412 | */
413 | lunr.tokenizer = function (obj, metadata) {
414 | if (obj == null || obj == undefined) {
415 | return []
416 | }
417 |
418 | if (Array.isArray(obj)) {
419 | return obj.map(function (t) {
420 | return new lunr.Token(
421 | lunr.utils.asString(t).toLowerCase(),
422 | lunr.utils.clone(metadata)
423 | )
424 | })
425 | }
426 |
427 | var str = obj.toString().toLowerCase(),
428 | len = str.length,
429 | tokens = []
430 |
431 | for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {
432 | var char = str.charAt(sliceEnd),
433 | sliceLength = sliceEnd - sliceStart
434 |
435 | if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {
436 |
437 | if (sliceLength > 0) {
438 | var tokenMetadata = lunr.utils.clone(metadata) || {}
439 | tokenMetadata["position"] = [sliceStart, sliceLength]
440 | tokenMetadata["index"] = tokens.length
441 |
442 | tokens.push(
443 | new lunr.Token (
444 | str.slice(sliceStart, sliceEnd),
445 | tokenMetadata
446 | )
447 | )
448 | }
449 |
450 | sliceStart = sliceEnd + 1
451 | }
452 |
453 | }
454 |
455 | return tokens
456 | }
457 |
458 | /**
459 | * The separator used to split a string into tokens. Override this property to change the behaviour of
460 | * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens.
461 | *
462 | * @static
463 | * @see lunr.tokenizer
464 | */
465 | lunr.tokenizer.separator = /[\s\-]+/
466 | /*!
467 | * lunr.Pipeline
468 | * Copyright (C) 2020 Oliver Nightingale
469 | */
470 |
471 | /**
472 | * lunr.Pipelines maintain an ordered list of functions to be applied to all
473 | * tokens in documents entering the search index and queries being ran against
474 | * the index.
475 | *
476 | * An instance of lunr.Index created with the lunr shortcut will contain a
477 | * pipeline with a stop word filter and an English language stemmer. Extra
478 | * functions can be added before or after either of these functions or these
479 | * default functions can be removed.
480 | *
481 | * When run the pipeline will call each function in turn, passing a token, the
482 | * index of that token in the original list of all tokens and finally a list of
483 | * all the original tokens.
484 | *
485 | * The output of functions in the pipeline will be passed to the next function
486 | * in the pipeline. To exclude a token from entering the index the function
487 | * should return undefined, the rest of the pipeline will not be called with
488 | * this token.
489 | *
490 | * For serialisation of pipelines to work, all functions used in an instance of
491 | * a pipeline should be registered with lunr.Pipeline. Registered functions can
492 | * then be loaded. If trying to load a serialised pipeline that uses functions
493 | * that are not registered an error will be thrown.
494 | *
495 | * If not planning on serialising the pipeline then registering pipeline functions
496 | * is not necessary.
497 | *
498 | * @constructor
499 | */
500 | lunr.Pipeline = function () {
501 | this._stack = []
502 | }
503 |
504 | lunr.Pipeline.registeredFunctions = Object.create(null)
505 |
506 | /**
507 | * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token
508 | * string as well as all known metadata. A pipeline function can mutate the token string
509 | * or mutate (or add) metadata for a given token.
510 | *
511 | * A pipeline function can indicate that the passed token should be discarded by returning
512 | * null, undefined or an empty string. This token will not be passed to any downstream pipeline
513 | * functions and will not be added to the index.
514 | *
515 | * Multiple tokens can be returned by returning an array of tokens. Each token will be passed
516 | * to any downstream pipeline functions and all will returned tokens will be added to the index.
517 | *
518 | * Any number of pipeline functions may be chained together using a lunr.Pipeline.
519 | *
520 | * @interface lunr.PipelineFunction
521 | * @param {lunr.Token} token - A token from the document being processed.
522 | * @param {number} i - The index of this token in the complete list of tokens for this document/field.
523 | * @param {lunr.Token[]} tokens - All tokens for this document/field.
524 | * @returns {(?lunr.Token|lunr.Token[])}
525 | */
526 |
527 | /**
528 | * Register a function with the pipeline.
529 | *
530 | * Functions that are used in the pipeline should be registered if the pipeline
531 | * needs to be serialised, or a serialised pipeline needs to be loaded.
532 | *
533 | * Registering a function does not add it to a pipeline, functions must still be
534 | * added to instances of the pipeline for them to be used when running a pipeline.
535 | *
536 | * @param {lunr.PipelineFunction} fn - The function to check for.
537 | * @param {String} label - The label to register this function with
538 | */
539 | lunr.Pipeline.registerFunction = function (fn, label) {
540 | if (label in this.registeredFunctions) {
541 | lunr.utils.warn('Overwriting existing registered function: ' + label)
542 | }
543 |
544 | fn.label = label
545 | lunr.Pipeline.registeredFunctions[fn.label] = fn
546 | }
547 |
548 | /**
549 | * Warns if the function is not registered as a Pipeline function.
550 | *
551 | * @param {lunr.PipelineFunction} fn - The function to check for.
552 | * @private
553 | */
554 | lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {
555 | var isRegistered = fn.label && (fn.label in this.registeredFunctions)
556 |
557 | if (!isRegistered) {
558 | lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn)
559 | }
560 | }
561 |
562 | /**
563 | * Loads a previously serialised pipeline.
564 | *
565 | * All functions to be loaded must already be registered with lunr.Pipeline.
566 | * If any function from the serialised data has not been registered then an
567 | * error will be thrown.
568 | *
569 | * @param {Object} serialised - The serialised pipeline to load.
570 | * @returns {lunr.Pipeline}
571 | */
572 | lunr.Pipeline.load = function (serialised) {
573 | var pipeline = new lunr.Pipeline
574 |
575 | serialised.forEach(function (fnName) {
576 | var fn = lunr.Pipeline.registeredFunctions[fnName]
577 |
578 | if (fn) {
579 | pipeline.add(fn)
580 | } else {
581 | throw new Error('Cannot load unregistered function: ' + fnName)
582 | }
583 | })
584 |
585 | return pipeline
586 | }
587 |
588 | /**
589 | * Adds new functions to the end of the pipeline.
590 | *
591 | * Logs a warning if the function has not been registered.
592 | *
593 | * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline.
594 | */
595 | lunr.Pipeline.prototype.add = function () {
596 | var fns = Array.prototype.slice.call(arguments)
597 |
598 | fns.forEach(function (fn) {
599 | lunr.Pipeline.warnIfFunctionNotRegistered(fn)
600 | this._stack.push(fn)
601 | }, this)
602 | }
603 |
604 | /**
605 | * Adds a single function after a function that already exists in the
606 | * pipeline.
607 | *
608 | * Logs a warning if the function has not been registered.
609 | *
610 | * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.
611 | * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.
612 | */
613 | lunr.Pipeline.prototype.after = function (existingFn, newFn) {
614 | lunr.Pipeline.warnIfFunctionNotRegistered(newFn)
615 |
616 | var pos = this._stack.indexOf(existingFn)
617 | if (pos == -1) {
618 | throw new Error('Cannot find existingFn')
619 | }
620 |
621 | pos = pos + 1
622 | this._stack.splice(pos, 0, newFn)
623 | }
624 |
625 | /**
626 | * Adds a single function before a function that already exists in the
627 | * pipeline.
628 | *
629 | * Logs a warning if the function has not been registered.
630 | *
631 | * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.
632 | * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.
633 | */
634 | lunr.Pipeline.prototype.before = function (existingFn, newFn) {
635 | lunr.Pipeline.warnIfFunctionNotRegistered(newFn)
636 |
637 | var pos = this._stack.indexOf(existingFn)
638 | if (pos == -1) {
639 | throw new Error('Cannot find existingFn')
640 | }
641 |
642 | this._stack.splice(pos, 0, newFn)
643 | }
644 |
645 | /**
646 | * Removes a function from the pipeline.
647 | *
648 | * @param {lunr.PipelineFunction} fn The function to remove from the pipeline.
649 | */
650 | lunr.Pipeline.prototype.remove = function (fn) {
651 | var pos = this._stack.indexOf(fn)
652 | if (pos == -1) {
653 | return
654 | }
655 |
656 | this._stack.splice(pos, 1)
657 | }
658 |
659 | /**
660 | * Runs the current list of functions that make up the pipeline against the
661 | * passed tokens.
662 | *
663 | * @param {Array} tokens The tokens to run through the pipeline.
664 | * @returns {Array}
665 | */
666 | lunr.Pipeline.prototype.run = function (tokens) {
667 | var stackLength = this._stack.length
668 |
669 | for (var i = 0; i < stackLength; i++) {
670 | var fn = this._stack[i]
671 | var memo = []
672 |
673 | for (var j = 0; j < tokens.length; j++) {
674 | var result = fn(tokens[j], j, tokens)
675 |
676 | if (result === null || result === void 0 || result === '') continue
677 |
678 | if (Array.isArray(result)) {
679 | for (var k = 0; k < result.length; k++) {
680 | memo.push(result[k])
681 | }
682 | } else {
683 | memo.push(result)
684 | }
685 | }
686 |
687 | tokens = memo
688 | }
689 |
690 | return tokens
691 | }
692 |
693 | /**
694 | * Convenience method for passing a string through a pipeline and getting
695 | * strings out. This method takes care of wrapping the passed string in a
696 | * token and mapping the resulting tokens back to strings.
697 | *
698 | * @param {string} str - The string to pass through the pipeline.
699 | * @param {?object} metadata - Optional metadata to associate with the token
700 | * passed to the pipeline.
701 | * @returns {string[]}
702 | */
703 | lunr.Pipeline.prototype.runString = function (str, metadata) {
704 | var token = new lunr.Token (str, metadata)
705 |
706 | return this.run([token]).map(function (t) {
707 | return t.toString()
708 | })
709 | }
710 |
711 | /**
712 | * Resets the pipeline by removing any existing processors.
713 | *
714 | */
715 | lunr.Pipeline.prototype.reset = function () {
716 | this._stack = []
717 | }
718 |
719 | /**
720 | * Returns a representation of the pipeline ready for serialisation.
721 | *
722 | * Logs a warning if the function has not been registered.
723 | *
724 | * @returns {Array}
725 | */
726 | lunr.Pipeline.prototype.toJSON = function () {
727 | return this._stack.map(function (fn) {
728 | lunr.Pipeline.warnIfFunctionNotRegistered(fn)
729 |
730 | return fn.label
731 | })
732 | }
733 | /*!
734 | * lunr.Vector
735 | * Copyright (C) 2020 Oliver Nightingale
736 | */
737 |
738 | /**
739 | * A vector is used to construct the vector space of documents and queries. These
740 | * vectors support operations to determine the similarity between two documents or
741 | * a document and a query.
742 | *
743 | * Normally no parameters are required for initializing a vector, but in the case of
744 | * loading a previously dumped vector the raw elements can be provided to the constructor.
745 | *
746 | * For performance reasons vectors are implemented with a flat array, where an elements
747 | * index is immediately followed by its value. E.g. [index, value, index, value]. This
748 | * allows the underlying array to be as sparse as possible and still offer decent
749 | * performance when being used for vector calculations.
750 | *
751 | * @constructor
752 | * @param {Number[]} [elements] - The flat list of element index and element value pairs.
753 | */
754 | lunr.Vector = function (elements) {
755 | this._magnitude = 0
756 | this.elements = elements || []
757 | }
758 |
759 |
760 | /**
761 | * Calculates the position within the vector to insert a given index.
762 | *
763 | * This is used internally by insert and upsert. If there are duplicate indexes then
764 | * the position is returned as if the value for that index were to be updated, but it
765 | * is the callers responsibility to check whether there is a duplicate at that index
766 | *
767 | * @param {Number} insertIdx - The index at which the element should be inserted.
768 | * @returns {Number}
769 | */
770 | lunr.Vector.prototype.positionForIndex = function (index) {
771 | // For an empty vector the tuple can be inserted at the beginning
772 | if (this.elements.length == 0) {
773 | return 0
774 | }
775 |
776 | var start = 0,
777 | end = this.elements.length / 2,
778 | sliceLength = end - start,
779 | pivotPoint = Math.floor(sliceLength / 2),
780 | pivotIndex = this.elements[pivotPoint * 2]
781 |
782 | while (sliceLength > 1) {
783 | if (pivotIndex < index) {
784 | start = pivotPoint
785 | }
786 |
787 | if (pivotIndex > index) {
788 | end = pivotPoint
789 | }
790 |
791 | if (pivotIndex == index) {
792 | break
793 | }
794 |
795 | sliceLength = end - start
796 | pivotPoint = start + Math.floor(sliceLength / 2)
797 | pivotIndex = this.elements[pivotPoint * 2]
798 | }
799 |
800 | if (pivotIndex == index) {
801 | return pivotPoint * 2
802 | }
803 |
804 | if (pivotIndex > index) {
805 | return pivotPoint * 2
806 | }
807 |
808 | if (pivotIndex < index) {
809 | return (pivotPoint + 1) * 2
810 | }
811 | }
812 |
813 | /**
814 | * Inserts an element at an index within the vector.
815 | *
816 | * Does not allow duplicates, will throw an error if there is already an entry
817 | * for this index.
818 | *
819 | * @param {Number} insertIdx - The index at which the element should be inserted.
820 | * @param {Number} val - The value to be inserted into the vector.
821 | */
822 | lunr.Vector.prototype.insert = function (insertIdx, val) {
823 | this.upsert(insertIdx, val, function () {
824 | throw "duplicate index"
825 | })
826 | }
827 |
828 | /**
829 | * Inserts or updates an existing index within the vector.
830 | *
831 | * @param {Number} insertIdx - The index at which the element should be inserted.
832 | * @param {Number} val - The value to be inserted into the vector.
833 | * @param {function} fn - A function that is called for updates, the existing value and the
834 | * requested value are passed as arguments
835 | */
836 | lunr.Vector.prototype.upsert = function (insertIdx, val, fn) {
837 | this._magnitude = 0
838 | var position = this.positionForIndex(insertIdx)
839 |
840 | if (this.elements[position] == insertIdx) {
841 | this.elements[position + 1] = fn(this.elements[position + 1], val)
842 | } else {
843 | this.elements.splice(position, 0, insertIdx, val)
844 | }
845 | }
846 |
847 | /**
848 | * Calculates the magnitude of this vector.
849 | *
850 | * @returns {Number}
851 | */
852 | lunr.Vector.prototype.magnitude = function () {
853 | if (this._magnitude) return this._magnitude
854 |
855 | var sumOfSquares = 0,
856 | elementsLength = this.elements.length
857 |
858 | for (var i = 1; i < elementsLength; i += 2) {
859 | var val = this.elements[i]
860 | sumOfSquares += val * val
861 | }
862 |
863 | return this._magnitude = Math.sqrt(sumOfSquares)
864 | }
865 |
866 | /**
867 | * Calculates the dot product of this vector and another vector.
868 | *
869 | * @param {lunr.Vector} otherVector - The vector to compute the dot product with.
870 | * @returns {Number}
871 | */
872 | lunr.Vector.prototype.dot = function (otherVector) {
873 | var dotProduct = 0,
874 | a = this.elements, b = otherVector.elements,
875 | aLen = a.length, bLen = b.length,
876 | aVal = 0, bVal = 0,
877 | i = 0, j = 0
878 |
879 | while (i < aLen && j < bLen) {
880 | aVal = a[i], bVal = b[j]
881 | if (aVal < bVal) {
882 | i += 2
883 | } else if (aVal > bVal) {
884 | j += 2
885 | } else if (aVal == bVal) {
886 | dotProduct += a[i + 1] * b[j + 1]
887 | i += 2
888 | j += 2
889 | }
890 | }
891 |
892 | return dotProduct
893 | }
894 |
895 | /**
896 | * Calculates the similarity between this vector and another vector.
897 | *
898 | * @param {lunr.Vector} otherVector - The other vector to calculate the
899 | * similarity with.
900 | * @returns {Number}
901 | */
902 | lunr.Vector.prototype.similarity = function (otherVector) {
903 | return this.dot(otherVector) / this.magnitude() || 0
904 | }
905 |
906 | /**
907 | * Converts the vector to an array of the elements within the vector.
908 | *
909 | * @returns {Number[]}
910 | */
911 | lunr.Vector.prototype.toArray = function () {
912 | var output = new Array (this.elements.length / 2)
913 |
914 | for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) {
915 | output[j] = this.elements[i]
916 | }
917 |
918 | return output
919 | }
920 |
921 | /**
922 | * A JSON serializable representation of the vector.
923 | *
924 | * @returns {Number[]}
925 | */
926 | lunr.Vector.prototype.toJSON = function () {
927 | return this.elements
928 | }
929 | /* eslint-disable */
930 | /*!
931 | * lunr.stemmer
932 | * Copyright (C) 2020 Oliver Nightingale
933 | * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt
934 | */
935 |
936 | /**
937 | * lunr.stemmer is an english language stemmer, this is a JavaScript
938 | * implementation of the PorterStemmer taken from http://tartarus.org/~martin
939 | *
940 | * @static
941 | * @implements {lunr.PipelineFunction}
942 | * @param {lunr.Token} token - The string to stem
943 | * @returns {lunr.Token}
944 | * @see {@link lunr.Pipeline}
945 | * @function
946 | */
947 | lunr.stemmer = (function(){
948 | var step2list = {
949 | "ational" : "ate",
950 | "tional" : "tion",
951 | "enci" : "ence",
952 | "anci" : "ance",
953 | "izer" : "ize",
954 | "bli" : "ble",
955 | "alli" : "al",
956 | "entli" : "ent",
957 | "eli" : "e",
958 | "ousli" : "ous",
959 | "ization" : "ize",
960 | "ation" : "ate",
961 | "ator" : "ate",
962 | "alism" : "al",
963 | "iveness" : "ive",
964 | "fulness" : "ful",
965 | "ousness" : "ous",
966 | "aliti" : "al",
967 | "iviti" : "ive",
968 | "biliti" : "ble",
969 | "logi" : "log"
970 | },
971 |
972 | step3list = {
973 | "icate" : "ic",
974 | "ative" : "",
975 | "alize" : "al",
976 | "iciti" : "ic",
977 | "ical" : "ic",
978 | "ful" : "",
979 | "ness" : ""
980 | },
981 |
982 | c = "[^aeiou]", // consonant
983 | v = "[aeiouy]", // vowel
984 | C = c + "[^aeiouy]*", // consonant sequence
985 | V = v + "[aeiou]*", // vowel sequence
986 |
987 | mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0
988 | meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1
989 | mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1
990 | s_v = "^(" + C + ")?" + v; // vowel in stem
991 |
992 | var re_mgr0 = new RegExp(mgr0);
993 | var re_mgr1 = new RegExp(mgr1);
994 | var re_meq1 = new RegExp(meq1);
995 | var re_s_v = new RegExp(s_v);
996 |
997 | var re_1a = /^(.+?)(ss|i)es$/;
998 | var re2_1a = /^(.+?)([^s])s$/;
999 | var re_1b = /^(.+?)eed$/;
1000 | var re2_1b = /^(.+?)(ed|ing)$/;
1001 | var re_1b_2 = /.$/;
1002 | var re2_1b_2 = /(at|bl|iz)$/;
1003 | var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$");
1004 | var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$");
1005 |
1006 | var re_1c = /^(.+?[^aeiou])y$/;
1007 | var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
1008 |
1009 | var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
1010 |
1011 | var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
1012 | var re2_4 = /^(.+?)(s|t)(ion)$/;
1013 |
1014 | var re_5 = /^(.+?)e$/;
1015 | var re_5_1 = /ll$/;
1016 | var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$");
1017 |
1018 | var porterStemmer = function porterStemmer(w) {
1019 | var stem,
1020 | suffix,
1021 | firstch,
1022 | re,
1023 | re2,
1024 | re3,
1025 | re4;
1026 |
1027 | if (w.length < 3) { return w; }
1028 |
1029 | firstch = w.substr(0,1);
1030 | if (firstch == "y") {
1031 | w = firstch.toUpperCase() + w.substr(1);
1032 | }
1033 |
1034 | // Step 1a
1035 | re = re_1a
1036 | re2 = re2_1a;
1037 |
1038 | if (re.test(w)) { w = w.replace(re,"$1$2"); }
1039 | else if (re2.test(w)) { w = w.replace(re2,"$1$2"); }
1040 |
1041 | // Step 1b
1042 | re = re_1b;
1043 | re2 = re2_1b;
1044 | if (re.test(w)) {
1045 | var fp = re.exec(w);
1046 | re = re_mgr0;
1047 | if (re.test(fp[1])) {
1048 | re = re_1b_2;
1049 | w = w.replace(re,"");
1050 | }
1051 | } else if (re2.test(w)) {
1052 | var fp = re2.exec(w);
1053 | stem = fp[1];
1054 | re2 = re_s_v;
1055 | if (re2.test(stem)) {
1056 | w = stem;
1057 | re2 = re2_1b_2;
1058 | re3 = re3_1b_2;
1059 | re4 = re4_1b_2;
1060 | if (re2.test(w)) { w = w + "e"; }
1061 | else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); }
1062 | else if (re4.test(w)) { w = w + "e"; }
1063 | }
1064 | }
1065 |
1066 | // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)
1067 | re = re_1c;
1068 | if (re.test(w)) {
1069 | var fp = re.exec(w);
1070 | stem = fp[1];
1071 | w = stem + "i";
1072 | }
1073 |
1074 | // Step 2
1075 | re = re_2;
1076 | if (re.test(w)) {
1077 | var fp = re.exec(w);
1078 | stem = fp[1];
1079 | suffix = fp[2];
1080 | re = re_mgr0;
1081 | if (re.test(stem)) {
1082 | w = stem + step2list[suffix];
1083 | }
1084 | }
1085 |
1086 | // Step 3
1087 | re = re_3;
1088 | if (re.test(w)) {
1089 | var fp = re.exec(w);
1090 | stem = fp[1];
1091 | suffix = fp[2];
1092 | re = re_mgr0;
1093 | if (re.test(stem)) {
1094 | w = stem + step3list[suffix];
1095 | }
1096 | }
1097 |
1098 | // Step 4
1099 | re = re_4;
1100 | re2 = re2_4;
1101 | if (re.test(w)) {
1102 | var fp = re.exec(w);
1103 | stem = fp[1];
1104 | re = re_mgr1;
1105 | if (re.test(stem)) {
1106 | w = stem;
1107 | }
1108 | } else if (re2.test(w)) {
1109 | var fp = re2.exec(w);
1110 | stem = fp[1] + fp[2];
1111 | re2 = re_mgr1;
1112 | if (re2.test(stem)) {
1113 | w = stem;
1114 | }
1115 | }
1116 |
1117 | // Step 5
1118 | re = re_5;
1119 | if (re.test(w)) {
1120 | var fp = re.exec(w);
1121 | stem = fp[1];
1122 | re = re_mgr1;
1123 | re2 = re_meq1;
1124 | re3 = re3_5;
1125 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {
1126 | w = stem;
1127 | }
1128 | }
1129 |
1130 | re = re_5_1;
1131 | re2 = re_mgr1;
1132 | if (re.test(w) && re2.test(w)) {
1133 | re = re_1b_2;
1134 | w = w.replace(re,"");
1135 | }
1136 |
1137 | // and turn initial Y back to y
1138 |
1139 | if (firstch == "y") {
1140 | w = firstch.toLowerCase() + w.substr(1);
1141 | }
1142 |
1143 | return w;
1144 | };
1145 |
1146 | return function (token) {
1147 | return token.update(porterStemmer);
1148 | }
1149 | })();
1150 |
1151 | lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')
1152 | /*!
1153 | * lunr.stopWordFilter
1154 | * Copyright (C) 2020 Oliver Nightingale
1155 | */
1156 |
1157 | /**
1158 | * lunr.generateStopWordFilter builds a stopWordFilter function from the provided
1159 | * list of stop words.
1160 | *
1161 | * The built in lunr.stopWordFilter is built using this generator and can be used
1162 | * to generate custom stopWordFilters for applications or non English languages.
1163 | *
1164 | * @function
1165 | * @param {Array} token The token to pass through the filter
1166 | * @returns {lunr.PipelineFunction}
1167 | * @see lunr.Pipeline
1168 | * @see lunr.stopWordFilter
1169 | */
1170 | lunr.generateStopWordFilter = function (stopWords) {
1171 | var words = stopWords.reduce(function (memo, stopWord) {
1172 | memo[stopWord] = stopWord
1173 | return memo
1174 | }, {})
1175 |
1176 | return function (token) {
1177 | if (token && words[token.toString()] !== token.toString()) return token
1178 | }
1179 | }
1180 |
1181 | /**
1182 | * lunr.stopWordFilter is an English language stop word list filter, any words
1183 | * contained in the list will not be passed through the filter.
1184 | *
1185 | * This is intended to be used in the Pipeline. If the token does not pass the
1186 | * filter then undefined will be returned.
1187 | *
1188 | * @function
1189 | * @implements {lunr.PipelineFunction}
1190 | * @params {lunr.Token} token - A token to check for being a stop word.
1191 | * @returns {lunr.Token}
1192 | * @see {@link lunr.Pipeline}
1193 | */
1194 | lunr.stopWordFilter = lunr.generateStopWordFilter([
1195 | 'a',
1196 | 'able',
1197 | 'about',
1198 | 'across',
1199 | 'after',
1200 | 'all',
1201 | 'almost',
1202 | 'also',
1203 | 'am',
1204 | 'among',
1205 | 'an',
1206 | 'and',
1207 | 'any',
1208 | 'are',
1209 | 'as',
1210 | 'at',
1211 | 'be',
1212 | 'because',
1213 | 'been',
1214 | 'but',
1215 | 'by',
1216 | 'can',
1217 | 'cannot',
1218 | 'could',
1219 | 'dear',
1220 | 'did',
1221 | 'do',
1222 | 'does',
1223 | 'either',
1224 | 'else',
1225 | 'ever',
1226 | 'every',
1227 | 'for',
1228 | 'from',
1229 | 'get',
1230 | 'got',
1231 | 'had',
1232 | 'has',
1233 | 'have',
1234 | 'he',
1235 | 'her',
1236 | 'hers',
1237 | 'him',
1238 | 'his',
1239 | 'how',
1240 | 'however',
1241 | 'i',
1242 | 'if',
1243 | 'in',
1244 | 'into',
1245 | 'is',
1246 | 'it',
1247 | 'its',
1248 | 'just',
1249 | 'least',
1250 | 'let',
1251 | 'like',
1252 | 'likely',
1253 | 'may',
1254 | 'me',
1255 | 'might',
1256 | 'most',
1257 | 'must',
1258 | 'my',
1259 | 'neither',
1260 | 'no',
1261 | 'nor',
1262 | 'not',
1263 | 'of',
1264 | 'off',
1265 | 'often',
1266 | 'on',
1267 | 'only',
1268 | 'or',
1269 | 'other',
1270 | 'our',
1271 | 'own',
1272 | 'rather',
1273 | 'said',
1274 | 'say',
1275 | 'says',
1276 | 'she',
1277 | 'should',
1278 | 'since',
1279 | 'so',
1280 | 'some',
1281 | 'than',
1282 | 'that',
1283 | 'the',
1284 | 'their',
1285 | 'them',
1286 | 'then',
1287 | 'there',
1288 | 'these',
1289 | 'they',
1290 | 'this',
1291 | 'tis',
1292 | 'to',
1293 | 'too',
1294 | 'twas',
1295 | 'us',
1296 | 'wants',
1297 | 'was',
1298 | 'we',
1299 | 'were',
1300 | 'what',
1301 | 'when',
1302 | 'where',
1303 | 'which',
1304 | 'while',
1305 | 'who',
1306 | 'whom',
1307 | 'why',
1308 | 'will',
1309 | 'with',
1310 | 'would',
1311 | 'yet',
1312 | 'you',
1313 | 'your'
1314 | ])
1315 |
1316 | lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')
1317 | /*!
1318 | * lunr.trimmer
1319 | * Copyright (C) 2020 Oliver Nightingale
1320 | */
1321 |
1322 | /**
1323 | * lunr.trimmer is a pipeline function for trimming non word
1324 | * characters from the beginning and end of tokens before they
1325 | * enter the index.
1326 | *
1327 | * This implementation may not work correctly for non latin
1328 | * characters and should either be removed or adapted for use
1329 | * with languages with non-latin characters.
1330 | *
1331 | * @static
1332 | * @implements {lunr.PipelineFunction}
1333 | * @param {lunr.Token} token The token to pass through the filter
1334 | * @returns {lunr.Token}
1335 | * @see lunr.Pipeline
1336 | */
1337 | lunr.trimmer = function (token) {
1338 | return token.update(function (s) {
1339 | return s.replace(/^\W+/, '').replace(/\W+$/, '')
1340 | })
1341 | }
1342 |
1343 | lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')
1344 | /*!
1345 | * lunr.TokenSet
1346 | * Copyright (C) 2020 Oliver Nightingale
1347 | */
1348 |
1349 | /**
1350 | * A token set is used to store the unique list of all tokens
1351 | * within an index. Token sets are also used to represent an
1352 | * incoming query to the index, this query token set and index
1353 | * token set are then intersected to find which tokens to look
1354 | * up in the inverted index.
1355 | *
1356 | * A token set can hold multiple tokens, as in the case of the
1357 | * index token set, or it can hold a single token as in the
1358 | * case of a simple query token set.
1359 | *
1360 | * Additionally token sets are used to perform wildcard matching.
1361 | * Leading, contained and trailing wildcards are supported, and
1362 | * from this edit distance matching can also be provided.
1363 | *
1364 | * Token sets are implemented as a minimal finite state automata,
1365 | * where both common prefixes and suffixes are shared between tokens.
1366 | * This helps to reduce the space used for storing the token set.
1367 | *
1368 | * @constructor
1369 | */
1370 | lunr.TokenSet = function () {
1371 | this.final = false
1372 | this.edges = {}
1373 | this.id = lunr.TokenSet._nextId
1374 | lunr.TokenSet._nextId += 1
1375 | }
1376 |
1377 | /**
1378 | * Keeps track of the next, auto increment, identifier to assign
1379 | * to a new tokenSet.
1380 | *
1381 | * TokenSets require a unique identifier to be correctly minimised.
1382 | *
1383 | * @private
1384 | */
1385 | lunr.TokenSet._nextId = 1
1386 |
1387 | /**
1388 | * Creates a TokenSet instance from the given sorted array of words.
1389 | *
1390 | * @param {String[]} arr - A sorted array of strings to create the set from.
1391 | * @returns {lunr.TokenSet}
1392 | * @throws Will throw an error if the input array is not sorted.
1393 | */
1394 | lunr.TokenSet.fromArray = function (arr) {
1395 | var builder = new lunr.TokenSet.Builder
1396 |
1397 | for (var i = 0, len = arr.length; i < len; i++) {
1398 | builder.insert(arr[i])
1399 | }
1400 |
1401 | builder.finish()
1402 | return builder.root
1403 | }
1404 |
1405 | /**
1406 | * Creates a token set from a query clause.
1407 | *
1408 | * @private
1409 | * @param {Object} clause - A single clause from lunr.Query.
1410 | * @param {string} clause.term - The query clause term.
1411 | * @param {number} [clause.editDistance] - The optional edit distance for the term.
1412 | * @returns {lunr.TokenSet}
1413 | */
1414 | lunr.TokenSet.fromClause = function (clause) {
1415 | if ('editDistance' in clause) {
1416 | return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance)
1417 | } else {
1418 | return lunr.TokenSet.fromString(clause.term)
1419 | }
1420 | }
1421 |
1422 | /**
1423 | * Creates a token set representing a single string with a specified
1424 | * edit distance.
1425 | *
1426 | * Insertions, deletions, substitutions and transpositions are each
1427 | * treated as an edit distance of 1.
1428 | *
1429 | * Increasing the allowed edit distance will have a dramatic impact
1430 | * on the performance of both creating and intersecting these TokenSets.
1431 | * It is advised to keep the edit distance less than 3.
1432 | *
1433 | * @param {string} str - The string to create the token set from.
1434 | * @param {number} editDistance - The allowed edit distance to match.
1435 | * @returns {lunr.Vector}
1436 | */
1437 | lunr.TokenSet.fromFuzzyString = function (str, editDistance) {
1438 | var root = new lunr.TokenSet
1439 |
1440 | var stack = [{
1441 | node: root,
1442 | editsRemaining: editDistance,
1443 | str: str
1444 | }]
1445 |
1446 | while (stack.length) {
1447 | var frame = stack.pop()
1448 |
1449 | // no edit
1450 | if (frame.str.length > 0) {
1451 | var char = frame.str.charAt(0),
1452 | noEditNode
1453 |
1454 | if (char in frame.node.edges) {
1455 | noEditNode = frame.node.edges[char]
1456 | } else {
1457 | noEditNode = new lunr.TokenSet
1458 | frame.node.edges[char] = noEditNode
1459 | }
1460 |
1461 | if (frame.str.length == 1) {
1462 | noEditNode.final = true
1463 | }
1464 |
1465 | stack.push({
1466 | node: noEditNode,
1467 | editsRemaining: frame.editsRemaining,
1468 | str: frame.str.slice(1)
1469 | })
1470 | }
1471 |
1472 | if (frame.editsRemaining == 0) {
1473 | continue
1474 | }
1475 |
1476 | // insertion
1477 | if ("*" in frame.node.edges) {
1478 | var insertionNode = frame.node.edges["*"]
1479 | } else {
1480 | var insertionNode = new lunr.TokenSet
1481 | frame.node.edges["*"] = insertionNode
1482 | }
1483 |
1484 | if (frame.str.length == 0) {
1485 | insertionNode.final = true
1486 | }
1487 |
1488 | stack.push({
1489 | node: insertionNode,
1490 | editsRemaining: frame.editsRemaining - 1,
1491 | str: frame.str
1492 | })
1493 |
1494 | // deletion
1495 | // can only do a deletion if we have enough edits remaining
1496 | // and if there are characters left to delete in the string
1497 | if (frame.str.length > 1) {
1498 | stack.push({
1499 | node: frame.node,
1500 | editsRemaining: frame.editsRemaining - 1,
1501 | str: frame.str.slice(1)
1502 | })
1503 | }
1504 |
1505 | // deletion
1506 | // just removing the last character from the str
1507 | if (frame.str.length == 1) {
1508 | frame.node.final = true
1509 | }
1510 |
1511 | // substitution
1512 | // can only do a substitution if we have enough edits remaining
1513 | // and if there are characters left to substitute
1514 | if (frame.str.length >= 1) {
1515 | if ("*" in frame.node.edges) {
1516 | var substitutionNode = frame.node.edges["*"]
1517 | } else {
1518 | var substitutionNode = new lunr.TokenSet
1519 | frame.node.edges["*"] = substitutionNode
1520 | }
1521 |
1522 | if (frame.str.length == 1) {
1523 | substitutionNode.final = true
1524 | }
1525 |
1526 | stack.push({
1527 | node: substitutionNode,
1528 | editsRemaining: frame.editsRemaining - 1,
1529 | str: frame.str.slice(1)
1530 | })
1531 | }
1532 |
1533 | // transposition
1534 | // can only do a transposition if there are edits remaining
1535 | // and there are enough characters to transpose
1536 | if (frame.str.length > 1) {
1537 | var charA = frame.str.charAt(0),
1538 | charB = frame.str.charAt(1),
1539 | transposeNode
1540 |
1541 | if (charB in frame.node.edges) {
1542 | transposeNode = frame.node.edges[charB]
1543 | } else {
1544 | transposeNode = new lunr.TokenSet
1545 | frame.node.edges[charB] = transposeNode
1546 | }
1547 |
1548 | if (frame.str.length == 1) {
1549 | transposeNode.final = true
1550 | }
1551 |
1552 | stack.push({
1553 | node: transposeNode,
1554 | editsRemaining: frame.editsRemaining - 1,
1555 | str: charA + frame.str.slice(2)
1556 | })
1557 | }
1558 | }
1559 |
1560 | return root
1561 | }
1562 |
1563 | /**
1564 | * Creates a TokenSet from a string.
1565 | *
1566 | * The string may contain one or more wildcard characters (*)
1567 | * that will allow wildcard matching when intersecting with
1568 | * another TokenSet.
1569 | *
1570 | * @param {string} str - The string to create a TokenSet from.
1571 | * @returns {lunr.TokenSet}
1572 | */
1573 | lunr.TokenSet.fromString = function (str) {
1574 | var node = new lunr.TokenSet,
1575 | root = node
1576 |
1577 | /*
1578 | * Iterates through all characters within the passed string
1579 | * appending a node for each character.
1580 | *
1581 | * When a wildcard character is found then a self
1582 | * referencing edge is introduced to continually match
1583 | * any number of any characters.
1584 | */
1585 | for (var i = 0, len = str.length; i < len; i++) {
1586 | var char = str[i],
1587 | final = (i == len - 1)
1588 |
1589 | if (char == "*") {
1590 | node.edges[char] = node
1591 | node.final = final
1592 |
1593 | } else {
1594 | var next = new lunr.TokenSet
1595 | next.final = final
1596 |
1597 | node.edges[char] = next
1598 | node = next
1599 | }
1600 | }
1601 |
1602 | return root
1603 | }
1604 |
1605 | /**
1606 | * Converts this TokenSet into an array of strings
1607 | * contained within the TokenSet.
1608 | *
1609 | * This is not intended to be used on a TokenSet that
1610 | * contains wildcards, in these cases the results are
1611 | * undefined and are likely to cause an infinite loop.
1612 | *
1613 | * @returns {string[]}
1614 | */
1615 | lunr.TokenSet.prototype.toArray = function () {
1616 | var words = []
1617 |
1618 | var stack = [{
1619 | prefix: "",
1620 | node: this
1621 | }]
1622 |
1623 | while (stack.length) {
1624 | var frame = stack.pop(),
1625 | edges = Object.keys(frame.node.edges),
1626 | len = edges.length
1627 |
1628 | if (frame.node.final) {
1629 | /* In Safari, at this point the prefix is sometimes corrupted, see:
1630 | * https://github.com/olivernn/lunr.js/issues/279 Calling any
1631 | * String.prototype method forces Safari to "cast" this string to what
1632 | * it's supposed to be, fixing the bug. */
1633 | frame.prefix.charAt(0)
1634 | words.push(frame.prefix)
1635 | }
1636 |
1637 | for (var i = 0; i < len; i++) {
1638 | var edge = edges[i]
1639 |
1640 | stack.push({
1641 | prefix: frame.prefix.concat(edge),
1642 | node: frame.node.edges[edge]
1643 | })
1644 | }
1645 | }
1646 |
1647 | return words
1648 | }
1649 |
1650 | /**
1651 | * Generates a string representation of a TokenSet.
1652 | *
1653 | * This is intended to allow TokenSets to be used as keys
1654 | * in objects, largely to aid the construction and minimisation
1655 | * of a TokenSet. As such it is not designed to be a human
1656 | * friendly representation of the TokenSet.
1657 | *
1658 | * @returns {string}
1659 | */
1660 | lunr.TokenSet.prototype.toString = function () {
1661 | // NOTE: Using Object.keys here as this.edges is very likely
1662 | // to enter 'hash-mode' with many keys being added
1663 | //
1664 | // avoiding a for-in loop here as it leads to the function
1665 | // being de-optimised (at least in V8). From some simple
1666 | // benchmarks the performance is comparable, but allowing
1667 | // V8 to optimize may mean easy performance wins in the future.
1668 |
1669 | if (this._str) {
1670 | return this._str
1671 | }
1672 |
1673 | var str = this.final ? '1' : '0',
1674 | labels = Object.keys(this.edges).sort(),
1675 | len = labels.length
1676 |
1677 | for (var i = 0; i < len; i++) {
1678 | var label = labels[i],
1679 | node = this.edges[label]
1680 |
1681 | str = str + label + node.id
1682 | }
1683 |
1684 | return str
1685 | }
1686 |
1687 | /**
1688 | * Returns a new TokenSet that is the intersection of
1689 | * this TokenSet and the passed TokenSet.
1690 | *
1691 | * This intersection will take into account any wildcards
1692 | * contained within the TokenSet.
1693 | *
1694 | * @param {lunr.TokenSet} b - An other TokenSet to intersect with.
1695 | * @returns {lunr.TokenSet}
1696 | */
1697 | lunr.TokenSet.prototype.intersect = function (b) {
1698 | var output = new lunr.TokenSet,
1699 | frame = undefined
1700 |
1701 | var stack = [{
1702 | qNode: b,
1703 | output: output,
1704 | node: this
1705 | }]
1706 |
1707 | while (stack.length) {
1708 | frame = stack.pop()
1709 |
1710 | // NOTE: As with the #toString method, we are using
1711 | // Object.keys and a for loop instead of a for-in loop
1712 | // as both of these objects enter 'hash' mode, causing
1713 | // the function to be de-optimised in V8
1714 | var qEdges = Object.keys(frame.qNode.edges),
1715 | qLen = qEdges.length,
1716 | nEdges = Object.keys(frame.node.edges),
1717 | nLen = nEdges.length
1718 |
1719 | for (var q = 0; q < qLen; q++) {
1720 | var qEdge = qEdges[q]
1721 |
1722 | for (var n = 0; n < nLen; n++) {
1723 | var nEdge = nEdges[n]
1724 |
1725 | if (nEdge == qEdge || qEdge == '*') {
1726 | var node = frame.node.edges[nEdge],
1727 | qNode = frame.qNode.edges[qEdge],
1728 | final = node.final && qNode.final,
1729 | next = undefined
1730 |
1731 | if (nEdge in frame.output.edges) {
1732 | // an edge already exists for this character
1733 | // no need to create a new node, just set the finality
1734 | // bit unless this node is already final
1735 | next = frame.output.edges[nEdge]
1736 | next.final = next.final || final
1737 |
1738 | } else {
1739 | // no edge exists yet, must create one
1740 | // set the finality bit and insert it
1741 | // into the output
1742 | next = new lunr.TokenSet
1743 | next.final = final
1744 | frame.output.edges[nEdge] = next
1745 | }
1746 |
1747 | stack.push({
1748 | qNode: qNode,
1749 | output: next,
1750 | node: node
1751 | })
1752 | }
1753 | }
1754 | }
1755 | }
1756 |
1757 | return output
1758 | }
1759 | lunr.TokenSet.Builder = function () {
1760 | this.previousWord = ""
1761 | this.root = new lunr.TokenSet
1762 | this.uncheckedNodes = []
1763 | this.minimizedNodes = {}
1764 | }
1765 |
1766 | lunr.TokenSet.Builder.prototype.insert = function (word) {
1767 | var node,
1768 | commonPrefix = 0
1769 |
1770 | if (word < this.previousWord) {
1771 | throw new Error ("Out of order word insertion")
1772 | }
1773 |
1774 | for (var i = 0; i < word.length && i < this.previousWord.length; i++) {
1775 | if (word[i] != this.previousWord[i]) break
1776 | commonPrefix++
1777 | }
1778 |
1779 | this.minimize(commonPrefix)
1780 |
1781 | if (this.uncheckedNodes.length == 0) {
1782 | node = this.root
1783 | } else {
1784 | node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child
1785 | }
1786 |
1787 | for (var i = commonPrefix; i < word.length; i++) {
1788 | var nextNode = new lunr.TokenSet,
1789 | char = word[i]
1790 |
1791 | node.edges[char] = nextNode
1792 |
1793 | this.uncheckedNodes.push({
1794 | parent: node,
1795 | char: char,
1796 | child: nextNode
1797 | })
1798 |
1799 | node = nextNode
1800 | }
1801 |
1802 | node.final = true
1803 | this.previousWord = word
1804 | }
1805 |
1806 | lunr.TokenSet.Builder.prototype.finish = function () {
1807 | this.minimize(0)
1808 | }
1809 |
1810 | lunr.TokenSet.Builder.prototype.minimize = function (downTo) {
1811 | for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) {
1812 | var node = this.uncheckedNodes[i],
1813 | childKey = node.child.toString()
1814 |
1815 | if (childKey in this.minimizedNodes) {
1816 | node.parent.edges[node.char] = this.minimizedNodes[childKey]
1817 | } else {
1818 | // Cache the key for this node since
1819 | // we know it can't change anymore
1820 | node.child._str = childKey
1821 |
1822 | this.minimizedNodes[childKey] = node.child
1823 | }
1824 |
1825 | this.uncheckedNodes.pop()
1826 | }
1827 | }
1828 | /*!
1829 | * lunr.Index
1830 | * Copyright (C) 2020 Oliver Nightingale
1831 | */
1832 |
1833 | /**
1834 | * An index contains the built index of all documents and provides a query interface
1835 | * to the index.
1836 | *
1837 | * Usually instances of lunr.Index will not be created using this constructor, instead
1838 | * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be
1839 | * used to load previously built and serialized indexes.
1840 | *
1841 | * @constructor
1842 | * @param {Object} attrs - The attributes of the built search index.
1843 | * @param {Object} attrs.invertedIndex - An index of term/field to document reference.
1844 | * @param {Object} attrs.fieldVectors - Field vectors
1845 | * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.
1846 | * @param {string[]} attrs.fields - The names of indexed document fields.
1847 | * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.
1848 | */
1849 | lunr.Index = function (attrs) {
1850 | this.invertedIndex = attrs.invertedIndex
1851 | this.fieldVectors = attrs.fieldVectors
1852 | this.tokenSet = attrs.tokenSet
1853 | this.fields = attrs.fields
1854 | this.pipeline = attrs.pipeline
1855 | }
1856 |
1857 | /**
1858 | * A result contains details of a document matching a search query.
1859 | * @typedef {Object} lunr.Index~Result
1860 | * @property {string} ref - The reference of the document this result represents.
1861 | * @property {number} score - A number between 0 and 1 representing how similar this document is to the query.
1862 | * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match.
1863 | */
1864 |
1865 | /**
1866 | * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple
1867 | * query language which itself is parsed into an instance of lunr.Query.
1868 | *
1869 | * For programmatically building queries it is advised to directly use lunr.Query, the query language
1870 | * is best used for human entered text rather than program generated text.
1871 | *
1872 | * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported
1873 | * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello'
1874 | * or 'world', though those that contain both will rank higher in the results.
1875 | *
1876 | * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can
1877 | * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding
1878 | * wildcards will increase the number of documents that will be found but can also have a negative
1879 | * impact on query performance, especially with wildcards at the beginning of a term.
1880 | *
1881 | * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term
1882 | * hello in the title field will match this query. Using a field not present in the index will lead
1883 | * to an error being thrown.
1884 | *
1885 | * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term
1886 | * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported
1887 | * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.
1888 | * Avoid large values for edit distance to improve query performance.
1889 | *
1890 | * Each term also supports a presence modifier. By default a term's presence in document is optional, however
1891 | * this can be changed to either required or prohibited. For a term's presence to be required in a document the
1892 | * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and
1893 | * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not
1894 | * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.
1895 | *
1896 | * To escape special characters the backslash character '\' can be used, this allows searches to include
1897 | * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead
1898 | * of attempting to apply a boost of 2 to the search term "foo".
1899 | *
1900 | * @typedef {string} lunr.Index~QueryString
1901 | * @example
Simple single term query
1902 | * hello
1903 | * @example
Multiple term query
1904 | * hello world
1905 | * @example
term scoped to a field
1906 | * title:hello
1907 | * @example
term with a boost of 10
1908 | * hello^10
1909 | * @example
term with an edit distance of 2
1910 | * hello~2
1911 | * @example
terms with presence modifiers
1912 | * -foo +bar baz
1913 | */
1914 |
1915 | /**
1916 | * Performs a search against the index using lunr query syntax.
1917 | *
1918 | * Results will be returned sorted by their score, the most relevant results
1919 | * will be returned first. For details on how the score is calculated, please see
1920 | * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.
1921 | *
1922 | * For more programmatic querying use lunr.Index#query.
1923 | *
1924 | * @param {lunr.Index~QueryString} queryString - A string containing a lunr query.
1925 | * @throws {lunr.QueryParseError} If the passed query string cannot be parsed.
1926 | * @returns {lunr.Index~Result[]}
1927 | */
1928 | lunr.Index.prototype.search = function (queryString) {
1929 | return this.query(function (query) {
1930 | var parser = new lunr.QueryParser(queryString, query)
1931 | parser.parse()
1932 | })
1933 | }
1934 |
1935 | /**
1936 | * A query builder callback provides a query object to be used to express
1937 | * the query to perform on the index.
1938 | *
1939 | * @callback lunr.Index~queryBuilder
1940 | * @param {lunr.Query} query - The query object to build up.
1941 | * @this lunr.Query
1942 | */
1943 |
1944 | /**
1945 | * Performs a query against the index using the yielded lunr.Query object.
1946 | *
1947 | * If performing programmatic queries against the index, this method is preferred
1948 | * over lunr.Index#search so as to avoid the additional query parsing overhead.
1949 | *
1950 | * A query object is yielded to the supplied function which should be used to
1951 | * express the query to be run against the index.
1952 | *
1953 | * Note that although this function takes a callback parameter it is _not_ an
1954 | * asynchronous operation, the callback is just yielded a query object to be
1955 | * customized.
1956 | *
1957 | * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query.
1958 | * @returns {lunr.Index~Result[]}
1959 | */
1960 | lunr.Index.prototype.query = function (fn) {
1961 | // for each query clause
1962 | // * process terms
1963 | // * expand terms from token set
1964 | // * find matching documents and metadata
1965 | // * get document vectors
1966 | // * score documents
1967 |
1968 | var query = new lunr.Query(this.fields),
1969 | matchingFields = Object.create(null),
1970 | queryVectors = Object.create(null),
1971 | termFieldCache = Object.create(null),
1972 | requiredMatches = Object.create(null),
1973 | prohibitedMatches = Object.create(null)
1974 |
1975 | /*
1976 | * To support field level boosts a query vector is created per
1977 | * field. An empty vector is eagerly created to support negated
1978 | * queries.
1979 | */
1980 | for (var i = 0; i < this.fields.length; i++) {
1981 | queryVectors[this.fields[i]] = new lunr.Vector
1982 | }
1983 |
1984 | fn.call(query, query)
1985 |
1986 | for (var i = 0; i < query.clauses.length; i++) {
1987 | /*
1988 | * Unless the pipeline has been disabled for this term, which is
1989 | * the case for terms with wildcards, we need to pass the clause
1990 | * term through the search pipeline. A pipeline returns an array
1991 | * of processed terms. Pipeline functions may expand the passed
1992 | * term, which means we may end up performing multiple index lookups
1993 | * for a single query term.
1994 | */
1995 | var clause = query.clauses[i],
1996 | terms = null,
1997 | clauseMatches = lunr.Set.empty
1998 |
1999 | if (clause.usePipeline) {
2000 | terms = this.pipeline.runString(clause.term, {
2001 | fields: clause.fields
2002 | })
2003 | } else {
2004 | terms = [clause.term]
2005 | }
2006 |
2007 | for (var m = 0; m < terms.length; m++) {
2008 | var term = terms[m]
2009 |
2010 | /*
2011 | * Each term returned from the pipeline needs to use the same query
2012 | * clause object, e.g. the same boost and or edit distance. The
2013 | * simplest way to do this is to re-use the clause object but mutate
2014 | * its term property.
2015 | */
2016 | clause.term = term
2017 |
2018 | /*
2019 | * From the term in the clause we create a token set which will then
2020 | * be used to intersect the indexes token set to get a list of terms
2021 | * to lookup in the inverted index
2022 | */
2023 | var termTokenSet = lunr.TokenSet.fromClause(clause),
2024 | expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()
2025 |
2026 | /*
2027 | * If a term marked as required does not exist in the tokenSet it is
2028 | * impossible for the search to return any matches. We set all the field
2029 | * scoped required matches set to empty and stop examining any further
2030 | * clauses.
2031 | */
2032 | if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {
2033 | for (var k = 0; k < clause.fields.length; k++) {
2034 | var field = clause.fields[k]
2035 | requiredMatches[field] = lunr.Set.empty
2036 | }
2037 |
2038 | break
2039 | }
2040 |
2041 | for (var j = 0; j < expandedTerms.length; j++) {
2042 | /*
2043 | * For each term get the posting and termIndex, this is required for
2044 | * building the query vector.
2045 | */
2046 | var expandedTerm = expandedTerms[j],
2047 | posting = this.invertedIndex[expandedTerm],
2048 | termIndex = posting._index
2049 |
2050 | for (var k = 0; k < clause.fields.length; k++) {
2051 | /*
2052 | * For each field that this query term is scoped by (by default
2053 | * all fields are in scope) we need to get all the document refs
2054 | * that have this term in that field.
2055 | *
2056 | * The posting is the entry in the invertedIndex for the matching
2057 | * term from above.
2058 | */
2059 | var field = clause.fields[k],
2060 | fieldPosting = posting[field],
2061 | matchingDocumentRefs = Object.keys(fieldPosting),
2062 | termField = expandedTerm + "/" + field,
2063 | matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)
2064 |
2065 | /*
2066 | * if the presence of this term is required ensure that the matching
2067 | * documents are added to the set of required matches for this clause.
2068 | *
2069 | */
2070 | if (clause.presence == lunr.Query.presence.REQUIRED) {
2071 | clauseMatches = clauseMatches.union(matchingDocumentsSet)
2072 |
2073 | if (requiredMatches[field] === undefined) {
2074 | requiredMatches[field] = lunr.Set.complete
2075 | }
2076 | }
2077 |
2078 | /*
2079 | * if the presence of this term is prohibited ensure that the matching
2080 | * documents are added to the set of prohibited matches for this field,
2081 | * creating that set if it does not yet exist.
2082 | */
2083 | if (clause.presence == lunr.Query.presence.PROHIBITED) {
2084 | if (prohibitedMatches[field] === undefined) {
2085 | prohibitedMatches[field] = lunr.Set.empty
2086 | }
2087 |
2088 | prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)
2089 |
2090 | /*
2091 | * Prohibited matches should not be part of the query vector used for
2092 | * similarity scoring and no metadata should be extracted so we continue
2093 | * to the next field
2094 | */
2095 | continue
2096 | }
2097 |
2098 | /*
2099 | * The query field vector is populated using the termIndex found for
2100 | * the term and a unit value with the appropriate boost applied.
2101 | * Using upsert because there could already be an entry in the vector
2102 | * for the term we are working with. In that case we just add the scores
2103 | * together.
2104 | */
2105 | queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })
2106 |
2107 | /**
2108 | * If we've already seen this term, field combo then we've already collected
2109 | * the matching documents and metadata, no need to go through all that again
2110 | */
2111 | if (termFieldCache[termField]) {
2112 | continue
2113 | }
2114 |
2115 | for (var l = 0; l < matchingDocumentRefs.length; l++) {
2116 | /*
2117 | * All metadata for this term/field/document triple
2118 | * are then extracted and collected into an instance
2119 | * of lunr.MatchData ready to be returned in the query
2120 | * results
2121 | */
2122 | var matchingDocumentRef = matchingDocumentRefs[l],
2123 | matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field),
2124 | metadata = fieldPosting[matchingDocumentRef],
2125 | fieldMatch
2126 |
2127 | if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) {
2128 | matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata)
2129 | } else {
2130 | fieldMatch.add(expandedTerm, field, metadata)
2131 | }
2132 |
2133 | }
2134 |
2135 | termFieldCache[termField] = true
2136 | }
2137 | }
2138 | }
2139 |
2140 | /**
2141 | * If the presence was required we need to update the requiredMatches field sets.
2142 | * We do this after all fields for the term have collected their matches because
2143 | * the clause terms presence is required in _any_ of the fields not _all_ of the
2144 | * fields.
2145 | */
2146 | if (clause.presence === lunr.Query.presence.REQUIRED) {
2147 | for (var k = 0; k < clause.fields.length; k++) {
2148 | var field = clause.fields[k]
2149 | requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)
2150 | }
2151 | }
2152 | }
2153 |
2154 | /**
2155 | * Need to combine the field scoped required and prohibited
2156 | * matching documents into a global set of required and prohibited
2157 | * matches
2158 | */
2159 | var allRequiredMatches = lunr.Set.complete,
2160 | allProhibitedMatches = lunr.Set.empty
2161 |
2162 | for (var i = 0; i < this.fields.length; i++) {
2163 | var field = this.fields[i]
2164 |
2165 | if (requiredMatches[field]) {
2166 | allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])
2167 | }
2168 |
2169 | if (prohibitedMatches[field]) {
2170 | allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])
2171 | }
2172 | }
2173 |
2174 | var matchingFieldRefs = Object.keys(matchingFields),
2175 | results = [],
2176 | matches = Object.create(null)
2177 |
2178 | /*
2179 | * If the query is negated (contains only prohibited terms)
2180 | * we need to get _all_ fieldRefs currently existing in the
2181 | * index. This is only done when we know that the query is
2182 | * entirely prohibited terms to avoid any cost of getting all
2183 | * fieldRefs unnecessarily.
2184 | *
2185 | * Additionally, blank MatchData must be created to correctly
2186 | * populate the results.
2187 | */
2188 | if (query.isNegated()) {
2189 | matchingFieldRefs = Object.keys(this.fieldVectors)
2190 |
2191 | for (var i = 0; i < matchingFieldRefs.length; i++) {
2192 | var matchingFieldRef = matchingFieldRefs[i]
2193 | var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)
2194 | matchingFields[matchingFieldRef] = new lunr.MatchData
2195 | }
2196 | }
2197 |
2198 | for (var i = 0; i < matchingFieldRefs.length; i++) {
2199 | /*
2200 | * Currently we have document fields that match the query, but we
2201 | * need to return documents. The matchData and scores are combined
2202 | * from multiple fields belonging to the same document.
2203 | *
2204 | * Scores are calculated by field, using the query vectors created
2205 | * above, and combined into a final document score using addition.
2206 | */
2207 | var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),
2208 | docRef = fieldRef.docRef
2209 |
2210 | if (!allRequiredMatches.contains(docRef)) {
2211 | continue
2212 | }
2213 |
2214 | if (allProhibitedMatches.contains(docRef)) {
2215 | continue
2216 | }
2217 |
2218 | var fieldVector = this.fieldVectors[fieldRef],
2219 | score = queryVectors[fieldRef.fieldName].similarity(fieldVector),
2220 | docMatch
2221 |
2222 | if ((docMatch = matches[docRef]) !== undefined) {
2223 | docMatch.score += score
2224 | docMatch.matchData.combine(matchingFields[fieldRef])
2225 | } else {
2226 | var match = {
2227 | ref: docRef,
2228 | score: score,
2229 | matchData: matchingFields[fieldRef]
2230 | }
2231 | matches[docRef] = match
2232 | results.push(match)
2233 | }
2234 | }
2235 |
2236 | /*
2237 | * Sort the results objects by score, highest first.
2238 | */
2239 | return results.sort(function (a, b) {
2240 | return b.score - a.score
2241 | })
2242 | }
2243 |
2244 | /**
2245 | * Prepares the index for JSON serialization.
2246 | *
2247 | * The schema for this JSON blob will be described in a
2248 | * separate JSON schema file.
2249 | *
2250 | * @returns {Object}
2251 | */
2252 | lunr.Index.prototype.toJSON = function () {
2253 | var invertedIndex = Object.keys(this.invertedIndex)
2254 | .sort()
2255 | .map(function (term) {
2256 | return [term, this.invertedIndex[term]]
2257 | }, this)
2258 |
2259 | var fieldVectors = Object.keys(this.fieldVectors)
2260 | .map(function (ref) {
2261 | return [ref, this.fieldVectors[ref].toJSON()]
2262 | }, this)
2263 |
2264 | return {
2265 | version: lunr.version,
2266 | fields: this.fields,
2267 | fieldVectors: fieldVectors,
2268 | invertedIndex: invertedIndex,
2269 | pipeline: this.pipeline.toJSON()
2270 | }
2271 | }
2272 |
2273 | /**
2274 | * Loads a previously serialized lunr.Index
2275 | *
2276 | * @param {Object} serializedIndex - A previously serialized lunr.Index
2277 | * @returns {lunr.Index}
2278 | */
2279 | lunr.Index.load = function (serializedIndex) {
2280 | var attrs = {},
2281 | fieldVectors = {},
2282 | serializedVectors = serializedIndex.fieldVectors,
2283 | invertedIndex = Object.create(null),
2284 | serializedInvertedIndex = serializedIndex.invertedIndex,
2285 | tokenSetBuilder = new lunr.TokenSet.Builder,
2286 | pipeline = lunr.Pipeline.load(serializedIndex.pipeline)
2287 |
2288 | if (serializedIndex.version != lunr.version) {
2289 | lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'")
2290 | }
2291 |
2292 | for (var i = 0; i < serializedVectors.length; i++) {
2293 | var tuple = serializedVectors[i],
2294 | ref = tuple[0],
2295 | elements = tuple[1]
2296 |
2297 | fieldVectors[ref] = new lunr.Vector(elements)
2298 | }
2299 |
2300 | for (var i = 0; i < serializedInvertedIndex.length; i++) {
2301 | var tuple = serializedInvertedIndex[i],
2302 | term = tuple[0],
2303 | posting = tuple[1]
2304 |
2305 | tokenSetBuilder.insert(term)
2306 | invertedIndex[term] = posting
2307 | }
2308 |
2309 | tokenSetBuilder.finish()
2310 |
2311 | attrs.fields = serializedIndex.fields
2312 |
2313 | attrs.fieldVectors = fieldVectors
2314 | attrs.invertedIndex = invertedIndex
2315 | attrs.tokenSet = tokenSetBuilder.root
2316 | attrs.pipeline = pipeline
2317 |
2318 | return new lunr.Index(attrs)
2319 | }
2320 | /*!
2321 | * lunr.Builder
2322 | * Copyright (C) 2020 Oliver Nightingale
2323 | */
2324 |
2325 | /**
2326 | * lunr.Builder performs indexing on a set of documents and
2327 | * returns instances of lunr.Index ready for querying.
2328 | *
2329 | * All configuration of the index is done via the builder, the
2330 | * fields to index, the document reference, the text processing
2331 | * pipeline and document scoring parameters are all set on the
2332 | * builder before indexing.
2333 | *
2334 | * @constructor
2335 | * @property {string} _ref - Internal reference to the document reference field.
2336 | * @property {string[]} _fields - Internal reference to the document fields to index.
2337 | * @property {object} invertedIndex - The inverted index maps terms to document fields.
2338 | * @property {object} documentTermFrequencies - Keeps track of document term frequencies.
2339 | * @property {object} documentLengths - Keeps track of the length of documents added to the index.
2340 | * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing.
2341 | * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing.
2342 | * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index.
2343 | * @property {number} documentCount - Keeps track of the total number of documents indexed.
2344 | * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75.
2345 | * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2.
2346 | * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space.
2347 | * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index.
2348 | */
2349 | lunr.Builder = function () {
2350 | this._ref = "id"
2351 | this._fields = Object.create(null)
2352 | this._documents = Object.create(null)
2353 | this.invertedIndex = Object.create(null)
2354 | this.fieldTermFrequencies = {}
2355 | this.fieldLengths = {}
2356 | this.tokenizer = lunr.tokenizer
2357 | this.pipeline = new lunr.Pipeline
2358 | this.searchPipeline = new lunr.Pipeline
2359 | this.documentCount = 0
2360 | this._b = 0.75
2361 | this._k1 = 1.2
2362 | this.termIndex = 0
2363 | this.metadataWhitelist = []
2364 | }
2365 |
2366 | /**
2367 | * Sets the document field used as the document reference. Every document must have this field.
2368 | * The type of this field in the document should be a string, if it is not a string it will be
2369 | * coerced into a string by calling toString.
2370 | *
2371 | * The default ref is 'id'.
2372 | *
2373 | * The ref should _not_ be changed during indexing, it should be set before any documents are
2374 | * added to the index. Changing it during indexing can lead to inconsistent results.
2375 | *
2376 | * @param {string} ref - The name of the reference field in the document.
2377 | */
2378 | lunr.Builder.prototype.ref = function (ref) {
2379 | this._ref = ref
2380 | }
2381 |
2382 | /**
2383 | * A function that is used to extract a field from a document.
2384 | *
2385 | * Lunr expects a field to be at the top level of a document, if however the field
2386 | * is deeply nested within a document an extractor function can be used to extract
2387 | * the right field for indexing.
2388 | *
2389 | * @callback fieldExtractor
2390 | * @param {object} doc - The document being added to the index.
2391 | * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.
2392 | * @example
Extracting a nested field
2393 | * function (doc) { return doc.nested.field }
2394 | */
2395 |
2396 | /**
2397 | * Adds a field to the list of document fields that will be indexed. Every document being
2398 | * indexed should have this field. Null values for this field in indexed documents will
2399 | * not cause errors but will limit the chance of that document being retrieved by searches.
2400 | *
2401 | * All fields should be added before adding documents to the index. Adding fields after
2402 | * a document has been indexed will have no effect on already indexed documents.
2403 | *
2404 | * Fields can be boosted at build time. This allows terms within that field to have more
2405 | * importance when ranking search results. Use a field boost to specify that matches within
2406 | * one field are more important than other fields.
2407 | *
2408 | * @param {string} fieldName - The name of a field to index in all documents.
2409 | * @param {object} attributes - Optional attributes associated with this field.
2410 | * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.
2411 | * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.
2412 | * @throws {RangeError} fieldName cannot contain unsupported characters '/'
2413 | */
2414 | lunr.Builder.prototype.field = function (fieldName, attributes) {
2415 | if (/\//.test(fieldName)) {
2416 | throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'")
2417 | }
2418 |
2419 | this._fields[fieldName] = attributes || {}
2420 | }
2421 |
2422 | /**
2423 | * A parameter to tune the amount of field length normalisation that is applied when
2424 | * calculating relevance scores. A value of 0 will completely disable any normalisation
2425 | * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b
2426 | * will be clamped to the range 0 - 1.
2427 | *
2428 | * @param {number} number - The value to set for this tuning parameter.
2429 | */
2430 | lunr.Builder.prototype.b = function (number) {
2431 | if (number < 0) {
2432 | this._b = 0
2433 | } else if (number > 1) {
2434 | this._b = 1
2435 | } else {
2436 | this._b = number
2437 | }
2438 | }
2439 |
2440 | /**
2441 | * A parameter that controls the speed at which a rise in term frequency results in term
2442 | * frequency saturation. The default value is 1.2. Setting this to a higher value will give
2443 | * slower saturation levels, a lower value will result in quicker saturation.
2444 | *
2445 | * @param {number} number - The value to set for this tuning parameter.
2446 | */
2447 | lunr.Builder.prototype.k1 = function (number) {
2448 | this._k1 = number
2449 | }
2450 |
2451 | /**
2452 | * Adds a document to the index.
2453 | *
2454 | * Before adding fields to the index the index should have been fully setup, with the document
2455 | * ref and all fields to index already having been specified.
2456 | *
2457 | * The document must have a field name as specified by the ref (by default this is 'id') and
2458 | * it should have all fields defined for indexing, though null or undefined values will not
2459 | * cause errors.
2460 | *
2461 | * Entire documents can be boosted at build time. Applying a boost to a document indicates that
2462 | * this document should rank higher in search results than other documents.
2463 | *
2464 | * @param {object} doc - The document to add to the index.
2465 | * @param {object} attributes - Optional attributes associated with this document.
2466 | * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.
2467 | */
2468 | lunr.Builder.prototype.add = function (doc, attributes) {
2469 | var docRef = doc[this._ref],
2470 | fields = Object.keys(this._fields)
2471 |
2472 | this._documents[docRef] = attributes || {}
2473 | this.documentCount += 1
2474 |
2475 | for (var i = 0; i < fields.length; i++) {
2476 | var fieldName = fields[i],
2477 | extractor = this._fields[fieldName].extractor,
2478 | field = extractor ? extractor(doc) : doc[fieldName],
2479 | tokens = this.tokenizer(field, {
2480 | fields: [fieldName]
2481 | }),
2482 | terms = this.pipeline.run(tokens),
2483 | fieldRef = new lunr.FieldRef (docRef, fieldName),
2484 | fieldTerms = Object.create(null)
2485 |
2486 | this.fieldTermFrequencies[fieldRef] = fieldTerms
2487 | this.fieldLengths[fieldRef] = 0
2488 |
2489 | // store the length of this field for this document
2490 | this.fieldLengths[fieldRef] += terms.length
2491 |
2492 | // calculate term frequencies for this field
2493 | for (var j = 0; j < terms.length; j++) {
2494 | var term = terms[j]
2495 |
2496 | if (fieldTerms[term] == undefined) {
2497 | fieldTerms[term] = 0
2498 | }
2499 |
2500 | fieldTerms[term] += 1
2501 |
2502 | // add to inverted index
2503 | // create an initial posting if one doesn't exist
2504 | if (this.invertedIndex[term] == undefined) {
2505 | var posting = Object.create(null)
2506 | posting["_index"] = this.termIndex
2507 | this.termIndex += 1
2508 |
2509 | for (var k = 0; k < fields.length; k++) {
2510 | posting[fields[k]] = Object.create(null)
2511 | }
2512 |
2513 | this.invertedIndex[term] = posting
2514 | }
2515 |
2516 | // add an entry for this term/fieldName/docRef to the invertedIndex
2517 | if (this.invertedIndex[term][fieldName][docRef] == undefined) {
2518 | this.invertedIndex[term][fieldName][docRef] = Object.create(null)
2519 | }
2520 |
2521 | // store all whitelisted metadata about this token in the
2522 | // inverted index
2523 | for (var l = 0; l < this.metadataWhitelist.length; l++) {
2524 | var metadataKey = this.metadataWhitelist[l],
2525 | metadata = term.metadata[metadataKey]
2526 |
2527 | if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) {
2528 | this.invertedIndex[term][fieldName][docRef][metadataKey] = []
2529 | }
2530 |
2531 | this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata)
2532 | }
2533 | }
2534 |
2535 | }
2536 | }
2537 |
2538 | /**
2539 | * Calculates the average document length for this index
2540 | *
2541 | * @private
2542 | */
2543 | lunr.Builder.prototype.calculateAverageFieldLengths = function () {
2544 |
2545 | var fieldRefs = Object.keys(this.fieldLengths),
2546 | numberOfFields = fieldRefs.length,
2547 | accumulator = {},
2548 | documentsWithField = {}
2549 |
2550 | for (var i = 0; i < numberOfFields; i++) {
2551 | var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),
2552 | field = fieldRef.fieldName
2553 |
2554 | documentsWithField[field] || (documentsWithField[field] = 0)
2555 | documentsWithField[field] += 1
2556 |
2557 | accumulator[field] || (accumulator[field] = 0)
2558 | accumulator[field] += this.fieldLengths[fieldRef]
2559 | }
2560 |
2561 | var fields = Object.keys(this._fields)
2562 |
2563 | for (var i = 0; i < fields.length; i++) {
2564 | var fieldName = fields[i]
2565 | accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]
2566 | }
2567 |
2568 | this.averageFieldLength = accumulator
2569 | }
2570 |
2571 | /**
2572 | * Builds a vector space model of every document using lunr.Vector
2573 | *
2574 | * @private
2575 | */
2576 | lunr.Builder.prototype.createFieldVectors = function () {
2577 | var fieldVectors = {},
2578 | fieldRefs = Object.keys(this.fieldTermFrequencies),
2579 | fieldRefsLength = fieldRefs.length,
2580 | termIdfCache = Object.create(null)
2581 |
2582 | for (var i = 0; i < fieldRefsLength; i++) {
2583 | var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),
2584 | fieldName = fieldRef.fieldName,
2585 | fieldLength = this.fieldLengths[fieldRef],
2586 | fieldVector = new lunr.Vector,
2587 | termFrequencies = this.fieldTermFrequencies[fieldRef],
2588 | terms = Object.keys(termFrequencies),
2589 | termsLength = terms.length
2590 |
2591 |
2592 | var fieldBoost = this._fields[fieldName].boost || 1,
2593 | docBoost = this._documents[fieldRef.docRef].boost || 1
2594 |
2595 | for (var j = 0; j < termsLength; j++) {
2596 | var term = terms[j],
2597 | tf = termFrequencies[term],
2598 | termIndex = this.invertedIndex[term]._index,
2599 | idf, score, scoreWithPrecision
2600 |
2601 | if (termIdfCache[term] === undefined) {
2602 | idf = lunr.idf(this.invertedIndex[term], this.documentCount)
2603 | termIdfCache[term] = idf
2604 | } else {
2605 | idf = termIdfCache[term]
2606 | }
2607 |
2608 | score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)
2609 | score *= fieldBoost
2610 | score *= docBoost
2611 | scoreWithPrecision = Math.round(score * 1000) / 1000
2612 | // Converts 1.23456789 to 1.234.
2613 | // Reducing the precision so that the vectors take up less
2614 | // space when serialised. Doing it now so that they behave
2615 | // the same before and after serialisation. Also, this is
2616 | // the fastest approach to reducing a number's precision in
2617 | // JavaScript.
2618 |
2619 | fieldVector.insert(termIndex, scoreWithPrecision)
2620 | }
2621 |
2622 | fieldVectors[fieldRef] = fieldVector
2623 | }
2624 |
2625 | this.fieldVectors = fieldVectors
2626 | }
2627 |
2628 | /**
2629 | * Creates a token set of all tokens in the index using lunr.TokenSet
2630 | *
2631 | * @private
2632 | */
2633 | lunr.Builder.prototype.createTokenSet = function () {
2634 | this.tokenSet = lunr.TokenSet.fromArray(
2635 | Object.keys(this.invertedIndex).sort()
2636 | )
2637 | }
2638 |
2639 | /**
2640 | * Builds the index, creating an instance of lunr.Index.
2641 | *
2642 | * This completes the indexing process and should only be called
2643 | * once all documents have been added to the index.
2644 | *
2645 | * @returns {lunr.Index}
2646 | */
2647 | lunr.Builder.prototype.build = function () {
2648 | this.calculateAverageFieldLengths()
2649 | this.createFieldVectors()
2650 | this.createTokenSet()
2651 |
2652 | return new lunr.Index({
2653 | invertedIndex: this.invertedIndex,
2654 | fieldVectors: this.fieldVectors,
2655 | tokenSet: this.tokenSet,
2656 | fields: Object.keys(this._fields),
2657 | pipeline: this.searchPipeline
2658 | })
2659 | }
2660 |
2661 | /**
2662 | * Applies a plugin to the index builder.
2663 | *
2664 | * A plugin is a function that is called with the index builder as its context.
2665 | * Plugins can be used to customise or extend the behaviour of the index
2666 | * in some way. A plugin is just a function, that encapsulated the custom
2667 | * behaviour that should be applied when building the index.
2668 | *
2669 | * The plugin function will be called with the index builder as its argument, additional
2670 | * arguments can also be passed when calling use. The function will be called
2671 | * with the index builder as its context.
2672 | *
2673 | * @param {Function} plugin The plugin to apply.
2674 | */
2675 | lunr.Builder.prototype.use = function (fn) {
2676 | var args = Array.prototype.slice.call(arguments, 1)
2677 | args.unshift(this)
2678 | fn.apply(this, args)
2679 | }
2680 | /**
2681 | * Contains and collects metadata about a matching document.
2682 | * A single instance of lunr.MatchData is returned as part of every
2683 | * lunr.Index~Result.
2684 | *
2685 | * @constructor
2686 | * @param {string} term - The term this match data is associated with
2687 | * @param {string} field - The field in which the term was found
2688 | * @param {object} metadata - The metadata recorded about this term in this field
2689 | * @property {object} metadata - A cloned collection of metadata associated with this document.
2690 | * @see {@link lunr.Index~Result}
2691 | */
2692 | lunr.MatchData = function (term, field, metadata) {
2693 | var clonedMetadata = Object.create(null),
2694 | metadataKeys = Object.keys(metadata || {})
2695 |
2696 | // Cloning the metadata to prevent the original
2697 | // being mutated during match data combination.
2698 | // Metadata is kept in an array within the inverted
2699 | // index so cloning the data can be done with
2700 | // Array#slice
2701 | for (var i = 0; i < metadataKeys.length; i++) {
2702 | var key = metadataKeys[i]
2703 | clonedMetadata[key] = metadata[key].slice()
2704 | }
2705 |
2706 | this.metadata = Object.create(null)
2707 |
2708 | if (term !== undefined) {
2709 | this.metadata[term] = Object.create(null)
2710 | this.metadata[term][field] = clonedMetadata
2711 | }
2712 | }
2713 |
2714 | /**
2715 | * An instance of lunr.MatchData will be created for every term that matches a
2716 | * document. However only one instance is required in a lunr.Index~Result. This
2717 | * method combines metadata from another instance of lunr.MatchData with this
2718 | * objects metadata.
2719 | *
2720 | * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one.
2721 | * @see {@link lunr.Index~Result}
2722 | */
2723 | lunr.MatchData.prototype.combine = function (otherMatchData) {
2724 | var terms = Object.keys(otherMatchData.metadata)
2725 |
2726 | for (var i = 0; i < terms.length; i++) {
2727 | var term = terms[i],
2728 | fields = Object.keys(otherMatchData.metadata[term])
2729 |
2730 | if (this.metadata[term] == undefined) {
2731 | this.metadata[term] = Object.create(null)
2732 | }
2733 |
2734 | for (var j = 0; j < fields.length; j++) {
2735 | var field = fields[j],
2736 | keys = Object.keys(otherMatchData.metadata[term][field])
2737 |
2738 | if (this.metadata[term][field] == undefined) {
2739 | this.metadata[term][field] = Object.create(null)
2740 | }
2741 |
2742 | for (var k = 0; k < keys.length; k++) {
2743 | var key = keys[k]
2744 |
2745 | if (this.metadata[term][field][key] == undefined) {
2746 | this.metadata[term][field][key] = otherMatchData.metadata[term][field][key]
2747 | } else {
2748 | this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key])
2749 | }
2750 |
2751 | }
2752 | }
2753 | }
2754 | }
2755 |
2756 | /**
2757 | * Add metadata for a term/field pair to this instance of match data.
2758 | *
2759 | * @param {string} term - The term this match data is associated with
2760 | * @param {string} field - The field in which the term was found
2761 | * @param {object} metadata - The metadata recorded about this term in this field
2762 | */
2763 | lunr.MatchData.prototype.add = function (term, field, metadata) {
2764 | if (!(term in this.metadata)) {
2765 | this.metadata[term] = Object.create(null)
2766 | this.metadata[term][field] = metadata
2767 | return
2768 | }
2769 |
2770 | if (!(field in this.metadata[term])) {
2771 | this.metadata[term][field] = metadata
2772 | return
2773 | }
2774 |
2775 | var metadataKeys = Object.keys(metadata)
2776 |
2777 | for (var i = 0; i < metadataKeys.length; i++) {
2778 | var key = metadataKeys[i]
2779 |
2780 | if (key in this.metadata[term][field]) {
2781 | this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key])
2782 | } else {
2783 | this.metadata[term][field][key] = metadata[key]
2784 | }
2785 | }
2786 | }
2787 | /**
2788 | * A lunr.Query provides a programmatic way of defining queries to be performed
2789 | * against a {@link lunr.Index}.
2790 | *
2791 | * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method
2792 | * so the query object is pre-initialized with the right index fields.
2793 | *
2794 | * @constructor
2795 | * @property {lunr.Query~Clause[]} clauses - An array of query clauses.
2796 | * @property {string[]} allFields - An array of all available fields in a lunr.Index.
2797 | */
2798 | lunr.Query = function (allFields) {
2799 | this.clauses = []
2800 | this.allFields = allFields
2801 | }
2802 |
2803 | /**
2804 | * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause.
2805 | *
2806 | * This allows wildcards to be added to the beginning and end of a term without having to manually do any string
2807 | * concatenation.
2808 | *
2809 | * The wildcard constants can be bitwise combined to select both leading and trailing wildcards.
2810 | *
2811 | * @constant
2812 | * @default
2813 | * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour
2814 | * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists
2815 | * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists
2816 | * @see lunr.Query~Clause
2817 | * @see lunr.Query#clause
2818 | * @see lunr.Query#term
2819 | * @example
2841 | * query.term('foo', { presence: lunr.Query.presence.REQUIRED })
2842 | */
2843 | lunr.Query.presence = {
2844 | /**
2845 | * Term's presence in a document is optional, this is the default value.
2846 | */
2847 | OPTIONAL: 1,
2848 |
2849 | /**
2850 | * Term's presence in a document is required, documents that do not contain
2851 | * this term will not be returned.
2852 | */
2853 | REQUIRED: 2,
2854 |
2855 | /**
2856 | * Term's presence in a document is prohibited, documents that do contain
2857 | * this term will not be returned.
2858 | */
2859 | PROHIBITED: 3
2860 | }
2861 |
2862 | /**
2863 | * A single clause in a {@link lunr.Query} contains a term and details on how to
2864 | * match that term against a {@link lunr.Index}.
2865 | *
2866 | * @typedef {Object} lunr.Query~Clause
2867 | * @property {string[]} fields - The fields in an index this clause should be matched against.
2868 | * @property {number} [boost=1] - Any boost that should be applied when matching this clause.
2869 | * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.
2870 | * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.
2871 | * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.
2872 | * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.
2873 | */
2874 |
2875 | /**
2876 | * Adds a {@link lunr.Query~Clause} to this query.
2877 | *
2878 | * Unless the clause contains the fields to be matched all fields will be matched. In addition
2879 | * a default boost of 1 is applied to the clause.
2880 | *
2881 | * @param {lunr.Query~Clause} clause - The clause to add to this query.
2882 | * @see lunr.Query~Clause
2883 | * @returns {lunr.Query}
2884 | */
2885 | lunr.Query.prototype.clause = function (clause) {
2886 | if (!('fields' in clause)) {
2887 | clause.fields = this.allFields
2888 | }
2889 |
2890 | if (!('boost' in clause)) {
2891 | clause.boost = 1
2892 | }
2893 |
2894 | if (!('usePipeline' in clause)) {
2895 | clause.usePipeline = true
2896 | }
2897 |
2898 | if (!('wildcard' in clause)) {
2899 | clause.wildcard = lunr.Query.wildcard.NONE
2900 | }
2901 |
2902 | if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) {
2903 | clause.term = "*" + clause.term
2904 | }
2905 |
2906 | if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) {
2907 | clause.term = "" + clause.term + "*"
2908 | }
2909 |
2910 | if (!('presence' in clause)) {
2911 | clause.presence = lunr.Query.presence.OPTIONAL
2912 | }
2913 |
2914 | this.clauses.push(clause)
2915 |
2916 | return this
2917 | }
2918 |
2919 | /**
2920 | * A negated query is one in which every clause has a presence of
2921 | * prohibited. These queries require some special processing to return
2922 | * the expected results.
2923 | *
2924 | * @returns boolean
2925 | */
2926 | lunr.Query.prototype.isNegated = function () {
2927 | for (var i = 0; i < this.clauses.length; i++) {
2928 | if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {
2929 | return false
2930 | }
2931 | }
2932 |
2933 | return true
2934 | }
2935 |
2936 | /**
2937 | * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}
2938 | * to the list of clauses that make up this query.
2939 | *
2940 | * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion
2941 | * to a token or token-like string should be done before calling this method.
2942 | *
2943 | * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an
2944 | * array, each term in the array will share the same options.
2945 | *
2946 | * @param {object|object[]} term - The term(s) to add to the query.
2947 | * @param {object} [options] - Any additional properties to add to the query clause.
2948 | * @returns {lunr.Query}
2949 | * @see lunr.Query#clause
2950 | * @see lunr.Query~Clause
2951 | * @example
adding a single term to a query
2952 | * query.term("foo")
2953 | * @example
adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard