Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | 21x 21x 21x 21x 21x 34x 14x 14x 14x 14x 21x 1x 21x 24x 21x 16x 21x 21x 1941x 45x 1896x 460x 460x 24x 436x 1361x 1361x 1361x 28x 1333x 10x 10x 10x 10x 168x 10x 10x 10x 7x 10x 21x 21x 21x 21x 454x 21x 433x 433x 21x 1181x 19x 1162x 21x 21x 15x 15x 1x 1x 20x 20x 17x 17x 17x 14x 2x 12x 12x 12x 12x 1x 12x 12x 12x 13x 13x 13x 13x 13x 13x 13x 13x 12x 12x 12x 3x 12x 17x 17x 17x 17x 17x 16x 17x 17x 7x 10x 13x 13x | /*
* Helper functions used to extract text for display in search results
* from lunr metadata.
*
* Part of Pleiar.no - a collection of tools for nurses
*
* Copyright (C) Fagforbundet 2019
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// @flow
import type { lunrMetadata, lunrStringOffsets } from './types/libs';
/**
* This is the end result that we return (in the form of an array of
* searchSnippetComponents)
*/
export type searchSnippetComponent =
| {| type: 'normal', str: string |}
| {| type: 'highlight', str: string |}
| {| type: 'delimiter' |};
/**
* This is the index used to indicate the location of a string that should be
* highlighted
*/
type hightlightIndex = {|
start: number,
stop: number
|};
/**
* This is a container used to indicate a single sentence of text that should
* be highlighted.
*/
type singleSnippetContainer = {|
highlight: Array<hightlightIndex>,
from: number,
length: number,
snippet: string
|};
/**
* A lunrResultSnippet object represents a single search result
*/
class lunrResultSnippet
{
// This is the text string that we are extracting a snippet from
_body: string;
// This is the metadata that was provided by lunr
_metadata: lunrMetadata;
// The primary queue of text instances to highlight
_targets: Array<lunrStringOffsets>;
// The secondary queue of text instances to highlight. Used if we exhaust
// _targets
_highlight: Array<lunrStringOffsets>;
// eslint-disable-next-line require-jsdoc
constructor(bodyString: string, metadata: lunrMetadata)
{
this._body = bodyString;
this._metadata = metadata;
let highlight: Array<lunrStringOffsets> = [];
const targets = ([]: Array<lunrStringOffsets>);
for(const sub in metadata)
{
if(metadata[sub].body && metadata[sub].body.position.length > 0)
{
// Take a shallow copy of the array
const metaEntryCopy = metadata[sub].body.position.concat([]);
(metaEntryCopy: Array<lunrStringOffsets>);
// Queue the first hit for display already now
targets.push( metaEntryCopy.shift() );
// And add the remaining entries to the secondary queue, which
// will be pulled from if we don't have enough obligatory
// targets
highlight = highlight.concat(metaEntryCopy);
}
}
// It should have at most three targets, if we have more, we move them
// to the highlight queue
while(targets.length > 3)
{
highlight.push( targets.pop() );
}
// Sort the highlight queue, so that we pull the earliest instance (if
// needed) in the next block
highlight.sort((a: lunrStringOffsets, b: lunrStringOffsets) =>
{
return a[0] - b[0];
});
// Make sure we have at least 3 target entries queued up
while(targets.length < 3 && highlight.length > 0)
{
targets.push( highlight.shift() );
}
this._highlight = highlight;
this._targets = targets;
}
/**
* Checks if the string supplied is a sentence boundary character,
* that is, on of . … ! or ?
*
* If it is, it returns true, otherwise false
*/
_strIsBoundaryCharacter (str: string): boolean
{
if(str === "." || str === "…" || str === "!" || str === "?")
{
return true;
}
return false;
}
/**
* This checks if the index supplied is at the absolute start of a sentence
* (ie. first character of first word). Returns true if it is, otherwise
* returns false.
*/
_idxIsStartOfSentence (idx: number): boolean
{
const body = this._body;
if(
// A . at the start indicates a sentence boundary
this._strIsBoundaryCharacter(body.substr(idx,1)) ||
// A . immediately preceeding a " " indicates a sentence boundary
(body.substr(idx,1) === " " && this._strIsBoundaryCharacter(body.substr( (idx-1),1))) ||
// Two newlines indicate a sentence boundary
body.substr( (idx-1),2) === "\n\n" ||
// If we're at index zero, we are by definition at the start of a sentence
idx === 0)
{
return true;
}
return false;
}
/**
* This checks if the index supplied is at the absolute end of a sentence
* (ie. the last character of the last word (including punctuation).
* Returns true if it is, otherwise returns false
*/
_idxIsEndOfSentence (from: number, to: number): boolean
{
// FIXME: Should figure out if this is actually a mid-sentence entry, so it
// should check if the following character is upper case or not.
const body = this._body;
const idx = from+to;
if(
// A . is the end of the sentence
this._strIsBoundaryCharacter(body.substr(idx,1)) ||
// Two newlines indicate a sentence boundary
body.substr(idx,2) === "\n\n"
)
{
return true;
}
return false;
}
/**
* This returns a "generic" text snippet from the body that we were
* provided. This is used in cases where none of the hits were in the body
* (but in other fields), so that we return something in those cases as
* well.
*/
getGenericSnippet (): Array<searchSnippetComponent>
{
// The start position is always 0 for the generic snippet
const start = 0;
// We start with a minimum length of 120
let end: number = 120;
// The resulting searchSnippetComponents will be stored here
const result = ([]: Array<searchSnippetComponent>);
// While end is shorter than the total length of the body, and isn't
// the last character of a sentence, we bump end by 1. This is used to
// find the end of the sentence that is active at character 120.
while(end < this._body.length && !this._idxIsEndOfSentence(end,0))
{
end++;
}
// The delimiter isn't included in end at the moment, so we bump it by
// one so that we include it
end++;
result.push({
// We replace \n\n by punctuation, since previous algorithms treats
// "\n\n" as a boundary between sentences (and this avoids some
// strangeness with headers that have no punctuation immediately
// following some other text
str: this._body.substr(start,end).replace("\n\n",". "),
type: 'normal'
});
// If there's more text in the body than we have included (which it
// would be strange if there weren't, but we check anyway, then we tell
// the renderer to add a delimiter to the end of it.
if(end < this._body.length)
{
result.push({
type: 'delimiter'
});
}
return result;
}
/**
* Locates a single highlight snippet in the body, then expands from there
* to find the start and end of the sentence of said snippet, finally it
* returns a singleSnippetContainer. If the snippet provided is already
* within anoter, previously found snippet (in the alreadyFound array),
* then it will latch on to that singleSnippetContainer by appending to its
* highlight array instead and will then return null.
*/
findSingleSnippet (start: number, stop: number, alreadyFound: Array<singleSnippetContainer>): singleSnippetContainer | null
{
const body = this._body;
// From indicates the start of our string. This will end up as the
// first character of the first word of a sentence
let from: number = start;
// To indicates how many characters from "from" to include. This will
// end up as the number of characters up to and including the
// punctuation for this sentence.
let to: number = stop;
// Locate the beginning of the sentence
while(from > 0)
{
// If this index is the start, then we can break out of the loop
if(this._idxIsStartOfSentence(from))
{
break;
}
// Reduce from by 1 to try the next one
from--;
// Increase to by 1 to compensate for from being reduced by one
to++;
}
// Locate the end of the sentence
while(to < body.length)
{
// If this index is at the end, then we break out of the loop
if(this._idxIsEndOfSentence(from,to))
{
break;
}
// Increase to by 1 to try the next character
to++;
}
// Okay, so now from is the substring index in body that where the snippet
// should start. To is the length of the snippet.
// Modify the start index so that it is relative to our new string
start = start - from;
// Figure out if an existing entry is already using this snippet. If it
// is, we latch on to that instead of doing our own thing.
//
// We make the assumption that all indexes within a sentence, no matter
// their location, will always end up with the same start and end
// locations. Thus all we do is look through alreadyFound to see if any
// of the entries there have the same .from as we do
for(let existingIDX: number = 0; existingIDX < alreadyFound.length; existingIDX++)
{
const found = alreadyFound[existingIDX];
if(found.from == from)
{
found.highlight.push(({
start,
stop
}: hightlightIndex));
return null;
}
}
// Finally, get the end result from this entry
const sub = body.substr(from,to+1);
return ({
highlight: [
{
start,
stop
},
],
from,
length: to,
snippet: sub,
}: singleSnippetContainer);
}
/**
* This builds a single array of searchSnippetComponents from
* an array of singleSnippetContainers.
*
* It splits up all strings into "normal" (non-higlighted) text components,
* "highlight" (highlighted, ie. render in bold) text components, and
* "delimiter" indicators (which is where something like "(…)" should
* appear).
*/
buildSnippetTree (from: Array<singleSnippetContainer>): Array<searchSnippetComponent>
{
const final = ([]: Array<searchSnippetComponent>);
// This is the total length of the text string that we have processed
// so far. Used to apply additional limitations in case one highlighted
// sentence is extremely long, to avoid the result growing out of
// proportions because of missing punctuation
let totalLength: number = 0;
// Loop through all sentences
for(let idx: number = 0; idx < from.length; idx++)
{
// If we have reached a length of over 150 characters, then skip
// adding this and any following sentences.
if(totalLength > 150)
{
break;
}
// The current sentence
const current = from[idx];
// Indicates the offset in the string that we are at currently,
// used to slurp non-highlighted text
let offset: number = 0;
// Sort the highlights, so that we are sure that the one at the
// lowest index comes first (which it might not at the moment)
const highlight = current.highlight;
highlight.sort((a: hightlightIndex, b: hightlightIndex) =>
{
return a.start - b.start;
});
// If this text is not at the start of the body, then we prefix the
// whole thing with a delimiter
Eif(current.from > 0)
{
final.push({
type: 'delimiter'
});
}
// Iterate through all hightlights
while(highlight.length > 0)
{
const part = highlight.shift();
const start = part.start;
const length = part.stop;
// If this highlights starts later than the current offset,
// then there's text between the last highlight and this one,
// so we build an entry with that text
Eif(start > offset)
{
const textLength = start-offset;
final.push({
str: current.snippet.substr(offset,textLength),
type: 'normal'
});
}
// Add the highlight snippet
final.push({
str: current.snippet.substr(start,length),
type: 'highlight'
});
// Update the offset
offset = start+length;
}
// Slurp up any remaining text we haven't processed
Eif(current.snippet.length > offset)
{
final.push({
str: current.snippet.substr(offset),
type: 'normal'
});
}
// If the body has text after this sentence, add a delimiter at the
// end
if((idx+1) == from.length && current.from+current.length < this._body.length)
{
final.push({
type: 'delimiter'
});
}
// Update the total length
totalLength += current.snippet.length;
}
return final;
}
/**
* Retrieves a snippet tree for this lunrResultSnippet instance
*/
get(): Array<searchSnippetComponent>
{
const results = ([]: Array<singleSnippetContainer>);
for(const entry of this._targets)
{
// Get this snippet
const snippet = this.findSingleSnippet(entry[0], entry[1], results);
// If we got null, then the snippet is being handled by another
// entry. Otherwise we add it to our results array.
if(snippet !== null)
{
results.push(snippet);
}
}
// Build and return our array of searchSnippetComponents
const snippetTree = this.buildSnippetTree(results);
if(snippetTree.length > 0)
{
return snippetTree;
}
// If we didn't get any searchSnippetComponents then return a generic
// string
return this.getGenericSnippet();
}
}
export default (bodyString: string, metadata: lunrMetadata): Array<searchSnippetComponent> =>
{
// XXX: We might want to update the api and expose the class instead
const snippet = new lunrResultSnippet(bodyString,metadata);
return snippet.get();
};
// Testing exports
export { lunrResultSnippet };
|