mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-02-20 12:24:23 +01:00
Merge pull request #420 from koloml/suggestions-aliases-handling-in-local-ac
Fixed aliases still displaying for namespaced tags, fixing the actual cause of the issue inside local autocomplete
This commit is contained in:
commit
eb164ab11f
5 changed files with 39 additions and 64 deletions
|
@ -8,7 +8,7 @@ import store from './utils/store';
|
||||||
import { TermContext } from './query/lex';
|
import { TermContext } from './query/lex';
|
||||||
import { $$ } from './utils/dom';
|
import { $$ } from './utils/dom';
|
||||||
import {
|
import {
|
||||||
createLocalAutocompleteResultFormatter,
|
formatLocalAutocompleteResult,
|
||||||
fetchLocalAutocomplete,
|
fetchLocalAutocomplete,
|
||||||
fetchSuggestions,
|
fetchSuggestions,
|
||||||
SuggestionsPopup,
|
SuggestionsPopup,
|
||||||
|
@ -196,11 +196,9 @@ function listenAutocomplete() {
|
||||||
originalTerm = `${inputField.value}`.toLowerCase();
|
originalTerm = `${inputField.value}`.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedTerm = trimPrefixes(originalTerm);
|
|
||||||
|
|
||||||
const suggestions = localAc
|
const suggestions = localAc
|
||||||
.matchPrefix(matchedTerm, suggestionsCount)
|
.matchPrefix(trimPrefixes(originalTerm), suggestionsCount)
|
||||||
.map(createLocalAutocompleteResultFormatter(matchedTerm));
|
.map(formatLocalAutocompleteResult);
|
||||||
|
|
||||||
if (suggestions.length) {
|
if (suggestions.length) {
|
||||||
popup.renderSuggestions(suggestions).showForField(targetedInput);
|
popup.renderSuggestions(suggestions).showForField(targetedInput);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { fetchMock } from '../../../test/fetch-mock.ts';
|
||||||
import {
|
import {
|
||||||
fetchLocalAutocomplete,
|
fetchLocalAutocomplete,
|
||||||
fetchSuggestions,
|
fetchSuggestions,
|
||||||
createLocalAutocompleteResultFormatter,
|
formatLocalAutocompleteResult,
|
||||||
purgeSuggestionsCache,
|
purgeSuggestionsCache,
|
||||||
SuggestionsPopup,
|
SuggestionsPopup,
|
||||||
TermSuggestion,
|
TermSuggestion,
|
||||||
|
@ -334,13 +334,12 @@ describe('Suggestions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createLocalAutocompleteResultFormatter', () => {
|
describe('formatLocalAutocompleteResult', () => {
|
||||||
it('should format suggested tags as tag name and the count', () => {
|
it('should format suggested tags as tag name and the count', () => {
|
||||||
const tagName = 'safe';
|
const tagName = 'safe';
|
||||||
const tagCount = getRandomIntBetween(5, 10);
|
const tagCount = getRandomIntBetween(5, 10);
|
||||||
|
|
||||||
const formatter = createLocalAutocompleteResultFormatter();
|
const resultObject = formatLocalAutocompleteResult({
|
||||||
const resultObject = formatter({
|
|
||||||
name: tagName,
|
name: tagName,
|
||||||
aliasName: tagName,
|
aliasName: tagName,
|
||||||
imageCount: tagCount,
|
imageCount: tagCount,
|
||||||
|
@ -355,8 +354,7 @@ describe('Suggestions', () => {
|
||||||
const tagAlias = 'rating:safe';
|
const tagAlias = 'rating:safe';
|
||||||
const tagCount = getRandomIntBetween(5, 10);
|
const tagCount = getRandomIntBetween(5, 10);
|
||||||
|
|
||||||
const formatter = createLocalAutocompleteResultFormatter();
|
const resultObject = formatLocalAutocompleteResult({
|
||||||
const resultObject = formatter({
|
|
||||||
name: tagName,
|
name: tagName,
|
||||||
aliasName: tagAlias,
|
aliasName: tagAlias,
|
||||||
imageCount: tagCount,
|
imageCount: tagCount,
|
||||||
|
@ -365,39 +363,5 @@ describe('Suggestions', () => {
|
||||||
expect(resultObject.label).toBe(`${tagAlias} ⇒ ${tagName} (${tagCount})`);
|
expect(resultObject.label).toBe(`${tagAlias} ⇒ ${tagName} (${tagCount})`);
|
||||||
expect(resultObject.value).toBe(tagName);
|
expect(resultObject.value).toBe(tagName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not display aliases when tag is starting with the same matched', () => {
|
|
||||||
const tagName = 'chest fluff';
|
|
||||||
const tagAlias = 'chest floof';
|
|
||||||
const tagCount = getRandomIntBetween(5, 10);
|
|
||||||
|
|
||||||
const prefix = 'ch';
|
|
||||||
|
|
||||||
const formatter = createLocalAutocompleteResultFormatter(prefix);
|
|
||||||
const resultObject = formatter({
|
|
||||||
name: tagName,
|
|
||||||
aliasName: tagAlias,
|
|
||||||
imageCount: tagCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resultObject.label).toBe(`${tagName} (${tagCount})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display aliases if matched prefix is different from the tag name', () => {
|
|
||||||
const tagName = 'queen chrysalis';
|
|
||||||
const tagAlias = 'chrysalis';
|
|
||||||
const tagCount = getRandomIntBetween(5, 10);
|
|
||||||
|
|
||||||
const prefix = 'ch';
|
|
||||||
|
|
||||||
const formatter = createLocalAutocompleteResultFormatter(prefix);
|
|
||||||
const resultObject = formatter({
|
|
||||||
name: tagName,
|
|
||||||
aliasName: tagAlias,
|
|
||||||
imageCount: tagCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(resultObject.label).toBe(`${tagAlias} ⇒ ${tagName} (${tagCount})`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -100,6 +100,13 @@ export class LocalAutocompleter {
|
||||||
return tagPointer;
|
return tagPointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the tag pointed to by the reference index is an alias.
|
||||||
|
*/
|
||||||
|
private tagReferenceIsAlias(i: TagReferenceIndex): boolean {
|
||||||
|
return this.view.getInt32(this.referenceStart + i * 8 + 4, true) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the images count for the given reference index.
|
* Get the images count for the given reference index.
|
||||||
*/
|
*/
|
||||||
|
@ -173,6 +180,7 @@ export class LocalAutocompleter {
|
||||||
getResult: (i: number) => TagReferenceIndex,
|
getResult: (i: number) => TagReferenceIndex,
|
||||||
compare: (result: TagReferenceIndex) => number,
|
compare: (result: TagReferenceIndex) => number,
|
||||||
hasFilteredAssociation: (result: TagReferenceIndex) => boolean,
|
hasFilteredAssociation: (result: TagReferenceIndex) => boolean,
|
||||||
|
isAlias: (result: TagReferenceIndex) => boolean,
|
||||||
results: UniqueHeap<TagReferenceIndex>,
|
results: UniqueHeap<TagReferenceIndex>,
|
||||||
) {
|
) {
|
||||||
const filter = !store.get('unfilter_tag_suggestions');
|
const filter = !store.get('unfilter_tag_suggestions');
|
||||||
|
@ -207,7 +215,7 @@ export class LocalAutocompleter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing was filtered, so add
|
// Nothing was filtered, so add
|
||||||
results.append(referenceIndex);
|
results.append(referenceIndex, !isAlias(referenceIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,18 +238,19 @@ export class LocalAutocompleter {
|
||||||
// Set up filter context
|
// Set up filter context
|
||||||
const hiddenTags = new Set(window.booru.hiddenTagList);
|
const hiddenTags = new Set(window.booru.hiddenTagList);
|
||||||
const hasFilteredAssociation = this.isFilteredByReference.bind(this, hiddenTags);
|
const hasFilteredAssociation = this.isFilteredByReference.bind(this, hiddenTags);
|
||||||
|
const isAlias = this.tagReferenceIsAlias.bind(this);
|
||||||
|
|
||||||
// Find tags ordered by full name
|
// Find tags ordered by full name
|
||||||
const prefixMatch = (i: TagReferenceIndex) =>
|
const prefixMatch = (i: TagReferenceIndex) =>
|
||||||
strcmp(this.referenceToName(i, false).slice(0, prefix.length), prefix);
|
strcmp(this.referenceToName(i, false).slice(0, prefix.length), prefix);
|
||||||
const referenceToNameIndex = (i: number) => i;
|
const referenceToNameIndex = (i: number) => i;
|
||||||
this.scanResults(referenceToNameIndex, prefixMatch, hasFilteredAssociation, results);
|
this.scanResults(referenceToNameIndex, prefixMatch, hasFilteredAssociation, isAlias, results);
|
||||||
|
|
||||||
// Find tags ordered by name in namespace
|
// Find tags ordered by name in namespace
|
||||||
const namespaceMatch = (i: TagReferenceIndex) =>
|
const namespaceMatch = (i: TagReferenceIndex) =>
|
||||||
strcmp(nameInNamespace(this.referenceToName(i, false)).slice(0, prefix.length), prefix);
|
strcmp(nameInNamespace(this.referenceToName(i, false)).slice(0, prefix.length), prefix);
|
||||||
const referenceToAliasIndex = this.getSecondaryResultAt.bind(this);
|
const referenceToAliasIndex = this.getSecondaryResultAt.bind(this);
|
||||||
this.scanResults(referenceToAliasIndex, namespaceMatch, hasFilteredAssociation, results);
|
this.scanResults(referenceToAliasIndex, namespaceMatch, hasFilteredAssociation, isAlias, results);
|
||||||
|
|
||||||
// Convert top K from heap into result array
|
// Convert top K from heap into result array
|
||||||
return results.topK(k).map((i: TagReferenceIndex) => ({
|
return results.topK(k).map((i: TagReferenceIndex) => ({
|
||||||
|
|
|
@ -176,17 +176,15 @@ export async function fetchLocalAutocomplete(): Promise<LocalAutocompleter> {
|
||||||
.then(buf => new LocalAutocompleter(buf));
|
.then(buf => new LocalAutocompleter(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLocalAutocompleteResultFormatter(matchedPrefix?: string): (result: Result) => TermSuggestion {
|
export function formatLocalAutocompleteResult(result: Result): TermSuggestion {
|
||||||
return result => {
|
let tagName = result.name;
|
||||||
let tagName = result.name;
|
|
||||||
|
|
||||||
if (tagName !== result.aliasName && (!matchedPrefix || !tagName.startsWith(matchedPrefix))) {
|
if (tagName !== result.aliasName) {
|
||||||
tagName = `${result.aliasName} ⇒ ${tagName}`;
|
tagName = `${result.aliasName} ⇒ ${tagName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: result.name,
|
value: result.name,
|
||||||
label: `${tagName} (${result.imageCount})`,
|
label: `${tagName} (${result.imageCount})`,
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,34 @@
|
||||||
export type Compare<T> = (a: T, b: T) => number;
|
export type Compare<T> = (a: T, b: T) => number;
|
||||||
export type Unique<T> = (a: T) => unknown;
|
export type Unique<T> = (a: T) => unknown;
|
||||||
export type Collection<T> = { [index: number]: T; length: number };
|
export type Collection<T> = {
|
||||||
|
[index: number]: T;
|
||||||
|
length: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class UniqueHeap<T> {
|
export class UniqueHeap<T> {
|
||||||
private keys: Set<unknown>;
|
private keys: Map<unknown, number>;
|
||||||
private values: Collection<T>;
|
private values: Collection<T>;
|
||||||
private length: number;
|
private length: number;
|
||||||
private compare: Compare<T>;
|
private compare: Compare<T>;
|
||||||
private unique: Unique<T>;
|
private unique: Unique<T>;
|
||||||
|
|
||||||
constructor(compare: Compare<T>, unique: Unique<T>, values: Collection<T>) {
|
constructor(compare: Compare<T>, unique: Unique<T>, values: Collection<T>) {
|
||||||
this.keys = new Set();
|
this.keys = new Map();
|
||||||
this.values = values;
|
this.values = values;
|
||||||
this.length = 0;
|
this.length = 0;
|
||||||
this.compare = compare;
|
this.compare = compare;
|
||||||
this.unique = unique;
|
this.unique = unique;
|
||||||
}
|
}
|
||||||
|
|
||||||
append(value: T) {
|
append(value: T, forceReplace: boolean = false) {
|
||||||
const key = this.unique(value);
|
const key = this.unique(value);
|
||||||
|
const prevIndex = this.keys.get(key);
|
||||||
|
|
||||||
if (!this.keys.has(key)) {
|
if (typeof prevIndex === 'undefined') {
|
||||||
this.keys.add(key);
|
this.keys.set(key, this.length);
|
||||||
this.values[this.length++] = value;
|
this.values[this.length++] = value;
|
||||||
|
} else if (forceReplace) {
|
||||||
|
this.values[prevIndex] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue