diff --git a/source/core/Vault.ts b/source/core/Vault.ts index 52044317..0fbdc21e 100644 --- a/source/core/Vault.ts +++ b/source/core/Vault.ts @@ -173,8 +173,26 @@ export class Vault extends EventEmitter { return findEntriesByProperty(this._entries, property, value); } - findEntriesByTag(tag: string): Array { + /** + * Find entries by a certain tag + * @param tag The case-insensitive tag name + * @param exact Whether to match exact tag names or use partial + * matching. Default is true (exact). + * @returns An array of entries + */ + findEntriesByTag(tag: string, exact: boolean = true): Array { const tagLower = tag.toLowerCase(); + if (!exact) { + const entryIDs = new Set(); + for (const [currentTag, currentIDs] of this._tagMap.entries()) { + if (currentTag.toLowerCase().indexOf(tagLower) === 0) { + for (const id of currentIDs) { + entryIDs.add(id); + } + } + } + return [...entryIDs].map((id) => this.findEntryByID(id)); + } const entryIDs = this._tagMap.has(tagLower) ? this._tagMap.get(tagLower) : []; return entryIDs.map((id) => this.findEntryByID(id)); } diff --git a/source/facades/entry.ts b/source/facades/entry.ts index 3b80ac4d..726ffdb1 100644 --- a/source/facades/entry.ts +++ b/source/facades/entry.ts @@ -197,6 +197,22 @@ export function createEntryFromFacade(group: Group, facade: EntryFacade): Entry return entry; } +/** + * Convert an array of entry facade fields to a + * key-value object with only attributes + * @param facadeFields Array of fields + * @memberof module:Buttercup + */ +export function fieldsToAttributes(facadeFields: Array): { + [key: string]: string; +} { + return facadeFields.reduce((output, field) => { + if (field.propertyType !== EntryPropertyType.Attribute) return output; + output[field.property] = field.value; + return output; + }, {}); +} + /** * Convert an array of entry facade fields to a * key-value object with only properties @@ -207,7 +223,7 @@ export function fieldsToProperties(facadeFields: Array): { [key: string]: string; } { return facadeFields.reduce((output, field) => { - if (field.propertyType !== "property") return output; + if (field.propertyType !== EntryPropertyType.Property) return output; output[field.property] = field.value; return output; }, {}); diff --git a/source/search/BaseSearch.ts b/source/search/BaseSearch.ts index 5b94c1ea..0de499a2 100644 --- a/source/search/BaseSearch.ts +++ b/source/search/BaseSearch.ts @@ -3,6 +3,7 @@ import { StorageInterface } from "../storage/StorageInterface.js"; import { buildSearcher } from "./searcher.js"; import { Vault } from "../core/Vault.js"; import { EntryID, EntryType, GroupID, VaultFacade, VaultID } from "../types.js"; +import { extractTagsFromSearchTerm } from "./tags.js"; interface DomainScores { [domain: string]: number; @@ -22,10 +23,11 @@ export interface SearcherFactory { } export interface SearchResult { - id: EntryID; + entryType: EntryType; groupID: GroupID; + id: EntryID; properties: { [property: string]: string }; - entryType: EntryType; + tags: Array; urls: Array; vaultID: VaultID; } @@ -137,6 +139,17 @@ export class BaseSearch { if (!this._fuse) { throw new Error("Searching interface not prepared"); } + const { tags, term: searchTerm } = extractTagsFromSearchTerm(term); + if (tags.length > 0) { + // Instantiate new searcher based on a subset of entries + const subset = this._entries.filter((entry) => + entry.tags.some((entryTag) => tags.includes(entryTag)) + ); + this._fuse = this._searcherFactory(subset); + } else { + // Reset instance + this._fuse = this._searcherFactory(this._entries); + } this._results = this._fuse.search(term).map((result) => result.item); return this._results; } diff --git a/source/search/VaultEntrySearch.ts b/source/search/VaultEntrySearch.ts index 2fa35a4a..57aa7419 100644 --- a/source/search/VaultEntrySearch.ts +++ b/source/search/VaultEntrySearch.ts @@ -25,13 +25,14 @@ async function extractEntries( const properties = entry.getProperties(); const urls = getEntryURLs(properties, EntryURLType.General); return { + domainScores: vaultScore[entry.id] || {}, + entryType: entry.getType(), + groupID: entry.getGroup().id, id: entry.id, properties, - entryType: entry.getType(), + tags: entry.getTags(), urls, - groupID: entry.getGroup().id, - vaultID: vault.id, - domainScores: vaultScore[entry.id] || {} + vaultID: vault.id }; }); } diff --git a/source/search/VaultFacadeEntrySearch.ts b/source/search/VaultFacadeEntrySearch.ts index a5becc6f..fc29f0f0 100644 --- a/source/search/VaultFacadeEntrySearch.ts +++ b/source/search/VaultFacadeEntrySearch.ts @@ -1,8 +1,10 @@ +import { Entry } from "../core/Entry.js"; import { BaseSearch, ProcessedSearchEntry, SearcherFactory } from "./BaseSearch.js"; import { EntryURLType, getEntryURLs } from "../tools/entry.js"; -import { fieldsToProperties } from "../facades/entry.js"; +import { fieldsToAttributes, fieldsToProperties } from "../facades/entry.js"; import { StorageInterface } from "../storage/StorageInterface.js"; import { EntryFacade, VaultFacade } from "../types.js"; +import { isValidTag } from "../tools/tag.js"; async function extractEntries( facade: VaultFacade, @@ -20,16 +22,23 @@ async function extractEntries( // Get entries return facade.entries.reduce((entries: Array, nextEntry: EntryFacade) => { // @todo in trash + const attributes = fieldsToAttributes(nextEntry.fields); + const tags = attributes[Entry.Attributes.Tags] + ? attributes[Entry.Attributes.Tags].split(",").reduce((output, tag) => { + return isValidTag(tag) ? [...output, tag] : output; + }, []) + : []; const properties = fieldsToProperties(nextEntry.fields); const urls = getEntryURLs(properties, EntryURLType.General); entries.push({ + domainScores: vaultScore[nextEntry.id] || {}, + entryType: nextEntry.type, + groupID: nextEntry.parentID, id: nextEntry.id, properties, - entryType: nextEntry.type, + tags, urls, - groupID: nextEntry.parentID, - vaultID: facade.id, - domainScores: vaultScore[nextEntry.id] || {} + vaultID: facade.id }); return entries; }, []); diff --git a/source/search/tags.ts b/source/search/tags.ts new file mode 100644 index 00000000..784ab49f --- /dev/null +++ b/source/search/tags.ts @@ -0,0 +1,20 @@ +export function extractTagsFromSearchTerm(term: string): { + tags: Array; + term: string; +} { + const searchItems: Array = []; + const tags = new Set(); + const parts = term.split(/\s+/g); + for (const part of parts) { + if (/^#.+/.test(part)) { + const raw = part.replace(/^#/, ""); + tags.add(raw.toLowerCase()); + } else if (part.length > 0) { + searchItems.push(part); + } + } + return { + tags: [...tags], + term: searchItems.join(" ").trim() + }; +}