@flesh-and-blood/search 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,6 @@ import {
2
2
  Release,
3
3
  setToSetIdentifierMappings
4
4
  } from "@flesh-and-blood/types";
5
- import { getAbbreviation } from "./abbreviations.js";
6
5
  import { PUNCTUATION } from "./constants.js";
7
6
  import {
8
7
  aliasesByFilterCategory,
@@ -55,7 +54,7 @@ const setNameInSetFilterPattern = new RegExp(
55
54
  )}):(?:[^\\s]*[,+])?"?)(${setNamesLongestFirst})(?="?(?:[,+]|\\s|$))`,
56
55
  "g"
57
56
  );
58
- const getPreparedQuery = (text, index) => {
57
+ const getPreparedText = (text, index) => {
59
58
  let preparedText = text.trim().toLowerCase();
60
59
  for (const smartQuote of SMART_DOUBLE_QUOTES) {
61
60
  preparedText = preparedText.replaceAll(smartQuote, '"');
@@ -85,12 +84,7 @@ const getPreparedQuery = (text, index) => {
85
84
  preparedText = preparedText.replace(punctuatedName, override);
86
85
  }
87
86
  }
88
- return {
89
- // A query that is nothing but a card's abbreviation names that card whole,
90
- // so its own spaces and commas are not separators.
91
- isWholeQueryAbbreviation: !!getAbbreviation(preparedText)?.card,
92
- text: preparedText
93
- };
87
+ return preparedText;
94
88
  };
95
89
  const getExpandedTokens = (token) => {
96
90
  const matchingShorthand = singleWordShorthands.find(
@@ -102,13 +96,9 @@ const getExpandedTokens = (token) => {
102
96
  tokens: isExpanded ? matchingShorthand.expanded : [token]
103
97
  };
104
98
  };
105
- const getQueryTermSpans = ({
106
- isWholeQueryAbbreviation,
107
- text
108
- }) => {
99
+ const getQueryTermSpans = (text) => {
109
100
  const termSpans = [];
110
- const tokenSpans = isWholeQueryAbbreviation ? [{ end: text.length, start: 0, token: text }] : getQueryTokenSpans(text);
111
- for (const { end, start, token } of tokenSpans) {
101
+ for (const { end, start, token } of getQueryTokenSpans(text)) {
112
102
  const { isExpanded, tokens } = getExpandedTokens(token);
113
103
  for (const expandedToken of tokens) {
114
104
  termSpans.push({ end, isExpanded, start, token: expandedToken });
@@ -146,7 +136,7 @@ const getParsedQuery = (text, index, {
146
136
  additionalSets = [],
147
137
  today = getTodayAsReleaseDate()
148
138
  } = {}) => {
149
- const preparedQuery = getPreparedQuery(text, index);
139
+ const preparedText = getPreparedText(text, index);
150
140
  const context = {
151
141
  additionalHeroes,
152
142
  additionalSets,
@@ -166,7 +156,7 @@ const getParsedQuery = (text, index, {
166
156
  const keywords = [];
167
157
  const nodes = [];
168
158
  const unresolvedFilters = [];
169
- for (const termSpan of getQueryTermSpans(preparedQuery)) {
159
+ for (const termSpan of getQueryTermSpans(preparedText)) {
170
160
  const filterToken = getQueryFilterToken(termSpan.token) || getIncompleteFilterToken(termSpan.token);
171
161
  if (filterToken) {
172
162
  const { isAndList, isExcluded, key, valueText } = filterToken;
@@ -225,13 +215,8 @@ const getParsedQuery = (text, index, {
225
215
  const { end, start, token } = termSpan;
226
216
  nodes.push({ end, isFilter: false, start, token });
227
217
  const freeText = getUnquotedValue(termSpan.token);
228
- const abbreviatedCard = getAbbreviation(freeText)?.card;
229
218
  const excludedMetaFilters = getExcludedMetaFilters(freeText);
230
- if (abbreviatedCard) {
231
- keywords.push(
232
- `"${abbreviatedCard.toLowerCase().replace(PUNCTUATION, "")}"`
233
- );
234
- } else if (excludedMetaFilters.length > 0) {
219
+ if (excludedMetaFilters.length > 0) {
235
220
  appliedFilters.push(...excludedMetaFilters);
236
221
  } else if (freeText) {
237
222
  keywords.push(freeText.replace(PUNCTUATION, ""));
@@ -243,7 +228,7 @@ const getParsedQuery = (text, index, {
243
228
  attributes,
244
229
  keywords,
245
230
  nodes,
246
- text: preparedQuery.text,
231
+ text: preparedText,
247
232
  unresolvedFilters
248
233
  };
249
234
  };
package/dist/search.d.ts CHANGED
@@ -18,7 +18,14 @@ export interface SearchOptions {
18
18
  */
19
19
  index?: CatalogueIndex;
20
20
  }
21
+ /** How the free text named cards other than by their names. */
22
+ export interface SearchAlias {
23
+ kind: "nickname" | "initials";
24
+ text: string;
25
+ }
21
26
  export interface SearchResults {
27
+ /** Set when the free text was a nickname or the initials of the cards found. */
28
+ alias?: SearchAlias;
22
29
  appliedFilters: AppliedFilter[];
23
30
  keywords: string[];
24
31
  attributes: QueryAttributes;
@@ -35,9 +42,15 @@ declare class Search {
35
42
  private cards;
36
43
  private fuse;
37
44
  private index;
45
+ private poolCardByCardIdentifier;
38
46
  constructor(cards: DoubleSidedCard[], options?: SearchOptions);
39
47
  constructor(cards: DoubleSidedCard[], additionalHeroes?: Hero[], additionalSets?: Release[]);
40
48
  private getFuse;
49
+ /**
50
+ * The pool's own copies of index cards, dropping any the pool lacks, so a pool
51
+ * searched out of a larger catalogue returns its cards alone.
52
+ */
53
+ private getPoolCards;
41
54
  search: (text: string, includeMemes?: boolean) => SearchResults;
42
55
  }
43
56
  export default Search;
package/dist/search.js CHANGED
@@ -11,7 +11,11 @@ import {
11
11
  getParsedQuery
12
12
  } from "./queryParse.js";
13
13
  import { memes } from "./memes.js";
14
- import { getNormalizedText, getTextWithoutMarkup } from "./helpers.js";
14
+ import {
15
+ getCleanText,
16
+ getNormalizedText,
17
+ getTextWithoutMarkup
18
+ } from "./helpers.js";
15
19
  import { FilterProperty } from "./metaFilters.js";
16
20
  import { getCatalogueIndex } from "./searchIndex.js";
17
21
  const searchOptions = {
@@ -52,27 +56,78 @@ class Search {
52
56
  }
53
57
  return this.fuse;
54
58
  };
59
+ /**
60
+ * The pool's own copies of index cards, dropping any the pool lacks, so a pool
61
+ * searched out of a larger catalogue returns its cards alone.
62
+ */
63
+ this.getPoolCards = (indexCards) => {
64
+ if (!this.poolCardByCardIdentifier) {
65
+ this.poolCardByCardIdentifier = new Map(
66
+ this.cards.map((card) => [card.cardIdentifier, card])
67
+ );
68
+ }
69
+ const poolCards = [];
70
+ for (const { cardIdentifier } of indexCards) {
71
+ const poolCard = this.poolCardByCardIdentifier.get(cardIdentifier);
72
+ if (poolCard) {
73
+ poolCards.push(poolCard);
74
+ }
75
+ }
76
+ return poolCards;
77
+ };
55
78
  this.search = (text, includeMemes) => {
56
79
  let results;
80
+ let alias;
57
81
  const { appliedFilters, attributes, keywords, unresolvedFilters } = getParsedQuery(text, this.index, {
58
82
  additionalHeroes: this.additionalHeroes,
59
83
  additionalSets: this.additionalSets
60
84
  });
61
85
  const keyword = keywords.join(" ");
62
86
  const matchingMemes = includeMemes ? memes.filter((meme) => meme.keyword === keyword) : [];
87
+ const getFilteredCards = (cardsToFilter) => appliedFilters.length > 0 ? cardsToFilter.filter(
88
+ (card) => card && filterCard(card, appliedFilters)
89
+ ) : cardsToFilter;
63
90
  if (matchingMemes.length > 0) {
64
- results = matchingMemes.map(({ card }) => card);
91
+ results = getFilteredCards(matchingMemes.map(({ card }) => card));
65
92
  } else if (keywords.length) {
66
- results = this.getFuse().search(keyword).map((result) => result.item);
67
- } else {
68
- results = [...this.cards];
69
- }
70
- if (appliedFilters.length) {
71
- results = results.filter(
72
- (card) => card && filterCard(card, appliedFilters)
93
+ const freeText = getCleanText(keyword);
94
+ const nicknamedCards = this.getPoolCards(
95
+ this.index.getCardsByNickname(freeText)
96
+ );
97
+ const namedCardByCardIdentifier = /* @__PURE__ */ new Map();
98
+ for (const card of [
99
+ ...this.getPoolCards(this.index.getCardsByExactName(freeText)),
100
+ ...nicknamedCards
101
+ ]) {
102
+ namedCardByCardIdentifier.set(card.cardIdentifier, card);
103
+ }
104
+ const textMatches = this.getFuse().search(keyword).map((result) => result.item).filter(
105
+ ({ cardIdentifier }) => !namedCardByCardIdentifier.has(cardIdentifier)
73
106
  );
107
+ results = getFilteredCards([
108
+ ...namedCardByCardIdentifier.values(),
109
+ ...textMatches
110
+ ]);
111
+ const hasNicknamedResult = results.some(
112
+ (card) => nicknamedCards.includes(card)
113
+ );
114
+ if (hasNicknamedResult) {
115
+ alias = { kind: "nickname", text: freeText };
116
+ }
117
+ const canFallBackToInitials = results.length === 0 && keywords.length === 1;
118
+ if (canFallBackToInitials) {
119
+ results = getFilteredCards(
120
+ this.getPoolCards(this.index.getCardsByInitials(freeText))
121
+ );
122
+ if (results.length > 0) {
123
+ alias = { kind: "initials", text: freeText };
124
+ }
125
+ }
126
+ } else {
127
+ results = getFilteredCards([...this.cards]);
74
128
  }
75
- if (keywords.length === 0) {
129
+ const isRankedByText = keywords.length > 0 && alias?.kind !== "initials";
130
+ if (!isRankedByText) {
76
131
  let setIdentifierToSortBy = "";
77
132
  const shouldSortByRelease = attributes.releases.length === 1;
78
133
  if (shouldSortByRelease) {
@@ -101,18 +156,6 @@ class Search {
101
156
  (c1, c2) => c1.name === c2.name ? `${c1.pitch}`.localeCompare(`${c2.pitch}`) : c1.name.localeCompare(c2.name)
102
157
  );
103
158
  }
104
- } else {
105
- const nameMatches = [];
106
- const nonMatches = [];
107
- const potentialCardName = keywords.map((keyword2) => keyword2.toLowerCase().replace(PUNCTUATION, "")).join(" ");
108
- for (const card of results) {
109
- if (card.name.toLowerCase().replace(PUNCTUATION, "") === potentialCardName) {
110
- nameMatches.push(card);
111
- } else {
112
- nonMatches.push(card);
113
- }
114
- }
115
- results = [...nameMatches, ...nonMatches];
116
159
  }
117
160
  let searchResultsWithMatchingPrinting = [];
118
161
  const {
@@ -155,6 +198,7 @@ class Search {
155
198
  }
156
199
  const searchResults = searchResultsWithMatchingPrinting.length > 0 ? searchResultsWithMatchingPrinting : results;
157
200
  return {
201
+ alias,
158
202
  appliedFilters,
159
203
  attributes,
160
204
  keywords,
@@ -1,4 +1,4 @@
1
- import { Card, CardRole, DoubleSidedCard } from "@flesh-and-blood/types";
1
+ import { CardRole, DoubleSidedCard } from "@flesh-and-blood/types";
2
2
  /**
3
3
  * Everything a corpus can be asked about its cards: lookups, the relations the
4
4
  * cards carry, and what each card is for. Every relation is identifier-level,
@@ -26,6 +26,18 @@ export interface CatalogueIndex<CardType extends DoubleSidedCard = DoubleSidedCa
26
26
  * a fragment of a name the corpus carries finds nothing.
27
27
  */
28
28
  getCardsByExactName: (name: string) => readonly CardType[];
29
+ /** Every card carrying the nickname, matched whole, hyphens ignored. */
30
+ getCardsByNickname: (nickname: string) => readonly CardType[];
31
+ /**
32
+ * Every card carrying the type, subtype or trait, matched whole, hyphens
33
+ * ignored.
34
+ */
35
+ getCardsByGroup: (group: string) => readonly CardType[];
36
+ /**
37
+ * Every card whose name has the initials, in corpus order, counting a
38
+ * hyphenated word both as one word and as the words the hyphens join.
39
+ */
40
+ getCardsByInitials: (initials: string) => readonly CardType[];
29
41
  /**
30
42
  * Every artist the corpus credits, each once, ordered by the `en` locale
31
43
  * ignoring case and diacritics so a browser and a server agree on the list.
@@ -65,7 +77,3 @@ export interface CatalogueIndex<CardType extends DoubleSidedCard = DoubleSidedCa
65
77
  export declare const getCatalogueIndex: <CardType extends DoubleSidedCard = DoubleSidedCard>(cards: readonly CardType[]) => CatalogueIndex<CardType>;
66
78
  /** {@link CatalogueIndex.getCardsByName}, as a free function over an index. */
67
79
  export declare const getCardsByName: (index: CatalogueIndex, name: string) => readonly DoubleSidedCard[];
68
- /** The cards naming the card, at every pitch of both. */
69
- export declare const getCardsReferencing: (index: CatalogueIndex, card: Card) => DoubleSidedCard[];
70
- /** The cards the card names, at every pitch of both. */
71
- export declare const getCardsReferencedBy: (index: CatalogueIndex, card: Card) => DoubleSidedCard[];
@@ -3,7 +3,37 @@ import {
3
3
  getCardRole,
4
4
  Type
5
5
  } from "@flesh-and-blood/types";
6
- import { getCleanText } from "./helpers.js";
6
+ import { getCleanText, getNormalizedText } from "./helpers.js";
7
+ const MINIMUM_INITIALS_LENGTH = 3;
8
+ const getInitialsOfName = (name) => {
9
+ const letters = getNormalizedText(name.toLowerCase()).replace(
10
+ /[^a-z\s-]/g,
11
+ ""
12
+ );
13
+ const initialsOfName = /* @__PURE__ */ new Set();
14
+ for (const wordBreak of [/[\s-]+/, /\s+/]) {
15
+ let initials = "";
16
+ for (const word of letters.split(wordBreak)) {
17
+ const wordLetters = word.replaceAll("-", "");
18
+ if (wordLetters) {
19
+ initials += wordLetters[0];
20
+ }
21
+ }
22
+ if (initials.length >= MINIMUM_INITIALS_LENGTH) {
23
+ initialsOfName.add(initials);
24
+ }
25
+ }
26
+ return [...initialsOfName];
27
+ };
28
+ const getHyphenFreeKey = (text) => getCleanText(text).replaceAll("-", "");
29
+ const addToCardList = (cardsByKey, key, card) => {
30
+ const cardsForKey = cardsByKey.get(key);
31
+ if (cardsForKey) {
32
+ cardsForKey.push(card);
33
+ } else {
34
+ cardsByKey.set(key, [card]);
35
+ }
36
+ };
7
37
  const catalogueIndexByCards = /* @__PURE__ */ new WeakMap();
8
38
  const noCards = Object.freeze([]);
9
39
  const strayCorpusPosition = Number.MAX_SAFE_INTEGER;
@@ -44,6 +74,9 @@ const getNewCatalogueIndex = (cards) => {
44
74
  let creatingCardsByCardIdentifier;
45
75
  let cardsByRole;
46
76
  let artists;
77
+ let cardsByNicknameKey;
78
+ let cardsByGroup;
79
+ let cardsByInitials;
47
80
  const getCardLookups = () => {
48
81
  if (!cardLookups) {
49
82
  const cardByCardIdentifier = /* @__PURE__ */ new Map();
@@ -132,6 +165,47 @@ const getNewCatalogueIndex = (cards) => {
132
165
  const { pitchCycleByCleanedName } = getCardLookups();
133
166
  return pitchCycleByCleanedName.get(getCleanText(name)) ?? noCards;
134
167
  };
168
+ const getCardsByNickname = (nickname) => {
169
+ if (!cardsByNicknameKey) {
170
+ cardsByNicknameKey = /* @__PURE__ */ new Map();
171
+ for (const card of cards) {
172
+ for (const cardNickname of card.nicknames ?? []) {
173
+ addToCardList(
174
+ cardsByNicknameKey,
175
+ getHyphenFreeKey(cardNickname),
176
+ card
177
+ );
178
+ }
179
+ }
180
+ }
181
+ return cardsByNicknameKey.get(getHyphenFreeKey(nickname)) ?? noCards;
182
+ };
183
+ const getCardsByGroup = (group) => {
184
+ if (!cardsByGroup) {
185
+ cardsByGroup = /* @__PURE__ */ new Map();
186
+ for (const card of cards) {
187
+ for (const cardGroup of [
188
+ ...card.types,
189
+ ...card.subtypes,
190
+ ...card.traits ?? []
191
+ ]) {
192
+ addToCardList(cardsByGroup, getHyphenFreeKey(cardGroup), card);
193
+ }
194
+ }
195
+ }
196
+ return cardsByGroup.get(getHyphenFreeKey(group)) ?? noCards;
197
+ };
198
+ const getCardsByInitials = (initials) => {
199
+ if (!cardsByInitials) {
200
+ cardsByInitials = /* @__PURE__ */ new Map();
201
+ for (const card of cards) {
202
+ for (const initialsOfName of getInitialsOfName(card.name)) {
203
+ addToCardList(cardsByInitials, initialsOfName, card);
204
+ }
205
+ }
206
+ }
207
+ return cardsByInitials.get(initials) ?? noCards;
208
+ };
135
209
  const getArtists = () => {
136
210
  if (!artists) {
137
211
  const creditedArtists = /* @__PURE__ */ new Set();
@@ -202,6 +276,9 @@ const getNewCatalogueIndex = (cards) => {
202
276
  getPitchCycle,
203
277
  getCardsByName: getCardsByName2,
204
278
  getCardsByExactName,
279
+ getCardsByNickname,
280
+ getCardsByGroup,
281
+ getCardsByInitials,
205
282
  getArtists,
206
283
  getOppositeSide,
207
284
  getReferences,
@@ -221,34 +298,8 @@ const getCatalogueIndex = (cards) => {
221
298
  }
222
299
  return catalogueIndex;
223
300
  };
224
- const getPitchCycleOfCard = (index, card) => index.getPitchCycle(card.cardIdentifier);
225
301
  const getCardsByName = (index, name) => index.getCardsByName(name);
226
- const getCardsWithPitchSiblings = (index, cards) => {
227
- const cardByCardIdentifier = /* @__PURE__ */ new Map();
228
- for (const card of cards) {
229
- for (const pitch of getPitchCycleOfCard(index, card)) {
230
- cardByCardIdentifier.set(pitch.cardIdentifier, pitch);
231
- }
232
- }
233
- return index.getCardsInCorpusOrder([...cardByCardIdentifier.values()]);
234
- };
235
- const getCardsReferencing = (index, card) => {
236
- const referencingCards = [];
237
- for (const pitch of getPitchCycleOfCard(index, card)) {
238
- referencingCards.push(...index.getReferencedBy(pitch.cardIdentifier));
239
- }
240
- return getCardsWithPitchSiblings(index, referencingCards);
241
- };
242
- const getCardsReferencedBy = (index, card) => {
243
- const referencedCards = [];
244
- for (const pitch of getPitchCycleOfCard(index, card)) {
245
- referencedCards.push(...index.getReferences(pitch.cardIdentifier));
246
- }
247
- return getCardsWithPitchSiblings(index, referencedCards);
248
- };
249
302
  export {
250
303
  getCardsByName,
251
- getCardsReferencedBy,
252
- getCardsReferencing,
253
304
  getCatalogueIndex
254
305
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flesh-and-blood/search",
3
3
  "description": "TypeScript search engine for Flesh and Blood cards",
4
- "version": "5.3.0",
4
+ "version": "5.4.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "main": "dist/index.cjs",
@@ -47,7 +47,7 @@
47
47
  "@flesh-and-blood/types": "^5.0.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@flesh-and-blood/cards": "^5.3.0",
50
+ "@flesh-and-blood/cards": "^5.4.0",
51
51
  "@flesh-and-blood/types": "^5.3.0",
52
52
  "@types/node": "^26.6.3",
53
53
  "esbuild": "^0.28.2",
@@ -75,5 +75,5 @@
75
75
  "FAB",
76
76
  "FABTCG"
77
77
  ],
78
- "gitHead": "8bbc088b56ea541fccbc3885bbf2edcd3ea9983f"
78
+ "gitHead": "b9859b97493dfa6b6d0990be6b19341ec54a315e"
79
79
  }
@@ -1,9 +0,0 @@
1
- import { Card } from "@flesh-and-blood/types";
2
- export interface Abbreviation {
3
- abbreviations: string[];
4
- card: string;
5
- isHidden?: boolean;
6
- }
7
- export declare const getAbbreviation: (abbreviation: string) => Abbreviation | undefined;
8
- export declare const getAbbreviationByCard: (c: Card) => Abbreviation | undefined;
9
- export declare const abbreviations: Abbreviation[];