tdk-api-wrapper 1.2.0 → 1.2.2
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 +3 -3
- package/dist/{chunk-2TA5PMVZ.mjs → chunk-SNY3KUCF.mjs} +71 -32
- package/dist/cli.js +71 -32
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +32 -9
- package/dist/index.d.ts +32 -9
- package/dist/index.js +71 -32
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/tdk.ts +90 -38
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
|
|
|
68
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.
|
|
69
69
|
- **`TDK.checkVowelHarmony(word)`**: Kelimenin büyük ünlü uyumuna uyup uymadığını (boolean) kontrol eder.
|
|
70
70
|
- **`TDK.getPartOfSpeech(word)`**: Kelimenin sözcük türünü (isim, sıfat, zarf vb.) döndürür.
|
|
71
|
-
- **`TDK.checkSpelling(word)`**:
|
|
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 Damerau-Levenshtein edit-distance ile en yakın kelimeyi önerir (bitişik harf yer değiştirmelerini de tek düzeltme sayar; örn. `herkez` → `herkes`, `mektub` → `mektup`, `yanlız` → `yalnız`). Kelime sıklığı verisi olmadığı için nadiren aynı mesafedeki iki aday arasında beklenenden farklı biri seçilebilir.
|
|
72
72
|
- **`TDK.getCompoundWords(word)`**: Aranan kelime ile oluşturulmuş birleşik kelimeleri (Örn: dolma kalem) listeler.
|
|
73
73
|
|
|
74
74
|
### 3. Edebi ve Kültürel Analiz
|
|
@@ -79,7 +79,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
|
|
|
79
79
|
- **`TDK.groupByOrigin(words)`**: Bir kelime listesini etimolojik kökenlerine göre gruplar (bulunamayanlar `"Bilinmiyor"` altında toplanır).
|
|
80
80
|
- **`TDK.getSynonyms(word)`** / **`TDK.getAntonyms(word)`**: Kelimenin eş/zıt anlamlılarını döner (undocumented `gts-yeni` endpoint'i üzerinden; sonuç bulunamazsa `[]`).
|
|
81
81
|
- **`TDK.compareWords(a, b)`**: İki kelimeyi anlam sayısı, köken, hece bölünüşü ve büyük ünlü uyumu açısından yan yana karşılaştırır.
|
|
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
|
+
- **`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. Not: TDK yalnızca yalın (sözlük) biçimleri indeksliyor, morfolojik analiz yapmıyor — bu yüzden "evde", "dildir" gibi ek almış kelimeler kökleri (`ev`, `dil`) sözlükte olsa bile `found: false` döner; bu veri kaynağının doğal bir sınırlılığıdır.
|
|
83
83
|
|
|
84
84
|
### 4. Yardımcı Metotlar
|
|
85
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).
|
|
@@ -88,7 +88,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
|
|
|
88
88
|
- **`TDK.getWordOfTheDay()`**: `getDailyContent()`'in üzerine ince bir katman; günün kelimesini ve tüm anlamlarını `{ word, meanings }` şeklinde döner.
|
|
89
89
|
- **`TDK.getRandomWord()`**: Günün içeriğindeki kelime ve atasözü havuzundan rastgele bir tanesini `{ type: "kelime" | "atasoz", madde, anlam }` şeklinde seçer (not: tüm sözlük değil, sadece o günkü içerik havuzundan seçim yapar).
|
|
90
90
|
- **`TDK.getKurallar()`**: TDK'nin `/icerik` akışının o an döndürdüğü yazım kuralı sayfa(lar)ını `{ adi, url }` şeklinde listeler. Not: bu sabit bir katalog değildir — `/icerik` her istekte, yaklaşık yirmi kurallık bir havuzdan rastgele tek bir kural döndürür.
|
|
91
|
-
- **`TDK.getRule(name)`**: Adı verilen (küçük/büyük harf duyarsız, alt dize eşleşmesi) yazım kuralının tam metnini `tdk.gov.tr`'den çekip düz metne çevirir. `getKurallar()`'ın rastgeleliği yüzünden istenen kuralı bulana kadar
|
|
91
|
+
- **`TDK.getRule(name)`**: Adı verilen (küçük/büyük harf duyarsız, alt dize eşleşmesi) yazım kuralının tam metnini `tdk.gov.tr`'den çekip düz metne çevirir. `getKurallar()`'ın rastgeleliği yüzünden istenen kuralı bulana kadar eşzamanlı gruplar hâlinde (toplam en fazla 25 deneme, ~5 round-trip'e sığdırılmış) yeniden dener; bulamazsa veya sayfa ayrıştırılamazsa `null` döner.
|
|
92
92
|
|
|
93
93
|
## Hata Yönetimi
|
|
94
94
|
|
|
@@ -390,29 +390,42 @@ var TDK = class {
|
|
|
390
390
|
if (mixMatch) {
|
|
391
391
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
392
392
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
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
|
+
if (Math.abs(candidate.length - cleanWord.length) > 2)
|
|
403
|
+
continue;
|
|
404
|
+
const distance = this.damerauLevenshtein(cleanWord, candidate);
|
|
405
|
+
if (distance === 0)
|
|
406
|
+
continue;
|
|
407
|
+
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
408
|
+
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
409
|
+
const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
|
|
410
|
+
if (better) {
|
|
411
|
+
best = { candidate, distance, firstMismatch, lengthMismatch };
|
|
407
412
|
}
|
|
408
413
|
}
|
|
414
|
+
if (best && best.distance <= 2) {
|
|
415
|
+
return { isCorrect: false, word, suggestion: best.candidate };
|
|
416
|
+
}
|
|
409
417
|
return { isCorrect: false, word };
|
|
410
418
|
}
|
|
411
419
|
/**
|
|
412
420
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
421
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
422
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
423
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
424
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
425
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
413
426
|
*/
|
|
414
|
-
static async getDailyContent() {
|
|
415
|
-
if (this.isCacheEnabled && this.dailyContentCache)
|
|
427
|
+
static async getDailyContent(bypassCache = false) {
|
|
428
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
|
|
416
429
|
return this.dailyContentCache;
|
|
417
430
|
try {
|
|
418
431
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
@@ -420,7 +433,7 @@ var TDK = class {
|
|
|
420
433
|
});
|
|
421
434
|
if (response.ok) {
|
|
422
435
|
const data = await response.json();
|
|
423
|
-
if (this.isCacheEnabled)
|
|
436
|
+
if (!bypassCache && this.isCacheEnabled)
|
|
424
437
|
this.dailyContentCache = data;
|
|
425
438
|
return data;
|
|
426
439
|
}
|
|
@@ -461,10 +474,12 @@ var TDK = class {
|
|
|
461
474
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
462
475
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
463
476
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
464
|
-
* calls a second apart can return entirely different rules.
|
|
477
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
478
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
479
|
+
* draw even when `enableCache(true)` is on.
|
|
465
480
|
*/
|
|
466
|
-
static async getKurallar() {
|
|
467
|
-
const daily = await this.getDailyContent();
|
|
481
|
+
static async getKurallar(bypassCache = false) {
|
|
482
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
468
483
|
return daily?.kural ?? [];
|
|
469
484
|
}
|
|
470
485
|
/**
|
|
@@ -472,21 +487,33 @@ var TDK = class {
|
|
|
472
487
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
473
488
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
474
489
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
475
|
-
* draw would rarely match a given name — this re-draws
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
490
|
+
* draw would rarely match a given name — this re-draws until it finds a
|
|
491
|
+
* match or gives up. Draws happen in concurrent batches (each `/icerik`
|
|
492
|
+
* request is independent and stateless) rather than one-at-a-time with a
|
|
493
|
+
* delay: same total sample size (25) and hit probability as a sequential
|
|
494
|
+
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
495
|
+
* a miss resolves in roughly one round-trip time instead of several
|
|
496
|
+
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
497
|
+
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
498
|
+
* cached `/icerik` response and could never find a rule outside whatever
|
|
499
|
+
* the first draw happened to be. Returns `null` if no match turns up
|
|
500
|
+
* within the attempt budget or the matched page can't be parsed.
|
|
479
501
|
*/
|
|
480
502
|
static async getRule(name) {
|
|
481
503
|
if (!name || name.trim() === "")
|
|
482
504
|
return null;
|
|
483
505
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
506
|
+
const BATCH_SIZE = 5;
|
|
507
|
+
const ROUNDS = 5;
|
|
508
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
509
|
+
const batches = await Promise.all(
|
|
510
|
+
Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
|
|
511
|
+
);
|
|
512
|
+
for (const rules of batches) {
|
|
513
|
+
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
514
|
+
if (match)
|
|
515
|
+
return this.fetchRuleText(match.url);
|
|
516
|
+
}
|
|
490
517
|
}
|
|
491
518
|
return null;
|
|
492
519
|
}
|
|
@@ -634,6 +661,11 @@ var TDK = class {
|
|
|
634
661
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
635
662
|
* returning each word's first meaning and etymological origin if found.
|
|
636
663
|
* Looks each word up individually (throttled), so scales with text length.
|
|
664
|
+
* TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
|
|
665
|
+
* it does no morphological analysis, and neither does this method: a
|
|
666
|
+
* suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
|
|
667
|
+
* suffix) will come back `found: false` even though the root is a real
|
|
668
|
+
* headword. This is an inherent limitation of the data source, not a bug.
|
|
637
669
|
*/
|
|
638
670
|
static async analyzeText(text) {
|
|
639
671
|
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
@@ -653,9 +685,13 @@ var TDK = class {
|
|
|
653
685
|
return analyses;
|
|
654
686
|
}
|
|
655
687
|
/**
|
|
656
|
-
*
|
|
688
|
+
* Damerau-Levenshtein edit-distance (optimal string alignment variant):
|
|
689
|
+
* like classic Levenshtein but also counts an adjacent-character
|
|
690
|
+
* transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
|
|
691
|
+
* two substitutions — a very common class of typo that plain Levenshtein
|
|
692
|
+
* otherwise misses.
|
|
657
693
|
*/
|
|
658
|
-
static
|
|
694
|
+
static damerauLevenshtein(a, b) {
|
|
659
695
|
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
660
696
|
for (let i = 0; i <= a.length; i++)
|
|
661
697
|
dp[i][0] = i;
|
|
@@ -665,6 +701,9 @@ var TDK = class {
|
|
|
665
701
|
for (let j = 1; j <= b.length; j++) {
|
|
666
702
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
667
703
|
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
704
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
705
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
|
|
706
|
+
}
|
|
668
707
|
}
|
|
669
708
|
}
|
|
670
709
|
return dp[a.length][b.length];
|
package/dist/cli.js
CHANGED
|
@@ -415,29 +415,42 @@ var TDK = class {
|
|
|
415
415
|
if (mixMatch) {
|
|
416
416
|
return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
|
|
417
417
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
|
|
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
|
+
if (Math.abs(candidate.length - cleanWord.length) > 2)
|
|
428
|
+
continue;
|
|
429
|
+
const distance = this.damerauLevenshtein(cleanWord, candidate);
|
|
430
|
+
if (distance === 0)
|
|
431
|
+
continue;
|
|
432
|
+
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
433
|
+
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
434
|
+
const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
|
|
435
|
+
if (better) {
|
|
436
|
+
best = { candidate, distance, firstMismatch, lengthMismatch };
|
|
432
437
|
}
|
|
433
438
|
}
|
|
439
|
+
if (best && best.distance <= 2) {
|
|
440
|
+
return { isCorrect: false, word: word2, suggestion: best.candidate };
|
|
441
|
+
}
|
|
434
442
|
return { isCorrect: false, word: word2 };
|
|
435
443
|
}
|
|
436
444
|
/**
|
|
437
445
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
446
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
447
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
448
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
449
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
450
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
438
451
|
*/
|
|
439
|
-
static async getDailyContent() {
|
|
440
|
-
if (this.isCacheEnabled && this.dailyContentCache)
|
|
452
|
+
static async getDailyContent(bypassCache = false) {
|
|
453
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
|
|
441
454
|
return this.dailyContentCache;
|
|
442
455
|
try {
|
|
443
456
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
@@ -445,7 +458,7 @@ var TDK = class {
|
|
|
445
458
|
});
|
|
446
459
|
if (response.ok) {
|
|
447
460
|
const data = await response.json();
|
|
448
|
-
if (this.isCacheEnabled)
|
|
461
|
+
if (!bypassCache && this.isCacheEnabled)
|
|
449
462
|
this.dailyContentCache = data;
|
|
450
463
|
return data;
|
|
451
464
|
}
|
|
@@ -486,10 +499,12 @@ var TDK = class {
|
|
|
486
499
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
487
500
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
488
501
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
489
|
-
* calls a second apart can return entirely different rules.
|
|
502
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
503
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
504
|
+
* draw even when `enableCache(true)` is on.
|
|
490
505
|
*/
|
|
491
|
-
static async getKurallar() {
|
|
492
|
-
const daily = await this.getDailyContent();
|
|
506
|
+
static async getKurallar(bypassCache = false) {
|
|
507
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
493
508
|
return daily?.kural ?? [];
|
|
494
509
|
}
|
|
495
510
|
/**
|
|
@@ -497,21 +512,33 @@ var TDK = class {
|
|
|
497
512
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
498
513
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
499
514
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
500
|
-
* draw would rarely match a given name — this re-draws
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
515
|
+
* draw would rarely match a given name — this re-draws until it finds a
|
|
516
|
+
* match or gives up. Draws happen in concurrent batches (each `/icerik`
|
|
517
|
+
* request is independent and stateless) rather than one-at-a-time with a
|
|
518
|
+
* delay: same total sample size (25) and hit probability as a sequential
|
|
519
|
+
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
520
|
+
* a miss resolves in roughly one round-trip time instead of several
|
|
521
|
+
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
522
|
+
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
523
|
+
* cached `/icerik` response and could never find a rule outside whatever
|
|
524
|
+
* the first draw happened to be. Returns `null` if no match turns up
|
|
525
|
+
* within the attempt budget or the matched page can't be parsed.
|
|
504
526
|
*/
|
|
505
527
|
static async getRule(name) {
|
|
506
528
|
if (!name || name.trim() === "")
|
|
507
529
|
return null;
|
|
508
530
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
531
|
+
const BATCH_SIZE = 5;
|
|
532
|
+
const ROUNDS = 5;
|
|
533
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
534
|
+
const batches = await Promise.all(
|
|
535
|
+
Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
|
|
536
|
+
);
|
|
537
|
+
for (const rules of batches) {
|
|
538
|
+
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
539
|
+
if (match)
|
|
540
|
+
return this.fetchRuleText(match.url);
|
|
541
|
+
}
|
|
515
542
|
}
|
|
516
543
|
return null;
|
|
517
544
|
}
|
|
@@ -659,6 +686,11 @@ var TDK = class {
|
|
|
659
686
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
660
687
|
* returning each word's first meaning and etymological origin if found.
|
|
661
688
|
* Looks each word up individually (throttled), so scales with text length.
|
|
689
|
+
* TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
|
|
690
|
+
* it does no morphological analysis, and neither does this method: a
|
|
691
|
+
* suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
|
|
692
|
+
* suffix) will come back `found: false` even though the root is a real
|
|
693
|
+
* headword. This is an inherent limitation of the data source, not a bug.
|
|
662
694
|
*/
|
|
663
695
|
static async analyzeText(text) {
|
|
664
696
|
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
@@ -678,9 +710,13 @@ var TDK = class {
|
|
|
678
710
|
return analyses;
|
|
679
711
|
}
|
|
680
712
|
/**
|
|
681
|
-
*
|
|
713
|
+
* Damerau-Levenshtein edit-distance (optimal string alignment variant):
|
|
714
|
+
* like classic Levenshtein but also counts an adjacent-character
|
|
715
|
+
* transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
|
|
716
|
+
* two substitutions — a very common class of typo that plain Levenshtein
|
|
717
|
+
* otherwise misses.
|
|
682
718
|
*/
|
|
683
|
-
static
|
|
719
|
+
static damerauLevenshtein(a, b) {
|
|
684
720
|
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
685
721
|
for (let i = 0; i <= a.length; i++)
|
|
686
722
|
dp[i][0] = i;
|
|
@@ -690,6 +726,9 @@ var TDK = class {
|
|
|
690
726
|
for (let j = 1; j <= b.length; j++) {
|
|
691
727
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
692
728
|
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
729
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
730
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
|
|
731
|
+
}
|
|
693
732
|
}
|
|
694
733
|
}
|
|
695
734
|
return dp[a.length][b.length];
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -244,8 +244,13 @@ declare class TDK {
|
|
|
244
244
|
static checkSpelling(word: string): Promise<SpellCheckResult>;
|
|
245
245
|
/**
|
|
246
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.
|
|
247
252
|
*/
|
|
248
|
-
static getDailyContent(): Promise<DailyContent | null>;
|
|
253
|
+
static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
|
|
249
254
|
/**
|
|
250
255
|
* Returns today's word of the day along with all of its listed meanings.
|
|
251
256
|
*/
|
|
@@ -260,18 +265,27 @@ declare class TDK {
|
|
|
260
265
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
261
266
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
262
267
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
263
|
-
* 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.
|
|
264
271
|
*/
|
|
265
|
-
static getKurallar(): Promise<TDKRule[]>;
|
|
272
|
+
static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
|
|
266
273
|
/**
|
|
267
274
|
* Fetches the full plain-text content of a named spelling rule (matched
|
|
268
275
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
269
276
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
270
277
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
271
|
-
* draw would rarely match a given name — this re-draws
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
278
|
+
* draw would rarely match a given name — this re-draws until it finds a
|
|
279
|
+
* match or gives up. Draws happen in concurrent batches (each `/icerik`
|
|
280
|
+
* request is independent and stateless) rather than one-at-a-time with a
|
|
281
|
+
* delay: same total sample size (25) and hit probability as a sequential
|
|
282
|
+
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
283
|
+
* a miss resolves in roughly one round-trip time instead of several
|
|
284
|
+
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
285
|
+
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
286
|
+
* cached `/icerik` response and could never find a rule outside whatever
|
|
287
|
+
* the first draw happened to be. Returns `null` if no match turns up
|
|
288
|
+
* within the attempt budget or the matched page can't be parsed.
|
|
275
289
|
*/
|
|
276
290
|
static getRule(name: string): Promise<string | null>;
|
|
277
291
|
/**
|
|
@@ -304,12 +318,21 @@ declare class TDK {
|
|
|
304
318
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
305
319
|
* returning each word's first meaning and etymological origin if found.
|
|
306
320
|
* Looks each word up individually (throttled), so scales with text length.
|
|
321
|
+
* TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
|
|
322
|
+
* it does no morphological analysis, and neither does this method: a
|
|
323
|
+
* suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
|
|
324
|
+
* suffix) will come back `found: false` even though the root is a real
|
|
325
|
+
* headword. This is an inherent limitation of the data source, not a bug.
|
|
307
326
|
*/
|
|
308
327
|
static analyzeText(text: string): Promise<WordAnalysis[]>;
|
|
309
328
|
/**
|
|
310
|
-
*
|
|
329
|
+
* Damerau-Levenshtein edit-distance (optimal string alignment variant):
|
|
330
|
+
* like classic Levenshtein but also counts an adjacent-character
|
|
331
|
+
* transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
|
|
332
|
+
* two substitutions — a very common class of typo that plain Levenshtein
|
|
333
|
+
* otherwise misses.
|
|
311
334
|
*/
|
|
312
|
-
private static
|
|
335
|
+
private static damerauLevenshtein;
|
|
313
336
|
/**
|
|
314
337
|
* Fetches multiple words concurrently with a small delay to avoid rate limiting.
|
|
315
338
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -244,8 +244,13 @@ declare class TDK {
|
|
|
244
244
|
static checkSpelling(word: string): Promise<SpellCheckResult>;
|
|
245
245
|
/**
|
|
246
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.
|
|
247
252
|
*/
|
|
248
|
-
static getDailyContent(): Promise<DailyContent | null>;
|
|
253
|
+
static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
|
|
249
254
|
/**
|
|
250
255
|
* Returns today's word of the day along with all of its listed meanings.
|
|
251
256
|
*/
|
|
@@ -260,18 +265,27 @@ declare class TDK {
|
|
|
260
265
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
261
266
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
262
267
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
263
|
-
* 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.
|
|
264
271
|
*/
|
|
265
|
-
static getKurallar(): Promise<TDKRule[]>;
|
|
272
|
+
static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
|
|
266
273
|
/**
|
|
267
274
|
* Fetches the full plain-text content of a named spelling rule (matched
|
|
268
275
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
269
276
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
270
277
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
271
|
-
* draw would rarely match a given name — this re-draws
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
278
|
+
* draw would rarely match a given name — this re-draws until it finds a
|
|
279
|
+
* match or gives up. Draws happen in concurrent batches (each `/icerik`
|
|
280
|
+
* request is independent and stateless) rather than one-at-a-time with a
|
|
281
|
+
* delay: same total sample size (25) and hit probability as a sequential
|
|
282
|
+
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
283
|
+
* a miss resolves in roughly one round-trip time instead of several
|
|
284
|
+
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
285
|
+
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
286
|
+
* cached `/icerik` response and could never find a rule outside whatever
|
|
287
|
+
* the first draw happened to be. Returns `null` if no match turns up
|
|
288
|
+
* within the attempt budget or the matched page can't be parsed.
|
|
275
289
|
*/
|
|
276
290
|
static getRule(name: string): Promise<string | null>;
|
|
277
291
|
/**
|
|
@@ -304,12 +318,21 @@ declare class TDK {
|
|
|
304
318
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
305
319
|
* returning each word's first meaning and etymological origin if found.
|
|
306
320
|
* Looks each word up individually (throttled), so scales with text length.
|
|
321
|
+
* TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
|
|
322
|
+
* it does no morphological analysis, and neither does this method: a
|
|
323
|
+
* suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
|
|
324
|
+
* suffix) will come back `found: false` even though the root is a real
|
|
325
|
+
* headword. This is an inherent limitation of the data source, not a bug.
|
|
307
326
|
*/
|
|
308
327
|
static analyzeText(text: string): Promise<WordAnalysis[]>;
|
|
309
328
|
/**
|
|
310
|
-
*
|
|
329
|
+
* Damerau-Levenshtein edit-distance (optimal string alignment variant):
|
|
330
|
+
* like classic Levenshtein but also counts an adjacent-character
|
|
331
|
+
* transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
|
|
332
|
+
* two substitutions — a very common class of typo that plain Levenshtein
|
|
333
|
+
* otherwise misses.
|
|
311
334
|
*/
|
|
312
|
-
private static
|
|
335
|
+
private static damerauLevenshtein;
|
|
313
336
|
/**
|
|
314
337
|
* Fetches multiple words concurrently with a small delay to avoid rate limiting.
|
|
315
338
|
*/
|
package/dist/index.js
CHANGED
|
@@ -429,29 +429,42 @@ var TDK = class {
|
|
|
429
429
|
if (mixMatch) {
|
|
430
430
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
431
431
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (
|
|
445
|
-
|
|
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
|
+
if (Math.abs(candidate.length - cleanWord.length) > 2)
|
|
442
|
+
continue;
|
|
443
|
+
const distance = this.damerauLevenshtein(cleanWord, candidate);
|
|
444
|
+
if (distance === 0)
|
|
445
|
+
continue;
|
|
446
|
+
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
447
|
+
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
448
|
+
const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
|
|
449
|
+
if (better) {
|
|
450
|
+
best = { candidate, distance, firstMismatch, lengthMismatch };
|
|
446
451
|
}
|
|
447
452
|
}
|
|
453
|
+
if (best && best.distance <= 2) {
|
|
454
|
+
return { isCorrect: false, word, suggestion: best.candidate };
|
|
455
|
+
}
|
|
448
456
|
return { isCorrect: false, word };
|
|
449
457
|
}
|
|
450
458
|
/**
|
|
451
459
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
460
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
461
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
462
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
463
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
464
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
452
465
|
*/
|
|
453
|
-
static async getDailyContent() {
|
|
454
|
-
if (this.isCacheEnabled && this.dailyContentCache)
|
|
466
|
+
static async getDailyContent(bypassCache = false) {
|
|
467
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
|
|
455
468
|
return this.dailyContentCache;
|
|
456
469
|
try {
|
|
457
470
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
@@ -459,7 +472,7 @@ var TDK = class {
|
|
|
459
472
|
});
|
|
460
473
|
if (response.ok) {
|
|
461
474
|
const data = await response.json();
|
|
462
|
-
if (this.isCacheEnabled)
|
|
475
|
+
if (!bypassCache && this.isCacheEnabled)
|
|
463
476
|
this.dailyContentCache = data;
|
|
464
477
|
return data;
|
|
465
478
|
}
|
|
@@ -500,10 +513,12 @@ var TDK = class {
|
|
|
500
513
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
501
514
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
502
515
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
503
|
-
* calls a second apart can return entirely different rules.
|
|
516
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
517
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
518
|
+
* draw even when `enableCache(true)` is on.
|
|
504
519
|
*/
|
|
505
|
-
static async getKurallar() {
|
|
506
|
-
const daily = await this.getDailyContent();
|
|
520
|
+
static async getKurallar(bypassCache = false) {
|
|
521
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
507
522
|
return daily?.kural ?? [];
|
|
508
523
|
}
|
|
509
524
|
/**
|
|
@@ -511,21 +526,33 @@ var TDK = class {
|
|
|
511
526
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
512
527
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
513
528
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
514
|
-
* draw would rarely match a given name — this re-draws
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
529
|
+
* draw would rarely match a given name — this re-draws until it finds a
|
|
530
|
+
* match or gives up. Draws happen in concurrent batches (each `/icerik`
|
|
531
|
+
* request is independent and stateless) rather than one-at-a-time with a
|
|
532
|
+
* delay: same total sample size (25) and hit probability as a sequential
|
|
533
|
+
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
534
|
+
* a miss resolves in roughly one round-trip time instead of several
|
|
535
|
+
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
536
|
+
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
537
|
+
* cached `/icerik` response and could never find a rule outside whatever
|
|
538
|
+
* the first draw happened to be. Returns `null` if no match turns up
|
|
539
|
+
* within the attempt budget or the matched page can't be parsed.
|
|
518
540
|
*/
|
|
519
541
|
static async getRule(name) {
|
|
520
542
|
if (!name || name.trim() === "")
|
|
521
543
|
return null;
|
|
522
544
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
545
|
+
const BATCH_SIZE = 5;
|
|
546
|
+
const ROUNDS = 5;
|
|
547
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
548
|
+
const batches = await Promise.all(
|
|
549
|
+
Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
|
|
550
|
+
);
|
|
551
|
+
for (const rules of batches) {
|
|
552
|
+
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
553
|
+
if (match)
|
|
554
|
+
return this.fetchRuleText(match.url);
|
|
555
|
+
}
|
|
529
556
|
}
|
|
530
557
|
return null;
|
|
531
558
|
}
|
|
@@ -673,6 +700,11 @@ var TDK = class {
|
|
|
673
700
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
674
701
|
* returning each word's first meaning and etymological origin if found.
|
|
675
702
|
* Looks each word up individually (throttled), so scales with text length.
|
|
703
|
+
* TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
|
|
704
|
+
* it does no morphological analysis, and neither does this method: a
|
|
705
|
+
* suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
|
|
706
|
+
* suffix) will come back `found: false` even though the root is a real
|
|
707
|
+
* headword. This is an inherent limitation of the data source, not a bug.
|
|
676
708
|
*/
|
|
677
709
|
static async analyzeText(text) {
|
|
678
710
|
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
@@ -692,9 +724,13 @@ var TDK = class {
|
|
|
692
724
|
return analyses;
|
|
693
725
|
}
|
|
694
726
|
/**
|
|
695
|
-
*
|
|
727
|
+
* Damerau-Levenshtein edit-distance (optimal string alignment variant):
|
|
728
|
+
* like classic Levenshtein but also counts an adjacent-character
|
|
729
|
+
* transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
|
|
730
|
+
* two substitutions — a very common class of typo that plain Levenshtein
|
|
731
|
+
* otherwise misses.
|
|
696
732
|
*/
|
|
697
|
-
static
|
|
733
|
+
static damerauLevenshtein(a, b) {
|
|
698
734
|
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
699
735
|
for (let i = 0; i <= a.length; i++)
|
|
700
736
|
dp[i][0] = i;
|
|
@@ -704,6 +740,9 @@ var TDK = class {
|
|
|
704
740
|
for (let j = 1; j <= b.length; j++) {
|
|
705
741
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
706
742
|
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
743
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
744
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
|
|
745
|
+
}
|
|
707
746
|
}
|
|
708
747
|
}
|
|
709
748
|
return dp[a.length][b.length];
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
package/src/tdk.ts
CHANGED
|
@@ -389,8 +389,10 @@ export class TDK {
|
|
|
389
389
|
if (results.length > 0) {
|
|
390
390
|
return { isCorrect: true, word };
|
|
391
391
|
}
|
|
392
|
-
|
|
393
|
-
// 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).
|
|
394
396
|
const daily = await this.getDailyContent();
|
|
395
397
|
if (daily) {
|
|
396
398
|
const syydMatch = daily.syyd.find(s => s.yanliskelime.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
|
|
@@ -401,43 +403,67 @@ export class TDK {
|
|
|
401
403
|
if (mixMatch) {
|
|
402
404
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
403
405
|
}
|
|
406
|
+
}
|
|
404
407
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
// Candidates whose length differs too much are skipped before running
|
|
413
|
+
// the O(n*m) distance calculation, both for speed and because a huge
|
|
414
|
+
// length gap can't be within the distance threshold anyway. Ties (same
|
|
415
|
+
// distance) prefer a matching first letter, then a matching length —
|
|
416
|
+
// typos rarely change the first letter, and this avoids picking
|
|
417
|
+
// whatever happens to sort alphabetically first. There's no word
|
|
418
|
+
// frequency data available, so a genuine tie can still land on a
|
|
419
|
+
// technically-correct but less commonly intended word.
|
|
420
|
+
if (this.autocompleteCache.length === 0) {
|
|
421
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
422
|
+
}
|
|
423
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
424
|
+
let best: { candidate: string; distance: number; firstMismatch: number; lengthMismatch: number } | null = null;
|
|
425
|
+
for (const candidate of this.autocompleteCache) {
|
|
426
|
+
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
|
|
427
|
+
if (Math.abs(candidate.length - cleanWord.length) > 2) continue;
|
|
428
|
+
|
|
429
|
+
const distance = this.damerauLevenshtein(cleanWord, candidate);
|
|
430
|
+
if (distance === 0) continue;
|
|
431
|
+
|
|
432
|
+
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
433
|
+
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
434
|
+
const better =
|
|
435
|
+
!best ||
|
|
436
|
+
distance < best.distance ||
|
|
437
|
+
(distance === best.distance && firstMismatch < best.firstMismatch) ||
|
|
438
|
+
(distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch);
|
|
439
|
+
if (better) {
|
|
440
|
+
best = { candidate, distance, firstMismatch, lengthMismatch };
|
|
423
441
|
}
|
|
424
442
|
}
|
|
443
|
+
if (best && best.distance <= 2) {
|
|
444
|
+
return { isCorrect: false, word, suggestion: best.candidate };
|
|
445
|
+
}
|
|
425
446
|
return { isCorrect: false, word };
|
|
426
447
|
}
|
|
427
448
|
|
|
428
449
|
/**
|
|
429
450
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
451
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
452
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
453
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
454
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
455
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
430
456
|
*/
|
|
431
|
-
public static async getDailyContent(): Promise<DailyContent | null> {
|
|
432
|
-
if (this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
|
|
433
|
-
|
|
457
|
+
public static async getDailyContent(bypassCache = false): Promise<DailyContent | null> {
|
|
458
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
|
|
459
|
+
|
|
434
460
|
try {
|
|
435
461
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
436
462
|
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
|
|
437
463
|
});
|
|
438
464
|
if (response.ok) {
|
|
439
465
|
const data = await response.json() as DailyContent;
|
|
440
|
-
if (this.isCacheEnabled) this.dailyContentCache = data;
|
|
466
|
+
if (!bypassCache && this.isCacheEnabled) this.dailyContentCache = data;
|
|
441
467
|
return data;
|
|
442
468
|
}
|
|
443
469
|
} catch {
|
|
@@ -480,10 +506,12 @@ export class TDK {
|
|
|
480
506
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
481
507
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
482
508
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
483
|
-
* calls a second apart can return entirely different rules.
|
|
509
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
510
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
511
|
+
* draw even when `enableCache(true)` is on.
|
|
484
512
|
*/
|
|
485
|
-
public static async getKurallar(): Promise<TDKRule[]> {
|
|
486
|
-
const daily = await this.getDailyContent();
|
|
513
|
+
public static async getKurallar(bypassCache = false): Promise<TDKRule[]> {
|
|
514
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
487
515
|
return daily?.kural ?? [];
|
|
488
516
|
}
|
|
489
517
|
|
|
@@ -492,20 +520,32 @@ export class TDK {
|
|
|
492
520
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
493
521
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
494
522
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
495
|
-
* draw would rarely match a given name — this re-draws
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
523
|
+
* draw would rarely match a given name — this re-draws until it finds a
|
|
524
|
+
* match or gives up. Draws happen in concurrent batches (each `/icerik`
|
|
525
|
+
* request is independent and stateless) rather than one-at-a-time with a
|
|
526
|
+
* delay: same total sample size (25) and hit probability as a sequential
|
|
527
|
+
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
528
|
+
* a miss resolves in roughly one round-trip time instead of several
|
|
529
|
+
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
530
|
+
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
531
|
+
* cached `/icerik` response and could never find a rule outside whatever
|
|
532
|
+
* the first draw happened to be. Returns `null` if no match turns up
|
|
533
|
+
* within the attempt budget or the matched page can't be parsed.
|
|
499
534
|
*/
|
|
500
535
|
public static async getRule(name: string): Promise<string | null> {
|
|
501
536
|
if (!name || name.trim() === "") return null;
|
|
502
537
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
503
538
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
539
|
+
const BATCH_SIZE = 5;
|
|
540
|
+
const ROUNDS = 5;
|
|
541
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
542
|
+
const batches = await Promise.all(
|
|
543
|
+
Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
|
|
544
|
+
);
|
|
545
|
+
for (const rules of batches) {
|
|
546
|
+
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
547
|
+
if (match) return this.fetchRuleText(match.url);
|
|
548
|
+
}
|
|
509
549
|
}
|
|
510
550
|
return null;
|
|
511
551
|
}
|
|
@@ -641,6 +681,11 @@ export class TDK {
|
|
|
641
681
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
642
682
|
* returning each word's first meaning and etymological origin if found.
|
|
643
683
|
* Looks each word up individually (throttled), so scales with text length.
|
|
684
|
+
* TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
|
|
685
|
+
* it does no morphological analysis, and neither does this method: a
|
|
686
|
+
* suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
|
|
687
|
+
* suffix) will come back `found: false` even though the root is a real
|
|
688
|
+
* headword. This is an inherent limitation of the data source, not a bug.
|
|
644
689
|
*/
|
|
645
690
|
public static async analyzeText(text: string): Promise<WordAnalysis[]> {
|
|
646
691
|
const words = text
|
|
@@ -666,9 +711,13 @@ export class TDK {
|
|
|
666
711
|
}
|
|
667
712
|
|
|
668
713
|
/**
|
|
669
|
-
*
|
|
714
|
+
* Damerau-Levenshtein edit-distance (optimal string alignment variant):
|
|
715
|
+
* like classic Levenshtein but also counts an adjacent-character
|
|
716
|
+
* transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
|
|
717
|
+
* two substitutions — a very common class of typo that plain Levenshtein
|
|
718
|
+
* otherwise misses.
|
|
670
719
|
*/
|
|
671
|
-
private static
|
|
720
|
+
private static damerauLevenshtein(a: string, b: string): number {
|
|
672
721
|
const dp: number[][] = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
673
722
|
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
674
723
|
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
@@ -676,6 +725,9 @@ export class TDK {
|
|
|
676
725
|
for (let j = 1; j <= b.length; j++) {
|
|
677
726
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
678
727
|
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
728
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
729
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
|
|
730
|
+
}
|
|
679
731
|
}
|
|
680
732
|
}
|
|
681
733
|
return dp[a.length][b.length];
|