@pulsar-edit/fuzzy-native 1.3.1 → 1.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.
package/README.md CHANGED
@@ -8,54 +8,7 @@ The scoring algorithm is heavily tuned for file paths, but should work for gener
8
8
 
9
9
  ## API
10
10
 
11
- ```ts
12
- export type MatcherOptions = {
13
- // Default: false
14
- caseSensitive?: boolean,
15
-
16
- // Default: infinite
17
- maxResults?: number,
18
-
19
- // Maximum gap to allow between consecutive letters in a match.
20
- // Provide a smaller maxGap to speed up query results.
21
- // Default: unlimited
22
- maxGap?: number;
23
-
24
- // Default: 1
25
- numThreads?: number,
26
-
27
- // Default: false
28
- recordMatchIndexes?: boolean,
29
-
30
- // can be either "fuzzaldrin" or anything else to use the file-based option
31
- algorithm?: string
32
- }
33
-
34
- export type MatchResult = {
35
- id: number,
36
- value: string,
37
-
38
- // A number in the range (0-1]. Higher scores are more relevant.
39
- // 0 denotes "no match" and will never be returned.
40
- score: number,
41
-
42
- // Matching character index in `value` for each character in `query`.
43
- // This can be costly, so this is only returned if `recordMatchIndexes` was set in `options`.
44
- matchIndexes?: Array<number>,
45
- }
46
-
47
- export class Matcher {
48
- constructor(candidates: Array<string>) {}
49
-
50
- // Returns all matching candidates (subject to `options`).
51
- // Will be ordered by score, descending.
52
- match: (query: string, options?: MatcherOptions) => Array<MatchResult>;
53
-
54
- addCandidates: (ids: Array<number>, candidates: Array<string>) => void;
55
- removeCandidates: (ids: Array<number>) => void;
56
- setCandidates: (ids: Array<number>, candidates: Array<string>) => void;
57
- }
58
- ```
11
+ Read `lib/main.d.ts` for the API of the `Matcher` class.
59
12
 
60
13
  See also the [spec](spec/fuzzy-native-spec.js) for basic usage.
61
14
 
package/binding.gyp CHANGED
@@ -2,21 +2,24 @@
2
2
  'targets': [
3
3
  {
4
4
  'target_name': 'fuzzy-native',
5
- 'include_dirs': [ '<!(node -e "require(\'nan\')")' ],
5
+ 'include_dirs': [
6
+ '<!@(node -p "require(\'node-addon-api\').include")'
7
+ ],
8
+ 'defines': ['NAPI_DISABLE_CPP_EXCEPTIONS'],
6
9
  'cflags': [
7
- '-std=c++17',
10
+ '-std=c++20',
8
11
  '-O3',
9
12
  ],
10
13
  'xcode_settings': {
11
14
  'OTHER_CPLUSPLUSFLAGS': [
12
- '-std=c++17',
15
+ '-std=c++20',
13
16
  '-O3',
14
17
  '-stdlib=libc++',
15
18
  ],
16
19
  'MACOSX_DEPLOYMENT_TARGET': '10.7',
17
20
  },
18
21
  'sources': [
19
- 'src/binding.cpp',
22
+ 'src/fuzzy-native.cpp',
20
23
  'src/fuzzaldrin_score.cpp',
21
24
  'src/score_match.cpp',
22
25
  'src/MatcherBase.cpp',
package/lib/main.d.ts ADDED
@@ -0,0 +1,119 @@
1
+
2
+ /**
3
+ * The options that can be passed to {@link Matcher#match}.
4
+ */
5
+ export type MatcherOptions = {
6
+ /** Whether matching is case-sensitive. Defaults to `false`. */
7
+ caseSensitive?: boolean;
8
+
9
+ /** How many results to return at the maximum. Defaults to no limit. */
10
+ maxResults?: number;
11
+
12
+ /**
13
+ * Maximum “gap” to allow between consecutive letters for a match candiate.
14
+ * Provide a smaller value to speed up query results. Defaults to no limit.
15
+ */
16
+ maxGap?: number;
17
+
18
+ /**
19
+ * How many threads to use while searching. Defaults to `1`.
20
+ */
21
+ numThreads?: number;
22
+
23
+ /**
24
+ * Whether to return metadata about the indices of the characters that
25
+ * matched in each returned max. Defaults to `false`.
26
+ */
27
+ recordMatchIndexes?: boolean;
28
+
29
+ /**
30
+ * The algorithm to use for fuzzy-matching. If `"fuzzaldrin"`, will use that
31
+ * algorithm for legacy support. Any other value, including the default of
32
+ * `undefined`, will trigger use of the default algorithm.
33
+ */
34
+ algorithm?: 'fuzzaldrin' | undefined;
35
+ };
36
+
37
+ /**
38
+ * A single result returned by {@link Matcher#match}.
39
+ */
40
+ export type MatchResult = {
41
+ /** A unique identifier for the match. */
42
+ id: number;
43
+
44
+ /** The string value of the match. */
45
+ value: string;
46
+
47
+ /**
48
+ * A number in the range (0, 1] — i.e., the maximum value is `1` and the
49
+ * minimum value is the smallest possible positive value. Higher scores mean
50
+ * more relevant matches. `0` means “no match” and will never be returned.
51
+ */
52
+ score: number;
53
+
54
+ /**
55
+ * Matching charcter index in `value` for each character in `query`. This can
56
+ * be costly, so this information is returned only when
57
+ * {@link MatcherOptions.recordMatchIndexes} is `true`.
58
+ */
59
+ matchIndexes?: number[];
60
+ }
61
+
62
+ export class Matcher {
63
+ /**
64
+ * Construct a new {@link Matcher} object.
65
+ *
66
+ * You may specify candidates at instantiation time (with the same arguments
67
+ * used by {@link addCandidates} and {@link setCandidates}) or you may wait
68
+ * and add candidates later.
69
+ *
70
+ * @param ids A list of numeric IDs. Must correspond to the candidates
71
+ * themselves.
72
+ * @param candidates A list of candidates against which we will be matching.
73
+ */
74
+ constructor();
75
+ constructor(ids: number[], candidates: string[]);
76
+
77
+ /**
78
+ * Find all candidates that match the given query.
79
+ *
80
+ * @param query The input against which candidates will be searched.
81
+ * @param options Any {@link MatcherOptions}.
82
+ */
83
+ match(query: string, options?: MatcherOptions): MatchResult[];
84
+
85
+ /**
86
+ * Add candidates to the list.
87
+ *
88
+ * You are responsible for ensuring that the IDs you use do not match the IDs
89
+ * of any candidates that are already present in the `Matcher`. Any
90
+ * candidates whose IDs already exist in the `Matcher` are silently ignored.
91
+ *
92
+ * @param ids A list of numeric IDs. Must correspond to the candidates
93
+ * themselves.
94
+ * @param candidates A list of candidates against which we will be matching.
95
+ */
96
+ addCandidates(ids: number[], candidates: string[]): void;
97
+
98
+ /**
99
+ * Remove candidates from the list.
100
+ *
101
+ * @param ids The unique identifiers for each of the candidates you want to
102
+ * remove. Must be an array; if you want to remove only one candidate, wrap
103
+ * the value in an array first.
104
+ */
105
+ removeCandidates(ids: number[]): void;
106
+
107
+ /**
108
+ * Set a complete list of candidates, removing any candidate that may already
109
+ * be defined.
110
+ *
111
+ * If you want to add candidates instead without removing any that may
112
+ * already exist, use {@link addCandidates}.
113
+ *
114
+ * @param ids A list of numeric IDs. Must correspond to the candidates
115
+ * themselves.
116
+ * @param candidates A list of candidates against which we will be matching.
117
+ */
118
+ setCandidates(ids: number[], candidates: string[]): void;
119
+ }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@pulsar-edit/fuzzy-native",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Native C++ implementation of a fuzzy string matcher.",
5
5
  "main": "lib/main.js",
6
+ "types": "lib/main.d.ts",
6
7
  "scripts": {
7
8
  "test": "jasmine-node --captureExceptions spec"
8
9
  },
@@ -21,7 +22,7 @@
21
22
  "repository": "https://github.com/pulsar-edit/fuzzy-native",
22
23
  "license": "MIT",
23
24
  "dependencies": {
24
- "nan": "^2.18.0"
25
+ "node-addon-api": "^8.8.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "jasmine-node": "^1.14.5",
@@ -1,6 +1,6 @@
1
1
  #include "MatcherBase.h"
2
- #include "score_match.h"
3
2
  #include "fuzzaldrin_score.h"
3
+ #include "score_match.h"
4
4
 
5
5
  #include <algorithm>
6
6
  #include <atomic>
@@ -34,7 +34,7 @@ inline uint64_t letter_bitmask(const std::string &str) {
34
34
 
35
35
  inline string str_to_lower(const std::string &s) {
36
36
  string lower(s);
37
- for (auto& c : lower) {
37
+ for (auto &c : lower) {
38
38
  if (c >= 'A' && c <= 'Z') {
39
39
  c += 'a' - 'A';
40
40
  }
@@ -42,9 +42,7 @@ inline string str_to_lower(const std::string &s) {
42
42
  return lower;
43
43
  }
44
44
 
45
- bool is_slash(char c) {
46
- return c == '/' || c == '\\';
47
- }
45
+ bool is_slash(char c) { return c == '/' || c == '\\'; }
48
46
 
49
47
  int num_dirs(const std::string &path) {
50
48
  int num = 0;
@@ -86,12 +84,8 @@ int score_based_root_path(const MatchOptions &options,
86
84
  }
87
85
 
88
86
  // Push a new entry on the heap while ensuring size <= max_results.
89
- void push_heap(ResultHeap &heap,
90
- float score,
91
- int score_based_root_path,
92
- uint32_t id,
93
- const std::string *value,
94
- size_t max_results) {
87
+ void push_heap(ResultHeap &heap, float score, int score_based_root_path,
88
+ uint32_t id, const std::string *value, size_t max_results) {
95
89
  MatchResult result(score, score_based_root_path, id, value);
96
90
  if (heap.size() < max_results || result < heap.top()) {
97
91
  heap.push(std::move(result));
@@ -101,26 +95,17 @@ void push_heap(ResultHeap &heap,
101
95
  }
102
96
  }
103
97
 
104
- vector<MatchResult> finalize(const string &query,
105
- const string &query_case,
98
+ vector<MatchResult> finalize(const string &query, const string &query_case,
106
99
  const MatchOptions &options,
107
- bool record_match_indexes,
108
- ResultHeap &&heap) {
100
+ bool record_match_indexes, ResultHeap &&heap) {
109
101
  vector<MatchResult> vec;
110
102
  while (heap.size()) {
111
103
  const MatchResult &result = heap.top();
112
104
  if (record_match_indexes) {
113
105
  result.matchIndexes.reset(new vector<int>(query.size()));
114
106
  string lower = str_to_lower(*result.value);
115
- score_match(
116
- result.value->c_str(),
117
- lower.c_str(),
118
- query.c_str(),
119
- query_case.c_str(),
120
- options,
121
- 0.0,
122
- result.matchIndexes.get()
123
- );
107
+ score_match(result.value->c_str(), lower.c_str(), query.c_str(),
108
+ query_case.c_str(), options, 0.0, result.matchIndexes.get());
124
109
  }
125
110
  vec.push_back(result);
126
111
  heap.pop();
@@ -129,18 +114,11 @@ vector<MatchResult> finalize(const string &query,
129
114
  return vec;
130
115
  }
131
116
 
132
- void thread_worker(
133
- const string &query,
134
- const string &query_case,
135
- const MatchOptions &options,
136
- bool use_last_match,
137
- std::atomic<float>* min_score,
138
- size_t max_results,
139
- vector<MatcherBase::CandidateData> &candidates,
140
- size_t start,
141
- size_t end,
142
- ResultHeap &result
143
- ) {
117
+ void thread_worker(const string &query, const string &query_case,
118
+ const MatchOptions &options, bool use_last_match,
119
+ std::atomic<float> *min_score, size_t max_results,
120
+ vector<MatcherBase::CandidateData> &candidates, size_t start,
121
+ size_t end, ResultHeap &result) {
144
122
  uint64_t bitmask = letter_bitmask(query_case.c_str());
145
123
  for (size_t i = start; i < end; i++) {
146
124
  auto &candidate = candidates[i];
@@ -149,30 +127,19 @@ void thread_worker(
149
127
  }
150
128
  if ((bitmask & candidate.bitmask) == bitmask) {
151
129
  float score;
152
- if(query == "") {
130
+ if (query == "") {
153
131
  score = 1;
154
- } else if(options.fuzzaldrin) {
132
+ } else if (options.fuzzaldrin) {
155
133
  score = fuzzaldrin_score(candidate.value, query);
156
134
  score = fuzzaldrin_basename_score(candidate.value, query, score);
157
135
  } else {
158
- score = score_match(
159
- candidate.value.c_str(),
160
- candidate.lowercase.c_str(),
161
- query.c_str(),
162
- query_case.c_str(),
163
- options,
164
- min_score->load()
165
- );
136
+ score = score_match(candidate.value.c_str(),
137
+ candidate.lowercase.c_str(), query.c_str(),
138
+ query_case.c_str(), options, min_score->load());
166
139
  }
167
140
  if (score > 0) {
168
- push_heap(
169
- result,
170
- score,
171
- score_based_root_path(options, candidate),
172
- candidate.id,
173
- &candidate.value,
174
- max_results
175
- );
141
+ push_heap(result, score, score_based_root_path(options, candidate),
142
+ candidate.id, &candidate.value, max_results);
176
143
  if (result.size() == max_results) {
177
144
  float current_max = result.top().score;
178
145
  float min_score_value = min_score->load();
@@ -232,8 +199,9 @@ vector<MatchResult> MatcherBase::findMatches(const std::string &query,
232
199
  ResultHeap combined;
233
200
  std::atomic<float> min_score(0);
234
201
  if (num_threads == 0 || candidates_.size() < 10000) {
235
- thread_worker(new_query, query_case, matchOptions, use_last_match, &min_score,
236
- max_results, candidates_, 0, candidates_.size(), combined);
202
+ thread_worker(new_query, query_case, matchOptions, use_last_match,
203
+ &min_score, max_results, candidates_, 0, candidates_.size(),
204
+ combined);
237
205
  } else {
238
206
  vector<ResultHeap> thread_results(num_threads);
239
207
  vector<thread> threads;
@@ -244,19 +212,10 @@ vector<MatchResult> MatcherBase::findMatches(const std::string &query,
244
212
  if (i < candidates_.size() % num_threads) {
245
213
  chunk_size++;
246
214
  }
247
- threads.emplace_back(
248
- thread_worker,
249
- ref(new_query),
250
- ref(query_case),
251
- ref(matchOptions),
252
- use_last_match,
253
- &min_score,
254
- max_results,
255
- ref(candidates_),
256
- cur_start,
257
- cur_start + chunk_size,
258
- ref(thread_results[i])
259
- );
215
+ threads.emplace_back(thread_worker, ref(new_query), ref(query_case),
216
+ ref(matchOptions), use_last_match, &min_score,
217
+ max_results, ref(candidates_), cur_start,
218
+ cur_start + chunk_size, ref(thread_results[i]));
260
219
  cur_start += chunk_size;
261
220
  }
262
221
 
@@ -264,26 +223,15 @@ vector<MatchResult> MatcherBase::findMatches(const std::string &query,
264
223
  threads[i].join();
265
224
  while (thread_results[i].size()) {
266
225
  auto &top = thread_results[i].top();
267
- push_heap(
268
- combined,
269
- top.score,
270
- top.score_based_root_path,
271
- top.id,
272
- top.value,
273
- max_results
274
- );
226
+ push_heap(combined, top.score, top.score_based_root_path, top.id,
227
+ top.value, max_results);
275
228
  thread_results[i].pop();
276
229
  }
277
230
  }
278
231
  }
279
232
 
280
- return finalize(
281
- new_query,
282
- query_case,
283
- matchOptions,
284
- options.record_match_indexes,
285
- move(combined)
286
- );
233
+ return finalize(new_query, query_case, matchOptions,
234
+ options.record_match_indexes, std::move(combined));
287
235
  }
288
236
 
289
237
  void MatcherBase::addCandidate(uint32_t id, const string &candidate) {
@@ -295,10 +243,10 @@ void MatcherBase::addCandidate(uint32_t id, const string &candidate) {
295
243
  data.id = id;
296
244
  data.value = candidate;
297
245
  data.bitmask = letter_bitmask(lowercase.c_str());
298
- data.lowercase = move(lowercase);
246
+ data.lowercase = std::move(lowercase);
299
247
  data.last_match = true;
300
248
  data.num_dirs = num_dirs(candidate);
301
- candidates_.emplace_back(move(data));
249
+ candidates_.emplace_back(std::move(data));
302
250
  }
303
251
  }
304
252
 
@@ -324,6 +272,4 @@ void MatcherBase::reserve(size_t n) {
324
272
  lookup_.reserve(n);
325
273
  }
326
274
 
327
- size_t MatcherBase::size() const {
328
- return candidates_.size();
329
- }
275
+ size_t MatcherBase::size() const { return candidates_.size(); }
@@ -0,0 +1,155 @@
1
+ #include "fuzzy-native.h"
2
+
3
+ #include <cmath>
4
+
5
+ using namespace Napi;
6
+
7
+ #define CHECK(env, cond, msg) \
8
+ if (!(cond)) \
9
+ { \
10
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException(); \
11
+ return; \
12
+ }
13
+
14
+ #define CHECK_OR_RETURN(env, cond, msg, retval) \
15
+ if (!(cond)) \
16
+ { \
17
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException(); \
18
+ return retval; \
19
+ }
20
+
21
+ Matcher::Matcher(const Napi::CallbackInfo &info) : ObjectWrap<Matcher>(info)
22
+ {
23
+ if (info.Length() > 0) {
24
+ AddCandidates(info);
25
+ }
26
+ }
27
+
28
+ Napi::Value Matcher::Match(const Napi::CallbackInfo &info)
29
+ {
30
+ Napi::Env env = info.Env();
31
+ CHECK_OR_RETURN(env, info.Length() >= 1, "Wrong Number of arguments", env.Null());
32
+ CHECK_OR_RETURN(env, info[0].IsString(), "First argument should be a query string", env.Null());
33
+ auto query = info[0].As<Napi::String>().Utf8Value();
34
+ MatcherOptions options;
35
+ if (info.Length() > 1) {
36
+ CHECK_OR_RETURN(env, info[1].IsObject(), "Second argument should be an options object", env.Null());
37
+ Napi::Object options_obj = info[1].As<Napi::Object>();
38
+
39
+ options.case_sensitive = options_obj.Get("caseSensitive").ToBoolean();
40
+ options.smart_case = options_obj.Get("smartCase").ToBoolean();
41
+ options.num_threads = options_obj.Get("numThreads").ToNumber().Uint32Value();
42
+ options.max_results = options_obj.Get("maxResults").ToNumber().Uint32Value();
43
+ options.max_gap = options_obj.Get("maxGap").ToNumber().Uint32Value();
44
+ options.record_match_indexes = options_obj.Get("recordMatchIndexes");
45
+ options.fuzzaldrin = options_obj.Get("algorithm").ToString().Utf8Value() == "fuzzaldrin";
46
+ options.root_path = options_obj.Get("rootPath").ToString();
47
+ }
48
+ auto idKey = Napi::String::New(env, "id");
49
+ auto valueKey = Napi::String::New(env, "value");
50
+ auto scoreKey = Napi::String::New(env, "score");
51
+ auto matchIndexesKey = Napi::String::New(env, "matchIndexes");
52
+
53
+ std::vector<MatchResult> matches = this->_impl.findMatches(query, options);
54
+
55
+ auto result = Napi::Array::New(env);
56
+ size_t result_count = 0;
57
+
58
+ for (const auto &match : matches) {
59
+ auto obj = Napi::Object::New(env);
60
+ obj.Set(idKey, Napi::Number::New(env, match.id));
61
+ obj.Set(scoreKey, Napi::Number::New(env, match.score));
62
+ obj.Set(valueKey, Napi::String::New(env, *match.value));
63
+
64
+ if (match.matchIndexes != nullptr) {
65
+ Napi::Array array = Napi::Array::New(env, match.matchIndexes->size());
66
+ for (size_t i = 0; i < array.Length(); i++) {
67
+ array.Set(i, Napi::Number::New(env, match.matchIndexes->at(i)));
68
+ }
69
+ obj.Set(matchIndexesKey, array);
70
+ }
71
+ result.Set(result_count++, obj);
72
+ }
73
+ return result;
74
+ }
75
+
76
+ void Matcher::AddCandidates(const Napi::CallbackInfo &info)
77
+ {
78
+ auto env = info.Env();
79
+ if (info.Length() > 0) {
80
+ CHECK(env, info[0].IsArray(), "Expected an array of unsigned 32-bit integer ids as the first argument");
81
+ CHECK(env, info[1].IsArray(), "Expected an array of strings as the second argument");
82
+
83
+ auto ids = info[0].As<Napi::Array>();
84
+ auto values = info[1].As<Napi::Array>();
85
+
86
+ CHECK(env, ids.Length() == values.Length(), "Expected ids array and values array to have the same length");
87
+
88
+ // Create a random permutation so that candidates are shuffled.
89
+ std::vector<size_t> indexes(ids.Length());
90
+ for (size_t i = 0; i < indexes.size(); i++)
91
+ {
92
+ indexes[i] = i;
93
+ if (i > 0)
94
+ {
95
+ std::swap(indexes[rand() % i], indexes[i]);
96
+ }
97
+ }
98
+ this->_impl.reserve(this->_impl.size() + ids.Length());
99
+ for (auto i : indexes) {
100
+ auto id_value = ids.Get(i);
101
+ double raw = id_value.As<Napi::Number>().DoubleValue();
102
+ CHECK(env, raw >= 0 && raw == std::floor(raw), "Expected first array to only contain unsigned 32-bit integer ids");
103
+ uint32_t id = id_value.As<Napi::Number>().Uint32Value();
104
+ auto value = values.Get(i).As<Napi::String>();
105
+ this->_impl.addCandidate(id, value.Utf8Value());
106
+ }
107
+ }
108
+ }
109
+
110
+ void Matcher::RemoveCandidates(const Napi::CallbackInfo &info)
111
+ {
112
+ auto env = info.Env();
113
+ if (info.Length() > 0) {
114
+ CHECK(env, info[0].IsArray(), "Expected an array of unsigned 32-bit integer ids");
115
+ auto ids = info[0].As<Napi::Array>();
116
+ for (size_t i = 0; i < ids.Length(); i++) {
117
+ auto id_value = ids.Get(i);
118
+ double raw = id_value.As<Napi::Number>().DoubleValue();
119
+ CHECK(env, raw >= 0 && raw == std::floor(raw), "Expected array to only contain unsigned 32-bit integer ids");
120
+ uint32_t id = id_value.As<Napi::Number>().Uint32Value();
121
+ this->_impl.removeCandidate(id);
122
+ }
123
+ }
124
+ }
125
+
126
+ void Matcher::SetCandidates(const Napi::CallbackInfo &info)
127
+ {
128
+ this->_impl.clear();
129
+ AddCandidates(info);
130
+ }
131
+
132
+ Napi::FunctionReference Matcher::constructor;
133
+
134
+ Napi::Function Matcher::GetClass(Napi::Env env) {
135
+ Napi::Function func = DefineClass(
136
+ env, "Matcher",
137
+ {InstanceMethod("match", &Matcher::Match),
138
+ InstanceMethod("addCandidates", &Matcher::AddCandidates),
139
+ InstanceMethod("removeCandidates", &Matcher::RemoveCandidates),
140
+ InstanceMethod("setCandidates", &Matcher::SetCandidates)});
141
+ constructor = Napi::Persistent(func);
142
+ constructor.SuppressDestruct();
143
+
144
+ return func;
145
+ }
146
+
147
+ Napi::Object Init(Napi::Env env, Napi::Object exports)
148
+ {
149
+ Napi::String matcher = Napi::String::New(env, "Matcher");
150
+ exports.Set(matcher, Matcher::GetClass(env));
151
+
152
+ return exports;
153
+ }
154
+
155
+ NODE_API_MODULE(addon, Init)
@@ -0,0 +1,20 @@
1
+ #pragma once
2
+
3
+ #include <napi.h>
4
+ #include "MatcherBase.h"
5
+
6
+ class Matcher : public Napi::ObjectWrap<Matcher>
7
+ {
8
+ private:
9
+ static Napi::FunctionReference constructor;
10
+ MatcherBase _impl;
11
+
12
+ public:
13
+ Matcher(const Napi::CallbackInfo &info);
14
+ Napi::Value Match(const Napi::CallbackInfo &info);
15
+ void AddCandidates(const Napi::CallbackInfo &info);
16
+ void RemoveCandidates(const Napi::CallbackInfo &info);
17
+ void SetCandidates(const Napi::CallbackInfo &info);
18
+
19
+ static Napi::Function GetClass(Napi::Env);
20
+ };
package/src/binding.cpp DELETED
@@ -1,233 +0,0 @@
1
- #include <nan.h>
2
- #include <vector>
3
- #include <unordered_map>
4
- #include <chrono>
5
-
6
- #include "MatcherBase.h"
7
-
8
- using namespace Nan;
9
-
10
- #define CHECK(cond, msg) \
11
- if (!(cond)) \
12
- { \
13
- ThrowTypeError(msg); \
14
- return; \
15
- }
16
-
17
- template <typename T>
18
- T get_property(const v8::Local<v8::Object> &object, const char *name)
19
- {
20
- auto prop = Nan::Get(object, Nan::New(name).ToLocalChecked());
21
- if (prop.IsEmpty())
22
- {
23
- return T();
24
- }
25
- Nan::Maybe<T> result = Nan::To<T>(prop.ToLocalChecked());
26
- if (result.IsNothing())
27
- {
28
- return T();
29
- }
30
- return result.FromJust();
31
- }
32
-
33
- /**
34
- * This saves one string copy over using v8::String::Utf8Value.
35
- */
36
- std::string to_std_string(const v8::Local<v8::String> &v8str)
37
- {
38
- v8::Isolate *isolate = v8::Isolate::GetCurrent();
39
- std::string str(v8str->Utf8Length(isolate), ' ');
40
- v8str->WriteUtf8(isolate, &str[0]);
41
- return str;
42
- }
43
-
44
- std::string get_string_property(const v8::Local<v8::Object> &object,
45
- const char *name)
46
- {
47
- auto prop =
48
- Nan::Get(object, Nan::New(name).ToLocalChecked());
49
- if (!prop.IsEmpty())
50
- {
51
- auto propLocal = prop.ToLocalChecked();
52
- if (propLocal->IsNull() || propLocal->IsUndefined())
53
- {
54
- return std::string("");
55
- }
56
- if (!propLocal->IsString())
57
- {
58
- std::string msg =
59
- std::string("property ") +
60
- name +
61
- std::string(" must be a string");
62
- ThrowTypeError(msg.c_str());
63
- }
64
-
65
- return to_std_string(Nan::To<v8::String>(propLocal).ToLocalChecked());
66
- }
67
- return std::string("");
68
- }
69
-
70
- Persistent<v8::Function> MatcherConstructor;
71
-
72
- class Matcher : public ObjectWrap
73
- {
74
- public:
75
- static void Init(v8::Local<v8::Object> exports)
76
- {
77
- // Prepare constructor template
78
- v8::Local<v8::FunctionTemplate> tpl =
79
- New<v8::FunctionTemplate>(Matcher::Create);
80
- tpl->SetClassName(New("Matcher").ToLocalChecked());
81
- tpl->InstanceTemplate()->SetInternalFieldCount(1);
82
-
83
- // Prototype
84
- SetPrototypeMethod(tpl, "match", Match);
85
- SetPrototypeMethod(tpl, "addCandidates", AddCandidates);
86
- SetPrototypeMethod(tpl, "removeCandidates", RemoveCandidates);
87
- SetPrototypeMethod(tpl, "setCandidates", SetCandidates);
88
-
89
- v8::Local<v8::Context> context = Nan::GetCurrentContext();
90
-
91
- MatcherConstructor.Reset(tpl->GetFunction(context).ToLocalChecked());
92
- Set(exports, Nan::New("Matcher").ToLocalChecked(), tpl->GetFunction(context).ToLocalChecked());
93
- }
94
-
95
- static void Create(const Nan::FunctionCallbackInfo<v8::Value> &info)
96
- {
97
- CHECK(info.IsConstructCall(), "Use 'new' to construct Matcher");
98
- auto obj = new Matcher();
99
- obj->Wrap(info.This());
100
- AddCandidates(info);
101
- info.GetReturnValue().Set(info.This());
102
- }
103
-
104
- static void Match(const FunctionCallbackInfo<v8::Value> &info)
105
- {
106
- if (info.Length() < 1)
107
- {
108
- Nan::ThrowTypeError("Wrong number of arguments");
109
- return;
110
- }
111
-
112
- CHECK(info[0]->IsString(), "First argument should be a query string");
113
- std::string query(to_std_string(Nan::To<v8::String>(info[0]).ToLocalChecked()));
114
-
115
- MatcherOptions options;
116
- if (info.Length() > 1)
117
- {
118
- CHECK(info[1]->IsObject(), "Second argument should be an options object");
119
- auto options_obj = Nan::To<v8::Object>(info[1]).ToLocalChecked();
120
- options.case_sensitive = get_property<bool>(options_obj, "caseSensitive");
121
- options.smart_case = get_property<bool>(options_obj, "smartCase");
122
- options.num_threads = get_property<int>(options_obj, "numThreads");
123
- options.max_results = get_property<int>(options_obj, "maxResults");
124
- options.max_gap = get_property<int>(options_obj, "maxGap");
125
- options.record_match_indexes =
126
- get_property<bool>(options_obj, "recordMatchIndexes");
127
- options.fuzzaldrin =
128
- get_string_property(options_obj, "algorithm") == "fuzzaldrin";
129
- options.root_path = get_string_property(options_obj, "rootPath");
130
- }
131
-
132
- auto idKey = New("id").ToLocalChecked();
133
- auto valueKey = New("value").ToLocalChecked();
134
- auto scoreKey = New("score").ToLocalChecked();
135
- auto matchIndexesKey = New("matchIndexes").ToLocalChecked();
136
-
137
- auto matcher = Unwrap<Matcher>(info.This());
138
- std::vector<MatchResult> matches =
139
- matcher->impl_.findMatches(query, options);
140
- auto result = New<v8::Array>();
141
- size_t result_count = 0;
142
- for (const auto &match : matches)
143
- {
144
- auto obj = New<v8::Object>();
145
- Set(obj, idKey, New<v8::Uint32>(match.id));
146
- Set(obj, scoreKey, New(match.score));
147
- Set(obj, valueKey, New(*match.value).ToLocalChecked());
148
-
149
- v8::Local<v8::Context> context = Nan::GetCurrentContext();
150
- if (match.matchIndexes != nullptr)
151
- {
152
- auto array = New<v8::Array>(match.matchIndexes->size());
153
- for (size_t i = 0; i < array->Length(); i++)
154
- {
155
- array->Set(context, i, New(match.matchIndexes->at(i)));
156
- }
157
- Set(obj, matchIndexesKey, array);
158
- }
159
- result->Set(context, result_count++, obj);
160
- }
161
- info.GetReturnValue().Set(result);
162
- }
163
-
164
- static void AddCandidates(const FunctionCallbackInfo<v8::Value> &info)
165
- {
166
- auto matcher = Unwrap<Matcher>(info.This());
167
- if (info.Length() > 0)
168
- {
169
- CHECK(info[0]->IsArray(), "Expected an array of unsigned 32-bit integer ids as the first argument");
170
- CHECK(info[1]->IsArray(), "Expected an array of strings as the second argument");
171
-
172
- auto ids = v8::Local<v8::Array>::Cast(info[0]);
173
- auto values = v8::Local<v8::Array>::Cast(info[1]);
174
-
175
- CHECK(ids->Length() == values->Length(), "Expected ids array and values array to have the same length");
176
-
177
- // Create a random permutation so that candidates are shuffled.
178
- std::vector<size_t> indexes(ids->Length());
179
- for (size_t i = 0; i < indexes.size(); i++)
180
- {
181
- indexes[i] = i;
182
- if (i > 0)
183
- {
184
- std::swap(indexes[rand() % i], indexes[i]);
185
- }
186
- }
187
- matcher->impl_.reserve(matcher->impl_.size() + ids->Length());
188
-
189
- v8::Local<v8::Context> context = Nan::GetCurrentContext();
190
- for (auto i : indexes)
191
- {
192
- auto id_value = ids->Get(context, i).ToLocalChecked();
193
- CHECK(id_value->IsUint32(), "Expected first array to only contain unsigned 32-bit integer ids");
194
- auto id = v8::Local<v8::Uint32>::Cast(id_value)->Value();
195
- auto value = to_std_string(Nan::To<v8::String>(values->Get(context, i).ToLocalChecked()).ToLocalChecked());
196
- matcher->impl_.addCandidate(id, value);
197
- }
198
- }
199
- }
200
-
201
- static void RemoveCandidates(const FunctionCallbackInfo<v8::Value> &info)
202
- {
203
- auto matcher = Unwrap<Matcher>(info.This());
204
- if (info.Length() > 0)
205
- {
206
- CHECK(info[0]->IsArray(), "Expected an array of unsigned 32-bit integer ids");
207
- auto ids = v8::Local<v8::Array>::Cast(info[0]);
208
-
209
- v8::Local<v8::Context> context = Nan::GetCurrentContext();
210
- for (size_t i = 0; i < ids->Length(); i++)
211
- {
212
- auto id_value = ids->Get(context, i).ToLocalChecked();
213
- CHECK(id_value->IsUint32(), "Expected array to only contain unsigned 32-bit integer ids");
214
- auto id = v8::Local<v8::Uint32>::Cast(id_value)->Value();
215
- matcher->impl_.removeCandidate(id);
216
- }
217
- }
218
- }
219
-
220
- static void SetCandidates(const FunctionCallbackInfo<v8::Value> &info)
221
- {
222
- auto matcher = Unwrap<Matcher>(info.This());
223
- matcher->impl_.clear();
224
- AddCandidates(info);
225
- }
226
-
227
- private:
228
- MatcherBase impl_;
229
- };
230
-
231
- void Init(v8::Local<v8::Object> exports) { Matcher::Init(exports); }
232
-
233
- NAN_MODULE_WORKER_ENABLED(addon, Init)