Files
iTop/node_modules/@orchidjs/sifter/lib/sifter.ts
Benjamin Dalsass b35151f421 N°7063 - Forms SDK - Add Symfony forms component
error forms issue
2023-12-20 14:26:31 +01:00

356 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* sifter.js
* Copyright (c) 20132020 Brian Reavis & contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
* @author Brian Reavis <brian@thirdroute.com>
*/
import { scoreValue, getAttr, getAttrNesting, propToArray, iterate, cmp } from './utils';
import { getPattern, escape_regex } from '@orchidjs/unicode-variants';
import * as T from './types';
class Sifter{
public items: any; // []|{};
public settings: T.Settings;
/**
* Textually searches arrays and hashes of objects
* by property (or multiple properties). Designed
* specifically for autocomplete.
*
*/
constructor(items:any, settings:T.Settings) {
this.items = items;
this.settings = settings || {diacritics: true};
};
/**
* Splits a search string into an array of individual
* regexps to be used to match results.
*
*/
tokenize(query:string, respect_word_boundaries?:boolean, weights?:T.Weights ):T.Token[] {
if (!query || !query.length) return [];
const tokens:T.Token[] = [];
const words = query.split(/\s+/);
var field_regex:RegExp;
if( weights ){
field_regex = new RegExp( '^('+ Object.keys(weights).map(escape_regex).join('|')+')\:(.*)$');
}
words.forEach((word:string) => {
let field_match;
let field:null|string = null;
let regex:null|string = null;
// look for "field:query" tokens
if( field_regex && (field_match = word.match(field_regex)) ){
field = field_match[1]!;
word = field_match[2]!;
}
if( word.length > 0 ){
if( this.settings.diacritics ){
regex = getPattern(word) || null;
}else{
regex = escape_regex(word);
}
if( regex && respect_word_boundaries ) regex = "\\b"+regex;
}
tokens.push({
string : word,
regex : regex ? new RegExp(regex,'iu') : null,
field : field,
});
});
return tokens;
};
/**
* Returns a function to be used to score individual results.
*
* Good matches will have a higher score than poor matches.
* If an item is not a match, 0 will be returned by the function.
*
* @returns {T.ScoreFn}
*/
getScoreFunction(query:string, options:T.UserOptions ){
var search = this.prepareSearch(query, options);
return this._getScoreFunction(search);
}
/**
* @returns {T.ScoreFn}
*
*/
_getScoreFunction(search:T.PrepareObj ){
const tokens = search.tokens,
token_count = tokens.length;
if (!token_count) {
return function() { return 0; };
}
const fields = search.options.fields,
weights = search.weights,
field_count = fields.length,
getAttrFn = search.getAttrFn;
if (!field_count) {
return function() { return 1; };
}
/**
* Calculates the score of an object
* against the search query.
*
*/
const scoreObject = (function() {
if (field_count === 1) {
return function(token:T.Token, data:{}) {
const field = fields[0]!.field;
return scoreValue(getAttrFn(data, field), token, weights[field]||1);
};
}
return function(token:T.Token, data:{}) {
var sum = 0;
// is the token specific to a field?
if( token.field ){
const value = getAttrFn(data, token.field);
if( !token.regex && value ){
sum += (1/field_count);
}else{
sum += scoreValue(value, token, 1);
}
}else{
iterate(weights, (weight:number, field:string) => {
sum += scoreValue(getAttrFn(data, field), token, weight);
});
}
return sum / field_count;
};
})();
if (token_count === 1) {
return function(data:{}) {
return scoreObject(tokens[0]!, data);
};
}
if (search.options.conjunction === 'and') {
return function(data:{}) {
var score, sum = 0;
for( let token of tokens){
score = scoreObject(token, data);
if (score <= 0) return 0;
sum += score;
}
return sum / token_count;
};
} else {
return function(data:{}) {
var sum = 0;
iterate(tokens,(token:T.Token)=>{
sum += scoreObject(token, data);
});
return sum / token_count;
};
}
};
/**
* Returns a function that can be used to compare two
* results, for sorting purposes. If no sorting should
* be performed, `null` will be returned.
*
* @return function(a,b)
*/
getSortFunction(query:string, options:T.UserOptions) {
var search = this.prepareSearch(query, options);
return this._getSortFunction(search);
}
_getSortFunction(search:T.PrepareObj){
var implicit_score,
sort_flds:T.Sort[] = [];
const self = this,
options = search.options,
sort = (!search.query && options.sort_empty) ? options.sort_empty : options.sort;
if( typeof sort == 'function' ){
return sort.bind(this);
}
/**
* Fetches the specified sort field value
* from a search result item.
*
*/
const get_field = function(name:string, result:T.ResultItem):string|number {
if (name === '$score') return result.score;
return search.getAttrFn(self.items[result.id], name);
};
// parse options
if (sort) {
for( let s of sort ){
if (search.query || s.field !== '$score') {
sort_flds.push(s);
}
}
}
// the "$score" field is implied to be the primary
// sort field, unless it's manually specified
if (search.query) {
implicit_score = true;
for( let fld of sort_flds ){
if( fld.field === '$score' ){
implicit_score = false;
break;
}
}
if (implicit_score) {
sort_flds.unshift({field: '$score', direction: 'desc'});
}
// without a search.query, all items will have the same score
} else {
sort_flds = sort_flds.filter((fld) => fld.field !== '$score' );
}
// build function
const sort_flds_count = sort_flds.length;
if (!sort_flds_count) {
return null;
}
return function(a:T.ResultItem, b:T.ResultItem) {
var result, field;
for( let sort_fld of sort_flds ){
field = sort_fld.field;
let multiplier = sort_fld.direction === 'desc' ? -1 : 1;
result = multiplier * cmp(
get_field(field, a),
get_field(field, b)
);
if (result) return result;
}
return 0;
};
};
/**
* Parses a search query and returns an object
* with tokens and fields ready to be populated
* with results.
*
*/
prepareSearch(query:string, optsUser:T.UserOptions):T.PrepareObj {
const weights:T.Weights = {};
var options = Object.assign({},optsUser) as T.Options;
propToArray(options,'sort');
propToArray(options,'sort_empty');
// convert fields to new format
if( options.fields ){
propToArray(options,'fields');
const fields:T.Field[] = [];
options.fields.forEach((field:string|T.Field) => {
if( typeof field == 'string' ){
field = {field:field,weight:1};
}
fields.push(field);
weights[field.field] = ('weight' in field) ? field.weight : 1;
});
options.fields = fields;
}
return {
options : options as T.Options,
query : query.toLowerCase().trim(),
tokens : this.tokenize(query, options.respect_word_boundaries, weights),
total : 0,
items : [],
weights : weights,
getAttrFn : (options.nesting) ? getAttrNesting : getAttr,
};
};
/**
* Searches through all items and returns a sorted array of matches.
*
*/
search(query:string, options:T.UserOptions) : T.PrepareObj {
var self = this, score, search: T.PrepareObj;
search = this.prepareSearch(query, options);
options = search.options;
query = search.query;
// generate result scoring function
const fn_score:T.ScoreFn = options.score || self._getScoreFunction(search);
// perform search and sort
if (query.length) {
iterate(self.items, (item:T.ResultItem, id:string|number) => {
score = fn_score(item);
if (options.filter === false || score > 0) {
search.items.push({'score': score, 'id': id});
}
});
} else {
iterate(self.items, (_:T.ResultItem, id:string|number) => {
search.items.push({'score': 1, 'id': id});
});
}
const fn_sort = self._getSortFunction(search);
if (fn_sort) search.items.sort(fn_sort);
// apply limits
search.total = search.items.length;
if (typeof options.limit === 'number') {
search.items = search.items.slice(0, options.limit);
}
return search;
};
}
export { Sifter, scoreValue, getAttr, getAttrNesting, propToArray, iterate, cmp, getPattern }