tdk-api-wrapper 1.2.1 → 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-6BTOGV2M.mjs → chunk-SNY3KUCF.mjs} +46 -20
- package/dist/cli.js +46 -20
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +22 -9
- package/dist/index.d.ts +22 -9
- package/dist/index.js +46 -20
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/tdk.ts +58 -19
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)`**: 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
|
|
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
|
|
|
@@ -399,11 +399,16 @@ var TDK = class {
|
|
|
399
399
|
for (const candidate of this.autocompleteCache) {
|
|
400
400
|
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
401
401
|
continue;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
}
|
|
409
414
|
if (best && best.distance <= 2) {
|
|
@@ -482,24 +487,33 @@ var TDK = class {
|
|
|
482
487
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
483
488
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
484
489
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
485
|
-
* draw would rarely match a given name — this re-draws
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
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.
|
|
492
501
|
*/
|
|
493
502
|
static async getRule(name) {
|
|
494
503
|
if (!name || name.trim() === "")
|
|
495
504
|
return null;
|
|
496
505
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
+
}
|
|
503
517
|
}
|
|
504
518
|
return null;
|
|
505
519
|
}
|
|
@@ -647,6 +661,11 @@ var TDK = class {
|
|
|
647
661
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
648
662
|
* returning each word's first meaning and etymological origin if found.
|
|
649
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.
|
|
650
669
|
*/
|
|
651
670
|
static async analyzeText(text) {
|
|
652
671
|
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
@@ -666,9 +685,13 @@ var TDK = class {
|
|
|
666
685
|
return analyses;
|
|
667
686
|
}
|
|
668
687
|
/**
|
|
669
|
-
*
|
|
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.
|
|
670
693
|
*/
|
|
671
|
-
static
|
|
694
|
+
static damerauLevenshtein(a, b) {
|
|
672
695
|
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
673
696
|
for (let i = 0; i <= a.length; i++)
|
|
674
697
|
dp[i][0] = i;
|
|
@@ -678,6 +701,9 @@ var TDK = class {
|
|
|
678
701
|
for (let j = 1; j <= b.length; j++) {
|
|
679
702
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
680
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
|
+
}
|
|
681
707
|
}
|
|
682
708
|
}
|
|
683
709
|
return dp[a.length][b.length];
|
package/dist/cli.js
CHANGED
|
@@ -424,11 +424,16 @@ var TDK = class {
|
|
|
424
424
|
for (const candidate of this.autocompleteCache) {
|
|
425
425
|
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
426
426
|
continue;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
}
|
|
434
439
|
if (best && best.distance <= 2) {
|
|
@@ -507,24 +512,33 @@ var TDK = class {
|
|
|
507
512
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
508
513
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
509
514
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
510
|
-
* draw would rarely match a given name — this re-draws
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
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.
|
|
517
526
|
*/
|
|
518
527
|
static async getRule(name) {
|
|
519
528
|
if (!name || name.trim() === "")
|
|
520
529
|
return null;
|
|
521
530
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
+
}
|
|
528
542
|
}
|
|
529
543
|
return null;
|
|
530
544
|
}
|
|
@@ -672,6 +686,11 @@ var TDK = class {
|
|
|
672
686
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
673
687
|
* returning each word's first meaning and etymological origin if found.
|
|
674
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.
|
|
675
694
|
*/
|
|
676
695
|
static async analyzeText(text) {
|
|
677
696
|
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
@@ -691,9 +710,13 @@ var TDK = class {
|
|
|
691
710
|
return analyses;
|
|
692
711
|
}
|
|
693
712
|
/**
|
|
694
|
-
*
|
|
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.
|
|
695
718
|
*/
|
|
696
|
-
static
|
|
719
|
+
static damerauLevenshtein(a, b) {
|
|
697
720
|
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
698
721
|
for (let i = 0; i <= a.length; i++)
|
|
699
722
|
dp[i][0] = i;
|
|
@@ -703,6 +726,9 @@ var TDK = class {
|
|
|
703
726
|
for (let j = 1; j <= b.length; j++) {
|
|
704
727
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
705
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
|
+
}
|
|
706
732
|
}
|
|
707
733
|
}
|
|
708
734
|
return dp[a.length][b.length];
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -275,13 +275,17 @@ declare class TDK {
|
|
|
275
275
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
276
276
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
277
277
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
278
|
-
* draw would rarely match a given name — this re-draws
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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.
|
|
285
289
|
*/
|
|
286
290
|
static getRule(name: string): Promise<string | null>;
|
|
287
291
|
/**
|
|
@@ -314,12 +318,21 @@ declare class TDK {
|
|
|
314
318
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
315
319
|
* returning each word's first meaning and etymological origin if found.
|
|
316
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.
|
|
317
326
|
*/
|
|
318
327
|
static analyzeText(text: string): Promise<WordAnalysis[]>;
|
|
319
328
|
/**
|
|
320
|
-
*
|
|
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.
|
|
321
334
|
*/
|
|
322
|
-
private static
|
|
335
|
+
private static damerauLevenshtein;
|
|
323
336
|
/**
|
|
324
337
|
* Fetches multiple words concurrently with a small delay to avoid rate limiting.
|
|
325
338
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -275,13 +275,17 @@ declare class TDK {
|
|
|
275
275
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
276
276
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
277
277
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
278
|
-
* draw would rarely match a given name — this re-draws
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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.
|
|
285
289
|
*/
|
|
286
290
|
static getRule(name: string): Promise<string | null>;
|
|
287
291
|
/**
|
|
@@ -314,12 +318,21 @@ declare class TDK {
|
|
|
314
318
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
315
319
|
* returning each word's first meaning and etymological origin if found.
|
|
316
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.
|
|
317
326
|
*/
|
|
318
327
|
static analyzeText(text: string): Promise<WordAnalysis[]>;
|
|
319
328
|
/**
|
|
320
|
-
*
|
|
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.
|
|
321
334
|
*/
|
|
322
|
-
private static
|
|
335
|
+
private static damerauLevenshtein;
|
|
323
336
|
/**
|
|
324
337
|
* Fetches multiple words concurrently with a small delay to avoid rate limiting.
|
|
325
338
|
*/
|
package/dist/index.js
CHANGED
|
@@ -438,11 +438,16 @@ var TDK = class {
|
|
|
438
438
|
for (const candidate of this.autocompleteCache) {
|
|
439
439
|
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
440
440
|
continue;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
}
|
|
448
453
|
if (best && best.distance <= 2) {
|
|
@@ -521,24 +526,33 @@ var TDK = class {
|
|
|
521
526
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
522
527
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
523
528
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
524
|
-
* draw would rarely match a given name — this re-draws
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
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.
|
|
531
540
|
*/
|
|
532
541
|
static async getRule(name) {
|
|
533
542
|
if (!name || name.trim() === "")
|
|
534
543
|
return null;
|
|
535
544
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
+
}
|
|
542
556
|
}
|
|
543
557
|
return null;
|
|
544
558
|
}
|
|
@@ -686,6 +700,11 @@ var TDK = class {
|
|
|
686
700
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
687
701
|
* returning each word's first meaning and etymological origin if found.
|
|
688
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.
|
|
689
708
|
*/
|
|
690
709
|
static async analyzeText(text) {
|
|
691
710
|
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
@@ -705,9 +724,13 @@ var TDK = class {
|
|
|
705
724
|
return analyses;
|
|
706
725
|
}
|
|
707
726
|
/**
|
|
708
|
-
*
|
|
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.
|
|
709
732
|
*/
|
|
710
|
-
static
|
|
733
|
+
static damerauLevenshtein(a, b) {
|
|
711
734
|
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
712
735
|
for (let i = 0; i <= a.length; i++)
|
|
713
736
|
dp[i][0] = i;
|
|
@@ -717,6 +740,9 @@ var TDK = class {
|
|
|
717
740
|
for (let j = 1; j <= b.length; j++) {
|
|
718
741
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
719
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
|
+
}
|
|
720
746
|
}
|
|
721
747
|
}
|
|
722
748
|
return dp[a.length][b.length];
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
package/src/tdk.ts
CHANGED
|
@@ -409,17 +409,35 @@ export class TDK {
|
|
|
409
409
|
// headword (by edit distance) across TDK's full ~81k-word list (the same
|
|
410
410
|
// data `getSuggestions()` uses). Restricted to single-token, lowercase
|
|
411
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.
|
|
412
420
|
if (this.autocompleteCache.length === 0) {
|
|
413
421
|
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
414
422
|
}
|
|
415
423
|
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
416
|
-
let best: { candidate: string; distance: number } | null = null;
|
|
424
|
+
let best: { candidate: string; distance: number; firstMismatch: number; lengthMismatch: number } | null = null;
|
|
417
425
|
for (const candidate of this.autocompleteCache) {
|
|
418
426
|
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
}
|
|
425
443
|
if (best && best.distance <= 2) {
|
|
@@ -502,23 +520,32 @@ export class TDK {
|
|
|
502
520
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
503
521
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
504
522
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
505
|
-
* draw would rarely match a given name — this re-draws
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
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.
|
|
512
534
|
*/
|
|
513
535
|
public static async getRule(name: string): Promise<string | null> {
|
|
514
536
|
if (!name || name.trim() === "") return null;
|
|
515
537
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
516
538
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
+
}
|
|
522
549
|
}
|
|
523
550
|
return null;
|
|
524
551
|
}
|
|
@@ -654,6 +681,11 @@ export class TDK {
|
|
|
654
681
|
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
655
682
|
* returning each word's first meaning and etymological origin if found.
|
|
656
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.
|
|
657
689
|
*/
|
|
658
690
|
public static async analyzeText(text: string): Promise<WordAnalysis[]> {
|
|
659
691
|
const words = text
|
|
@@ -679,9 +711,13 @@ export class TDK {
|
|
|
679
711
|
}
|
|
680
712
|
|
|
681
713
|
/**
|
|
682
|
-
*
|
|
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.
|
|
683
719
|
*/
|
|
684
|
-
private static
|
|
720
|
+
private static damerauLevenshtein(a: string, b: string): number {
|
|
685
721
|
const dp: number[][] = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
686
722
|
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
|
|
687
723
|
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
@@ -689,6 +725,9 @@ export class TDK {
|
|
|
689
725
|
for (let j = 1; j <= b.length; j++) {
|
|
690
726
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
691
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
|
+
}
|
|
692
731
|
}
|
|
693
732
|
}
|
|
694
733
|
return dp[a.length][b.length];
|