tdk-api-wrapper 1.1.0 → 1.2.1

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
@@ -34,6 +34,7 @@ tdk kurallar
34
34
  tdk kural kısaltmalar
35
35
  tdk karsilastir kalem kağıt
36
36
  tdk analiz "Bu güzel kalem masanın üstünde duruyor"
37
+ tdk oneri kale
37
38
  ```
38
39
 
39
40
  Herhangi bir komuta `--json` bayrağı eklendiğinde çıktı, insan-okunur metin yerine tek satırlık JSON olarak basılır (script/otomasyon kullanımı için):
@@ -67,7 +68,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
67
68
  - **`TDK.syllabicate(word)`**: Kelimeyi Türkçe heceleme kurallarına göre doğru hecelerine ayırır (Örn: `['mu', 'vaf', 'fa', 'ki', 'yet']`). API isteği atmaz, çok hızlıdır.
68
69
  - **`TDK.checkVowelHarmony(word)`**: Kelimenin büyük ünlü uyumuna uyup uymadığını (boolean) kontrol eder.
69
70
  - **`TDK.getPartOfSpeech(word)`**: Kelimenin sözcük türünü (isim, sıfat, zarf vb.) döndürür.
70
- - **`TDK.checkSpelling(word)`**: Sıkça yapılan yanlışlar listesini ve TDK veritabanını kullanarak kelimenin doğru yazılıp yazılmadığını kontrol eder. Yanlışsa doğrusunu önerir; tam eşleşme yoksa, aynı listedeki kelimeler arasında edit-distance (Levenshtein) ile en yakınını önerir (not: tüm sözlükte değil, yalnızca bu küçük havuzda arama yapar).
71
+ - **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'nin "sık yapılan yanlışlar" listesinde tam eşleşme arar; bulamazsa TDK'nin ~81 bin kelimelik tam madde listesi üzerinde edit-distance (Levenshtein) ile en yakın kelimeyi önerir (örn. `herkez` `herkes`, `mektub` `mektup`).
71
72
  - **`TDK.getCompoundWords(word)`**: Aranan kelime ile oluşturulmuş birleşik kelimeleri (Örn: dolma kalem) listeler.
72
73
 
73
74
  ### 3. Edebi ve Kültürel Analiz
@@ -81,7 +82,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
81
82
  - **`TDK.analyzeText(text)`**: Bir metindeki (Türkçe bağlaçlar/edatlar hariç) her benzersiz kelimeyi tek tek arayıp ilk anlamını ve kökenini döner.
82
83
 
83
84
  ### 4. Yardımcı Metotlar
84
- - **`TDK.getSuggestions(prefix)`**: Kelimenin sadece ilk birkaç harfini girdiğinizde otomatik tamamlama önerilerini çeker.
85
+ - **`TDK.getSuggestions(prefix)`**: TDK'nin ~81 bin kelimelik tam madde listesi üzerinden önek bazlı otomatik tamamlama önerileri döner (ilk çağrıda listeyi indirip önbelleğe alır, sonraki çağrılar anlıktır).
85
86
  - **`TDK.getAudioUrl(word)`**: TDK'nin bu kelime için gerçekten bir ses kaydı varsa doğrudan indirme URL'sini döner, yoksa `null`. `downloadAudio(word, destPath)` ile cihazınıza indirebilirsiniz.
86
87
  - **`TDK.getDailyContent()`**: TDK anasayfasında yer alan "Günün Kelimesi, Atasözü ve Kuralı" içeriklerini çeker.
87
88
  - **`TDK.getWordOfTheDay()`**: `getDailyContent()`'in üzerine ince bir katman; günün kelimesini ve tüm anlamlarını `{ word, meanings }` şeklinde döner.
@@ -119,23 +119,60 @@ var TDK = class {
119
119
  return meanings;
120
120
  }
121
121
  /**
122
- * Returns suggestions (autocomplete) for a given prefix.
122
+ * `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
123
+ * routes no longer serve JSON — they fall through to the SPA's `index.html`.
124
+ * The full ~81k-word headword list the site's own autocomplete UI uses is
125
+ * instead bundled directly into its main JS asset as a
126
+ * `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
127
+ * page to find that asset's current hashed filename, downloads it (a few
128
+ * MB, only once per process), and extracts the literal out of it. Fragile
129
+ * scraping of an implementation detail — if TDK's build stops embedding
130
+ * this, this fails closed to `[]` rather than throwing.
131
+ */
132
+ static async fetchAutocompleteData() {
133
+ try {
134
+ const homeResponse = await fetch(`${this.BASE_URL}/`, {
135
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
136
+ });
137
+ if (!homeResponse.ok)
138
+ return [];
139
+ const html = await homeResponse.text();
140
+ const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
141
+ if (!scriptMatch)
142
+ return [];
143
+ const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
144
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
145
+ });
146
+ if (!bundleResponse.ok)
147
+ return [];
148
+ const bundleJs = await bundleResponse.text();
149
+ const startMarker = 'JSON.parse(`[{"madde":';
150
+ const startIdx = bundleJs.indexOf(startMarker);
151
+ if (startIdx === -1)
152
+ return [];
153
+ const jsonStart = startIdx + "JSON.parse(".length + 1;
154
+ const jsonEnd = bundleJs.indexOf("`)", jsonStart);
155
+ if (jsonEnd === -1)
156
+ return [];
157
+ const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
158
+ return data.map((item) => item.madde).filter(Boolean);
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+ /**
164
+ * Returns autocomplete suggestions for a given prefix, searched over TDK's
165
+ * full headword list (see `fetchAutocompleteData`). The list is fetched
166
+ * and cached once per process regardless of `enableCache()` — the same
167
+ * caching behavior as before — and only cleared by `clearCache()`.
123
168
  */
124
169
  static async getSuggestions(prefix) {
170
+ if (!prefix || prefix.trim() === "")
171
+ return [];
125
172
  if (this.autocompleteCache.length === 0) {
126
- try {
127
- const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
128
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
129
- });
130
- if (response.ok) {
131
- const data = await response.json();
132
- this.autocompleteCache = data.map((item) => item.madde);
133
- }
134
- } catch (e) {
135
- return [];
136
- }
173
+ this.autocompleteCache = await this.fetchAutocompleteData();
137
174
  }
138
- const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
175
+ const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
139
176
  return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
140
177
  }
141
178
  /**
@@ -353,29 +390,37 @@ var TDK = class {
353
390
  if (mixMatch) {
354
391
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
355
392
  }
356
- const candidates = [
357
- ...daily.syyd.map((s) => s.dogrukelime),
358
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
359
- ...daily.kelime.map((k) => k.madde)
360
- ];
361
- let best = null;
362
- for (const candidate of candidates) {
363
- const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
364
- if (distance > 0 && (!best || distance < best.distance)) {
365
- best = { candidate, distance };
366
- }
367
- }
368
- if (best && best.distance <= 2) {
369
- return { isCorrect: false, word, suggestion: best.candidate };
393
+ }
394
+ if (this.autocompleteCache.length === 0) {
395
+ this.autocompleteCache = await this.fetchAutocompleteData();
396
+ }
397
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
398
+ let best = null;
399
+ for (const candidate of this.autocompleteCache) {
400
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
401
+ continue;
402
+ const distance = this.levenshtein(cleanWord, candidate);
403
+ if (distance > 0 && (!best || distance < best.distance)) {
404
+ best = { candidate, distance };
405
+ if (distance === 1)
406
+ break;
370
407
  }
371
408
  }
409
+ if (best && best.distance <= 2) {
410
+ return { isCorrect: false, word, suggestion: best.candidate };
411
+ }
372
412
  return { isCorrect: false, word };
373
413
  }
374
414
  /**
375
415
  * Fetches daily content (word of the day, proverbs, rules, etc).
416
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
417
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
418
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
419
+ * caching is enabled the loop would just re-read the same cached response
420
+ * 25 times and could never find a rule outside that first random draw.
376
421
  */
377
- static async getDailyContent() {
378
- if (this.isCacheEnabled && this.dailyContentCache)
422
+ static async getDailyContent(bypassCache = false) {
423
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
379
424
  return this.dailyContentCache;
380
425
  try {
381
426
  const response = await fetch(`${this.BASE_URL}/icerik`, {
@@ -383,7 +428,7 @@ var TDK = class {
383
428
  });
384
429
  if (response.ok) {
385
430
  const data = await response.json();
386
- if (this.isCacheEnabled)
431
+ if (!bypassCache && this.isCacheEnabled)
387
432
  this.dailyContentCache = data;
388
433
  return data;
389
434
  }
@@ -424,10 +469,12 @@ var TDK = class {
424
469
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
425
470
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
426
471
  * appears to hand back a single randomly-rotated rule per request, so two
427
- * calls a second apart can return entirely different rules.
472
+ * calls a second apart can return entirely different rules. `bypassCache`
473
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
474
+ * draw even when `enableCache(true)` is on.
428
475
  */
429
- static async getKurallar() {
430
- const daily = await this.getDailyContent();
476
+ static async getKurallar(bypassCache = false) {
477
+ const daily = await this.getDailyContent(bypassCache);
431
478
  return daily?.kural ?? [];
432
479
  }
433
480
  /**
@@ -436,16 +483,19 @@ var TDK = class {
436
483
  * hands back a single randomly-rotated rule per request (out of a pool of
437
484
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
438
485
  * draw would rarely match a given name — this re-draws (bounded, with a
439
- * short delay) until it finds a match or gives up. Returns `null` if no
440
- * match turns up within the attempt budget or the matched page can't be
441
- * parsed.
486
+ * short delay) until it finds a match or gives up. Every attempt bypasses
487
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
488
+ * 25 attempts would just re-read the same cached `/icerik` response and
489
+ * could never find a rule outside whatever the first draw happened to be.
490
+ * Returns `null` if no match turns up within the attempt budget or the
491
+ * matched page can't be parsed.
442
492
  */
443
493
  static async getRule(name) {
444
494
  if (!name || name.trim() === "")
445
495
  return null;
446
496
  const target = name.trim().toLocaleLowerCase("tr-TR");
447
497
  for (let attempt = 0; attempt < 25; attempt++) {
448
- const rules = await this.getKurallar();
498
+ const rules = await this.getKurallar(true);
449
499
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
450
500
  if (match)
451
501
  return this.fetchRuleText(match.url);
package/dist/cli.js CHANGED
@@ -144,23 +144,60 @@ var TDK = class {
144
144
  return meanings;
145
145
  }
146
146
  /**
147
- * Returns suggestions (autocomplete) for a given prefix.
147
+ * `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
148
+ * routes no longer serve JSON — they fall through to the SPA's `index.html`.
149
+ * The full ~81k-word headword list the site's own autocomplete UI uses is
150
+ * instead bundled directly into its main JS asset as a
151
+ * `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
152
+ * page to find that asset's current hashed filename, downloads it (a few
153
+ * MB, only once per process), and extracts the literal out of it. Fragile
154
+ * scraping of an implementation detail — if TDK's build stops embedding
155
+ * this, this fails closed to `[]` rather than throwing.
156
+ */
157
+ static async fetchAutocompleteData() {
158
+ try {
159
+ const homeResponse = await fetch(`${this.BASE_URL}/`, {
160
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
161
+ });
162
+ if (!homeResponse.ok)
163
+ return [];
164
+ const html = await homeResponse.text();
165
+ const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
166
+ if (!scriptMatch)
167
+ return [];
168
+ const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
169
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
170
+ });
171
+ if (!bundleResponse.ok)
172
+ return [];
173
+ const bundleJs = await bundleResponse.text();
174
+ const startMarker = 'JSON.parse(`[{"madde":';
175
+ const startIdx = bundleJs.indexOf(startMarker);
176
+ if (startIdx === -1)
177
+ return [];
178
+ const jsonStart = startIdx + "JSON.parse(".length + 1;
179
+ const jsonEnd = bundleJs.indexOf("`)", jsonStart);
180
+ if (jsonEnd === -1)
181
+ return [];
182
+ const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
183
+ return data.map((item) => item.madde).filter(Boolean);
184
+ } catch {
185
+ return [];
186
+ }
187
+ }
188
+ /**
189
+ * Returns autocomplete suggestions for a given prefix, searched over TDK's
190
+ * full headword list (see `fetchAutocompleteData`). The list is fetched
191
+ * and cached once per process regardless of `enableCache()` — the same
192
+ * caching behavior as before — and only cleared by `clearCache()`.
148
193
  */
149
194
  static async getSuggestions(prefix) {
195
+ if (!prefix || prefix.trim() === "")
196
+ return [];
150
197
  if (this.autocompleteCache.length === 0) {
151
- try {
152
- const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
153
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
154
- });
155
- if (response.ok) {
156
- const data = await response.json();
157
- this.autocompleteCache = data.map((item) => item.madde);
158
- }
159
- } catch (e) {
160
- return [];
161
- }
198
+ this.autocompleteCache = await this.fetchAutocompleteData();
162
199
  }
163
- const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
200
+ const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
164
201
  return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
165
202
  }
166
203
  /**
@@ -378,29 +415,37 @@ var TDK = class {
378
415
  if (mixMatch) {
379
416
  return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
380
417
  }
381
- const candidates = [
382
- ...daily.syyd.map((s) => s.dogrukelime),
383
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
384
- ...daily.kelime.map((k) => k.madde)
385
- ];
386
- let best = null;
387
- for (const candidate of candidates) {
388
- const distance = this.levenshtein(word2.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
389
- if (distance > 0 && (!best || distance < best.distance)) {
390
- best = { candidate, distance };
391
- }
392
- }
393
- if (best && best.distance <= 2) {
394
- return { isCorrect: false, word: word2, suggestion: best.candidate };
418
+ }
419
+ if (this.autocompleteCache.length === 0) {
420
+ this.autocompleteCache = await this.fetchAutocompleteData();
421
+ }
422
+ const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
423
+ let best = null;
424
+ for (const candidate of this.autocompleteCache) {
425
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
426
+ continue;
427
+ const distance = this.levenshtein(cleanWord, candidate);
428
+ if (distance > 0 && (!best || distance < best.distance)) {
429
+ best = { candidate, distance };
430
+ if (distance === 1)
431
+ break;
395
432
  }
396
433
  }
434
+ if (best && best.distance <= 2) {
435
+ return { isCorrect: false, word: word2, suggestion: best.candidate };
436
+ }
397
437
  return { isCorrect: false, word: word2 };
398
438
  }
399
439
  /**
400
440
  * Fetches daily content (word of the day, proverbs, rules, etc).
441
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
442
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
443
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
444
+ * caching is enabled the loop would just re-read the same cached response
445
+ * 25 times and could never find a rule outside that first random draw.
401
446
  */
402
- static async getDailyContent() {
403
- if (this.isCacheEnabled && this.dailyContentCache)
447
+ static async getDailyContent(bypassCache = false) {
448
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
404
449
  return this.dailyContentCache;
405
450
  try {
406
451
  const response = await fetch(`${this.BASE_URL}/icerik`, {
@@ -408,7 +453,7 @@ var TDK = class {
408
453
  });
409
454
  if (response.ok) {
410
455
  const data = await response.json();
411
- if (this.isCacheEnabled)
456
+ if (!bypassCache && this.isCacheEnabled)
412
457
  this.dailyContentCache = data;
413
458
  return data;
414
459
  }
@@ -449,10 +494,12 @@ var TDK = class {
449
494
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
450
495
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
451
496
  * appears to hand back a single randomly-rotated rule per request, so two
452
- * calls a second apart can return entirely different rules.
497
+ * calls a second apart can return entirely different rules. `bypassCache`
498
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
499
+ * draw even when `enableCache(true)` is on.
453
500
  */
454
- static async getKurallar() {
455
- const daily = await this.getDailyContent();
501
+ static async getKurallar(bypassCache = false) {
502
+ const daily = await this.getDailyContent(bypassCache);
456
503
  return daily?.kural ?? [];
457
504
  }
458
505
  /**
@@ -461,16 +508,19 @@ var TDK = class {
461
508
  * hands back a single randomly-rotated rule per request (out of a pool of
462
509
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
463
510
  * draw would rarely match a given name — this re-draws (bounded, with a
464
- * short delay) until it finds a match or gives up. Returns `null` if no
465
- * match turns up within the attempt budget or the matched page can't be
466
- * parsed.
511
+ * short delay) until it finds a match or gives up. Every attempt bypasses
512
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
513
+ * 25 attempts would just re-read the same cached `/icerik` response and
514
+ * could never find a rule outside whatever the first draw happened to be.
515
+ * Returns `null` if no match turns up within the attempt budget or the
516
+ * matched page can't be parsed.
467
517
  */
468
518
  static async getRule(name) {
469
519
  if (!name || name.trim() === "")
470
520
  return null;
471
521
  const target = name.trim().toLocaleLowerCase("tr-TR");
472
522
  for (let attempt = 0; attempt < 25; attempt++) {
473
- const rules = await this.getKurallar();
523
+ const rules = await this.getKurallar(true);
474
524
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
475
525
  if (match)
476
526
  return this.fetchRuleText(match.url);
@@ -741,7 +791,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
741
791
  "kurallar",
742
792
  "kural",
743
793
  "karsilastir",
744
- "analiz"
794
+ "analiz",
795
+ "oneri"
745
796
  ]);
746
797
  var command = args[0];
747
798
  var word = args.slice(1).join(" ");
@@ -767,7 +818,7 @@ async function run() {
767
818
  if (!command || command === "--help" || command === "-h") {
768
819
  console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
769
820
  console.log(
770
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz"
821
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
771
822
  );
772
823
  console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
773
824
  process.exit(command ? 0 : 1);
@@ -957,6 +1008,19 @@ async function run() {
957
1008
  });
958
1009
  break;
959
1010
  }
1011
+ case "oneri": {
1012
+ if (!word)
1013
+ throw new Error("\xD6nek belirtmelisiniz.");
1014
+ const suggestions = await TDK.getSuggestions(word);
1015
+ printResult(suggestions, () => {
1016
+ if (suggestions.length === 0) {
1017
+ console.log("\xD6neri bulunamad\u0131.");
1018
+ } else {
1019
+ suggestions.forEach((s, i) => console.log(`${i + 1}. ${s}`));
1020
+ }
1021
+ });
1022
+ break;
1023
+ }
960
1024
  default:
961
1025
  printError("Bilinmeyen komut.");
962
1026
  }
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TDK
4
- } from "./chunk-P3GX7I53.mjs";
4
+ } from "./chunk-6BTOGV2M.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  var rawArgs = process.argv.slice(2);
@@ -23,7 +23,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
23
23
  "kurallar",
24
24
  "kural",
25
25
  "karsilastir",
26
- "analiz"
26
+ "analiz",
27
+ "oneri"
27
28
  ]);
28
29
  var command = args[0];
29
30
  var word = args.slice(1).join(" ");
@@ -49,7 +50,7 @@ async function run() {
49
50
  if (!command || command === "--help" || command === "-h") {
50
51
  console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
51
52
  console.log(
52
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz"
53
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
53
54
  );
54
55
  console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
55
56
  process.exit(command ? 0 : 1);
@@ -239,6 +240,19 @@ async function run() {
239
240
  });
240
241
  break;
241
242
  }
243
+ case "oneri": {
244
+ if (!word)
245
+ throw new Error("\xD6nek belirtmelisiniz.");
246
+ const suggestions = await TDK.getSuggestions(word);
247
+ printResult(suggestions, () => {
248
+ if (suggestions.length === 0) {
249
+ console.log("\xD6neri bulunamad\u0131.");
250
+ } else {
251
+ suggestions.forEach((s, i) => console.log(`${i + 1}. ${s}`));
252
+ }
253
+ });
254
+ break;
255
+ }
242
256
  default:
243
257
  printError("Bilinmeyen komut.");
244
258
  }
package/dist/index.d.mts CHANGED
@@ -158,7 +158,22 @@ declare class TDK {
158
158
  */
159
159
  static getMeanings(word: string): Promise<string[]>;
160
160
  /**
161
- * Returns suggestions (autocomplete) for a given prefix.
161
+ * `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
162
+ * routes no longer serve JSON — they fall through to the SPA's `index.html`.
163
+ * The full ~81k-word headword list the site's own autocomplete UI uses is
164
+ * instead bundled directly into its main JS asset as a
165
+ * `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
166
+ * page to find that asset's current hashed filename, downloads it (a few
167
+ * MB, only once per process), and extracts the literal out of it. Fragile
168
+ * scraping of an implementation detail — if TDK's build stops embedding
169
+ * this, this fails closed to `[]` rather than throwing.
170
+ */
171
+ private static fetchAutocompleteData;
172
+ /**
173
+ * Returns autocomplete suggestions for a given prefix, searched over TDK's
174
+ * full headword list (see `fetchAutocompleteData`). The list is fetched
175
+ * and cached once per process regardless of `enableCache()` — the same
176
+ * caching behavior as before — and only cleared by `clearCache()`.
162
177
  */
163
178
  static getSuggestions(prefix: string): Promise<string[]>;
164
179
  /**
@@ -229,8 +244,13 @@ declare class TDK {
229
244
  static checkSpelling(word: string): Promise<SpellCheckResult>;
230
245
  /**
231
246
  * Fetches daily content (word of the day, proverbs, rules, etc).
247
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
248
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
249
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
250
+ * caching is enabled the loop would just re-read the same cached response
251
+ * 25 times and could never find a rule outside that first random draw.
232
252
  */
233
- static getDailyContent(): Promise<DailyContent | null>;
253
+ static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
234
254
  /**
235
255
  * Returns today's word of the day along with all of its listed meanings.
236
256
  */
@@ -245,18 +265,23 @@ declare class TDK {
245
265
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
246
266
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
247
267
  * appears to hand back a single randomly-rotated rule per request, so two
248
- * calls a second apart can return entirely different rules.
268
+ * calls a second apart can return entirely different rules. `bypassCache`
269
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
270
+ * draw even when `enableCache(true)` is on.
249
271
  */
250
- static getKurallar(): Promise<TDKRule[]>;
272
+ static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
251
273
  /**
252
274
  * Fetches the full plain-text content of a named spelling rule (matched
253
275
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
254
276
  * hands back a single randomly-rotated rule per request (out of a pool of
255
277
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
256
278
  * draw would rarely match a given name — this re-draws (bounded, with a
257
- * short delay) until it finds a match or gives up. Returns `null` if no
258
- * match turns up within the attempt budget or the matched page can't be
259
- * parsed.
279
+ * short delay) until it finds a match or gives up. Every attempt bypasses
280
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
281
+ * 25 attempts would just re-read the same cached `/icerik` response and
282
+ * could never find a rule outside whatever the first draw happened to be.
283
+ * Returns `null` if no match turns up within the attempt budget or the
284
+ * matched page can't be parsed.
260
285
  */
261
286
  static getRule(name: string): Promise<string | null>;
262
287
  /**
package/dist/index.d.ts CHANGED
@@ -158,7 +158,22 @@ declare class TDK {
158
158
  */
159
159
  static getMeanings(word: string): Promise<string[]>;
160
160
  /**
161
- * Returns suggestions (autocomplete) for a given prefix.
161
+ * `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
162
+ * routes no longer serve JSON — they fall through to the SPA's `index.html`.
163
+ * The full ~81k-word headword list the site's own autocomplete UI uses is
164
+ * instead bundled directly into its main JS asset as a
165
+ * `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
166
+ * page to find that asset's current hashed filename, downloads it (a few
167
+ * MB, only once per process), and extracts the literal out of it. Fragile
168
+ * scraping of an implementation detail — if TDK's build stops embedding
169
+ * this, this fails closed to `[]` rather than throwing.
170
+ */
171
+ private static fetchAutocompleteData;
172
+ /**
173
+ * Returns autocomplete suggestions for a given prefix, searched over TDK's
174
+ * full headword list (see `fetchAutocompleteData`). The list is fetched
175
+ * and cached once per process regardless of `enableCache()` — the same
176
+ * caching behavior as before — and only cleared by `clearCache()`.
162
177
  */
163
178
  static getSuggestions(prefix: string): Promise<string[]>;
164
179
  /**
@@ -229,8 +244,13 @@ declare class TDK {
229
244
  static checkSpelling(word: string): Promise<SpellCheckResult>;
230
245
  /**
231
246
  * Fetches daily content (word of the day, proverbs, rules, etc).
247
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
248
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
249
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
250
+ * caching is enabled the loop would just re-read the same cached response
251
+ * 25 times and could never find a rule outside that first random draw.
232
252
  */
233
- static getDailyContent(): Promise<DailyContent | null>;
253
+ static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
234
254
  /**
235
255
  * Returns today's word of the day along with all of its listed meanings.
236
256
  */
@@ -245,18 +265,23 @@ declare class TDK {
245
265
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
246
266
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
247
267
  * appears to hand back a single randomly-rotated rule per request, so two
248
- * calls a second apart can return entirely different rules.
268
+ * calls a second apart can return entirely different rules. `bypassCache`
269
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
270
+ * draw even when `enableCache(true)` is on.
249
271
  */
250
- static getKurallar(): Promise<TDKRule[]>;
272
+ static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
251
273
  /**
252
274
  * Fetches the full plain-text content of a named spelling rule (matched
253
275
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
254
276
  * hands back a single randomly-rotated rule per request (out of a pool of
255
277
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
256
278
  * draw would rarely match a given name — this re-draws (bounded, with a
257
- * short delay) until it finds a match or gives up. Returns `null` if no
258
- * match turns up within the attempt budget or the matched page can't be
259
- * parsed.
279
+ * short delay) until it finds a match or gives up. Every attempt bypasses
280
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
281
+ * 25 attempts would just re-read the same cached `/icerik` response and
282
+ * could never find a rule outside whatever the first draw happened to be.
283
+ * Returns `null` if no match turns up within the attempt budget or the
284
+ * matched page can't be parsed.
260
285
  */
261
286
  static getRule(name: string): Promise<string | null>;
262
287
  /**
package/dist/index.js CHANGED
@@ -158,23 +158,60 @@ var TDK = class {
158
158
  return meanings;
159
159
  }
160
160
  /**
161
- * Returns suggestions (autocomplete) for a given prefix.
161
+ * `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
162
+ * routes no longer serve JSON — they fall through to the SPA's `index.html`.
163
+ * The full ~81k-word headword list the site's own autocomplete UI uses is
164
+ * instead bundled directly into its main JS asset as a
165
+ * `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
166
+ * page to find that asset's current hashed filename, downloads it (a few
167
+ * MB, only once per process), and extracts the literal out of it. Fragile
168
+ * scraping of an implementation detail — if TDK's build stops embedding
169
+ * this, this fails closed to `[]` rather than throwing.
170
+ */
171
+ static async fetchAutocompleteData() {
172
+ try {
173
+ const homeResponse = await fetch(`${this.BASE_URL}/`, {
174
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
175
+ });
176
+ if (!homeResponse.ok)
177
+ return [];
178
+ const html = await homeResponse.text();
179
+ const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
180
+ if (!scriptMatch)
181
+ return [];
182
+ const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
183
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
184
+ });
185
+ if (!bundleResponse.ok)
186
+ return [];
187
+ const bundleJs = await bundleResponse.text();
188
+ const startMarker = 'JSON.parse(`[{"madde":';
189
+ const startIdx = bundleJs.indexOf(startMarker);
190
+ if (startIdx === -1)
191
+ return [];
192
+ const jsonStart = startIdx + "JSON.parse(".length + 1;
193
+ const jsonEnd = bundleJs.indexOf("`)", jsonStart);
194
+ if (jsonEnd === -1)
195
+ return [];
196
+ const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
197
+ return data.map((item) => item.madde).filter(Boolean);
198
+ } catch {
199
+ return [];
200
+ }
201
+ }
202
+ /**
203
+ * Returns autocomplete suggestions for a given prefix, searched over TDK's
204
+ * full headword list (see `fetchAutocompleteData`). The list is fetched
205
+ * and cached once per process regardless of `enableCache()` — the same
206
+ * caching behavior as before — and only cleared by `clearCache()`.
162
207
  */
163
208
  static async getSuggestions(prefix) {
209
+ if (!prefix || prefix.trim() === "")
210
+ return [];
164
211
  if (this.autocompleteCache.length === 0) {
165
- try {
166
- const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
167
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
168
- });
169
- if (response.ok) {
170
- const data = await response.json();
171
- this.autocompleteCache = data.map((item) => item.madde);
172
- }
173
- } catch (e) {
174
- return [];
175
- }
212
+ this.autocompleteCache = await this.fetchAutocompleteData();
176
213
  }
177
- const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
214
+ const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
178
215
  return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
179
216
  }
180
217
  /**
@@ -392,29 +429,37 @@ var TDK = class {
392
429
  if (mixMatch) {
393
430
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
394
431
  }
395
- const candidates = [
396
- ...daily.syyd.map((s) => s.dogrukelime),
397
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
398
- ...daily.kelime.map((k) => k.madde)
399
- ];
400
- let best = null;
401
- for (const candidate of candidates) {
402
- const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
403
- if (distance > 0 && (!best || distance < best.distance)) {
404
- best = { candidate, distance };
405
- }
406
- }
407
- if (best && best.distance <= 2) {
408
- return { isCorrect: false, word, suggestion: best.candidate };
432
+ }
433
+ if (this.autocompleteCache.length === 0) {
434
+ this.autocompleteCache = await this.fetchAutocompleteData();
435
+ }
436
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
437
+ let best = null;
438
+ for (const candidate of this.autocompleteCache) {
439
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
440
+ continue;
441
+ const distance = this.levenshtein(cleanWord, candidate);
442
+ if (distance > 0 && (!best || distance < best.distance)) {
443
+ best = { candidate, distance };
444
+ if (distance === 1)
445
+ break;
409
446
  }
410
447
  }
448
+ if (best && best.distance <= 2) {
449
+ return { isCorrect: false, word, suggestion: best.candidate };
450
+ }
411
451
  return { isCorrect: false, word };
412
452
  }
413
453
  /**
414
454
  * Fetches daily content (word of the day, proverbs, rules, etc).
455
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
456
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
457
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
458
+ * caching is enabled the loop would just re-read the same cached response
459
+ * 25 times and could never find a rule outside that first random draw.
415
460
  */
416
- static async getDailyContent() {
417
- if (this.isCacheEnabled && this.dailyContentCache)
461
+ static async getDailyContent(bypassCache = false) {
462
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
418
463
  return this.dailyContentCache;
419
464
  try {
420
465
  const response = await fetch(`${this.BASE_URL}/icerik`, {
@@ -422,7 +467,7 @@ var TDK = class {
422
467
  });
423
468
  if (response.ok) {
424
469
  const data = await response.json();
425
- if (this.isCacheEnabled)
470
+ if (!bypassCache && this.isCacheEnabled)
426
471
  this.dailyContentCache = data;
427
472
  return data;
428
473
  }
@@ -463,10 +508,12 @@ var TDK = class {
463
508
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
464
509
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
465
510
  * appears to hand back a single randomly-rotated rule per request, so two
466
- * calls a second apart can return entirely different rules.
511
+ * calls a second apart can return entirely different rules. `bypassCache`
512
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
513
+ * draw even when `enableCache(true)` is on.
467
514
  */
468
- static async getKurallar() {
469
- const daily = await this.getDailyContent();
515
+ static async getKurallar(bypassCache = false) {
516
+ const daily = await this.getDailyContent(bypassCache);
470
517
  return daily?.kural ?? [];
471
518
  }
472
519
  /**
@@ -475,16 +522,19 @@ var TDK = class {
475
522
  * hands back a single randomly-rotated rule per request (out of a pool of
476
523
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
477
524
  * draw would rarely match a given name — this re-draws (bounded, with a
478
- * short delay) until it finds a match or gives up. Returns `null` if no
479
- * match turns up within the attempt budget or the matched page can't be
480
- * parsed.
525
+ * short delay) until it finds a match or gives up. Every attempt bypasses
526
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
527
+ * 25 attempts would just re-read the same cached `/icerik` response and
528
+ * could never find a rule outside whatever the first draw happened to be.
529
+ * Returns `null` if no match turns up within the attempt budget or the
530
+ * matched page can't be parsed.
481
531
  */
482
532
  static async getRule(name) {
483
533
  if (!name || name.trim() === "")
484
534
  return null;
485
535
  const target = name.trim().toLocaleLowerCase("tr-TR");
486
536
  for (let attempt = 0; attempt < 25; attempt++) {
487
- const rules = await this.getKurallar();
537
+ const rules = await this.getKurallar(true);
488
538
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
489
539
  if (match)
490
540
  return this.fetchRuleText(match.url);
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  TDKError,
4
4
  TDKNetworkError,
5
5
  TDKValidationError
6
- } from "./chunk-P3GX7I53.mjs";
6
+ } from "./chunk-6BTOGV2M.mjs";
7
7
  export {
8
8
  TDK,
9
9
  TDKError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdk-api-wrapper",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "TDK (Türk Dil Kurumu) unofficial live data API wrapper for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/cli.ts CHANGED
@@ -21,6 +21,7 @@ const KNOWN_COMMANDS = new Set([
21
21
  "kural",
22
22
  "karsilastir",
23
23
  "analiz",
24
+ "oneri",
24
25
  ]);
25
26
 
26
27
  let command = args[0];
@@ -51,7 +52,7 @@ async function run() {
51
52
  if (!command || command === "--help" || command === "-h") {
52
53
  console.log("Kullanım: tdk [komut] <kelime> [--json]");
53
54
  console.log(
54
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz"
55
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
55
56
  );
56
57
  console.log("Not: Komut belirtilmezse doğrudan kelime anlamı aranır (örn: tdk selam)");
57
58
  process.exit(command ? 0 : 1);
@@ -245,6 +246,19 @@ async function run() {
245
246
  break;
246
247
  }
247
248
 
249
+ case "oneri": {
250
+ if (!word) throw new Error("Önek belirtmelisiniz.");
251
+ const suggestions = await TDK.getSuggestions(word);
252
+ printResult(suggestions, () => {
253
+ if (suggestions.length === 0) {
254
+ console.log("Öneri bulunamadı.");
255
+ } else {
256
+ suggestions.forEach((s, i) => console.log(`${i + 1}. ${s}`));
257
+ }
258
+ });
259
+ break;
260
+ }
261
+
248
262
  default:
249
263
  printError("Bilinmeyen komut.");
250
264
  }
package/src/tdk.ts CHANGED
@@ -119,24 +119,61 @@ export class TDK {
119
119
  }
120
120
 
121
121
  /**
122
- * Returns suggestions (autocomplete) for a given prefix.
122
+ * `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
123
+ * routes no longer serve JSON — they fall through to the SPA's `index.html`.
124
+ * The full ~81k-word headword list the site's own autocomplete UI uses is
125
+ * instead bundled directly into its main JS asset as a
126
+ * `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
127
+ * page to find that asset's current hashed filename, downloads it (a few
128
+ * MB, only once per process), and extracts the literal out of it. Fragile
129
+ * scraping of an implementation detail — if TDK's build stops embedding
130
+ * this, this fails closed to `[]` rather than throwing.
131
+ */
132
+ private static async fetchAutocompleteData(): Promise<string[]> {
133
+ try {
134
+ const homeResponse = await fetch(`${this.BASE_URL}/`, {
135
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
136
+ });
137
+ if (!homeResponse.ok) return [];
138
+ const html = await homeResponse.text();
139
+
140
+ const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
141
+ if (!scriptMatch) return [];
142
+
143
+ const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
144
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
145
+ });
146
+ if (!bundleResponse.ok) return [];
147
+ const bundleJs = await bundleResponse.text();
148
+
149
+ const startMarker = 'JSON.parse(`[{"madde":';
150
+ const startIdx = bundleJs.indexOf(startMarker);
151
+ if (startIdx === -1) return [];
152
+ const jsonStart = startIdx + "JSON.parse(".length + 1;
153
+ const jsonEnd = bundleJs.indexOf("`)", jsonStart);
154
+ if (jsonEnd === -1) return [];
155
+
156
+ const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd)) as { madde: string }[];
157
+ return data.map((item) => item.madde).filter(Boolean);
158
+ } catch {
159
+ return [];
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Returns autocomplete suggestions for a given prefix, searched over TDK's
165
+ * full headword list (see `fetchAutocompleteData`). The list is fetched
166
+ * and cached once per process regardless of `enableCache()` — the same
167
+ * caching behavior as before — and only cleared by `clearCache()`.
123
168
  */
124
169
  public static async getSuggestions(prefix: string): Promise<string[]> {
170
+ if (!prefix || prefix.trim() === "") return [];
171
+
125
172
  if (this.autocompleteCache.length === 0) {
126
- try {
127
- const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
128
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
129
- });
130
- if (response.ok) {
131
- const data = await response.json() as { madde: string }[];
132
- this.autocompleteCache = data.map(item => item.madde);
133
- }
134
- } catch (e) {
135
- return [];
136
- }
173
+ this.autocompleteCache = await this.fetchAutocompleteData();
137
174
  }
138
-
139
- const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
175
+
176
+ const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
140
177
  return this.autocompleteCache
141
178
  .filter(w => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix))
142
179
  .slice(0, 10);
@@ -352,8 +389,10 @@ export class TDK {
352
389
  if (results.length > 0) {
353
390
  return { isCorrect: true, word };
354
391
  }
355
-
356
- // 2. If not, check "sıkça yapılan yanlışlar" from DailyContent
392
+
393
+ // 2. If not, check "sıkça yapılan yanlışlar" from DailyContent — an exact
394
+ // match here is TDK explicitly saying "X is often confused with Y", so
395
+ // it's authoritative when it hits (but only 2-3 rotating entries per call).
357
396
  const daily = await this.getDailyContent();
358
397
  if (daily) {
359
398
  const syydMatch = daily.syyd.find(s => s.yanliskelime.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
@@ -364,43 +403,49 @@ export class TDK {
364
403
  if (mixMatch) {
365
404
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
366
405
  }
406
+ }
367
407
 
368
- // 3. No exact match in TDK's fixed lists: fall back to the closest word
369
- // (by edit distance) within that same small pool. This is NOT a search
370
- // over the full dictionary TDK exposes no such lookup — just a
371
- // best-effort nudge using the "sık yapılan yanlışlar" data we already have.
372
- const candidates = [
373
- ...daily.syyd.map((s) => s.dogrukelime),
374
- ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
375
- ...daily.kelime.map((k) => k.madde),
376
- ];
377
- let best: { candidate: string; distance: number } | null = null;
378
- for (const candidate of candidates) {
379
- const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
380
- if (distance > 0 && (!best || distance < best.distance)) {
381
- best = { candidate, distance };
382
- }
383
- }
384
- if (best && best.distance <= 2) {
385
- return { isCorrect: false, word, suggestion: best.candidate };
408
+ // 3. No exact match in TDK's curated lists: fall back to the closest
409
+ // headword (by edit distance) across TDK's full ~81k-word list (the same
410
+ // data `getSuggestions()` uses). Restricted to single-token, lowercase
411
+ // headwords so it doesn't suggest compounds/phrases or proper nouns.
412
+ if (this.autocompleteCache.length === 0) {
413
+ this.autocompleteCache = await this.fetchAutocompleteData();
414
+ }
415
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
416
+ let best: { candidate: string; distance: number } | null = null;
417
+ for (const candidate of this.autocompleteCache) {
418
+ if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
419
+ const distance = this.levenshtein(cleanWord, candidate);
420
+ if (distance > 0 && (!best || distance < best.distance)) {
421
+ best = { candidate, distance };
422
+ if (distance === 1) break;
386
423
  }
387
424
  }
425
+ if (best && best.distance <= 2) {
426
+ return { isCorrect: false, word, suggestion: best.candidate };
427
+ }
388
428
  return { isCorrect: false, word };
389
429
  }
390
430
 
391
431
  /**
392
432
  * Fetches daily content (word of the day, proverbs, rules, etc).
433
+ * `bypassCache` skips both reading and writing `dailyContentCache` even
434
+ * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
435
+ * needs a fresh random `/icerik` draw on every attempt; without it, once
436
+ * caching is enabled the loop would just re-read the same cached response
437
+ * 25 times and could never find a rule outside that first random draw.
393
438
  */
394
- public static async getDailyContent(): Promise<DailyContent | null> {
395
- if (this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
396
-
439
+ public static async getDailyContent(bypassCache = false): Promise<DailyContent | null> {
440
+ if (!bypassCache && this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
441
+
397
442
  try {
398
443
  const response = await fetch(`${this.BASE_URL}/icerik`, {
399
444
  headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
400
445
  });
401
446
  if (response.ok) {
402
447
  const data = await response.json() as DailyContent;
403
- if (this.isCacheEnabled) this.dailyContentCache = data;
448
+ if (!bypassCache && this.isCacheEnabled) this.dailyContentCache = data;
404
449
  return data;
405
450
  }
406
451
  } catch {
@@ -443,10 +488,12 @@ export class TDK {
443
488
  * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
444
489
  * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
445
490
  * appears to hand back a single randomly-rotated rule per request, so two
446
- * calls a second apart can return entirely different rules.
491
+ * calls a second apart can return entirely different rules. `bypassCache`
492
+ * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
493
+ * draw even when `enableCache(true)` is on.
447
494
  */
448
- public static async getKurallar(): Promise<TDKRule[]> {
449
- const daily = await this.getDailyContent();
495
+ public static async getKurallar(bypassCache = false): Promise<TDKRule[]> {
496
+ const daily = await this.getDailyContent(bypassCache);
450
497
  return daily?.kural ?? [];
451
498
  }
452
499
 
@@ -456,16 +503,19 @@ export class TDK {
456
503
  * hands back a single randomly-rotated rule per request (out of a pool of
457
504
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
458
505
  * draw would rarely match a given name — this re-draws (bounded, with a
459
- * short delay) until it finds a match or gives up. Returns `null` if no
460
- * match turns up within the attempt budget or the matched page can't be
461
- * parsed.
506
+ * short delay) until it finds a match or gives up. Every attempt bypasses
507
+ * `dailyContentCache` without that, once `enableCache(true)` is on, all
508
+ * 25 attempts would just re-read the same cached `/icerik` response and
509
+ * could never find a rule outside whatever the first draw happened to be.
510
+ * Returns `null` if no match turns up within the attempt budget or the
511
+ * matched page can't be parsed.
462
512
  */
463
513
  public static async getRule(name: string): Promise<string | null> {
464
514
  if (!name || name.trim() === "") return null;
465
515
  const target = name.trim().toLocaleLowerCase("tr-TR");
466
516
 
467
517
  for (let attempt = 0; attempt < 25; attempt++) {
468
- const rules = await this.getKurallar();
518
+ const rules = await this.getKurallar(true);
469
519
  const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
470
520
  if (match) return this.fetchRuleText(match.url);
471
521
  await this.delay(100);