minimal-mistakes-jekyll 4.13.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,16 +11,17 @@
11
11
  .search__toggle {
12
12
  margin-left: 1rem;
13
13
  margin-right: 1rem;
14
+ height: $nav-toggle-height;
14
15
  border: 0;
15
16
  outline: none;
16
- color: $muted-text-color;
17
+ color: $primary-color;
17
18
  background-color: transparent;
18
19
  cursor: pointer;
19
20
  -webkit-transition: 0.2s;
20
21
  transition: 0.2s;
21
22
 
22
23
  &:hover {
23
- color: $text-color;
24
+ color: mix(#000, $primary-color, 25%);
24
25
  }
25
26
  }
26
27
 
@@ -114,12 +115,12 @@
114
115
  }
115
116
 
116
117
  .archive__item-title .ais-Highlight {
117
- color: $link-color;
118
+ color: $primary-color;
118
119
  font-style: normal;
119
120
  text-decoration: underline;
120
121
  }
121
122
  .archive__item-excerpt .ais-Highlight {
122
- color: $link-color;
123
+ color: $primary-color;
123
124
  font-style: normal;
124
125
  font-weight: bold;
125
126
  }
@@ -315,7 +315,7 @@ body:hover .visually-hidden button {
315
315
  position: relative;
316
316
  width: $navicon-width;
317
317
  height: $navicon-height;
318
- background: #fff;
318
+ background: $primary-color;
319
319
  margin: auto;
320
320
  -webkit-transition: 0.3s;
321
321
  transition: 0.3s;
@@ -327,7 +327,7 @@ body:hover .visually-hidden button {
327
327
  left: 0;
328
328
  width: $navicon-width;
329
329
  height: $navicon-height;
330
- background: #fff;
330
+ background: $primary-color;
331
331
  -webkit-transition: 0.3s;
332
332
  transition: 0.3s;
333
333
  }
@@ -366,6 +366,14 @@ body:hover .visually-hidden button {
366
366
  }
367
367
  }
368
368
 
369
+ .greedy-nav__toggle:hover {
370
+ .navicon,
371
+ .navicon:before,
372
+ .navicon:after {
373
+ background: mix(#000, $primary-color, 25%);
374
+ }
375
+ }
376
+
369
377
  /*
370
378
  Sticky, fixed to top content
371
379
  ========================================================================== */
@@ -62,11 +62,12 @@ $border-color: $lighter-gray !default;
62
62
  $form-background-color: $lighter-gray !default;
63
63
  $footer-background-color: $lighter-gray !default;
64
64
 
65
- $primary-color: #7a8288 !default;
66
- $success-color: #62c462 !default;
67
- $warning-color: #f89406 !default;
65
+ $primary-color: #6f777d !default;
66
+ $success-color: #3fa63f !default;
67
+ $warning-color: #d67f05 !default;
68
68
  $danger-color: #ee5f5b !default;
69
- $info-color: #52adc8 !default;
69
+ $info-color: #3b9cba !default;
70
+ $focus-color: $primary-color !default;
70
71
 
71
72
  /* YIQ color contrast */
72
73
  $yiq-contrasted-dark-default: $dark-gray !default;
@@ -101,9 +102,9 @@ $youtube-color: #bb0000 !default;
101
102
  $xing-color: #006567 !default;
102
103
 
103
104
  /* links */
104
- $link-color: $info-color !default;
105
+ $link-color: mix(#000, $info-color, 15%) !default;
105
106
  $link-color-hover: mix(#000, $link-color, 25%) !default;
106
- $link-color-visited: mix(#fff, $link-color, 25%) !default;
107
+ $link-color-visited: mix(#fff, $link-color, 15%) !default;
107
108
  $masthead-link-color: $primary-color !default;
108
109
  $masthead-link-color-hover: mix(#000, $primary-color, 25%) !default;
109
110
  $navicon-link-color-hover: mix(#fff, $primary-color, 75%) !default;
@@ -18,25 +18,11 @@ $masthead-link-color: $text-color !default;
18
18
  $masthead-link-color-hover: mix(#000, $text-color, 20%) !default;
19
19
  $navicon-link-color-hover: mix(#000, $background-color, 30%) !default;
20
20
 
21
- /* dark syntax highlighting (base16) */
22
- $base00: #ffffff !default;
23
- $base01: #e0e0e0 !default;
24
- $base02: #d0d0d0 !default;
25
- $base03: #b0b0b0 !default;
26
- $base04: #000000 !default;
27
- $base05: #101010 !default;
28
- $base06: #151515 !default;
29
- $base07: #202020 !default;
30
- $base08: #ff0086 !default;
31
- $base09: #fd8900 !default;
32
- $base0a: #aba800 !default;
33
- $base0b: #00c918 !default;
34
- $base0c: #1faaaa !default;
35
- $base0d: #3777e6 !default;
36
- $base0e: #ad00a1 !default;
37
- $base0f: #cc6633 !default;
38
-
39
21
  .author__urls.social-icons .svg-inline--fa,
40
22
  .page__footer-follow .social-icons .svg-inline--fa {
41
23
  color: inherit;
42
24
  }
25
+
26
+ .ais-search-box .ais-search-box--input {
27
+ background-color: $form-background-color;
28
+ }
@@ -51,3 +51,7 @@ $base0f: #cc6633 !default;
51
51
  color: $text-color;
52
52
  }
53
53
  }
54
+
55
+ .ais-search-box .ais-search-box--input {
56
+ background-color: $form-background-color;
57
+ }
@@ -58,3 +58,7 @@ $base0f: #cc6633 !default;
58
58
  color: $text-color;
59
59
  }
60
60
  }
61
+
62
+ .ais-search-box .ais-search-box--input {
63
+ background-color: $form-background-color;
64
+ }
@@ -18,7 +18,8 @@ var store = [
18
18
  "title": {{ doc.title | jsonify }},
19
19
  "excerpt":
20
20
  {%- if site.search_full_content == true -%}
21
- {{ doc.content |
21
+ {{ doc.content | newline_to_br |
22
+ replace:"<br />", " " |
22
23
  replace:"</p>", " " |
23
24
  replace:"</h1>", " " |
24
25
  replace:"</h2>", " " |
@@ -28,7 +29,8 @@ var store = [
28
29
  replace:"</h6>", " "|
29
30
  strip_html | strip_newlines | jsonify }},
30
31
  {%- else -%}
31
- {{ doc.content |
32
+ {{ doc.content | newline_to_br |
33
+ replace:"<br />", " " |
32
34
  replace:"</p>", " " |
33
35
  replace:"</h1>", " " |
34
36
  replace:"</h2>", " " |
@@ -1,6 +1,6 @@
1
1
  /**
2
- * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.1.5
3
- * Copyright (C) 2017 Oliver Nightingale
2
+ * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.3
3
+ * Copyright (C) 2018 Oliver Nightingale
4
4
  * @license MIT
5
5
  */
6
6
 
@@ -54,14 +54,15 @@ var lunr = function (config) {
54
54
  return builder.build()
55
55
  }
56
56
 
57
- lunr.version = "2.1.5"
57
+ lunr.version = "2.3.3"
58
58
  /*!
59
59
  * lunr.utils
60
- * Copyright (C) 2017 Oliver Nightingale
60
+ * Copyright (C) 2018 Oliver Nightingale
61
61
  */
62
62
 
63
63
  /**
64
64
  * A namespace containing utils for the rest of the lunr library
65
+ * @namespace lunr.utils
65
66
  */
66
67
  lunr.utils = {}
67
68
 
@@ -69,7 +70,8 @@ lunr.utils = {}
69
70
  * Print a warning message to the console.
70
71
  *
71
72
  * @param {String} message The message to be printed.
72
- * @memberOf Utils
73
+ * @memberOf lunr.utils
74
+ * @function
73
75
  */
74
76
  lunr.utils.warn = (function (global) {
75
77
  /* eslint-disable no-console */
@@ -90,7 +92,7 @@ lunr.utils.warn = (function (global) {
90
92
  *
91
93
  * @param {Any} obj The object to convert to a string.
92
94
  * @return {String} string representation of the passed object.
93
- * @memberOf Utils
95
+ * @memberOf lunr.utils
94
96
  */
95
97
  lunr.utils.asString = function (obj) {
96
98
  if (obj === void 0 || obj === null) {
@@ -99,6 +101,52 @@ lunr.utils.asString = function (obj) {
99
101
  return obj.toString()
100
102
  }
101
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
+ }
102
150
  lunr.FieldRef = function (docRef, fieldName, stringValue) {
103
151
  this.docRef = docRef
104
152
  this.fieldName = fieldName
@@ -127,6 +175,139 @@ lunr.FieldRef.prototype.toString = function () {
127
175
 
128
176
  return this._stringValue
129
177
  }
178
+ /*!
179
+ * lunr.Set
180
+ * Copyright (C) 2018 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 (other) {
215
+ return other
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
+ }
130
311
  /**
131
312
  * A function to calculate the inverse document frequency for
132
313
  * a posting. This is shared between the builder and the index
@@ -208,7 +389,7 @@ lunr.Token.prototype.clone = function (fn) {
208
389
  }
209
390
  /*!
210
391
  * lunr.tokenizer
211
- * Copyright (C) 2017 Oliver Nightingale
392
+ * Copyright (C) 2018 Oliver Nightingale
212
393
  */
213
394
 
214
395
  /**
@@ -220,18 +401,26 @@ lunr.Token.prototype.clone = function (fn) {
220
401
  * then will split this string on the character in `lunr.tokenizer.separator`.
221
402
  * Arrays will have their elements converted to strings and wrapped in a lunr.Token.
222
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
+ *
223
407
  * @static
224
408
  * @param {?(string|object|object[])} obj - The object to convert into tokens
409
+ * @param {?object} metadata - Optional metadata to associate with every token
225
410
  * @returns {lunr.Token[]}
411
+ * @see {@link lunr.Pipeline}
226
412
  */
227
- lunr.tokenizer = function (obj) {
413
+ lunr.tokenizer = function (obj, metadata) {
228
414
  if (obj == null || obj == undefined) {
229
415
  return []
230
416
  }
231
417
 
232
418
  if (Array.isArray(obj)) {
233
419
  return obj.map(function (t) {
234
- return new lunr.Token(lunr.utils.asString(t).toLowerCase())
420
+ return new lunr.Token(
421
+ lunr.utils.asString(t).toLowerCase(),
422
+ lunr.utils.clone(metadata)
423
+ )
235
424
  })
236
425
  }
237
426
 
@@ -246,11 +435,15 @@ lunr.tokenizer = function (obj) {
246
435
  if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {
247
436
 
248
437
  if (sliceLength > 0) {
438
+ var tokenMetadata = lunr.utils.clone(metadata) || {}
439
+ tokenMetadata["position"] = [sliceStart, sliceLength]
440
+ tokenMetadata["index"] = tokens.length
441
+
249
442
  tokens.push(
250
- new lunr.Token (str.slice(sliceStart, sliceEnd), {
251
- position: [sliceStart, sliceLength],
252
- index: tokens.length
253
- })
443
+ new lunr.Token (
444
+ str.slice(sliceStart, sliceEnd),
445
+ tokenMetadata
446
+ )
254
447
  )
255
448
  }
256
449
 
@@ -272,7 +465,7 @@ lunr.tokenizer = function (obj) {
272
465
  lunr.tokenizer.separator = /[\s\-]+/
273
466
  /*!
274
467
  * lunr.Pipeline
275
- * Copyright (C) 2017 Oliver Nightingale
468
+ * Copyright (C) 2018 Oliver Nightingale
276
469
  */
277
470
 
278
471
  /**
@@ -475,14 +668,23 @@ lunr.Pipeline.prototype.run = function (tokens) {
475
668
 
476
669
  for (var i = 0; i < stackLength; i++) {
477
670
  var fn = this._stack[i]
671
+ var memo = []
478
672
 
479
- tokens = tokens.reduce(function (memo, token, j) {
480
- var result = fn(token, j, tokens)
673
+ for (var j = 0; j < tokens.length; j++) {
674
+ var result = fn(tokens[j], j, tokens)
481
675
 
482
- if (result === void 0 || result === '') return memo
676
+ if (result === void 0 || result === '') continue
677
+
678
+ if (result instanceof Array) {
679
+ for (var k = 0; k < result.length; k++) {
680
+ memo.push(result[k])
681
+ }
682
+ } else {
683
+ memo.push(result)
684
+ }
685
+ }
483
686
 
484
- return memo.concat(result)
485
- }, [])
687
+ tokens = memo
486
688
  }
487
689
 
488
690
  return tokens
@@ -494,10 +696,12 @@ lunr.Pipeline.prototype.run = function (tokens) {
494
696
  * token and mapping the resulting tokens back to strings.
495
697
  *
496
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.
497
701
  * @returns {string[]}
498
702
  */
499
- lunr.Pipeline.prototype.runString = function (str) {
500
- var token = new lunr.Token (str)
703
+ lunr.Pipeline.prototype.runString = function (str, metadata) {
704
+ var token = new lunr.Token (str, metadata)
501
705
 
502
706
  return this.run([token]).map(function (t) {
503
707
  return t.toString()
@@ -528,7 +732,7 @@ lunr.Pipeline.prototype.toJSON = function () {
528
732
  }
529
733
  /*!
530
734
  * lunr.Vector
531
- * Copyright (C) 2017 Oliver Nightingale
735
+ * Copyright (C) 2018 Oliver Nightingale
532
736
  */
533
737
 
534
738
  /**
@@ -689,15 +893,14 @@ lunr.Vector.prototype.dot = function (otherVector) {
689
893
  }
690
894
 
691
895
  /**
692
- * Calculates the cosine similarity between this vector and another
693
- * vector.
896
+ * Calculates the similarity between this vector and another vector.
694
897
  *
695
898
  * @param {lunr.Vector} otherVector - The other vector to calculate the
696
899
  * similarity with.
697
900
  * @returns {Number}
698
901
  */
699
902
  lunr.Vector.prototype.similarity = function (otherVector) {
700
- return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude())
903
+ return this.dot(otherVector) / this.magnitude() || 0
701
904
  }
702
905
 
703
906
  /**
@@ -726,7 +929,7 @@ lunr.Vector.prototype.toJSON = function () {
726
929
  /* eslint-disable */
727
930
  /*!
728
931
  * lunr.stemmer
729
- * Copyright (C) 2017 Oliver Nightingale
932
+ * Copyright (C) 2018 Oliver Nightingale
730
933
  * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt
731
934
  */
732
935
 
@@ -739,6 +942,7 @@ lunr.Vector.prototype.toJSON = function () {
739
942
  * @param {lunr.Token} token - The string to stem
740
943
  * @returns {lunr.Token}
741
944
  * @see {@link lunr.Pipeline}
945
+ * @function
742
946
  */
743
947
  lunr.stemmer = (function(){
744
948
  var step2list = {
@@ -947,7 +1151,7 @@ lunr.stemmer = (function(){
947
1151
  lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')
948
1152
  /*!
949
1153
  * lunr.stopWordFilter
950
- * Copyright (C) 2017 Oliver Nightingale
1154
+ * Copyright (C) 2018 Oliver Nightingale
951
1155
  */
952
1156
 
953
1157
  /**
@@ -957,6 +1161,7 @@ lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')
957
1161
  * The built in lunr.stopWordFilter is built using this generator and can be used
958
1162
  * to generate custom stopWordFilters for applications or non English languages.
959
1163
  *
1164
+ * @function
960
1165
  * @param {Array} token The token to pass through the filter
961
1166
  * @returns {lunr.PipelineFunction}
962
1167
  * @see lunr.Pipeline
@@ -980,6 +1185,7 @@ lunr.generateStopWordFilter = function (stopWords) {
980
1185
  * This is intended to be used in the Pipeline. If the token does not pass the
981
1186
  * filter then undefined will be returned.
982
1187
  *
1188
+ * @function
983
1189
  * @implements {lunr.PipelineFunction}
984
1190
  * @params {lunr.Token} token - A token to check for being a stop word.
985
1191
  * @returns {lunr.Token}
@@ -1110,7 +1316,7 @@ lunr.stopWordFilter = lunr.generateStopWordFilter([
1110
1316
  lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')
1111
1317
  /*!
1112
1318
  * lunr.trimmer
1113
- * Copyright (C) 2017 Oliver Nightingale
1319
+ * Copyright (C) 2018 Oliver Nightingale
1114
1320
  */
1115
1321
 
1116
1322
  /**
@@ -1137,7 +1343,7 @@ lunr.trimmer = function (token) {
1137
1343
  lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')
1138
1344
  /*!
1139
1345
  * lunr.TokenSet
1140
- * Copyright (C) 2017 Oliver Nightingale
1346
+ * Copyright (C) 2018 Oliver Nightingale
1141
1347
  */
1142
1348
 
1143
1349
  /**
@@ -1379,14 +1585,13 @@ lunr.TokenSet.fromFuzzyString = function (str, editDistance) {
1379
1585
  */
1380
1586
  lunr.TokenSet.fromString = function (str) {
1381
1587
  var node = new lunr.TokenSet,
1382
- root = node,
1383
- wildcardFound = false
1588
+ root = node
1384
1589
 
1385
1590
  /*
1386
1591
  * Iterates through all characters within the passed string
1387
1592
  * appending a node for each character.
1388
1593
  *
1389
- * As soon as a wildcard character is found then a self
1594
+ * When a wildcard character is found then a self
1390
1595
  * referencing edge is introduced to continually match
1391
1596
  * any number of any characters.
1392
1597
  */
@@ -1395,7 +1600,6 @@ lunr.TokenSet.fromString = function (str) {
1395
1600
  final = (i == len - 1)
1396
1601
 
1397
1602
  if (char == "*") {
1398
- wildcardFound = true
1399
1603
  node.edges[char] = node
1400
1604
  node.final = final
1401
1605
 
@@ -1405,11 +1609,6 @@ lunr.TokenSet.fromString = function (str) {
1405
1609
 
1406
1610
  node.edges[char] = next
1407
1611
  node = next
1408
-
1409
- // TODO: is this needed anymore?
1410
- if (wildcardFound) {
1411
- node.edges["*"] = root
1412
- }
1413
1612
  }
1414
1613
  }
1415
1614
 
@@ -1436,6 +1635,11 @@ lunr.TokenSet.prototype.toArray = function () {
1436
1635
  len = edges.length
1437
1636
 
1438
1637
  if (frame.node.final) {
1638
+ /* In Safari, at this point the prefix is sometimes corrupted, see:
1639
+ * https://github.com/olivernn/lunr.js/issues/279 Calling any
1640
+ * String.prototype method forces Safari to "cast" this string to what
1641
+ * it's supposed to be, fixing the bug. */
1642
+ frame.prefix.charAt(0)
1439
1643
  words.push(frame.prefix)
1440
1644
  }
1441
1645
 
@@ -1632,7 +1836,7 @@ lunr.TokenSet.Builder.prototype.minimize = function (downTo) {
1632
1836
  }
1633
1837
  /*!
1634
1838
  * lunr.Index
1635
- * Copyright (C) 2017 Oliver Nightingale
1839
+ * Copyright (C) 2018 Oliver Nightingale
1636
1840
  */
1637
1841
 
1638
1842
  /**
@@ -1646,7 +1850,7 @@ lunr.TokenSet.Builder.prototype.minimize = function (downTo) {
1646
1850
  * @constructor
1647
1851
  * @param {Object} attrs - The attributes of the built search index.
1648
1852
  * @param {Object} attrs.invertedIndex - An index of term/field to document reference.
1649
- * @param {Object<string, lunr.Vector>} attrs.documentVectors - Document vectors keyed by document reference.
1853
+ * @param {Object<string, lunr.Vector>} attrs.fieldVectors - Field vectors
1650
1854
  * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.
1651
1855
  * @param {string[]} attrs.fields - The names of indexed document fields.
1652
1856
  * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.
@@ -1692,6 +1896,12 @@ lunr.Index = function (attrs) {
1692
1896
  * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.
1693
1897
  * Avoid large values for edit distance to improve query performance.
1694
1898
  *
1899
+ * Each term also supports a presence modifier. By default a term's presence in document is optional, however
1900
+ * this can be changed to either required or prohibited. For a term's presence to be required in a document the
1901
+ * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and
1902
+ * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not
1903
+ * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.
1904
+ *
1695
1905
  * To escape special characters the backslash character '\' can be used, this allows searches to include
1696
1906
  * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead
1697
1907
  * of attempting to apply a boost of 2 to the search term "foo".
@@ -1707,13 +1917,16 @@ lunr.Index = function (attrs) {
1707
1917
  * hello^10
1708
1918
  * @example <caption>term with an edit distance of 2</caption>
1709
1919
  * hello~2
1920
+ * @example <caption>terms with presence modifiers</caption>
1921
+ * -foo +bar baz
1710
1922
  */
1711
1923
 
1712
1924
  /**
1713
1925
  * Performs a search against the index using lunr query syntax.
1714
1926
  *
1715
1927
  * Results will be returned sorted by their score, the most relevant results
1716
- * will be returned first.
1928
+ * will be returned first. For details on how the score is calculated, please see
1929
+ * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.
1717
1930
  *
1718
1931
  * For more programmatic querying use lunr.Index#query.
1719
1932
  *
@@ -1764,7 +1977,18 @@ lunr.Index.prototype.query = function (fn) {
1764
1977
  var query = new lunr.Query(this.fields),
1765
1978
  matchingFields = Object.create(null),
1766
1979
  queryVectors = Object.create(null),
1767
- termFieldCache = Object.create(null)
1980
+ termFieldCache = Object.create(null),
1981
+ requiredMatches = Object.create(null),
1982
+ prohibitedMatches = Object.create(null)
1983
+
1984
+ /*
1985
+ * To support field level boosts a query vector is created per
1986
+ * field. An empty vector is eagerly created to support negated
1987
+ * queries.
1988
+ */
1989
+ for (var i = 0; i < this.fields.length; i++) {
1990
+ queryVectors[this.fields[i]] = new lunr.Vector
1991
+ }
1768
1992
 
1769
1993
  fn.call(query, query)
1770
1994
 
@@ -1778,10 +2002,13 @@ lunr.Index.prototype.query = function (fn) {
1778
2002
  * for a single query term.
1779
2003
  */
1780
2004
  var clause = query.clauses[i],
1781
- terms = null
2005
+ terms = null,
2006
+ clauseMatches = lunr.Set.complete
1782
2007
 
1783
2008
  if (clause.usePipeline) {
1784
- terms = this.pipeline.runString(clause.term)
2009
+ terms = this.pipeline.runString(clause.term, {
2010
+ fields: clause.fields
2011
+ })
1785
2012
  } else {
1786
2013
  terms = [clause.term]
1787
2014
  }
@@ -1805,6 +2032,21 @@ lunr.Index.prototype.query = function (fn) {
1805
2032
  var termTokenSet = lunr.TokenSet.fromClause(clause),
1806
2033
  expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()
1807
2034
 
2035
+ /*
2036
+ * If a term marked as required does not exist in the tokenSet it is
2037
+ * impossible for the search to return any matches. We set all the field
2038
+ * scoped required matches set to empty and stop examining any further
2039
+ * clauses.
2040
+ */
2041
+ if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {
2042
+ for (var k = 0; k < clause.fields.length; k++) {
2043
+ var field = clause.fields[k]
2044
+ requiredMatches[field] = lunr.Set.empty
2045
+ }
2046
+
2047
+ break
2048
+ }
2049
+
1808
2050
  for (var j = 0; j < expandedTerms.length; j++) {
1809
2051
  /*
1810
2052
  * For each term get the posting and termIndex, this is required for
@@ -1826,26 +2068,50 @@ lunr.Index.prototype.query = function (fn) {
1826
2068
  var field = clause.fields[k],
1827
2069
  fieldPosting = posting[field],
1828
2070
  matchingDocumentRefs = Object.keys(fieldPosting),
1829
- termField = expandedTerm + "/" + field
2071
+ termField = expandedTerm + "/" + field,
2072
+ matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)
1830
2073
 
1831
2074
  /*
1832
- * To support field level boosts a query vector is created per
1833
- * field. This vector is populated using the termIndex found for
1834
- * the term and a unit value with the appropriate boost applied.
2075
+ * if the presence of this term is required ensure that the matching
2076
+ * documents are added to the set of required matches for this clause.
1835
2077
  *
1836
- * If the query vector for this field does not exist yet it needs
1837
- * to be created.
1838
2078
  */
1839
- if (queryVectors[field] === undefined) {
1840
- queryVectors[field] = new lunr.Vector
2079
+ if (clause.presence == lunr.Query.presence.REQUIRED) {
2080
+ clauseMatches = clauseMatches.union(matchingDocumentsSet)
2081
+
2082
+ if (requiredMatches[field] === undefined) {
2083
+ requiredMatches[field] = lunr.Set.complete
2084
+ }
2085
+ }
2086
+
2087
+ /*
2088
+ * if the presence of this term is prohibited ensure that the matching
2089
+ * documents are added to the set of prohibited matches for this field,
2090
+ * creating that set if it does not yet exist.
2091
+ */
2092
+ if (clause.presence == lunr.Query.presence.PROHIBITED) {
2093
+ if (prohibitedMatches[field] === undefined) {
2094
+ prohibitedMatches[field] = lunr.Set.empty
2095
+ }
2096
+
2097
+ prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)
2098
+
2099
+ /*
2100
+ * Prohibited matches should not be part of the query vector used for
2101
+ * similarity scoring and no metadata should be extracted so we continue
2102
+ * to the next field
2103
+ */
2104
+ continue
1841
2105
  }
1842
2106
 
1843
2107
  /*
2108
+ * The query field vector is populated using the termIndex found for
2109
+ * the term and a unit value with the appropriate boost applied.
1844
2110
  * Using upsert because there could already be an entry in the vector
1845
2111
  * for the term we are working with. In that case we just add the scores
1846
2112
  * together.
1847
2113
  */
1848
- queryVectors[field].upsert(termIndex, 1 * clause.boost, function (a, b) { return a + b })
2114
+ queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })
1849
2115
 
1850
2116
  /**
1851
2117
  * If we've already seen this term, field combo then we've already collected
@@ -1879,12 +2145,65 @@ lunr.Index.prototype.query = function (fn) {
1879
2145
  }
1880
2146
  }
1881
2147
  }
2148
+
2149
+ /**
2150
+ * If the presence was required we need to update the requiredMatches field sets.
2151
+ * We do this after all fields for the term have collected their matches because
2152
+ * the clause terms presence is required in _any_ of the fields not _all_ of the
2153
+ * fields.
2154
+ */
2155
+ if (clause.presence === lunr.Query.presence.REQUIRED) {
2156
+ for (var k = 0; k < clause.fields.length; k++) {
2157
+ var field = clause.fields[k]
2158
+ requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)
2159
+ }
2160
+ }
2161
+ }
2162
+
2163
+ /**
2164
+ * Need to combine the field scoped required and prohibited
2165
+ * matching documents into a global set of required and prohibited
2166
+ * matches
2167
+ */
2168
+ var allRequiredMatches = lunr.Set.complete,
2169
+ allProhibitedMatches = lunr.Set.empty
2170
+
2171
+ for (var i = 0; i < this.fields.length; i++) {
2172
+ var field = this.fields[i]
2173
+
2174
+ if (requiredMatches[field]) {
2175
+ allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])
2176
+ }
2177
+
2178
+ if (prohibitedMatches[field]) {
2179
+ allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])
2180
+ }
1882
2181
  }
1883
2182
 
1884
2183
  var matchingFieldRefs = Object.keys(matchingFields),
1885
2184
  results = [],
1886
2185
  matches = Object.create(null)
1887
2186
 
2187
+ /*
2188
+ * If the query is negated (contains only prohibited terms)
2189
+ * we need to get _all_ fieldRefs currently existing in the
2190
+ * index. This is only done when we know that the query is
2191
+ * entirely prohibited terms to avoid any cost of getting all
2192
+ * fieldRefs unnecessarily.
2193
+ *
2194
+ * Additionally, blank MatchData must be created to correctly
2195
+ * populate the results.
2196
+ */
2197
+ if (query.isNegated()) {
2198
+ matchingFieldRefs = Object.keys(this.fieldVectors)
2199
+
2200
+ for (var i = 0; i < matchingFieldRefs.length; i++) {
2201
+ var matchingFieldRef = matchingFieldRefs[i]
2202
+ var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)
2203
+ matchingFields[matchingFieldRef] = new lunr.MatchData
2204
+ }
2205
+ }
2206
+
1888
2207
  for (var i = 0; i < matchingFieldRefs.length; i++) {
1889
2208
  /*
1890
2209
  * Currently we have document fields that match the query, but we
@@ -1895,8 +2214,17 @@ lunr.Index.prototype.query = function (fn) {
1895
2214
  * above, and combined into a final document score using addition.
1896
2215
  */
1897
2216
  var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),
1898
- docRef = fieldRef.docRef,
1899
- fieldVector = this.fieldVectors[fieldRef],
2217
+ docRef = fieldRef.docRef
2218
+
2219
+ if (!allRequiredMatches.contains(docRef)) {
2220
+ continue
2221
+ }
2222
+
2223
+ if (allProhibitedMatches.contains(docRef)) {
2224
+ continue
2225
+ }
2226
+
2227
+ var fieldVector = this.fieldVectors[fieldRef],
1900
2228
  score = queryVectors[fieldRef.fieldName].similarity(fieldVector),
1901
2229
  docMatch
1902
2230
 
@@ -2000,7 +2328,7 @@ lunr.Index.load = function (serializedIndex) {
2000
2328
  }
2001
2329
  /*!
2002
2330
  * lunr.Builder
2003
- * Copyright (C) 2017 Oliver Nightingale
2331
+ * Copyright (C) 2018 Oliver Nightingale
2004
2332
  */
2005
2333
 
2006
2334
  /**
@@ -2029,7 +2357,8 @@ lunr.Index.load = function (serializedIndex) {
2029
2357
  */
2030
2358
  lunr.Builder = function () {
2031
2359
  this._ref = "id"
2032
- this._fields = []
2360
+ this._fields = Object.create(null)
2361
+ this._documents = Object.create(null)
2033
2362
  this.invertedIndex = Object.create(null)
2034
2363
  this.fieldTermFrequencies = {}
2035
2364
  this.fieldLengths = {}
@@ -2059,6 +2388,20 @@ lunr.Builder.prototype.ref = function (ref) {
2059
2388
  this._ref = ref
2060
2389
  }
2061
2390
 
2391
+ /**
2392
+ * A function that is used to extract a field from a document.
2393
+ *
2394
+ * Lunr expects a field to be at the top level of a document, if however the field
2395
+ * is deeply nested within a document an extractor function can be used to extract
2396
+ * the right field for indexing.
2397
+ *
2398
+ * @callback fieldExtractor
2399
+ * @param {object} doc - The document being added to the index.
2400
+ * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.
2401
+ * @example <caption>Extracting a nested field</caption>
2402
+ * function (doc) { return doc.nested.field }
2403
+ */
2404
+
2062
2405
  /**
2063
2406
  * Adds a field to the list of document fields that will be indexed. Every document being
2064
2407
  * indexed should have this field. Null values for this field in indexed documents will
@@ -2067,10 +2410,22 @@ lunr.Builder.prototype.ref = function (ref) {
2067
2410
  * All fields should be added before adding documents to the index. Adding fields after
2068
2411
  * a document has been indexed will have no effect on already indexed documents.
2069
2412
  *
2070
- * @param {string} field - The name of a field to index in all documents.
2413
+ * Fields can be boosted at build time. This allows terms within that field to have more
2414
+ * importance when ranking search results. Use a field boost to specify that matches within
2415
+ * one field are more important than other fields.
2416
+ *
2417
+ * @param {string} fieldName - The name of a field to index in all documents.
2418
+ * @param {object} attributes - Optional attributes associated with this field.
2419
+ * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.
2420
+ * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.
2421
+ * @throws {RangeError} fieldName cannot contain unsupported characters '/'
2071
2422
  */
2072
- lunr.Builder.prototype.field = function (field) {
2073
- this._fields.push(field)
2423
+ lunr.Builder.prototype.field = function (fieldName, attributes) {
2424
+ if (/\//.test(fieldName)) {
2425
+ throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'")
2426
+ }
2427
+
2428
+ this._fields[fieldName] = attributes || {}
2074
2429
  }
2075
2430
 
2076
2431
  /**
@@ -2112,17 +2467,27 @@ lunr.Builder.prototype.k1 = function (number) {
2112
2467
  * it should have all fields defined for indexing, though null or undefined values will not
2113
2468
  * cause errors.
2114
2469
  *
2470
+ * Entire documents can be boosted at build time. Applying a boost to a document indicates that
2471
+ * this document should rank higher in search results than other documents.
2472
+ *
2115
2473
  * @param {object} doc - The document to add to the index.
2474
+ * @param {object} attributes - Optional attributes associated with this document.
2475
+ * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.
2116
2476
  */
2117
- lunr.Builder.prototype.add = function (doc) {
2118
- var docRef = doc[this._ref]
2477
+ lunr.Builder.prototype.add = function (doc, attributes) {
2478
+ var docRef = doc[this._ref],
2479
+ fields = Object.keys(this._fields)
2119
2480
 
2481
+ this._documents[docRef] = attributes || {}
2120
2482
  this.documentCount += 1
2121
2483
 
2122
- for (var i = 0; i < this._fields.length; i++) {
2123
- var fieldName = this._fields[i],
2124
- field = doc[fieldName],
2125
- tokens = this.tokenizer(field),
2484
+ for (var i = 0; i < fields.length; i++) {
2485
+ var fieldName = fields[i],
2486
+ extractor = this._fields[fieldName].extractor,
2487
+ field = extractor ? extractor(doc) : doc[fieldName],
2488
+ tokens = this.tokenizer(field, {
2489
+ fields: [fieldName]
2490
+ }),
2126
2491
  terms = this.pipeline.run(tokens),
2127
2492
  fieldRef = new lunr.FieldRef (docRef, fieldName),
2128
2493
  fieldTerms = Object.create(null)
@@ -2150,8 +2515,8 @@ lunr.Builder.prototype.add = function (doc) {
2150
2515
  posting["_index"] = this.termIndex
2151
2516
  this.termIndex += 1
2152
2517
 
2153
- for (var k = 0; k < this._fields.length; k++) {
2154
- posting[this._fields[k]] = Object.create(null)
2518
+ for (var k = 0; k < fields.length; k++) {
2519
+ posting[fields[k]] = Object.create(null)
2155
2520
  }
2156
2521
 
2157
2522
  this.invertedIndex[term] = posting
@@ -2202,9 +2567,11 @@ lunr.Builder.prototype.calculateAverageFieldLengths = function () {
2202
2567
  accumulator[field] += this.fieldLengths[fieldRef]
2203
2568
  }
2204
2569
 
2205
- for (var i = 0; i < this._fields.length; i++) {
2206
- var field = this._fields[i]
2207
- accumulator[field] = accumulator[field] / documentsWithField[field]
2570
+ var fields = Object.keys(this._fields)
2571
+
2572
+ for (var i = 0; i < fields.length; i++) {
2573
+ var fieldName = fields[i]
2574
+ accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]
2208
2575
  }
2209
2576
 
2210
2577
  this.averageFieldLength = accumulator
@@ -2223,13 +2590,17 @@ lunr.Builder.prototype.createFieldVectors = function () {
2223
2590
 
2224
2591
  for (var i = 0; i < fieldRefsLength; i++) {
2225
2592
  var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),
2226
- field = fieldRef.fieldName,
2593
+ fieldName = fieldRef.fieldName,
2227
2594
  fieldLength = this.fieldLengths[fieldRef],
2228
2595
  fieldVector = new lunr.Vector,
2229
2596
  termFrequencies = this.fieldTermFrequencies[fieldRef],
2230
2597
  terms = Object.keys(termFrequencies),
2231
2598
  termsLength = terms.length
2232
2599
 
2600
+
2601
+ var fieldBoost = this._fields[fieldName].boost || 1,
2602
+ docBoost = this._documents[fieldRef.docRef].boost || 1
2603
+
2233
2604
  for (var j = 0; j < termsLength; j++) {
2234
2605
  var term = terms[j],
2235
2606
  tf = termFrequencies[term],
@@ -2243,7 +2614,9 @@ lunr.Builder.prototype.createFieldVectors = function () {
2243
2614
  idf = termIdfCache[term]
2244
2615
  }
2245
2616
 
2246
- score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[field])) + tf)
2617
+ score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)
2618
+ score *= fieldBoost
2619
+ score *= docBoost
2247
2620
  scoreWithPrecision = Math.round(score * 1000) / 1000
2248
2621
  // Converts 1.23456789 to 1.234.
2249
2622
  // Reducing the precision so that the vectors take up less
@@ -2289,7 +2662,7 @@ lunr.Builder.prototype.build = function () {
2289
2662
  invertedIndex: this.invertedIndex,
2290
2663
  fieldVectors: this.fieldVectors,
2291
2664
  tokenSet: this.tokenSet,
2292
- fields: this._fields,
2665
+ fields: Object.keys(this._fields),
2293
2666
  pipeline: this.searchPipeline
2294
2667
  })
2295
2668
  }
@@ -2327,7 +2700,7 @@ lunr.Builder.prototype.use = function (fn) {
2327
2700
  */
2328
2701
  lunr.MatchData = function (term, field, metadata) {
2329
2702
  var clonedMetadata = Object.create(null),
2330
- metadataKeys = Object.keys(metadata)
2703
+ metadataKeys = Object.keys(metadata || {})
2331
2704
 
2332
2705
  // Cloning the metadata to prevent the original
2333
2706
  // being mutated during match data combination.
@@ -2340,8 +2713,11 @@ lunr.MatchData = function (term, field, metadata) {
2340
2713
  }
2341
2714
 
2342
2715
  this.metadata = Object.create(null)
2343
- this.metadata[term] = Object.create(null)
2344
- this.metadata[term][field] = clonedMetadata
2716
+
2717
+ if (term !== undefined) {
2718
+ this.metadata[term] = Object.create(null)
2719
+ this.metadata[term][field] = clonedMetadata
2720
+ }
2345
2721
  }
2346
2722
 
2347
2723
  /**
@@ -2456,11 +2832,42 @@ lunr.Query = function (allFields) {
2456
2832
  * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING
2457
2833
  * })
2458
2834
  */
2835
+
2459
2836
  lunr.Query.wildcard = new String ("*")
2460
2837
  lunr.Query.wildcard.NONE = 0
2461
2838
  lunr.Query.wildcard.LEADING = 1
2462
2839
  lunr.Query.wildcard.TRAILING = 2
2463
2840
 
2841
+ /**
2842
+ * Constants for indicating what kind of presence a term must have in matching documents.
2843
+ *
2844
+ * @constant
2845
+ * @enum {number}
2846
+ * @see lunr.Query~Clause
2847
+ * @see lunr.Query#clause
2848
+ * @see lunr.Query#term
2849
+ * @example <caption>query term with required presence</caption>
2850
+ * query.term('foo', { presence: lunr.Query.presence.REQUIRED })
2851
+ */
2852
+ lunr.Query.presence = {
2853
+ /**
2854
+ * Term's presence in a document is optional, this is the default value.
2855
+ */
2856
+ OPTIONAL: 1,
2857
+
2858
+ /**
2859
+ * Term's presence in a document is required, documents that do not contain
2860
+ * this term will not be returned.
2861
+ */
2862
+ REQUIRED: 2,
2863
+
2864
+ /**
2865
+ * Term's presence in a document is prohibited, documents that do contain
2866
+ * this term will not be returned.
2867
+ */
2868
+ PROHIBITED: 3
2869
+ }
2870
+
2464
2871
  /**
2465
2872
  * A single clause in a {@link lunr.Query} contains a term and details on how to
2466
2873
  * match that term against a {@link lunr.Index}.
@@ -2470,7 +2877,8 @@ lunr.Query.wildcard.TRAILING = 2
2470
2877
  * @property {number} [boost=1] - Any boost that should be applied when matching this clause.
2471
2878
  * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.
2472
2879
  * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.
2473
- * @property {number} [wildcard=0] - Whether the term should have wildcards appended or prepended.
2880
+ * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.
2881
+ * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.
2474
2882
  */
2475
2883
 
2476
2884
  /**
@@ -2508,17 +2916,44 @@ lunr.Query.prototype.clause = function (clause) {
2508
2916
  clause.term = "" + clause.term + "*"
2509
2917
  }
2510
2918
 
2919
+ if (!('presence' in clause)) {
2920
+ clause.presence = lunr.Query.presence.OPTIONAL
2921
+ }
2922
+
2511
2923
  this.clauses.push(clause)
2512
2924
 
2513
2925
  return this
2514
2926
  }
2515
2927
 
2928
+ /**
2929
+ * A negated query is one in which every clause has a presence of
2930
+ * prohibited. These queries require some special processing to return
2931
+ * the expected results.
2932
+ *
2933
+ * @returns boolean
2934
+ */
2935
+ lunr.Query.prototype.isNegated = function () {
2936
+ for (var i = 0; i < this.clauses.length; i++) {
2937
+ if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {
2938
+ return false
2939
+ }
2940
+ }
2941
+
2942
+ return true
2943
+ }
2944
+
2516
2945
  /**
2517
2946
  * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}
2518
2947
  * to the list of clauses that make up this query.
2519
2948
  *
2520
- * @param {string} term - The term to add to the query.
2521
- * @param {Object} [options] - Any additional properties to add to the query clause.
2949
+ * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion
2950
+ * to a token or token-like string should be done before calling this method.
2951
+ *
2952
+ * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an
2953
+ * array, each term in the array will share the same options.
2954
+ *
2955
+ * @param {object|object[]} term - The term(s) to add to the query.
2956
+ * @param {object} [options] - Any additional properties to add to the query clause.
2522
2957
  * @returns {lunr.Query}
2523
2958
  * @see lunr.Query#clause
2524
2959
  * @see lunr.Query~Clause
@@ -2530,10 +2965,17 @@ lunr.Query.prototype.clause = function (clause) {
2530
2965
  * boost: 10,
2531
2966
  * wildcard: lunr.Query.wildcard.TRAILING
2532
2967
  * })
2968
+ * @example <caption>using lunr.tokenizer to convert a string to tokens before using them as terms</caption>
2969
+ * query.term(lunr.tokenizer("foo bar"))
2533
2970
  */
2534
2971
  lunr.Query.prototype.term = function (term, options) {
2972
+ if (Array.isArray(term)) {
2973
+ term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this)
2974
+ return this
2975
+ }
2976
+
2535
2977
  var clause = options || {}
2536
- clause.term = term
2978
+ clause.term = term.toString()
2537
2979
 
2538
2980
  this.clause(clause)
2539
2981
 
@@ -2645,6 +3087,7 @@ lunr.QueryLexer.FIELD = 'FIELD'
2645
3087
  lunr.QueryLexer.TERM = 'TERM'
2646
3088
  lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE'
2647
3089
  lunr.QueryLexer.BOOST = 'BOOST'
3090
+ lunr.QueryLexer.PRESENCE = 'PRESENCE'
2648
3091
 
2649
3092
  lunr.QueryLexer.lexField = function (lexer) {
2650
3093
  lexer.backup()
@@ -2733,6 +3176,22 @@ lunr.QueryLexer.lexText = function (lexer) {
2733
3176
  return lunr.QueryLexer.lexBoost
2734
3177
  }
2735
3178
 
3179
+ // "+" indicates term presence is required
3180
+ // checking for length to ensure that only
3181
+ // leading "+" are considered
3182
+ if (char == "+" && lexer.width() === 1) {
3183
+ lexer.emit(lunr.QueryLexer.PRESENCE)
3184
+ return lunr.QueryLexer.lexText
3185
+ }
3186
+
3187
+ // "-" indicates term presence is prohibited
3188
+ // checking for length to ensure that only
3189
+ // leading "-" are considered
3190
+ if (char == "-" && lexer.width() === 1) {
3191
+ lexer.emit(lunr.QueryLexer.PRESENCE)
3192
+ return lunr.QueryLexer.lexText
3193
+ }
3194
+
2736
3195
  if (char.match(lunr.QueryLexer.termSeparator)) {
2737
3196
  return lunr.QueryLexer.lexTerm
2738
3197
  }
@@ -2750,7 +3209,7 @@ lunr.QueryParser.prototype.parse = function () {
2750
3209
  this.lexer.run()
2751
3210
  this.lexemes = this.lexer.lexemes
2752
3211
 
2753
- var state = lunr.QueryParser.parseFieldOrTerm
3212
+ var state = lunr.QueryParser.parseClause
2754
3213
 
2755
3214
  while (state) {
2756
3215
  state = state(this)
@@ -2775,7 +3234,7 @@ lunr.QueryParser.prototype.nextClause = function () {
2775
3234
  this.currentClause = {}
2776
3235
  }
2777
3236
 
2778
- lunr.QueryParser.parseFieldOrTerm = function (parser) {
3237
+ lunr.QueryParser.parseClause = function (parser) {
2779
3238
  var lexeme = parser.peekLexeme()
2780
3239
 
2781
3240
  if (lexeme == undefined) {
@@ -2783,6 +3242,8 @@ lunr.QueryParser.parseFieldOrTerm = function (parser) {
2783
3242
  }
2784
3243
 
2785
3244
  switch (lexeme.type) {
3245
+ case lunr.QueryLexer.PRESENCE:
3246
+ return lunr.QueryParser.parsePresence
2786
3247
  case lunr.QueryLexer.FIELD:
2787
3248
  return lunr.QueryParser.parseField
2788
3249
  case lunr.QueryLexer.TERM:
@@ -2798,6 +3259,43 @@ lunr.QueryParser.parseFieldOrTerm = function (parser) {
2798
3259
  }
2799
3260
  }
2800
3261
 
3262
+ lunr.QueryParser.parsePresence = function (parser) {
3263
+ var lexeme = parser.consumeLexeme()
3264
+
3265
+ if (lexeme == undefined) {
3266
+ return
3267
+ }
3268
+
3269
+ switch (lexeme.str) {
3270
+ case "-":
3271
+ parser.currentClause.presence = lunr.Query.presence.PROHIBITED
3272
+ break
3273
+ case "+":
3274
+ parser.currentClause.presence = lunr.Query.presence.REQUIRED
3275
+ break
3276
+ default:
3277
+ var errorMessage = "unrecognised presence operator'" + lexeme.str + "'"
3278
+ throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)
3279
+ }
3280
+
3281
+ var nextLexeme = parser.peekLexeme()
3282
+
3283
+ if (nextLexeme == undefined) {
3284
+ var errorMessage = "expecting term or field, found nothing"
3285
+ throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)
3286
+ }
3287
+
3288
+ switch (nextLexeme.type) {
3289
+ case lunr.QueryLexer.FIELD:
3290
+ return lunr.QueryParser.parseField
3291
+ case lunr.QueryLexer.TERM:
3292
+ return lunr.QueryParser.parseTerm
3293
+ default:
3294
+ var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'"
3295
+ throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)
3296
+ }
3297
+ }
3298
+
2801
3299
  lunr.QueryParser.parseField = function (parser) {
2802
3300
  var lexeme = parser.consumeLexeme()
2803
3301
 
@@ -2861,6 +3359,9 @@ lunr.QueryParser.parseTerm = function (parser) {
2861
3359
  return lunr.QueryParser.parseEditDistance
2862
3360
  case lunr.QueryLexer.BOOST:
2863
3361
  return lunr.QueryParser.parseBoost
3362
+ case lunr.QueryLexer.PRESENCE:
3363
+ parser.nextClause()
3364
+ return lunr.QueryParser.parsePresence
2864
3365
  default:
2865
3366
  var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'"
2866
3367
  throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)
@@ -2901,6 +3402,9 @@ lunr.QueryParser.parseEditDistance = function (parser) {
2901
3402
  return lunr.QueryParser.parseEditDistance
2902
3403
  case lunr.QueryLexer.BOOST:
2903
3404
  return lunr.QueryParser.parseBoost
3405
+ case lunr.QueryLexer.PRESENCE:
3406
+ parser.nextClause()
3407
+ return lunr.QueryParser.parsePresence
2904
3408
  default:
2905
3409
  var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'"
2906
3410
  throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)
@@ -2941,6 +3445,9 @@ lunr.QueryParser.parseBoost = function (parser) {
2941
3445
  return lunr.QueryParser.parseEditDistance
2942
3446
  case lunr.QueryLexer.BOOST:
2943
3447
  return lunr.QueryParser.parseBoost
3448
+ case lunr.QueryLexer.PRESENCE:
3449
+ parser.nextClause()
3450
+ return lunr.QueryParser.parsePresence
2944
3451
  default:
2945
3452
  var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'"
2946
3453
  throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)
@@ -2974,4 +3481,4 @@ lunr.QueryParser.parseBoost = function (parser) {
2974
3481
  */
2975
3482
  return lunr
2976
3483
  }))
2977
- })();
3484
+ })();