minimal-mistakes-jekyll 4.13.0 → 4.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ })();