tdk-api-wrapper 1.4.0 → 1.5.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.
@@ -332,6 +332,12 @@ var TURKISH_SUFFIXES = [
332
332
  "s\u0131n",
333
333
  "sun",
334
334
  "s\xFCn",
335
+ "sen",
336
+ "san",
337
+ "sem",
338
+ "sam",
339
+ "sek",
340
+ "sak",
335
341
  "siz",
336
342
  "s\u0131z",
337
343
  "suz",
@@ -469,6 +475,43 @@ function restoreVowelDrop(stem) {
469
475
  }
470
476
  return [];
471
477
  }
478
+ function restoreGemination(stem) {
479
+ if (stem.length < 3)
480
+ return [];
481
+ const c1 = stem[stem.length - 2];
482
+ const c2 = stem[stem.length - 1];
483
+ if (c1 === c2 && !isVowel(c1)) {
484
+ const single = stem.slice(0, -1);
485
+ const hardened = restoreConsonantSoftening(single);
486
+ return [single, ...hardened];
487
+ }
488
+ return [];
489
+ }
490
+ function restoreVowelNarrowing(stem) {
491
+ if (stem.length < 2)
492
+ return [];
493
+ if (stem === "di")
494
+ return ["de"];
495
+ if (stem === "yi")
496
+ return ["ye"];
497
+ const lastChar = stem[stem.length - 1];
498
+ const isLastNarrow = "\u0131iu\xFC".includes(lastChar);
499
+ if (isLastNarrow) {
500
+ const vowelsInBase = stem.slice(0, -1).split("").filter(isVowel);
501
+ const lastVowel = vowelsInBase.length > 0 ? vowelsInBase[vowelsInBase.length - 1] : lastChar;
502
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
503
+ return [stem.slice(0, -1) + widened];
504
+ }
505
+ if (!isVowel(lastChar)) {
506
+ const vowelsInBase = stem.split("").filter(isVowel);
507
+ if (vowelsInBase.length > 0) {
508
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
509
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
510
+ return [stem + widened];
511
+ }
512
+ }
513
+ return [];
514
+ }
472
515
  function restoreInfinitive(stem) {
473
516
  if (stem.length < 2)
474
517
  return [];
@@ -492,6 +535,13 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
492
535
  seen.add(apostropheStem);
493
536
  }
494
537
  }
538
+ const bareInfinitives = restoreInfinitive(normalized);
539
+ for (const inf of bareInfinitives) {
540
+ if (!seen.has(inf) && inf !== normalized) {
541
+ seen.add(inf);
542
+ candidatesWithWeight.push({ candidate: inf, baseLength: normalized.length });
543
+ }
544
+ }
495
545
  let frontier = [normalized];
496
546
  for (let depth = 0; depth < maxDepth; depth++) {
497
547
  const nextFrontier = [];
@@ -501,9 +551,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
501
551
  const stem = current.slice(0, -suffix.length);
502
552
  const hardened = restoreConsonantSoftening(stem);
503
553
  const vowelDropped = restoreVowelDrop(stem);
504
- const verbalBases = [stem, ...hardened];
554
+ const geminated = restoreGemination(stem);
555
+ const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
556
+ const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
557
+ const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
558
+ const isVerbSuffix = isNarrowingSuffix || suffix.includes("ecek") || suffix.includes("acak") || suffix.includes("mi\u015F") || suffix.includes("m\u0131\u015F") || suffix.includes("m\xFC\u015F") || suffix.includes("mu\u015F") || suffix.includes("mek") || suffix.includes("mak") || suffix.includes("erek") || suffix.includes("arak") || suffix.includes("dik") || suffix.includes("d\u0131k") || suffix.includes("duk") || suffix.includes("d\xFCk") || suffix.includes("tik") || suffix.includes("t\u0131k") || suffix.includes("tuk") || suffix.includes("t\xFCk") || suffix.includes("sen") || suffix.includes("san") || suffix.includes("sem") || suffix.includes("sam") || suffix.includes("sek") || suffix.includes("sak");
559
+ const verbalBases = [stem, ...hardened, ...narrowed];
505
560
  const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
506
- const variants = [stem, ...hardened, ...vowelDropped, ...infinitives];
561
+ const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
507
562
  for (const variant of variants) {
508
563
  if (!seen.has(variant) && variant !== normalized) {
509
564
  seen.add(variant);
@@ -511,6 +566,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
511
566
  candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
512
567
  }
513
568
  }
569
+ for (const inf of infinitives) {
570
+ if (!seen.has(inf) && inf !== normalized) {
571
+ seen.add(inf);
572
+ nextFrontier.push(inf);
573
+ const weight = isVerbSuffix ? stem.length + 5 : stem.length;
574
+ candidatesWithWeight.push({ candidate: inf, baseLength: weight });
575
+ }
576
+ }
514
577
  }
515
578
  }
516
579
  }
@@ -528,6 +591,159 @@ import * as path from "path";
528
591
  import * as os from "os";
529
592
  import * as https from "https";
530
593
  import * as tls from "tls";
594
+ var COMMON_MISSPELLINGS = {
595
+ // -şey ile biten ve ayrı yazılması zorunlu söz öbekleri
596
+ her\u015Fey: "her \u015Fey",
597
+ hersey: "her \u015Fey",
598
+ bir\u015Fey: "bir \u015Fey",
599
+ birsey: "bir \u015Fey",
600
+ hi\u00E7bir\u015Fey: "hi\xE7bir \u015Fey",
601
+ hicbirsey: "hi\xE7bir \u015Fey",
602
+ \u00E7ok\u015Fey: "\xE7ok \u015Fey",
603
+ coksey: "\xE7ok \u015Fey",
604
+ \u015Feyler: "\u015Feyler",
605
+ seyler: "\u015Feyler",
606
+ herhangibir\u015Fey: "herhangi bir \u015Fey",
607
+ herhangibirsey: "herhangi bir \u015Fey",
608
+ // Sıkça birleşik yazılan ama ayrı yazılması gereken sözler
609
+ herg\u00FCn: "her g\xFCn",
610
+ hergun: "her g\xFCn",
611
+ herzaman: "her zaman",
612
+ heran: "her an",
613
+ heryer: "her yer",
614
+ herbiri: "her biri",
615
+ pek\u00E7ok: "pek \xE7ok",
616
+ pekcok: "pek \xE7ok",
617
+ pekaz: "pek az",
618
+ yada: "ya da",
619
+ tabiki: "tabii ki",
620
+ tabiiki: "tabii ki",
621
+ sa\u011Fol: "sa\u011F ol",
622
+ sagol: "sa\u011F ol",
623
+ sa\u011Folun: "sa\u011F olun",
624
+ sagolun: "sa\u011F olun",
625
+ ho\u015F\u00E7akal: "ho\u015F\xE7a kal",
626
+ hoscakal: "ho\u015F\xE7a kal",
627
+ ho\u015Fgeldin: "ho\u015F geldin",
628
+ hosgeldin: "ho\u015F geldin",
629
+ ho\u015Fgeldiniz: "ho\u015F geldiniz",
630
+ hosgeldiniz: "ho\u015F geldiniz",
631
+ ho\u015Fbulduk: "ho\u015F bulduk",
632
+ hosbulduk: "ho\u015F bulduk",
633
+ yan\u0131s\u0131ra: "yan\u0131 s\u0131ra",
634
+ yanisira: "yan\u0131 s\u0131ra",
635
+ pe\u015Fis\u0131ra: "pe\u015Fi s\u0131ra",
636
+ pesisira: "pe\u015Fi s\u0131ra",
637
+ ard\u0131s\u0131ra: "ard\u0131 s\u0131ra",
638
+ ardisira: "ard\u0131 s\u0131ra",
639
+ artarda: "art arda",
640
+ y\u00FCzy\u00FCze: "y\xFCz y\xFCze",
641
+ yuzyuze: "y\xFCz y\xFCze",
642
+ elele: "el ele",
643
+ g\u00F6zg\u00F6ze: "g\xF6z g\xF6ze",
644
+ ba\u015Fba\u015Fa: "ba\u015F ba\u015Fa",
645
+ basbasa: "ba\u015F ba\u015Fa",
646
+ yanyana: "yan yana",
647
+ i\u00E7i\u00E7e: "i\xE7 i\xE7e",
648
+ icice: "i\xE7 i\xE7e",
649
+ \u00FCst\u00FCste: "\xFCst \xFCste",
650
+ ustuste: "\xFCst \xFCste",
651
+ altalta: "alt alta",
652
+ \u00F6ns\u00F6z: "\xF6n s\xF6z",
653
+ onsoz: "\xF6n s\xF6z",
654
+ \u00F6nyarg\u0131: "\xF6n yarg\u0131",
655
+ onyargi: "\xF6n yarg\u0131",
656
+ farketmek: "fark etmek",
657
+ farketti: "fark etti",
658
+ farkettim: "fark ettim",
659
+ farkeder: "fark eder",
660
+ farketmez: "fark etmez",
661
+ terketmek: "terk etmek",
662
+ terketti: "terk etti",
663
+ ay\u0131rdetmek: "ay\u0131rt etmek",
664
+ ay\u0131rtetmek: "ay\u0131rt etmek",
665
+ arzetmek: "arz etmek",
666
+ arzederim: "arz ederim",
667
+ varolmak: "var olmak",
668
+ yokolmak: "yok olmak",
669
+ haketmek: "hak etmek",
670
+ haketti: "hak etti",
671
+ hakkaten: "hakikaten",
672
+ hi\u00E7kimse: "hi\xE7 kimse",
673
+ hickimse: "hi\xE7 kimse",
674
+ // Ünlü düşmesi yapılmaması gereken yer bildiren sözler (TDK Kural 15)
675
+ burda: "burada",
676
+ burdan: "buradan",
677
+ \u015Furda: "\u015Furada",
678
+ surda: "\u015Furada",
679
+ \u015Furdan: "\u015Furadan",
680
+ surdan: "\u015Furadan",
681
+ orda: "orada",
682
+ ordan: "oradan",
683
+ i\u00E7erde: "i\xE7eride",
684
+ icerde: "i\xE7eride",
685
+ i\u00E7erden: "i\xE7eriden",
686
+ icerden: "i\xE7eriden",
687
+ d\u0131\u015Farda: "d\u0131\u015Far\u0131da",
688
+ disarda: "d\u0131\u015Far\u0131da",
689
+ d\u0131\u015Fardan: "d\u0131\u015Far\u0131dan",
690
+ disardan: "d\u0131\u015Far\u0131dan",
691
+ yukarda: "yukar\u0131da",
692
+ yukardan: "yukar\u0131dan",
693
+ // Sıkça yanlış yazılan sözcükler
694
+ herkez: "herkes",
695
+ yanl\u0131z: "yaln\u0131z",
696
+ yaln\u0131\u015F: "yanl\u0131\u015F",
697
+ orjinal: "orijinal",
698
+ labaratuar: "laboratuvar",
699
+ laboratuar: "laboratuvar",
700
+ \u015F\u00F6f\u00F6r: "\u015Fof\xF6r",
701
+ sofor: "\u015Fof\xF6r",
702
+ egzos: "egzoz",
703
+ eksoz: "egzoz",
704
+ ekzoz: "egzoz",
705
+ kiprik: "kirpik",
706
+ kirbit: "kibrit",
707
+ klavuz: "k\u0131lavuz",
708
+ k\u0131ravat: "kravat",
709
+ s\u00FCpriz: "s\xFCrpriz",
710
+ supriz: "s\xFCrpriz",
711
+ raslant\u0131: "rastlant\u0131",
712
+ hastahane: "hastane",
713
+ pastahane: "pastane",
714
+ postahane: "postane",
715
+ eczahane: "eczane",
716
+ meyva: "meyve",
717
+ sarm\u0131sak: "sar\u0131msak",
718
+ dinazor: "dinozor",
719
+ pantalon: "pantolon",
720
+ tesbih: "tespih",
721
+ ah\u00E7\u0131: "a\u015F\xE7\u0131",
722
+ matba: "matbaa",
723
+ idda: "iddia",
724
+ iddaa: "iddia",
725
+ muhattap: "muhatap",
726
+ tra\u015F: "t\u0131ra\u015F",
727
+ karn\u0131bahar: "karnabahar",
728
+ kareografi: "koreografi",
729
+ poa\u00E7a: "po\u011Fa\xE7a",
730
+ poha\u00E7a: "po\u011Fa\xE7a",
731
+ \u015Farz: "\u015Farj",
732
+ sarj: "\u015Farj",
733
+ makina: "makine",
734
+ m\u00FCsade: "m\xFCsaade",
735
+ entellekt\u00FCel: "entelekt\xFCel",
736
+ inisiyatif: "inisiyatif",
737
+ insiyatif: "inisiyatif",
738
+ sezeryan: "sezaryen",
739
+ dok\u00FCman: "dok\xFCman",
740
+ d\u00F6k\u00FCman: "dok\xFCman",
741
+ erozyon: "erozyon",
742
+ erizyon: "erozyon",
743
+ anane: "anneanne",
744
+ babaanne: "babaanne"
745
+ };
746
+ var SEY_EXCEPTIONS = /* @__PURE__ */ new Set(["d\xFC\u015Fey", "e\u015Fey", "konsey", "jersey", "\u015Fey"]);
531
747
  var TDK = class {
532
748
  static BASE_URL = "https://sozluk.gov.tr";
533
749
  static AUDIO_API_HOST = "api.sozluk.gov.tr";
@@ -608,6 +824,10 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
608
824
  yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
609
825
  -----END CERTIFICATE-----`
610
826
  ];
827
+ // Configuration
828
+ static defaultTimeoutMs = 8e3;
829
+ static defaultRetries = 1;
830
+ static maxCacheSize = 1e3;
611
831
  // Cache Mechanism
612
832
  static isCacheEnabled = false;
613
833
  static wordCache = /* @__PURE__ */ new Map();
@@ -615,6 +835,19 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
615
835
  static autocompleteCache = [];
616
836
  static autocompleteSet = /* @__PURE__ */ new Set();
617
837
  static stemCache = /* @__PURE__ */ new Map();
838
+ /**
839
+ * Configures global client options such as network timeout, retries, and cache size.
840
+ */
841
+ static configure(config) {
842
+ if (config.timeoutMs !== void 0)
843
+ this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
844
+ if (config.retries !== void 0)
845
+ this.defaultRetries = Math.max(0, config.retries);
846
+ if (config.cache !== void 0)
847
+ this.enableCache(config.cache);
848
+ if (config.maxCacheSize !== void 0)
849
+ this.maxCacheSize = Math.max(10, config.maxCacheSize);
850
+ }
618
851
  /**
619
852
  * Enables or disables in-memory caching for API requests.
620
853
  */
@@ -634,9 +867,50 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
634
867
  this.autocompleteSet.clear();
635
868
  this.stemCache.clear();
636
869
  }
870
+ static setBoundedCache(map, key, value) {
871
+ if (map.size >= this.maxCacheSize) {
872
+ const firstKey = map.keys().next().value;
873
+ if (firstKey !== void 0)
874
+ map.delete(firstKey);
875
+ }
876
+ map.set(key, value);
877
+ }
637
878
  static delay(ms) {
638
879
  return new Promise((resolve) => setTimeout(resolve, ms));
639
880
  }
881
+ /**
882
+ * Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
883
+ */
884
+ static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
885
+ let lastError;
886
+ for (let attempt = 0; attempt <= retries; attempt++) {
887
+ try {
888
+ const signal = AbortSignal.timeout(timeoutMs);
889
+ const headers = {
890
+ "User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
891
+ ...options.headers || {}
892
+ };
893
+ const res = await fetch(url, { ...options, headers, signal });
894
+ if (res.ok || res.status >= 400 && res.status < 500) {
895
+ return res;
896
+ }
897
+ if (attempt < retries) {
898
+ await this.delay(200 * (attempt + 1));
899
+ continue;
900
+ }
901
+ return res;
902
+ } catch (err) {
903
+ lastError = err;
904
+ if (attempt < retries) {
905
+ await this.delay(200 * (attempt + 1));
906
+ continue;
907
+ }
908
+ }
909
+ }
910
+ throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
911
+ cause: lastError
912
+ });
913
+ }
640
914
  /**
641
915
  * Fetches detailed information for a given word from the TDK Dictionary.
642
916
  */
@@ -651,9 +925,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
651
925
  const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
652
926
  let response;
653
927
  try {
654
- response = await fetch(url, {
655
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
656
- });
928
+ response = await this.fetchWithRetry(url);
657
929
  } catch (error) {
658
930
  throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
659
931
  }
@@ -670,12 +942,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
670
942
  }
671
943
  if (!Array.isArray(data) && data && "error" in data) {
672
944
  if (this.isCacheEnabled)
673
- this.wordCache.set(cleanWord, []);
945
+ this.setBoundedCache(this.wordCache, cleanWord, []);
674
946
  return [];
675
947
  }
676
948
  const results = data;
677
949
  if (this.isCacheEnabled) {
678
- this.wordCache.set(cleanWord, results);
950
+ this.setBoundedCache(this.wordCache, cleanWord, results);
679
951
  }
680
952
  return results;
681
953
  }
@@ -802,17 +1074,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
802
1074
  return this.stemCache.get(clean);
803
1075
  }
804
1076
  if (await this.isHeadword(clean)) {
805
- this.stemCache.set(clean, clean);
1077
+ this.setBoundedCache(this.stemCache, clean, clean);
806
1078
  return clean;
807
1079
  }
808
1080
  const candidates = getStemCandidates(clean);
809
1081
  for (const candidate of candidates) {
810
1082
  if (await this.isHeadword(candidate)) {
811
- this.stemCache.set(clean, candidate);
1083
+ this.setBoundedCache(this.stemCache, clean, candidate);
812
1084
  return candidate;
813
1085
  }
814
1086
  }
815
- this.stemCache.set(clean, null);
1087
+ this.setBoundedCache(this.stemCache, clean, null);
816
1088
  return null;
817
1089
  }
818
1090
  /**
@@ -1035,25 +1307,45 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1035
1307
  * Checks spelling and returns suggestions if wrong.
1036
1308
  */
1037
1309
  static async checkSpelling(word) {
1310
+ if (!word || word.trim() === "") {
1311
+ return { isCorrect: false, word };
1312
+ }
1313
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
1038
1314
  const results = await this.getWord(word);
1039
1315
  if (results.length > 0) {
1040
1316
  return { isCorrect: true, word };
1041
1317
  }
1318
+ if (COMMON_MISSPELLINGS[cleanWord]) {
1319
+ return { isCorrect: false, word, suggestion: COMMON_MISSPELLINGS[cleanWord] };
1320
+ }
1321
+ const seyMatch = cleanWord.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
1322
+ if (seyMatch && !SEY_EXCEPTIONS.has(cleanWord)) {
1323
+ let prefix = seyMatch[1];
1324
+ const suffix = seyMatch[2] || "";
1325
+ if (prefix === "hicbir")
1326
+ prefix = "hi\xE7bir";
1327
+ if (prefix === "cok")
1328
+ prefix = "\xE7ok";
1329
+ return {
1330
+ isCorrect: false,
1331
+ word,
1332
+ suggestion: `${prefix} \u015Fey${suffix}`
1333
+ };
1334
+ }
1042
1335
  const daily = await this.getDailyContent();
1043
1336
  if (daily) {
1044
- const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
1337
+ const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === cleanWord);
1045
1338
  if (syydMatch) {
1046
1339
  return { isCorrect: false, word, suggestion: syydMatch.dogrukelime };
1047
1340
  }
1048
- const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
1341
+ const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === cleanWord);
1049
1342
  if (mixMatch) {
1050
1343
  return { isCorrect: false, word, suggestion: mixMatch.dogru };
1051
1344
  }
1052
1345
  }
1053
1346
  const root = await this.findRoot(word);
1054
1347
  if (root) {
1055
- const cleanWord2 = word.trim().toLocaleLowerCase("tr-TR");
1056
- const isInflected = root !== cleanWord2;
1348
+ const isInflected = root !== cleanWord;
1057
1349
  return {
1058
1350
  isCorrect: true,
1059
1351
  word,
@@ -1064,24 +1356,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1064
1356
  if (this.autocompleteCache.length === 0) {
1065
1357
  this.autocompleteCache = await this.fetchAutocompleteData();
1066
1358
  }
1067
- const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
1359
+ for (const candidate of this.autocompleteCache) {
1360
+ if (candidate.includes(" ")) {
1361
+ const candidateNoSpace = candidate.replace(/\s+/g, "").toLocaleLowerCase("tr-TR");
1362
+ if (candidateNoSpace === cleanWord) {
1363
+ return { isCorrect: false, word, suggestion: candidate };
1364
+ }
1365
+ }
1366
+ }
1068
1367
  let best = null;
1069
1368
  for (const candidate of this.autocompleteCache) {
1070
1369
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
1071
1370
  continue;
1072
1371
  if (Math.abs(candidate.length - cleanWord.length) > 2)
1073
1372
  continue;
1074
- const distance = this.damerauLevenshtein(cleanWord, candidate);
1075
- if (distance === 0)
1373
+ const rawDist = this.damerauLevenshtein(cleanWord, candidate);
1374
+ if (rawDist === 0)
1076
1375
  continue;
1077
1376
  const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
1078
1377
  const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
1378
+ const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
1079
1379
  const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1080
1380
  if (better) {
1081
- best = { candidate, distance, firstMismatch, lengthMismatch };
1381
+ best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
1082
1382
  }
1083
1383
  }
1084
- if (best && best.distance <= 2) {
1384
+ if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
1085
1385
  return { isCorrect: false, word, suggestion: best.candidate };
1086
1386
  }
1087
1387
  return { isCorrect: false, word };
@@ -1488,14 +1788,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1488
1788
  meaningCount: meaningsA.length,
1489
1789
  origin: originA,
1490
1790
  syllables: this.syllabicate(a),
1491
- harmony: this.checkVowelHarmony(a)
1791
+ harmony: this.checkVowelHarmony(a),
1792
+ labialHarmony: this.checkLabialHarmony(a)
1492
1793
  },
1493
1794
  b: {
1494
1795
  word: b,
1495
1796
  meaningCount: meaningsB.length,
1496
1797
  origin: originB,
1497
1798
  syllables: this.syllabicate(b),
1498
- harmony: this.checkVowelHarmony(b)
1799
+ harmony: this.checkVowelHarmony(b),
1800
+ labialHarmony: this.checkLabialHarmony(b)
1499
1801
  }
1500
1802
  };
1501
1803
  }
@@ -1626,9 +1928,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1626
1928
  }
1627
1929
  /**
1628
1930
  * Syllabicates a Turkish word based on general grammar rules.
1931
+ * Handles syllable separation for vowels, single consonants, double consonants,
1932
+ * and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
1629
1933
  */
1630
1934
  static syllabicate(word) {
1631
1935
  const vowels = /[aeıioöuüAEIİOÖUÜ]/;
1936
+ const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
1632
1937
  const result = [];
1633
1938
  let currentSyllable = "";
1634
1939
  for (let i = word.length - 1; i >= 0; i--) {
@@ -1639,8 +1944,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1639
1944
  currentSyllable = word[i - 1] + currentSyllable;
1640
1945
  i--;
1641
1946
  } else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
1642
- currentSyllable = word[i - 1] + currentSyllable;
1643
- i--;
1947
+ if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
1948
+ currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
1949
+ i -= 2;
1950
+ } else {
1951
+ currentSyllable = word[i - 1] + currentSyllable;
1952
+ i--;
1953
+ }
1644
1954
  }
1645
1955
  }
1646
1956
  result.unshift(currentSyllable);
@@ -1670,6 +1980,403 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1670
1980
  const hasFront = frontVowels.test(lower);
1671
1981
  return !(hasBack && hasFront);
1672
1982
  }
1983
+ /**
1984
+ * Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
1985
+ * Rules:
1986
+ * 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
1987
+ * 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
1988
+ * Single-syllable words and words with <=1 vowel are considered compliant by convention.
1989
+ */
1990
+ static checkLabialHarmony(word) {
1991
+ const lower = word.toLocaleLowerCase("tr-TR");
1992
+ const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
1993
+ if (vowels.length <= 1)
1994
+ return true;
1995
+ for (let i = 0; i < vowels.length - 1; i++) {
1996
+ const v1 = vowels[i];
1997
+ const v2 = vowels[i + 1];
1998
+ if ("ae\u0131i".includes(v1)) {
1999
+ if (!"ae\u0131i".includes(v2))
2000
+ return false;
2001
+ } else if ("o\xF6u\xFC".includes(v1)) {
2002
+ if (!"aeu\xFC".includes(v2))
2003
+ return false;
2004
+ }
2005
+ }
2006
+ return true;
2007
+ }
2008
+ /**
2009
+ * Searches TDK headwords using a wildcard / pattern string.
2010
+ * Wildcards:
2011
+ * '_' or '?' matches any single character
2012
+ * '*' matches zero or more characters
2013
+ * Example: "k_l_m" matches "kalem", "kelam", "kilim".
2014
+ * Runs in-memory against TDK's 81k headword list.
2015
+ */
2016
+ static async patternSearch(pattern, options) {
2017
+ if (!pattern || pattern.trim() === "")
2018
+ return [];
2019
+ await this.ensureAutocompleteLoaded();
2020
+ const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
2021
+ const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
2022
+ const regex = new RegExp(`^${escaped}$`, "u");
2023
+ const max = options?.maxResults ?? 50;
2024
+ const matches = [];
2025
+ for (const headword of this.autocompleteCache) {
2026
+ const lower = headword.toLocaleLowerCase("tr-TR");
2027
+ if (regex.test(lower)) {
2028
+ matches.push(headword);
2029
+ if (matches.length >= max)
2030
+ break;
2031
+ }
2032
+ }
2033
+ return matches;
2034
+ }
2035
+ /**
2036
+ * Finds headwords in TDK that can be formed from the given letters (anagrams).
2037
+ * If exact-length anagrams exist, they are returned.
2038
+ * If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
2039
+ * minimum 3 letters) are returned, sorted by length descending.
2040
+ */
2041
+ static async findAnagrams(letters, options) {
2042
+ if (!letters || letters.trim() === "")
2043
+ return [];
2044
+ await this.ensureAutocompleteLoaded();
2045
+ const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
2046
+ if (clean.length === 0)
2047
+ return [];
2048
+ const forceExact = options?.exactLength === true;
2049
+ const max = options?.maxResults ?? 50;
2050
+ const getFrequency = (str) => {
2051
+ const freq = {};
2052
+ for (const ch of str) {
2053
+ freq[ch] = (freq[ch] || 0) + 1;
2054
+ }
2055
+ return freq;
2056
+ };
2057
+ const targetFreq = getFrequency(clean);
2058
+ const exactMatches = [];
2059
+ const subMatches = [];
2060
+ for (const headword of this.autocompleteCache) {
2061
+ const lower = headword.toLocaleLowerCase("tr-TR");
2062
+ if (lower.includes(" ") || lower.includes("-"))
2063
+ continue;
2064
+ if (lower.length > clean.length || lower.length < 3)
2065
+ continue;
2066
+ const wordFreq = getFrequency(lower);
2067
+ let isValid = true;
2068
+ for (const [ch, count] of Object.entries(wordFreq)) {
2069
+ if (!targetFreq[ch] || targetFreq[ch] < count) {
2070
+ isValid = false;
2071
+ break;
2072
+ }
2073
+ }
2074
+ if (isValid && lower !== clean) {
2075
+ if (lower.length === clean.length) {
2076
+ exactMatches.push(headword);
2077
+ } else {
2078
+ subMatches.push(headword);
2079
+ }
2080
+ }
2081
+ }
2082
+ if (exactMatches.length > 0 || forceExact) {
2083
+ return exactMatches.slice(0, max);
2084
+ }
2085
+ subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
2086
+ return subMatches.slice(0, max);
2087
+ }
2088
+ /**
2089
+ * Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
2090
+ * @param word The target word
2091
+ * @param options.minLetters Minimum number of ending characters that must match (default: 3)
2092
+ * @param options.maxResults Maximum number of rhyme results to return (default: 50)
2093
+ */
2094
+ static async findRhymes(word, options) {
2095
+ if (!word || word.trim() === "")
2096
+ return [];
2097
+ await this.ensureAutocompleteLoaded();
2098
+ const clean = word.trim().toLocaleLowerCase("tr-TR");
2099
+ const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
2100
+ const max = options?.maxResults ?? 50;
2101
+ const suffix = clean.slice(-minLetters);
2102
+ const results = [];
2103
+ for (const headword of this.autocompleteCache) {
2104
+ const lower = headword.toLocaleLowerCase("tr-TR");
2105
+ if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
2106
+ results.push(headword);
2107
+ if (results.length >= max)
2108
+ break;
2109
+ }
2110
+ }
2111
+ return results;
2112
+ }
2113
+ /**
2114
+ * Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
2115
+ * Detects:
2116
+ * 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
2117
+ * 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
2118
+ * 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
2119
+ * 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
2120
+ */
2121
+ static async proofread(text) {
2122
+ if (!text || text.trim() === "") {
2123
+ return { text: text || "", issues: [], isCorrect: true };
2124
+ }
2125
+ await this.ensureAutocompleteLoaded();
2126
+ const issues = [];
2127
+ const SOMBAHCEMI = /* @__PURE__ */ new Set([
2128
+ "sanki",
2129
+ "oysaki",
2130
+ "mademki",
2131
+ "belki",
2132
+ "halbuki",
2133
+ "\xE7\xFCnk\xFC",
2134
+ "me\u011Ferki",
2135
+ "illaki"
2136
+ ]);
2137
+ const PHRASE_MISTAKES = [
2138
+ {
2139
+ regex: /\bhiç\s+bir\b/gi,
2140
+ suggestion: "hi\xE7bir",
2141
+ message: "'hi\xE7bir' belgisiz s\u0131fat\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2142
+ type: "spelling"
2143
+ },
2144
+ {
2145
+ regex: /\bbir\s+çok\b/gi,
2146
+ suggestion: "bir\xE7ok",
2147
+ message: "'bir\xE7ok' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2148
+ type: "spelling"
2149
+ },
2150
+ {
2151
+ regex: /\bbir\s+kaç\b/gi,
2152
+ suggestion: "birka\xE7",
2153
+ message: "'birka\xE7' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2154
+ type: "spelling"
2155
+ },
2156
+ {
2157
+ regex: /\bbir\s+az\b/gi,
2158
+ suggestion: "biraz",
2159
+ message: "'biraz' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2160
+ type: "spelling"
2161
+ },
2162
+ {
2163
+ regex: /\bher\s+hangi\b/gi,
2164
+ suggestion: "herhangi",
2165
+ message: "'herhangi' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2166
+ type: "spelling"
2167
+ },
2168
+ {
2169
+ regex: /\bgit\s+gide\b/gi,
2170
+ suggestion: "gitgide",
2171
+ message: "'gitgide' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2172
+ type: "spelling"
2173
+ },
2174
+ {
2175
+ regex: /\bbirden\s+bire\b/gi,
2176
+ suggestion: "birdenbire",
2177
+ message: "'birdenbire' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2178
+ type: "spelling"
2179
+ },
2180
+ {
2181
+ regex: /\brast\s+gele\b/gi,
2182
+ suggestion: "rastgele",
2183
+ message: "'rastgele' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2184
+ type: "spelling"
2185
+ }
2186
+ ];
2187
+ const coveredRanges = [];
2188
+ for (const pm of PHRASE_MISTAKES) {
2189
+ let pmMatch;
2190
+ while ((pmMatch = pm.regex.exec(text)) !== null) {
2191
+ const start = pmMatch.index;
2192
+ const end = start + pmMatch[0].length;
2193
+ coveredRanges.push({ start, end });
2194
+ issues.push({
2195
+ type: pm.type,
2196
+ word: pmMatch[0],
2197
+ startIndex: start,
2198
+ endIndex: end,
2199
+ suggestion: pm.suggestion,
2200
+ message: pm.message
2201
+ });
2202
+ }
2203
+ }
2204
+ const tokenRegex = /[\p{L}0-9'’]+/gu;
2205
+ let match;
2206
+ while ((match = tokenRegex.exec(text)) !== null) {
2207
+ const rawWord = match[0];
2208
+ const startIndex = match.index;
2209
+ const endIndex = startIndex + rawWord.length;
2210
+ const lower = rawWord.toLocaleLowerCase("tr-TR");
2211
+ if (/^\d+$/.test(lower))
2212
+ continue;
2213
+ if (coveredRanges.some((r) => startIndex >= r.start && endIndex <= r.end))
2214
+ continue;
2215
+ let flagged = false;
2216
+ const questionMatch = lower.match(/^(.+?)(m[ıiuü](?:sin|sın|sun|sün|siniz|sınız|sunuz|sünüz|yiz|yız|yuz|yüz|m|k)?)$/);
2217
+ if (questionMatch) {
2218
+ const base = questionMatch[1];
2219
+ const particle = questionMatch[2];
2220
+ if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
2221
+ if (!await this.isHeadword(lower)) {
2222
+ issues.push({
2223
+ type: "question_particle",
2224
+ word: rawWord,
2225
+ startIndex,
2226
+ endIndex,
2227
+ suggestion: `${base} ${particle}`,
2228
+ message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
2229
+ });
2230
+ flagged = true;
2231
+ }
2232
+ }
2233
+ }
2234
+ const VERB_CONJUGATION_REGEX = /(?:d[ıiuü][kmmn]?|t[ıiuü][kmmn]?|d[ıiuü]n[ıiuü]z?|t[ıiuü]n[ıiuü]z?|m[ıiuü]ş(?:[szn][ıiuü]z?|lar)?|yor(?:um|sun|uz|lar)?|ecek(?:sin|iz|ler)?|acak(?:sın|ız|lar)?|s[ae][mnk]|s[ae]n[ıiz]?|meli|malı|me[mz]|ma[mz])$/i;
2235
+ if (!flagged && lower.endsWith("ki") && lower.length > 3) {
2236
+ const base = lower.slice(0, -2);
2237
+ if (!SOMBAHCEMI.has(lower)) {
2238
+ if (!await this.isHeadword(lower)) {
2239
+ const root = await this.findRoot(base);
2240
+ const isVerb = (base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base)) && (root ? root.endsWith("mek") || root.endsWith("mak") : true);
2241
+ if (isVerb) {
2242
+ issues.push({
2243
+ type: "conjunction_ki",
2244
+ word: rawWord,
2245
+ startIndex,
2246
+ endIndex,
2247
+ suggestion: `${base} ki`,
2248
+ message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
2249
+ });
2250
+ flagged = true;
2251
+ }
2252
+ }
2253
+ }
2254
+ }
2255
+ if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
2256
+ const base = lower.slice(0, -2);
2257
+ const ending = lower.slice(-2);
2258
+ if (!await this.isHeadword(lower)) {
2259
+ const root = await this.findRoot(base);
2260
+ const isVerb = VERB_CONJUGATION_REGEX.test(base) && (root ? root.endsWith("mek") || root.endsWith("mak") : false);
2261
+ if (isVerb) {
2262
+ const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
2263
+ issues.push({
2264
+ type: "conjunction_da",
2265
+ word: rawWord,
2266
+ startIndex,
2267
+ endIndex,
2268
+ suggestion: `${base} ${correctEnding}`,
2269
+ message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
2270
+ });
2271
+ flagged = true;
2272
+ }
2273
+ }
2274
+ }
2275
+ const seyMatch = lower.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
2276
+ if (!flagged && seyMatch && !SEY_EXCEPTIONS.has(lower)) {
2277
+ let prefix = seyMatch[1];
2278
+ const suffix = seyMatch[2] || "";
2279
+ if (prefix === "hicbir")
2280
+ prefix = "hi\xE7bir";
2281
+ if (prefix === "cok")
2282
+ prefix = "\xE7ok";
2283
+ issues.push({
2284
+ type: "spelling",
2285
+ word: rawWord,
2286
+ startIndex,
2287
+ endIndex,
2288
+ suggestion: `${prefix} \u015Fey${suffix}`,
2289
+ message: "'\u015Fey' s\xF6zc\xFC\u011F\xFC kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r."
2290
+ });
2291
+ flagged = true;
2292
+ }
2293
+ if (!flagged && lower === "yada") {
2294
+ issues.push({
2295
+ type: "spelling",
2296
+ word: rawWord,
2297
+ startIndex,
2298
+ endIndex,
2299
+ suggestion: "ya da",
2300
+ message: "'ya da' ba\u011Flac\u0131 her zaman ayr\u0131 yaz\u0131l\u0131r."
2301
+ });
2302
+ flagged = true;
2303
+ }
2304
+ if (!flagged && (lower === "burda" || lower === "\u015Furda" || lower === "surda" || lower === "orda" || lower === "i\xE7erde" || lower === "icerde" || lower === "d\u0131\u015Farda" || lower === "disarda" || lower === "yukarda")) {
2305
+ const correct = COMMON_MISSPELLINGS[lower] || lower;
2306
+ issues.push({
2307
+ type: "spelling",
2308
+ word: rawWord,
2309
+ startIndex,
2310
+ endIndex,
2311
+ suggestion: correct,
2312
+ message: `'${rawWord}' s\xF6zc\xFC\u011F\xFCnde \xFCnl\xFC d\xFC\u015Fmesi yap\u0131lmaz.`
2313
+ });
2314
+ flagged = true;
2315
+ }
2316
+ if (!flagged) {
2317
+ const check = await this.checkSpelling(rawWord);
2318
+ if (!check.isCorrect) {
2319
+ issues.push({
2320
+ type: "spelling",
2321
+ word: rawWord,
2322
+ startIndex,
2323
+ endIndex,
2324
+ suggestion: check.suggestion,
2325
+ message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir.` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
2326
+ });
2327
+ }
2328
+ }
2329
+ }
2330
+ issues.sort((a, b) => a.startIndex - b.startIndex);
2331
+ return {
2332
+ text,
2333
+ issues,
2334
+ isCorrect: issues.length === 0
2335
+ };
2336
+ }
2337
+ };
2338
+ var TDKClient = class {
2339
+ constructor(config) {
2340
+ if (config) {
2341
+ TDK.configure(config);
2342
+ }
2343
+ }
2344
+ getWord(word) {
2345
+ return TDK.getWord(word);
2346
+ }
2347
+ getMeanings(word) {
2348
+ return TDK.getMeanings(word);
2349
+ }
2350
+ checkSpelling(word) {
2351
+ return TDK.checkSpelling(word);
2352
+ }
2353
+ findRoot(word) {
2354
+ return TDK.findRoot(word);
2355
+ }
2356
+ stem(word) {
2357
+ return TDK.stem(word);
2358
+ }
2359
+ proofread(text) {
2360
+ return TDK.proofread(text);
2361
+ }
2362
+ patternSearch(pattern, options) {
2363
+ return TDK.patternSearch(pattern, options);
2364
+ }
2365
+ findAnagrams(letters, options) {
2366
+ return TDK.findAnagrams(letters, options);
2367
+ }
2368
+ findRhymes(word, options) {
2369
+ return TDK.findRhymes(word, options);
2370
+ }
2371
+ syllabicate(word) {
2372
+ return TDK.syllabicate(word);
2373
+ }
2374
+ checkVowelHarmony(word) {
2375
+ return TDK.checkVowelHarmony(word);
2376
+ }
2377
+ checkLabialHarmony(word) {
2378
+ return TDK.checkLabialHarmony(word);
2379
+ }
1673
2380
  };
1674
2381
 
1675
2382
  export {
@@ -1681,7 +2388,10 @@ export {
1681
2388
  TURKISH_SUFFIXES,
1682
2389
  restoreConsonantSoftening,
1683
2390
  restoreVowelDrop,
2391
+ restoreGemination,
2392
+ restoreVowelNarrowing,
1684
2393
  restoreInfinitive,
1685
2394
  getStemCandidates,
1686
- TDK
2395
+ TDK,
2396
+ TDKClient
1687
2397
  };