pragma Singleton import QtQuick import Quickshell Singleton { id: root // Public API function go(search, targets, options) { return _go(search, targets, options); } function single(search, target) { return _single(search, target); } function highlight(result, open, close) { if (open === undefined) open = ''; if (close === undefined) close = ''; return _highlight(result, open, close); } function prepare(target) { return _prepare(target); } function cleanup() { return _cleanup(); } // Internal implementation readonly property var _INFINITY: Infinity readonly property var _NEGATIVE_INFINITY: -Infinity readonly property var _NULL: null property var _noResults: { let r = []; r.total = 0; return r; } property var _noTarget: _prepare('') property var _preparedCache: new Map() property var _preparedSearchCache: new Map() property var _matchesSimple: [] property var _matchesStrict: [] property var _nextBeginningIndexesChanges: [] property var _keysSpacesBestScores: [] property var _allowPartialMatchScores: [] property var _tmpTargets: [] property var _tmpResults: [] property var _q: _fastpriorityqueue() function _fastpriorityqueue() { var e = [], o = 0, a = {}; var v = function (r) { for (var a = 0, vc = e[a], c = 1; c < o; ) { var s = c + 1; a = c; if (s < o && e[s]._score < e[c]._score) a = s; e[a - 1 >> 1] = e[a]; c = 1 + (a << 1); } for (var f = a - 1 >> 1; a > 0 && vc._score < e[f]._score; f = (a = f) - 1 >> 1) e[a] = e[f]; e[a] = vc; }; a.add = function (r) { var ac = o; e[o++] = r; for (var vc = ac - 1 >> 1; ac > 0 && r._score < e[vc]._score; vc = (ac = vc) - 1 >> 1) e[ac] = e[vc]; e[ac] = r; }; a.poll = function () { if (o !== 0) { var ac = e[0]; e[0] = e[--o]; v(); return ac; } }; a.peek = function () { if (o !== 0) return e[0]; }; a.replaceTop = function (r) { e[0] = r; v(); }; return a; } function _createResult() { return { target: '', obj: _NULL, _score: _NEGATIVE_INFINITY, _indexes: [], _targetLower: '', _targetLowerCodes: _NULL, _nextBeginningIndexes: _NULL, _bitflags: 0, get indexes() { return this._indexes.slice(0, this._indexes.len).sort((a, b) => a - b); }, set indexes(idx) { this._indexes = idx; }, highlight: function (open, close) { return root._highlight(this, open, close); }, get score() { return root._normalizeScore(this._score); }, set score(s) { this._score = root._denormalizeScore(s); } }; } function _createKeysResult(len) { var arr = new Array(len); arr._score = _NEGATIVE_INFINITY; arr.obj = _NULL; Object.defineProperty(arr, 'score', { get: function () { return root._normalizeScore(this._score); }, set: function (s) { this._score = root._denormalizeScore(s); } }); return arr; } function _new_result(target, options) { var result = _createResult(); result.target = target; result.obj = options.obj ?? _NULL; result._score = options._score ?? _NEGATIVE_INFINITY; result._indexes = options._indexes ?? []; result._targetLower = options._targetLower ?? ''; result._targetLowerCodes = options._targetLowerCodes ?? _NULL; result._nextBeginningIndexes = options._nextBeginningIndexes ?? _NULL; result._bitflags = options._bitflags ?? 0; return result; } function _normalizeScore(score) { if (score === _NEGATIVE_INFINITY) return 0; if (score > 1) return score; return Math.E ** (((-score + 1) ** 0.04307 - 1) * -2); } function _denormalizeScore(normalizedScore) { if (normalizedScore === 0) return _NEGATIVE_INFINITY; if (normalizedScore > 1) return normalizedScore; return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307); } function _remove_accents(str) { return str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, ''); } function _prepareLowerInfo(str) { str = _remove_accents(str); var strLen = str.length; var lower = str.toLowerCase(); var lowerCodes = []; var bitflags = 0; var containsSpace = false; for (var i = 0; i < strLen; ++i) { var lowerCode = lowerCodes[i] = lower.charCodeAt(i); if (lowerCode === 32) { containsSpace = true; continue; } var bit = lowerCode >= 97 && lowerCode <= 122 ? lowerCode - 97 : lowerCode >= 48 && lowerCode <= 57 ? 26 : lowerCode <= 127 ? 30 : 31; bitflags |= 1 << bit; } return { lowerCodes: lowerCodes, bitflags: bitflags, containsSpace: containsSpace, _lower: lower }; } function _prepareBeginningIndexes(target) { var targetLen = target.length; var beginningIndexes = []; var beginningIndexesLen = 0; var wasUpper = false; var wasAlphanum = false; for (var i = 0; i < targetLen; ++i) { var targetCode = target.charCodeAt(i); var isUpper = targetCode >= 65 && targetCode <= 90; var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57; var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum; wasUpper = isUpper; wasAlphanum = isAlphanum; if (isBeginning) beginningIndexes[beginningIndexesLen++] = i; } return beginningIndexes; } function _prepareNextBeginningIndexes(target) { target = _remove_accents(target); var targetLen = target.length; var beginningIndexes = _prepareBeginningIndexes(target); var nextBeginningIndexes = []; var lastIsBeginning = beginningIndexes[0]; var lastIsBeginningI = 0; for (var i = 0; i < targetLen; ++i) { if (lastIsBeginning > i) { nextBeginningIndexes[i] = lastIsBeginning; } else { lastIsBeginning = beginningIndexes[++lastIsBeginningI]; nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning; } } return nextBeginningIndexes; } function _prepareSearch(search) { if (typeof search === 'number') search = '' + search; else if (typeof search !== 'string') search = ''; search = search.trim(); var info = _prepareLowerInfo(search); var spaceSearches = []; if (info.containsSpace) { var searches = search.split(/\s+/); searches = [...new Set(searches)]; for (var i = 0; i < searches.length; i++) { if (searches[i] === '') continue; var _info = _prepareLowerInfo(searches[i]); spaceSearches.push({ lowerCodes: _info.lowerCodes, _lower: searches[i].toLowerCase(), containsSpace: false }); } } return { lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches }; } function _prepare(target) { if (typeof target === 'number') target = '' + target; else if (typeof target !== 'string') target = ''; var info = _prepareLowerInfo(target); return _new_result(target, { _targetLower: info._lower, _targetLowerCodes: info.lowerCodes, _bitflags: info.bitflags }); } function _cleanup() { _preparedCache.clear(); _preparedSearchCache.clear(); } function _isPrepared(x) { return typeof x === 'object' && typeof x._bitflags === 'number'; } function _getPrepared(target) { if (target.length > 999) return _prepare(target); var targetPrepared = _preparedCache.get(target); if (targetPrepared !== undefined) return targetPrepared; targetPrepared = _prepare(target); _preparedCache.set(target, targetPrepared); return targetPrepared; } function _getPreparedSearch(search) { if (search.length > 999) return _prepareSearch(search); var searchPrepared = _preparedSearchCache.get(search); if (searchPrepared !== undefined) return searchPrepared; searchPrepared = _prepareSearch(search); _preparedSearchCache.set(search, searchPrepared); return searchPrepared; } function _getValue(obj, prop) { var tmp = obj[prop]; if (tmp !== undefined) return tmp; if (typeof prop === 'function') return prop(obj); var segs = prop; if (!Array.isArray(prop)) segs = prop.split('.'); var len = segs.length; var i = -1; while (obj && (++i < len)) obj = obj[segs[i]]; return obj; } function _single(search, target) { if (!search || !target) return _NULL; var preparedSearch = _getPreparedSearch(search); if (!_isPrepared(target)) target = _getPrepared(target); var searchBitflags = preparedSearch.bitflags; if ((searchBitflags & target._bitflags) !== searchBitflags) return _NULL; return _algorithm(preparedSearch, target); } function _highlight(result, open, close) { if (open === undefined) open = ''; if (close === undefined) close = ''; var callback = typeof open === 'function' ? open : undefined; var target = result.target; var targetLen = target.length; var indexes = result.indexes; var highlighted = ''; var matchI = 0; var indexesI = 0; var opened = false; var parts = []; for (var i = 0; i < targetLen; ++i) { var ch = target[i]; if (indexes[indexesI] === i) { ++indexesI; if (!opened) { opened = true; if (callback) { parts.push(highlighted); highlighted = ''; } else { highlighted += open; } } if (indexesI === indexes.length) { if (callback) { highlighted += ch; parts.push(callback(highlighted, matchI++)); highlighted = ''; parts.push(target.substr(i + 1)); } else { highlighted += ch + close + target.substr(i + 1); } break; } } else { if (opened) { opened = false; if (callback) { parts.push(callback(highlighted, matchI++)); highlighted = ''; } else { highlighted += close; } } } highlighted += ch; } return callback ? parts : highlighted; } function _all(targets, options) { var results = []; results.total = targets.length; var limit = options?.limit || _INFINITY; if (options?.key) { for (var i = 0; i < targets.length; i++) { var obj = targets[i]; var target = _getValue(obj, options.key); if (target == _NULL) continue; if (!_isPrepared(target)) target = _getPrepared(target); var result = _new_result(target.target, { _score: target._score, obj: obj }); results.push(result); if (results.length >= limit) return results; } } else if (options?.keys) { for (var i = 0; i < targets.length; i++) { var obj = targets[i]; var objResults = _createKeysResult(options.keys.length); for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) { var target = _getValue(obj, options.keys[keyI]); if (!target) { objResults[keyI] = _noTarget; continue; } if (!_isPrepared(target)) target = _getPrepared(target); target._score = _NEGATIVE_INFINITY; target._indexes.len = 0; objResults[keyI] = target; } objResults.obj = obj; objResults._score = _NEGATIVE_INFINITY; results.push(objResults); if (results.length >= limit) return results; } } else { for (var i = 0; i < targets.length; i++) { var target = targets[i]; if (target == _NULL) continue; if (!_isPrepared(target)) target = _getPrepared(target); target._score = _NEGATIVE_INFINITY; target._indexes.len = 0; results.push(target); if (results.length >= limit) return results; } } return results; } function _algorithm(preparedSearch, prepared, allowSpaces, allowPartialMatch) { if (allowSpaces === undefined) allowSpaces = false; if (allowPartialMatch === undefined) allowPartialMatch = false; if (allowSpaces === false && preparedSearch.containsSpace) return _algorithmSpaces(preparedSearch, prepared, allowPartialMatch); var searchLower = preparedSearch._lower; var searchLowerCodes = preparedSearch.lowerCodes; var searchLowerCode = searchLowerCodes[0]; var targetLowerCodes = prepared._targetLowerCodes; var searchLen = searchLowerCodes.length; var targetLen = targetLowerCodes.length; var searchI = 0; var targetI = 0; var matchesSimpleLen = 0; for (; ; ) { var isMatch = searchLowerCode === targetLowerCodes[targetI]; if (isMatch) { _matchesSimple[matchesSimpleLen++] = targetI; ++searchI; if (searchI === searchLen) break; searchLowerCode = searchLowerCodes[searchI]; } ++targetI; if (targetI >= targetLen) return _NULL; } searchI = 0; var successStrict = false; var matchesStrictLen = 0; var nextBeginningIndexes = prepared._nextBeginningIndexes; if (nextBeginningIndexes === _NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = _prepareNextBeginningIndexes(prepared.target); targetI = _matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[_matchesSimple[0] - 1]; var backtrackCount = 0; if (targetI !== targetLen) for (; ; ) { if (targetI >= targetLen) { if (searchI <= 0) break; ++backtrackCount; if (backtrackCount > 200) break; --searchI; var lastMatch = _matchesStrict[--matchesStrictLen]; targetI = nextBeginningIndexes[lastMatch]; } else { var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]; if (isMatch) { _matchesStrict[matchesStrictLen++] = targetI; ++searchI; if (searchI === searchLen) { successStrict = true; break; } ++targetI; } else { targetI = nextBeginningIndexes[targetI]; } } } var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, _matchesSimple[0]); var isSubstring = !!~substringIndex; var isSubstringBeginning = !isSubstring ? false : substringIndex === 0 || prepared._nextBeginningIndexes[substringIndex - 1] === substringIndex; if (isSubstring && !isSubstringBeginning) { for (var i = 0; i < nextBeginningIndexes.length; i = nextBeginningIndexes[i]) { if (i <= substringIndex) continue; for (var s = 0; s < searchLen; s++) if (searchLowerCodes[s] !== prepared._targetLowerCodes[i + s]) break; if (s === searchLen) { substringIndex = i; isSubstringBeginning = true; break; } } } var calculateScore = function (matches) { var score = 0; var extraMatchGroupCount = 0; for (var i = 1; i < searchLen; ++i) { if (matches[i] - matches[i - 1] !== 1) { score -= matches[i]; ++extraMatchGroupCount; } } var unmatchedDistance = matches[searchLen - 1] - matches[0] - (searchLen - 1); score -= (12 + unmatchedDistance) * extraMatchGroupCount; if (matches[0] !== 0) score -= matches[0] * matches[0] * 0.2; if (!successStrict) { score *= 1000; } else { var uniqueBeginningIndexes = 1; for (var i = nextBeginningIndexes[0]; i < targetLen; i = nextBeginningIndexes[i]) ++uniqueBeginningIndexes; if (uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes - 24) * 10; } score -= (targetLen - searchLen) / 2; if (isSubstring) score /= 1 + searchLen * searchLen * 1; if (isSubstringBeginning) score /= 1 + searchLen * searchLen * 1; score -= (targetLen - searchLen) / 2; return score; }; var matchesBest, score; if (!successStrict) { if (isSubstring) for (var i = 0; i < searchLen; ++i) _matchesSimple[i] = substringIndex + i; matchesBest = _matchesSimple; score = calculateScore(matchesBest); } else { if (isSubstringBeginning) { for (var i = 0; i < searchLen; ++i) _matchesSimple[i] = substringIndex + i; matchesBest = _matchesSimple; score = calculateScore(_matchesSimple); } else { matchesBest = _matchesStrict; score = calculateScore(_matchesStrict); } } prepared._score = score; for (var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]; prepared._indexes.len = searchLen; var result = _createResult(); result.target = prepared.target; result._score = prepared._score; result._indexes = prepared._indexes; return result; } function _algorithmSpaces(preparedSearch, target, allowPartialMatch) { var seen_indexes = new Set(); var score = 0; var result = _NULL; var first_seen_index_last_search = 0; var searches = preparedSearch.spaceSearches; var searchesLen = searches.length; var changeslen = 0; var resetNextBeginningIndexes = function () { for (let i = changeslen - 1; i >= 0; i--) target._nextBeginningIndexes[_nextBeginningIndexesChanges[i * 2 + 0]] = _nextBeginningIndexesChanges[i * 2 + 1]; }; var hasAtLeast1Match = false; for (var i = 0; i < searchesLen; ++i) { _allowPartialMatchScores[i] = _NEGATIVE_INFINITY; var search = searches[i]; result = _algorithm(search, target); if (allowPartialMatch) { if (result === _NULL) continue; hasAtLeast1Match = true; } else { if (result === _NULL) { resetNextBeginningIndexes(); return _NULL; } } var isTheLastSearch = i === searchesLen - 1; if (!isTheLastSearch) { var indexes = result._indexes; var indexesIsConsecutiveSubstring = true; for (let j = 0; j < indexes.len - 1; j++) { if (indexes[j + 1] - indexes[j] !== 1) { indexesIsConsecutiveSubstring = false; break; } } if (indexesIsConsecutiveSubstring) { var newBeginningIndex = indexes[indexes.len - 1] + 1; var toReplace = target._nextBeginningIndexes[newBeginningIndex - 1]; for (let j = newBeginningIndex - 1; j >= 0; j--) { if (toReplace !== target._nextBeginningIndexes[j]) break; target._nextBeginningIndexes[j] = newBeginningIndex; _nextBeginningIndexesChanges[changeslen * 2 + 0] = j; _nextBeginningIndexesChanges[changeslen * 2 + 1] = toReplace; changeslen++; } } } score += result._score / searchesLen; _allowPartialMatchScores[i] = result._score / searchesLen; if (result._indexes[0] < first_seen_index_last_search) { score -= (first_seen_index_last_search - result._indexes[0]) * 2; } first_seen_index_last_search = result._indexes[0]; for (var j = 0; j < result._indexes.len; ++j) seen_indexes.add(result._indexes[j]); } if (allowPartialMatch && !hasAtLeast1Match) return _NULL; resetNextBeginningIndexes(); var allowSpacesResult = _algorithm(preparedSearch, target, true); if (allowSpacesResult !== _NULL && allowSpacesResult._score > score) { if (allowPartialMatch) { for (var i = 0; i < searchesLen; ++i) { _allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen; } } return allowSpacesResult; } if (allowPartialMatch) result = target; result._score = score; var idx = 0; for (let index of seen_indexes) result._indexes[idx++] = index; result._indexes.len = idx; return result; } function _go(search, targets, options) { if (!search) return options?.all ? _all(targets, options) : _noResults; var preparedSearch = _getPreparedSearch(search); var searchBitflags = preparedSearch.bitflags; var containsSpace = preparedSearch.containsSpace; var threshold = _denormalizeScore(options?.threshold ?? 0.35); var limit = options?.limit || _INFINITY; var resultsLen = 0; var limitedCount = 0; var targetsLen = targets.length; function push_result(result) { if (resultsLen < limit) { _q.add(result); ++resultsLen; } else { ++limitedCount; if (result._score > _q.peek()._score) _q.replaceTop(result); } } if (options?.key) { var key = options.key; for (var i = 0; i < targetsLen; ++i) { var obj = targets[i]; var target = _getValue(obj, key); if (!target) continue; if (!_isPrepared(target)) target = _getPrepared(target); if ((searchBitflags & target._bitflags) !== searchBitflags) continue; var result = _algorithm(preparedSearch, target); if (result === _NULL) continue; if (result._score < threshold) continue; result.obj = obj; push_result(result); } } else if (options?.keys) { var keys = options.keys; var keysLen = keys.length; outer: for (var i = 0; i < targetsLen; ++i) { var obj = targets[i]; var keysBitflags = 0; for (var keyI = 0; keyI < keysLen; ++keyI) { var key = keys[keyI]; var target = _getValue(obj, key); if (!target) { _tmpTargets[keyI] = _noTarget; continue; } if (!_isPrepared(target)) target = _getPrepared(target); _tmpTargets[keyI] = target; keysBitflags |= target._bitflags; } if ((searchBitflags & keysBitflags) !== searchBitflags) continue; if (containsSpace) for (let j = 0; j < preparedSearch.spaceSearches.length; j++) _keysSpacesBestScores[j] = _NEGATIVE_INFINITY; for (var keyI = 0; keyI < keysLen; ++keyI) { target = _tmpTargets[keyI]; if (target === _noTarget) { _tmpResults[keyI] = _noTarget; continue; } _tmpResults[keyI] = _algorithm(preparedSearch, target, false, containsSpace); if (_tmpResults[keyI] === _NULL) { _tmpResults[keyI] = _noTarget; continue; } if (containsSpace) for (let j = 0; j < preparedSearch.spaceSearches.length; j++) { if (_allowPartialMatchScores[j] > -1000) { if (_keysSpacesBestScores[j] > _NEGATIVE_INFINITY) { var tmp = (_keysSpacesBestScores[j] + _allowPartialMatchScores[j]) / 4; if (tmp > _keysSpacesBestScores[j]) _keysSpacesBestScores[j] = tmp; } } if (_allowPartialMatchScores[j] > _keysSpacesBestScores[j]) _keysSpacesBestScores[j] = _allowPartialMatchScores[j]; } } if (containsSpace) { for (let j = 0; j < preparedSearch.spaceSearches.length; j++) if (_keysSpacesBestScores[j] === _NEGATIVE_INFINITY) continue outer; } else { var hasAtLeast1Match = false; for (let j = 0; j < keysLen; j++) if (_tmpResults[j]._score !== _NEGATIVE_INFINITY) { hasAtLeast1Match = true; break; } if (!hasAtLeast1Match) continue; } var objResults = _createKeysResult(keysLen); for (let j = 0; j < keysLen; j++) objResults[j] = _tmpResults[j]; var score; if (containsSpace) { score = 0; for (let j = 0; j < preparedSearch.spaceSearches.length; j++) score += _keysSpacesBestScores[j]; } else { score = _NEGATIVE_INFINITY; for (let j = 0; j < keysLen; j++) { var res = objResults[j]; if (res._score > -1000) { if (score > _NEGATIVE_INFINITY) { var tmp = (score + res._score) / 4; if (tmp > score) score = tmp; } } if (res._score > score) score = res._score; } } objResults.obj = obj; objResults._score = score; if (options?.scoreFn) { score = options.scoreFn(objResults); if (!score) continue; score = _denormalizeScore(score); objResults._score = score; } if (score < threshold) continue; push_result(objResults); } } else { for (var i = 0; i < targetsLen; ++i) { var target = targets[i]; if (!target) continue; if (!_isPrepared(target)) target = _getPrepared(target); if ((searchBitflags & target._bitflags) !== searchBitflags) continue; var result = _algorithm(preparedSearch, target); if (result === _NULL) continue; if (result._score < threshold) continue; push_result(result); } } if (resultsLen === 0) return _noResults; var results = new Array(resultsLen); for (var i = resultsLen - 1; i >= 0; --i) results[i] = _q.poll(); results.total = resultsLen + limitedCount; return results; } }