tdk-api-wrapper 1.3.0 → 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/dist/cli.js CHANGED
@@ -50,6 +50,503 @@ var TDKNetworkError = class extends TDKError {
50
50
  }
51
51
  };
52
52
 
53
+ // src/morphology.ts
54
+ var TURKISH_VOWELS = "ae\u0131io\xF6u\xFC";
55
+ function isVowel(ch) {
56
+ return TURKISH_VOWELS.includes(ch);
57
+ }
58
+ var TURKISH_SUFFIXES = [
59
+ // 9-letter composite suffixes
60
+ "lerimizden",
61
+ "lar\u0131m\u0131zdan",
62
+ "lerinizden",
63
+ "lar\u0131n\u0131zdan",
64
+ // 8-letter composite suffixes
65
+ "lerinin",
66
+ "lar\u0131n\u0131n",
67
+ "lerinde",
68
+ "lar\u0131nda",
69
+ "lerinden",
70
+ "lar\u0131ndan",
71
+ "leriyle",
72
+ "lar\u0131yla",
73
+ "lerini",
74
+ "lar\u0131n\u0131",
75
+ "lerimize",
76
+ "lar\u0131m\u0131za",
77
+ "lerimizle",
78
+ "lar\u0131m\u0131zla",
79
+ "lerinizin",
80
+ "lar\u0131n\u0131z\u0131n",
81
+ "lerinizde",
82
+ "lar\u0131n\u0131zda",
83
+ "d\u0131\u011F\u0131ndan",
84
+ "di\u011Finden",
85
+ "du\u011Fundan",
86
+ "d\xFC\u011F\xFCnden",
87
+ "t\u0131\u011F\u0131ndan",
88
+ "ti\u011Finden",
89
+ "tu\u011Fundan",
90
+ "t\xFC\u011F\xFCnden",
91
+ // 7-letter composite suffixes
92
+ "ecektir",
93
+ "acakt\u0131r",
94
+ "ece\u011Fim",
95
+ "aca\u011F\u0131m",
96
+ "eceksin",
97
+ "acaks\u0131n",
98
+ "ece\u011Fiz",
99
+ "aca\u011F\u0131z",
100
+ "lerimiz",
101
+ "lar\u0131m\u0131z",
102
+ "leriniz",
103
+ "lar\u0131n\u0131z",
104
+ "umuzdan",
105
+ "\xFCm\xFCzden",
106
+ "inizden",
107
+ "\u0131n\u0131zdan",
108
+ "\xFCn\xFCzden",
109
+ "d\u0131\u011F\u0131nda",
110
+ "di\u011Finde",
111
+ "du\u011Funda",
112
+ "d\xFC\u011F\xFCnde",
113
+ "t\u0131\u011F\u0131nda",
114
+ "ti\u011Finde",
115
+ "tu\u011Funda",
116
+ "t\xFC\u011F\xFCnde",
117
+ "mas\u0131na",
118
+ "mesine",
119
+ "\u0131yorsunuz",
120
+ "iyorsunuz",
121
+ "uyorsunuz",
122
+ "\xFCyorsunuz",
123
+ "yorsunuz",
124
+ // 6-letter composite suffixes
125
+ "iyorsa",
126
+ "iyorduk",
127
+ "iyordu",
128
+ "iyormu\u015F",
129
+ "\u0131yorsa",
130
+ "\u0131yorduk",
131
+ "\u0131yordu",
132
+ "\u0131yormu\u015F",
133
+ "uyorsa",
134
+ "uyorduk",
135
+ "uyordu",
136
+ "uyormu\u015F",
137
+ "\xFCyorsa",
138
+ "\xFCyorduk",
139
+ "\xFCyordu",
140
+ "\xFCyormu\u015F",
141
+ "\u0131yorsun",
142
+ "iyorsun",
143
+ "uyorsun",
144
+ "\xFCyorsun",
145
+ "\u0131yorlar",
146
+ "iyorlar",
147
+ "uyorlar",
148
+ "\xFCyorlar",
149
+ "iyoruz",
150
+ "\u0131yoruz",
151
+ "uyoruz",
152
+ "\xFCyoruz",
153
+ "imizin",
154
+ "\u0131m\u0131z\u0131n",
155
+ "umuzun",
156
+ "\xFCm\xFCz\xFCn",
157
+ "imizde",
158
+ "\u0131m\u0131zda",
159
+ "umuzda",
160
+ "\xFCm\xFCzde",
161
+ "imizden",
162
+ "\u0131m\u0131zdan",
163
+ "imizle",
164
+ "\u0131m\u0131zla",
165
+ "umuzla",
166
+ "\xFCm\xFCzle",
167
+ "lerdir",
168
+ "lard\u0131r",
169
+ "mu\u015Ftur",
170
+ "mi\u015Ftir",
171
+ "mu\u015Ftur",
172
+ "m\xFC\u015Ft\xFCr",
173
+ "lerden",
174
+ "lardan",
175
+ "lerine",
176
+ "lar\u0131na",
177
+ "leriyle",
178
+ "lar\u0131yla",
179
+ "seniz",
180
+ "san\u0131z",
181
+ "diniz",
182
+ "d\u0131n\u0131z",
183
+ "dunuz",
184
+ "d\xFCn\xFCz",
185
+ "tiniz",
186
+ "t\u0131n\u0131z",
187
+ "tunuz",
188
+ "t\xFCn\xFCz",
189
+ "siniz",
190
+ "s\u0131n\u0131z",
191
+ "sunuz",
192
+ "s\xFCn\xFCz",
193
+ "d\u0131k\xE7a",
194
+ "dik\xE7e",
195
+ "duk\xE7a",
196
+ "d\xFCk\xE7e",
197
+ "t\u0131k\xE7a",
198
+ "tik\xE7e",
199
+ "tuk\xE7a",
200
+ "t\xFCk\xE7e",
201
+ "\u0131rken",
202
+ "irken",
203
+ "urken",
204
+ "\xFCrken",
205
+ "arken",
206
+ "erken",
207
+ // 5-letter suffixes
208
+ "lerde",
209
+ "larda",
210
+ "lerle",
211
+ "larla",
212
+ "lerin",
213
+ "lar\u0131n",
214
+ "lerim",
215
+ "lar\u0131m",
216
+ "dirler",
217
+ "d\u0131rlar",
218
+ "d\xFCrler",
219
+ "durlar",
220
+ "tirler",
221
+ "t\u0131rlar",
222
+ "t\xFCrler",
223
+ "turlar",
224
+ "siniz",
225
+ "s\u0131n\u0131z",
226
+ "sunuz",
227
+ "s\xFCn\xFCz",
228
+ "yorum",
229
+ "yorsun",
230
+ "uyoruz",
231
+ "yorsunuz",
232
+ "yorlar",
233
+ "eceks",
234
+ "acaks",
235
+ "eyim",
236
+ "ay\u0131m",
237
+ "indik",
238
+ "\u0131nd\u0131k",
239
+ "unduk",
240
+ "\xFCnd\xFCk",
241
+ "ildik",
242
+ "\u0131ld\u0131k",
243
+ "ulduk",
244
+ "\xFCld\xFCk",
245
+ "meden",
246
+ "madan",
247
+ "y\u0131n\u0131z",
248
+ "yiniz",
249
+ "yunuz",
250
+ "y\xFCn\xFCz",
251
+ // 4-letter suffixes
252
+ "imiz",
253
+ "\u0131m\u0131z",
254
+ "umuz",
255
+ "\xFCm\xFCz",
256
+ "iniz",
257
+ "\u0131n\u0131z",
258
+ "unuz",
259
+ "\xFCn\xFCz",
260
+ "leri",
261
+ "lar\u0131",
262
+ "idir",
263
+ "\u0131d\u0131r",
264
+ "udur",
265
+ "\xFCd\xFCr",
266
+ "ecek",
267
+ "acak",
268
+ "erek",
269
+ "arak",
270
+ "ince",
271
+ "\u0131nca",
272
+ "unca",
273
+ "\xFCnce",
274
+ "ken",
275
+ "meli",
276
+ "mal\u0131",
277
+ "iyor",
278
+ "\u0131yor",
279
+ "uyor",
280
+ "\xFCyor",
281
+ "mi\u015Fti",
282
+ "m\u0131\u015Ft\u0131",
283
+ "mu\u015Ftu",
284
+ "m\xFC\u015Ft\xFC",
285
+ "seydi",
286
+ "sayd\u0131",
287
+ "ydim",
288
+ "yd\u0131m",
289
+ "ydum",
290
+ "yd\xFCm",
291
+ "tiler",
292
+ "t\u0131lar",
293
+ "diler",
294
+ "d\u0131lar",
295
+ "ikten",
296
+ "\u0131ktan",
297
+ "uktan",
298
+ "\xFCkten",
299
+ // 3-letter suffixes
300
+ "ler",
301
+ "lar",
302
+ "den",
303
+ "dan",
304
+ "ten",
305
+ "tan",
306
+ "dir",
307
+ "d\u0131r",
308
+ "dur",
309
+ "d\xFCr",
310
+ "tir",
311
+ "t\u0131r",
312
+ "tur",
313
+ "t\xFCr",
314
+ "nin",
315
+ "n\u0131n",
316
+ "nun",
317
+ "n\xFCn",
318
+ "yle",
319
+ "yla",
320
+ "mi\u015F",
321
+ "m\u0131\u015F",
322
+ "mu\u015F",
323
+ "m\xFC\u015F",
324
+ "dim",
325
+ "d\u0131m",
326
+ "dum",
327
+ "d\xFCm",
328
+ "tim",
329
+ "t\u0131m",
330
+ "tum",
331
+ "t\xFCm",
332
+ "din",
333
+ "d\u0131n",
334
+ "dun",
335
+ "d\xFCn",
336
+ "tin",
337
+ "t\u0131n",
338
+ "tun",
339
+ "t\xFCn",
340
+ "dik",
341
+ "d\u0131k",
342
+ "duk",
343
+ "d\xFCk",
344
+ "tik",
345
+ "t\u0131k",
346
+ "tuk",
347
+ "t\xFCk",
348
+ "ydi",
349
+ "yd\u0131",
350
+ "ydu",
351
+ "yd\xFC",
352
+ "yim",
353
+ "y\u0131m",
354
+ "yum",
355
+ "y\xFCm",
356
+ "sin",
357
+ "s\u0131n",
358
+ "sun",
359
+ "s\xFCn",
360
+ "siz",
361
+ "s\u0131z",
362
+ "suz",
363
+ "s\xFCz",
364
+ "lik",
365
+ "l\u0131k",
366
+ "luk",
367
+ "l\xFCk",
368
+ "ici",
369
+ "\u0131c\u0131",
370
+ "ucu",
371
+ "\xFCc\xFC",
372
+ "gen",
373
+ "gan",
374
+ "ken",
375
+ "kan",
376
+ "len",
377
+ "lan",
378
+ "le\u015F",
379
+ "la\u015F",
380
+ "mek",
381
+ "mak",
382
+ "yor",
383
+ // 2-letter suffixes
384
+ "de",
385
+ "da",
386
+ "te",
387
+ "ta",
388
+ "im",
389
+ "\u0131m",
390
+ "um",
391
+ "\xFCm",
392
+ "in",
393
+ "\u0131n",
394
+ "un",
395
+ "\xFCn",
396
+ "iz",
397
+ "\u0131z",
398
+ "uz",
399
+ "\xFCz",
400
+ "si",
401
+ "s\u0131",
402
+ "su",
403
+ "s\xFC",
404
+ "ye",
405
+ "ya",
406
+ "le",
407
+ "la",
408
+ "di",
409
+ "d\u0131",
410
+ "du",
411
+ "d\xFC",
412
+ "ti",
413
+ "t\u0131",
414
+ "tu",
415
+ "t\xFC",
416
+ "se",
417
+ "sa",
418
+ "ce",
419
+ "ca",
420
+ "\xE7e",
421
+ "\xE7a",
422
+ "me",
423
+ "ma",
424
+ "ip",
425
+ "\u0131p",
426
+ "up",
427
+ "\xFCp",
428
+ "en",
429
+ "an",
430
+ "i\u015F",
431
+ "\u0131\u015F",
432
+ "u\u015F",
433
+ "\xFC\u015F",
434
+ "li",
435
+ "l\u0131",
436
+ "lu",
437
+ "l\xFC",
438
+ "ci",
439
+ "c\u0131",
440
+ "cu",
441
+ "c\xFC",
442
+ "\xE7i",
443
+ "\xE7\u0131",
444
+ "\xE7u",
445
+ "\xE7\xFC",
446
+ // 1-letter suffixes (vowels / basic case endings)
447
+ "e",
448
+ "a",
449
+ "i",
450
+ "\u0131",
451
+ "u",
452
+ "\xFC"
453
+ ];
454
+ function restoreConsonantSoftening(stem) {
455
+ if (stem.length < 2)
456
+ return [];
457
+ const last = stem.slice(-1);
458
+ const base = stem.slice(0, -1);
459
+ switch (last) {
460
+ case "b":
461
+ return [base + "p"];
462
+ case "c":
463
+ return [base + "\xE7"];
464
+ case "d":
465
+ return [base + "t"];
466
+ case "\u011F":
467
+ return [base + "k"];
468
+ case "g":
469
+ return [base + "k"];
470
+ default:
471
+ return [];
472
+ }
473
+ }
474
+ function restoreVowelDrop(stem) {
475
+ if (stem.length < 3)
476
+ return [];
477
+ const c1 = stem[stem.length - 2];
478
+ const c2 = stem[stem.length - 1];
479
+ if (!isVowel(c1) && !isVowel(c2)) {
480
+ const vowelsInBase = stem.slice(0, -2).split("").filter(isVowel);
481
+ if (vowelsInBase.length > 0) {
482
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
483
+ let inserted = "i";
484
+ if ("a\u0131".includes(lastVowel))
485
+ inserted = "\u0131";
486
+ else if ("ei".includes(lastVowel))
487
+ inserted = "i";
488
+ else if ("ou".includes(lastVowel))
489
+ inserted = "u";
490
+ else if ("\xF6\xFC".includes(lastVowel))
491
+ inserted = "\xFC";
492
+ return [stem.slice(0, -1) + inserted + c2];
493
+ }
494
+ }
495
+ return [];
496
+ }
497
+ function restoreInfinitive(stem) {
498
+ if (stem.length < 2)
499
+ return [];
500
+ const vowelsInBase = stem.split("").filter(isVowel);
501
+ if (vowelsInBase.length === 0)
502
+ return [];
503
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
504
+ return "a\u0131ou".includes(lastVowel) ? [stem + "mak"] : [stem + "mek"];
505
+ }
506
+ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
507
+ if (!word2 || word2.trim().length === 0)
508
+ return [];
509
+ const raw = word2.trim();
510
+ const normalized = raw.toLocaleLowerCase("tr-TR");
511
+ const candidatesWithWeight = [];
512
+ const seen = /* @__PURE__ */ new Set();
513
+ if (raw.includes("'") || raw.includes("\u2019")) {
514
+ const apostropheStem = normalized.split(/['’]/)[0];
515
+ if (apostropheStem.length >= minStemLength) {
516
+ candidatesWithWeight.push({ candidate: apostropheStem, baseLength: apostropheStem.length + 10 });
517
+ seen.add(apostropheStem);
518
+ }
519
+ }
520
+ let frontier = [normalized];
521
+ for (let depth = 0; depth < maxDepth; depth++) {
522
+ const nextFrontier = [];
523
+ for (const current of frontier) {
524
+ for (const suffix of TURKISH_SUFFIXES) {
525
+ if (current.length - suffix.length >= minStemLength && current.endsWith(suffix)) {
526
+ const stem = current.slice(0, -suffix.length);
527
+ const hardened = restoreConsonantSoftening(stem);
528
+ const vowelDropped = restoreVowelDrop(stem);
529
+ const verbalBases = [stem, ...hardened];
530
+ const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
531
+ const variants = [stem, ...hardened, ...vowelDropped, ...infinitives];
532
+ for (const variant of variants) {
533
+ if (!seen.has(variant) && variant !== normalized) {
534
+ seen.add(variant);
535
+ nextFrontier.push(variant);
536
+ candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
537
+ }
538
+ }
539
+ }
540
+ }
541
+ }
542
+ if (nextFrontier.length === 0)
543
+ break;
544
+ frontier = nextFrontier;
545
+ }
546
+ candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
547
+ return [...new Set(candidatesWithWeight.map((c) => c.candidate))];
548
+ }
549
+
53
550
  // src/tdk.ts
54
551
  var fs = __toESM(require("fs"));
55
552
  var path = __toESM(require("path"));
@@ -141,6 +638,8 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
141
638
  static wordCache = /* @__PURE__ */ new Map();
142
639
  static dailyContentCache = null;
143
640
  static autocompleteCache = [];
641
+ static autocompleteSet = /* @__PURE__ */ new Set();
642
+ static stemCache = /* @__PURE__ */ new Map();
144
643
  /**
145
644
  * Enables or disables in-memory caching for API requests.
146
645
  */
@@ -157,6 +656,8 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
157
656
  this.wordCache.clear();
158
657
  this.dailyContentCache = null;
159
658
  this.autocompleteCache = [];
659
+ this.autocompleteSet.clear();
660
+ this.stemCache.clear();
160
661
  }
161
662
  static delay(ms) {
162
663
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -263,6 +764,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
263
764
  return [];
264
765
  }
265
766
  }
767
+ /**
768
+ * Ensures TDK's ~81k headword list is loaded in memory for fast O(1) set operations.
769
+ */
770
+ static async ensureAutocompleteLoaded() {
771
+ if (this.autocompleteCache.length === 0) {
772
+ this.autocompleteCache = await this.fetchAutocompleteData();
773
+ this.autocompleteSet = new Set(
774
+ this.autocompleteCache.map((w) => w.toLocaleLowerCase("tr-TR"))
775
+ );
776
+ }
777
+ }
266
778
  /**
267
779
  * Returns autocomplete suggestions for a given prefix, searched over TDK's
268
780
  * full headword list (see `fetchAutocompleteData`). The list is fetched
@@ -272,12 +784,81 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
272
784
  static async getSuggestions(prefix) {
273
785
  if (!prefix || prefix.trim() === "")
274
786
  return [];
275
- if (this.autocompleteCache.length === 0) {
276
- this.autocompleteCache = await this.fetchAutocompleteData();
277
- }
787
+ await this.ensureAutocompleteLoaded();
278
788
  const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
279
789
  return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
280
790
  }
791
+ /**
792
+ * Checks whether a word exists as a known headword in TDK dictionary.
793
+ * Checks in-memory autocompleteSet (81k headwords) if loaded, or queries TDK API.
794
+ */
795
+ static async isHeadword(word2) {
796
+ if (!word2 || word2.trim() === "")
797
+ return false;
798
+ const clean = word2.trim().toLocaleLowerCase("tr-TR");
799
+ await this.ensureAutocompleteLoaded();
800
+ if (this.autocompleteSet.size > 0) {
801
+ return this.autocompleteSet.has(clean);
802
+ }
803
+ try {
804
+ const results = await this.getWord(clean);
805
+ return results.length > 0;
806
+ } catch {
807
+ return false;
808
+ }
809
+ }
810
+ /**
811
+ * Generates candidate roots for a given Turkish word using progressive BFS suffix stripping,
812
+ * consonant mutation restoration, and vowel drop restoration.
813
+ */
814
+ static getStemCandidates(word2) {
815
+ return getStemCandidates(word2);
816
+ }
817
+ /**
818
+ * Finds the dictionary root (headword) of a word by checking direct existence
819
+ * and evaluating candidate stems generated by morphological analysis.
820
+ * Returns the root headword string if found, or null if no match in TDK.
821
+ */
822
+ static async findRoot(word2) {
823
+ if (!word2 || word2.trim() === "")
824
+ return null;
825
+ const clean = word2.trim().toLocaleLowerCase("tr-TR");
826
+ if (this.stemCache.has(clean)) {
827
+ return this.stemCache.get(clean);
828
+ }
829
+ if (await this.isHeadword(clean)) {
830
+ this.stemCache.set(clean, clean);
831
+ return clean;
832
+ }
833
+ const candidates = getStemCandidates(clean);
834
+ for (const candidate of candidates) {
835
+ if (await this.isHeadword(candidate)) {
836
+ this.stemCache.set(clean, candidate);
837
+ return candidate;
838
+ }
839
+ }
840
+ this.stemCache.set(clean, null);
841
+ return null;
842
+ }
843
+ /**
844
+ * Performs morphological stemming on a Turkish word.
845
+ * Returns a StemResult containing the original word, resolved root, and whether it is inflected.
846
+ */
847
+ static async stem(word2) {
848
+ if (!word2 || word2.trim() === "")
849
+ return null;
850
+ const clean = word2.trim().toLocaleLowerCase("tr-TR");
851
+ const root = await this.findRoot(word2);
852
+ if (!root) {
853
+ return null;
854
+ }
855
+ return {
856
+ word: word2,
857
+ root,
858
+ isInflected: root !== clean,
859
+ candidates: getStemCandidates(word2)
860
+ };
861
+ }
281
862
  /**
282
863
  * Returns a list of proverbs and idioms containing the word.
283
864
  */
@@ -494,6 +1075,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
494
1075
  return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
495
1076
  }
496
1077
  }
1078
+ const root = await this.findRoot(word2);
1079
+ if (root) {
1080
+ const cleanWord2 = word2.trim().toLocaleLowerCase("tr-TR");
1081
+ const isInflected = root !== cleanWord2;
1082
+ return {
1083
+ isCorrect: true,
1084
+ word: word2,
1085
+ isInflected,
1086
+ root
1087
+ };
1088
+ }
497
1089
  if (this.autocompleteCache.length === 0) {
498
1090
  this.autocompleteCache = await this.fetchAutocompleteData();
499
1091
  }
@@ -686,14 +1278,46 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
686
1278
  req.end();
687
1279
  });
688
1280
  }
1281
+ /**
1282
+ * Kubbealtı indexes headwords with full classical Turkish orthography,
1283
+ * including letters that a plain-ASCII-ish query tends to drop — most
1284
+ * commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
1285
+ * Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
1286
+ * entirely (verified: even "ruzgâr" alone still misses — it's the missing
1287
+ * ü, not the missing â, that actually breaks the match). This generates
1288
+ * single-letter-substitution variants to retry, one substitution per
1289
+ * variant (not combinatorial) — covers the overwhelmingly common case of
1290
+ * one "de-Turkished" letter without an explosion of API calls for words
1291
+ * with several.
1292
+ */
1293
+ static TURKISH_DEASCII_MAP = {
1294
+ a: ["\xE2"],
1295
+ i: ["\u0131", "\xEE"],
1296
+ o: ["\xF6"],
1297
+ u: ["\xFC", "\xFB"],
1298
+ c: ["\xE7"],
1299
+ g: ["\u011F"],
1300
+ s: ["\u015F"]
1301
+ };
1302
+ static generateTurkishVariants(word2) {
1303
+ const lower = word2.trim().toLocaleLowerCase("tr-TR");
1304
+ const variants = [];
1305
+ for (let i = 0; i < lower.length; i++) {
1306
+ for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
1307
+ variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
1308
+ }
1309
+ }
1310
+ return variants;
1311
+ }
689
1312
  /**
690
1313
  * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
691
1314
  * word, scraped from the site's own data API — undocumented, and Kubbealtı
692
1315
  * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
693
1316
  * openly-published data, so use this in line with their terms. `anlam` is
694
1317
  * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
695
- * plain text. Returns `null` on any fetch/parse failure, `[]` if the word
696
- * isn't found.
1318
+ * plain text. Falls back to `generateTurkishVariants()` if the exact query
1319
+ * comes up empty (see its doc comment). Returns `null` on any fetch/parse
1320
+ * failure, `[]` if no variant matches either.
697
1321
  */
698
1322
  static async getKubbealti(word2) {
699
1323
  if (!word2 || word2.trim() === "")
@@ -701,7 +1325,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
701
1325
  const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word2.trim())}/`);
702
1326
  if (!data || !Array.isArray(data.content))
703
1327
  return null;
704
- return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
1328
+ if (data.content.length > 0) {
1329
+ return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
1330
+ }
1331
+ for (const variant of this.generateTurkishVariants(word2)) {
1332
+ const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
1333
+ if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
1334
+ return variantData.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
1335
+ }
1336
+ }
1337
+ return [];
705
1338
  }
706
1339
  /**
707
1340
  * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
@@ -755,21 +1388,10 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
755
1388
  return null;
756
1389
  }
757
1390
  }
758
- /**
759
- * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
760
- * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
761
- * scraping involved, this is a stable, documented public API. `sections`
762
- * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
763
- * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
764
- * unsplit text. Returns `null` if the page doesn't exist or the request
765
- * fails.
766
- */
767
- static async getWiktionary(word2) {
768
- if (!word2 || word2.trim() === "")
769
- return null;
1391
+ static async fetchWiktionaryEntry(title) {
770
1392
  try {
771
1393
  const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
772
- word2.trim()
1394
+ title
773
1395
  )}&format=json&explaintext=1&formatversion=2`;
774
1396
  const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
775
1397
  if (!response.ok)
@@ -782,16 +1404,42 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
782
1404
  const sections = {};
783
1405
  const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
784
1406
  for (let i = 1; i < parts.length; i += 3) {
785
- const title = parts[i + 1]?.trim();
1407
+ const title2 = parts[i + 1]?.trim();
786
1408
  const content = parts[i + 2]?.trim();
787
- if (title)
788
- sections[title] = content ?? "";
1409
+ if (title2)
1410
+ sections[title2] = content ?? "";
789
1411
  }
790
1412
  return { raw, sections };
791
1413
  } catch {
792
1414
  return null;
793
1415
  }
794
1416
  }
1417
+ /**
1418
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
1419
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
1420
+ * scraping involved, this is a stable, documented public API. `sections`
1421
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
1422
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
1423
+ * unsplit text. This wiki has title capitalization turned off
1424
+ * ($wgCapitalLinks=false — common for Wiktionaries, since case is
1425
+ * meaningful for a dictionary: "Türkiye" the country vs. a lowercase
1426
+ * common word), so an exact-case miss retries with the first letter
1427
+ * uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
1428
+ * "Istanbul") before giving up. Returns `null` if neither is found or the
1429
+ * request fails.
1430
+ */
1431
+ static async getWiktionary(word2) {
1432
+ if (!word2 || word2.trim() === "")
1433
+ return null;
1434
+ const trimmed = word2.trim();
1435
+ const direct = await this.fetchWiktionaryEntry(trimmed);
1436
+ if (direct)
1437
+ return direct;
1438
+ const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
1439
+ if (capitalized === trimmed)
1440
+ return null;
1441
+ return this.fetchWiktionaryEntry(capitalized);
1442
+ }
795
1443
  /**
796
1444
  * Convenience filter over `getWiktionary()`: returns just one section's
797
1445
  * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
@@ -934,13 +1582,28 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
934
1582
  const unique = [...new Set(words)];
935
1583
  const analyses = [];
936
1584
  for (const word2 of unique) {
937
- const results = await this.getWord(word2);
938
- const found = results.length > 0;
1585
+ let results = await this.getWord(word2);
1586
+ let found = results.length > 0;
1587
+ let root;
1588
+ let isInflected;
1589
+ if (!found) {
1590
+ const resolvedRoot = await this.findRoot(word2);
1591
+ if (resolvedRoot) {
1592
+ results = await this.getWord(resolvedRoot);
1593
+ if (results.length > 0) {
1594
+ found = true;
1595
+ root = resolvedRoot;
1596
+ isInflected = true;
1597
+ }
1598
+ }
1599
+ }
939
1600
  analyses.push({
940
1601
  word: word2,
941
1602
  found,
942
1603
  meaning: found ? this.firstMeaning(results) : null,
943
- origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null
1604
+ origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null,
1605
+ root,
1606
+ isInflected
944
1607
  });
945
1608
  await this.delay(200);
946
1609
  }
@@ -1046,6 +1709,9 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1046
1709
  "hece",
1047
1710
  "uyum",
1048
1711
  "yazim",
1712
+ "kok",
1713
+ "stem",
1714
+ "deyim",
1049
1715
  "gunun",
1050
1716
  "rastgele",
1051
1717
  "esanlam",
@@ -1084,7 +1750,7 @@ async function run() {
1084
1750
  if (!command || command === "--help" || command === "-h") {
1085
1751
  console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
1086
1752
  console.log(
1087
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, kubbealti, nisanyan, viki"
1753
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, kubbealti, nisanyan, viki"
1088
1754
  );
1089
1755
  console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
1090
1756
  process.exit(command ? 0 : 1);
@@ -1152,13 +1818,46 @@ async function run() {
1152
1818
  const spellResult = await TDK.checkSpelling(word);
1153
1819
  printResult(spellResult, () => {
1154
1820
  if (spellResult.isCorrect) {
1155
- console.log("Do\u011Fru yaz\u0131m.");
1821
+ if (spellResult.isInflected && spellResult.root) {
1822
+ console.log(`Do\u011Fru yaz\u0131m (\xE7ekimli bi\xE7im, k\xF6k: ${spellResult.root}).`);
1823
+ } else {
1824
+ console.log("Do\u011Fru yaz\u0131m.");
1825
+ }
1156
1826
  } else {
1157
1827
  console.log(`Yanl\u0131\u015F yaz\u0131m.${spellResult.suggestion ? " Do\u011Frusu: " + spellResult.suggestion : ""}`);
1158
1828
  }
1159
1829
  });
1160
1830
  break;
1161
1831
  }
1832
+ case "kok":
1833
+ case "stem": {
1834
+ if (!word)
1835
+ throw new Error("Kelime belirtmelisiniz.");
1836
+ const stemResult = await TDK.stem(word);
1837
+ printResult(stemResult, () => {
1838
+ if (!stemResult) {
1839
+ console.log("K\xF6k bulunamad\u0131.");
1840
+ } else if (stemResult.isInflected) {
1841
+ console.log(`K\xF6k: ${stemResult.root} (\xE7ekimli bi\xE7im)`);
1842
+ } else {
1843
+ console.log(`K\xF6k: ${stemResult.root} (yal\u0131n bi\xE7im)`);
1844
+ }
1845
+ });
1846
+ break;
1847
+ }
1848
+ case "deyim": {
1849
+ if (!word)
1850
+ throw new Error("Kelime belirtmelisiniz.");
1851
+ const proverbs = await TDK.getProverbs(word);
1852
+ printResult(proverbs, () => {
1853
+ if (proverbs.length === 0) {
1854
+ console.log("Atas\xF6z\xFC/deyim bulunamad\u0131.");
1855
+ } else {
1856
+ proverbs.forEach((p, i) => console.log(`${i + 1}. ${p}`));
1857
+ }
1858
+ });
1859
+ break;
1860
+ }
1162
1861
  case "gunun": {
1163
1862
  const wotd = await TDK.getWordOfTheDay();
1164
1863
  printResult(wotd, () => {
@@ -1265,7 +1964,8 @@ async function run() {
1265
1964
  } else {
1266
1965
  analysis.forEach((a) => {
1267
1966
  if (a.found) {
1268
- console.log(`${a.word}: ${a.meaning ?? "-"} (${a.origin})`);
1967
+ const rootLabel = a.isInflected && a.root ? ` (k\xF6k: ${a.root})` : "";
1968
+ console.log(`${a.word}${rootLabel}: ${a.meaning ?? "-"} (${a.origin})`);
1269
1969
  } else {
1270
1970
  console.log(`${a.word}: bulunamad\u0131`);
1271
1971
  }