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.
package/dist/cli.js CHANGED
@@ -357,6 +357,12 @@ var TURKISH_SUFFIXES = [
357
357
  "s\u0131n",
358
358
  "sun",
359
359
  "s\xFCn",
360
+ "sen",
361
+ "san",
362
+ "sem",
363
+ "sam",
364
+ "sek",
365
+ "sak",
360
366
  "siz",
361
367
  "s\u0131z",
362
368
  "suz",
@@ -494,6 +500,43 @@ function restoreVowelDrop(stem) {
494
500
  }
495
501
  return [];
496
502
  }
503
+ function restoreGemination(stem) {
504
+ if (stem.length < 3)
505
+ return [];
506
+ const c1 = stem[stem.length - 2];
507
+ const c2 = stem[stem.length - 1];
508
+ if (c1 === c2 && !isVowel(c1)) {
509
+ const single = stem.slice(0, -1);
510
+ const hardened = restoreConsonantSoftening(single);
511
+ return [single, ...hardened];
512
+ }
513
+ return [];
514
+ }
515
+ function restoreVowelNarrowing(stem) {
516
+ if (stem.length < 2)
517
+ return [];
518
+ if (stem === "di")
519
+ return ["de"];
520
+ if (stem === "yi")
521
+ return ["ye"];
522
+ const lastChar = stem[stem.length - 1];
523
+ const isLastNarrow = "\u0131iu\xFC".includes(lastChar);
524
+ if (isLastNarrow) {
525
+ const vowelsInBase = stem.slice(0, -1).split("").filter(isVowel);
526
+ const lastVowel = vowelsInBase.length > 0 ? vowelsInBase[vowelsInBase.length - 1] : lastChar;
527
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
528
+ return [stem.slice(0, -1) + widened];
529
+ }
530
+ if (!isVowel(lastChar)) {
531
+ const vowelsInBase = stem.split("").filter(isVowel);
532
+ if (vowelsInBase.length > 0) {
533
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
534
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
535
+ return [stem + widened];
536
+ }
537
+ }
538
+ return [];
539
+ }
497
540
  function restoreInfinitive(stem) {
498
541
  if (stem.length < 2)
499
542
  return [];
@@ -517,6 +560,13 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
517
560
  seen.add(apostropheStem);
518
561
  }
519
562
  }
563
+ const bareInfinitives = restoreInfinitive(normalized);
564
+ for (const inf of bareInfinitives) {
565
+ if (!seen.has(inf) && inf !== normalized) {
566
+ seen.add(inf);
567
+ candidatesWithWeight.push({ candidate: inf, baseLength: normalized.length });
568
+ }
569
+ }
520
570
  let frontier = [normalized];
521
571
  for (let depth = 0; depth < maxDepth; depth++) {
522
572
  const nextFrontier = [];
@@ -526,9 +576,14 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
526
576
  const stem = current.slice(0, -suffix.length);
527
577
  const hardened = restoreConsonantSoftening(stem);
528
578
  const vowelDropped = restoreVowelDrop(stem);
529
- const verbalBases = [stem, ...hardened];
579
+ const geminated = restoreGemination(stem);
580
+ const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
581
+ const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
582
+ const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
583
+ 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");
584
+ const verbalBases = [stem, ...hardened, ...narrowed];
530
585
  const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
531
- const variants = [stem, ...hardened, ...vowelDropped, ...infinitives];
586
+ const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
532
587
  for (const variant of variants) {
533
588
  if (!seen.has(variant) && variant !== normalized) {
534
589
  seen.add(variant);
@@ -536,6 +591,14 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
536
591
  candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
537
592
  }
538
593
  }
594
+ for (const inf of infinitives) {
595
+ if (!seen.has(inf) && inf !== normalized) {
596
+ seen.add(inf);
597
+ nextFrontier.push(inf);
598
+ const weight = isVerbSuffix ? stem.length + 5 : stem.length;
599
+ candidatesWithWeight.push({ candidate: inf, baseLength: weight });
600
+ }
601
+ }
539
602
  }
540
603
  }
541
604
  }
@@ -544,7 +607,7 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
544
607
  frontier = nextFrontier;
545
608
  }
546
609
  candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
547
- return [...new Set(candidatesWithWeight.map((c) => c.candidate))];
610
+ return [...new Set(candidatesWithWeight.map((c2) => c2.candidate))];
548
611
  }
549
612
 
550
613
  // src/tdk.ts
@@ -553,6 +616,159 @@ var path = __toESM(require("path"));
553
616
  var os = __toESM(require("os"));
554
617
  var https = __toESM(require("https"));
555
618
  var tls = __toESM(require("tls"));
619
+ var COMMON_MISSPELLINGS = {
620
+ // -şey ile biten ve ayrı yazılması zorunlu söz öbekleri
621
+ her\u015Fey: "her \u015Fey",
622
+ hersey: "her \u015Fey",
623
+ bir\u015Fey: "bir \u015Fey",
624
+ birsey: "bir \u015Fey",
625
+ hi\u00E7bir\u015Fey: "hi\xE7bir \u015Fey",
626
+ hicbirsey: "hi\xE7bir \u015Fey",
627
+ \u00E7ok\u015Fey: "\xE7ok \u015Fey",
628
+ coksey: "\xE7ok \u015Fey",
629
+ \u015Feyler: "\u015Feyler",
630
+ seyler: "\u015Feyler",
631
+ herhangibir\u015Fey: "herhangi bir \u015Fey",
632
+ herhangibirsey: "herhangi bir \u015Fey",
633
+ // Sıkça birleşik yazılan ama ayrı yazılması gereken sözler
634
+ herg\u00FCn: "her g\xFCn",
635
+ hergun: "her g\xFCn",
636
+ herzaman: "her zaman",
637
+ heran: "her an",
638
+ heryer: "her yer",
639
+ herbiri: "her biri",
640
+ pek\u00E7ok: "pek \xE7ok",
641
+ pekcok: "pek \xE7ok",
642
+ pekaz: "pek az",
643
+ yada: "ya da",
644
+ tabiki: "tabii ki",
645
+ tabiiki: "tabii ki",
646
+ sa\u011Fol: "sa\u011F ol",
647
+ sagol: "sa\u011F ol",
648
+ sa\u011Folun: "sa\u011F olun",
649
+ sagolun: "sa\u011F olun",
650
+ ho\u015F\u00E7akal: "ho\u015F\xE7a kal",
651
+ hoscakal: "ho\u015F\xE7a kal",
652
+ ho\u015Fgeldin: "ho\u015F geldin",
653
+ hosgeldin: "ho\u015F geldin",
654
+ ho\u015Fgeldiniz: "ho\u015F geldiniz",
655
+ hosgeldiniz: "ho\u015F geldiniz",
656
+ ho\u015Fbulduk: "ho\u015F bulduk",
657
+ hosbulduk: "ho\u015F bulduk",
658
+ yan\u0131s\u0131ra: "yan\u0131 s\u0131ra",
659
+ yanisira: "yan\u0131 s\u0131ra",
660
+ pe\u015Fis\u0131ra: "pe\u015Fi s\u0131ra",
661
+ pesisira: "pe\u015Fi s\u0131ra",
662
+ ard\u0131s\u0131ra: "ard\u0131 s\u0131ra",
663
+ ardisira: "ard\u0131 s\u0131ra",
664
+ artarda: "art arda",
665
+ y\u00FCzy\u00FCze: "y\xFCz y\xFCze",
666
+ yuzyuze: "y\xFCz y\xFCze",
667
+ elele: "el ele",
668
+ g\u00F6zg\u00F6ze: "g\xF6z g\xF6ze",
669
+ ba\u015Fba\u015Fa: "ba\u015F ba\u015Fa",
670
+ basbasa: "ba\u015F ba\u015Fa",
671
+ yanyana: "yan yana",
672
+ i\u00E7i\u00E7e: "i\xE7 i\xE7e",
673
+ icice: "i\xE7 i\xE7e",
674
+ \u00FCst\u00FCste: "\xFCst \xFCste",
675
+ ustuste: "\xFCst \xFCste",
676
+ altalta: "alt alta",
677
+ \u00F6ns\u00F6z: "\xF6n s\xF6z",
678
+ onsoz: "\xF6n s\xF6z",
679
+ \u00F6nyarg\u0131: "\xF6n yarg\u0131",
680
+ onyargi: "\xF6n yarg\u0131",
681
+ farketmek: "fark etmek",
682
+ farketti: "fark etti",
683
+ farkettim: "fark ettim",
684
+ farkeder: "fark eder",
685
+ farketmez: "fark etmez",
686
+ terketmek: "terk etmek",
687
+ terketti: "terk etti",
688
+ ay\u0131rdetmek: "ay\u0131rt etmek",
689
+ ay\u0131rtetmek: "ay\u0131rt etmek",
690
+ arzetmek: "arz etmek",
691
+ arzederim: "arz ederim",
692
+ varolmak: "var olmak",
693
+ yokolmak: "yok olmak",
694
+ haketmek: "hak etmek",
695
+ haketti: "hak etti",
696
+ hakkaten: "hakikaten",
697
+ hi\u00E7kimse: "hi\xE7 kimse",
698
+ hickimse: "hi\xE7 kimse",
699
+ // Ünlü düşmesi yapılmaması gereken yer bildiren sözler (TDK Kural 15)
700
+ burda: "burada",
701
+ burdan: "buradan",
702
+ \u015Furda: "\u015Furada",
703
+ surda: "\u015Furada",
704
+ \u015Furdan: "\u015Furadan",
705
+ surdan: "\u015Furadan",
706
+ orda: "orada",
707
+ ordan: "oradan",
708
+ i\u00E7erde: "i\xE7eride",
709
+ icerde: "i\xE7eride",
710
+ i\u00E7erden: "i\xE7eriden",
711
+ icerden: "i\xE7eriden",
712
+ d\u0131\u015Farda: "d\u0131\u015Far\u0131da",
713
+ disarda: "d\u0131\u015Far\u0131da",
714
+ d\u0131\u015Fardan: "d\u0131\u015Far\u0131dan",
715
+ disardan: "d\u0131\u015Far\u0131dan",
716
+ yukarda: "yukar\u0131da",
717
+ yukardan: "yukar\u0131dan",
718
+ // Sıkça yanlış yazılan sözcükler
719
+ herkez: "herkes",
720
+ yanl\u0131z: "yaln\u0131z",
721
+ yaln\u0131\u015F: "yanl\u0131\u015F",
722
+ orjinal: "orijinal",
723
+ labaratuar: "laboratuvar",
724
+ laboratuar: "laboratuvar",
725
+ \u015F\u00F6f\u00F6r: "\u015Fof\xF6r",
726
+ sofor: "\u015Fof\xF6r",
727
+ egzos: "egzoz",
728
+ eksoz: "egzoz",
729
+ ekzoz: "egzoz",
730
+ kiprik: "kirpik",
731
+ kirbit: "kibrit",
732
+ klavuz: "k\u0131lavuz",
733
+ k\u0131ravat: "kravat",
734
+ s\u00FCpriz: "s\xFCrpriz",
735
+ supriz: "s\xFCrpriz",
736
+ raslant\u0131: "rastlant\u0131",
737
+ hastahane: "hastane",
738
+ pastahane: "pastane",
739
+ postahane: "postane",
740
+ eczahane: "eczane",
741
+ meyva: "meyve",
742
+ sarm\u0131sak: "sar\u0131msak",
743
+ dinazor: "dinozor",
744
+ pantalon: "pantolon",
745
+ tesbih: "tespih",
746
+ ah\u00E7\u0131: "a\u015F\xE7\u0131",
747
+ matba: "matbaa",
748
+ idda: "iddia",
749
+ iddaa: "iddia",
750
+ muhattap: "muhatap",
751
+ tra\u015F: "t\u0131ra\u015F",
752
+ karn\u0131bahar: "karnabahar",
753
+ kareografi: "koreografi",
754
+ poa\u00E7a: "po\u011Fa\xE7a",
755
+ poha\u00E7a: "po\u011Fa\xE7a",
756
+ \u015Farz: "\u015Farj",
757
+ sarj: "\u015Farj",
758
+ makina: "makine",
759
+ m\u00FCsade: "m\xFCsaade",
760
+ entellekt\u00FCel: "entelekt\xFCel",
761
+ inisiyatif: "inisiyatif",
762
+ insiyatif: "inisiyatif",
763
+ sezeryan: "sezaryen",
764
+ dok\u00FCman: "dok\xFCman",
765
+ d\u00F6k\u00FCman: "dok\xFCman",
766
+ erozyon: "erozyon",
767
+ erizyon: "erozyon",
768
+ anane: "anneanne",
769
+ babaanne: "babaanne"
770
+ };
771
+ var SEY_EXCEPTIONS = /* @__PURE__ */ new Set(["d\xFC\u015Fey", "e\u015Fey", "konsey", "jersey", "\u015Fey"]);
556
772
  var TDK = class {
557
773
  static BASE_URL = "https://sozluk.gov.tr";
558
774
  static AUDIO_API_HOST = "api.sozluk.gov.tr";
@@ -633,6 +849,10 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
633
849
  yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
634
850
  -----END CERTIFICATE-----`
635
851
  ];
852
+ // Configuration
853
+ static defaultTimeoutMs = 8e3;
854
+ static defaultRetries = 1;
855
+ static maxCacheSize = 1e3;
636
856
  // Cache Mechanism
637
857
  static isCacheEnabled = false;
638
858
  static wordCache = /* @__PURE__ */ new Map();
@@ -640,6 +860,19 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
640
860
  static autocompleteCache = [];
641
861
  static autocompleteSet = /* @__PURE__ */ new Set();
642
862
  static stemCache = /* @__PURE__ */ new Map();
863
+ /**
864
+ * Configures global client options such as network timeout, retries, and cache size.
865
+ */
866
+ static configure(config) {
867
+ if (config.timeoutMs !== void 0)
868
+ this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
869
+ if (config.retries !== void 0)
870
+ this.defaultRetries = Math.max(0, config.retries);
871
+ if (config.cache !== void 0)
872
+ this.enableCache(config.cache);
873
+ if (config.maxCacheSize !== void 0)
874
+ this.maxCacheSize = Math.max(10, config.maxCacheSize);
875
+ }
643
876
  /**
644
877
  * Enables or disables in-memory caching for API requests.
645
878
  */
@@ -659,9 +892,50 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
659
892
  this.autocompleteSet.clear();
660
893
  this.stemCache.clear();
661
894
  }
895
+ static setBoundedCache(map, key, value) {
896
+ if (map.size >= this.maxCacheSize) {
897
+ const firstKey = map.keys().next().value;
898
+ if (firstKey !== void 0)
899
+ map.delete(firstKey);
900
+ }
901
+ map.set(key, value);
902
+ }
662
903
  static delay(ms) {
663
904
  return new Promise((resolve) => setTimeout(resolve, ms));
664
905
  }
906
+ /**
907
+ * Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
908
+ */
909
+ static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
910
+ let lastError;
911
+ for (let attempt = 0; attempt <= retries; attempt++) {
912
+ try {
913
+ const signal = AbortSignal.timeout(timeoutMs);
914
+ const headers = {
915
+ "User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
916
+ ...options.headers || {}
917
+ };
918
+ const res = await fetch(url, { ...options, headers, signal });
919
+ if (res.ok || res.status >= 400 && res.status < 500) {
920
+ return res;
921
+ }
922
+ if (attempt < retries) {
923
+ await this.delay(200 * (attempt + 1));
924
+ continue;
925
+ }
926
+ return res;
927
+ } catch (err) {
928
+ lastError = err;
929
+ if (attempt < retries) {
930
+ await this.delay(200 * (attempt + 1));
931
+ continue;
932
+ }
933
+ }
934
+ }
935
+ throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
936
+ cause: lastError
937
+ });
938
+ }
665
939
  /**
666
940
  * Fetches detailed information for a given word from the TDK Dictionary.
667
941
  */
@@ -676,9 +950,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
676
950
  const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
677
951
  let response;
678
952
  try {
679
- response = await fetch(url, {
680
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
681
- });
953
+ response = await this.fetchWithRetry(url);
682
954
  } catch (error) {
683
955
  throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
684
956
  }
@@ -695,12 +967,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
695
967
  }
696
968
  if (!Array.isArray(data) && data && "error" in data) {
697
969
  if (this.isCacheEnabled)
698
- this.wordCache.set(cleanWord, []);
970
+ this.setBoundedCache(this.wordCache, cleanWord, []);
699
971
  return [];
700
972
  }
701
973
  const results = data;
702
974
  if (this.isCacheEnabled) {
703
- this.wordCache.set(cleanWord, results);
975
+ this.setBoundedCache(this.wordCache, cleanWord, results);
704
976
  }
705
977
  return results;
706
978
  }
@@ -827,17 +1099,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
827
1099
  return this.stemCache.get(clean);
828
1100
  }
829
1101
  if (await this.isHeadword(clean)) {
830
- this.stemCache.set(clean, clean);
1102
+ this.setBoundedCache(this.stemCache, clean, clean);
831
1103
  return clean;
832
1104
  }
833
1105
  const candidates = getStemCandidates(clean);
834
1106
  for (const candidate of candidates) {
835
1107
  if (await this.isHeadword(candidate)) {
836
- this.stemCache.set(clean, candidate);
1108
+ this.setBoundedCache(this.stemCache, clean, candidate);
837
1109
  return candidate;
838
1110
  }
839
1111
  }
840
- this.stemCache.set(clean, null);
1112
+ this.setBoundedCache(this.stemCache, clean, null);
841
1113
  return null;
842
1114
  }
843
1115
  /**
@@ -1060,25 +1332,45 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1060
1332
  * Checks spelling and returns suggestions if wrong.
1061
1333
  */
1062
1334
  static async checkSpelling(word2) {
1335
+ if (!word2 || word2.trim() === "") {
1336
+ return { isCorrect: false, word: word2 };
1337
+ }
1338
+ const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
1063
1339
  const results = await this.getWord(word2);
1064
1340
  if (results.length > 0) {
1065
1341
  return { isCorrect: true, word: word2 };
1066
1342
  }
1343
+ if (COMMON_MISSPELLINGS[cleanWord]) {
1344
+ return { isCorrect: false, word: word2, suggestion: COMMON_MISSPELLINGS[cleanWord] };
1345
+ }
1346
+ const seyMatch = cleanWord.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
1347
+ if (seyMatch && !SEY_EXCEPTIONS.has(cleanWord)) {
1348
+ let prefix = seyMatch[1];
1349
+ const suffix = seyMatch[2] || "";
1350
+ if (prefix === "hicbir")
1351
+ prefix = "hi\xE7bir";
1352
+ if (prefix === "cok")
1353
+ prefix = "\xE7ok";
1354
+ return {
1355
+ isCorrect: false,
1356
+ word: word2,
1357
+ suggestion: `${prefix} \u015Fey${suffix}`
1358
+ };
1359
+ }
1067
1360
  const daily = await this.getDailyContent();
1068
1361
  if (daily) {
1069
- const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === word2.toLocaleLowerCase("tr-TR"));
1362
+ const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === cleanWord);
1070
1363
  if (syydMatch) {
1071
1364
  return { isCorrect: false, word: word2, suggestion: syydMatch.dogrukelime };
1072
1365
  }
1073
- const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === word2.toLocaleLowerCase("tr-TR"));
1366
+ const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === cleanWord);
1074
1367
  if (mixMatch) {
1075
1368
  return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
1076
1369
  }
1077
1370
  }
1078
1371
  const root = await this.findRoot(word2);
1079
1372
  if (root) {
1080
- const cleanWord2 = word2.trim().toLocaleLowerCase("tr-TR");
1081
- const isInflected = root !== cleanWord2;
1373
+ const isInflected = root !== cleanWord;
1082
1374
  return {
1083
1375
  isCorrect: true,
1084
1376
  word: word2,
@@ -1089,24 +1381,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1089
1381
  if (this.autocompleteCache.length === 0) {
1090
1382
  this.autocompleteCache = await this.fetchAutocompleteData();
1091
1383
  }
1092
- const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
1384
+ for (const candidate of this.autocompleteCache) {
1385
+ if (candidate.includes(" ")) {
1386
+ const candidateNoSpace = candidate.replace(/\s+/g, "").toLocaleLowerCase("tr-TR");
1387
+ if (candidateNoSpace === cleanWord) {
1388
+ return { isCorrect: false, word: word2, suggestion: candidate };
1389
+ }
1390
+ }
1391
+ }
1093
1392
  let best = null;
1094
1393
  for (const candidate of this.autocompleteCache) {
1095
1394
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
1096
1395
  continue;
1097
1396
  if (Math.abs(candidate.length - cleanWord.length) > 2)
1098
1397
  continue;
1099
- const distance = this.damerauLevenshtein(cleanWord, candidate);
1100
- if (distance === 0)
1398
+ const rawDist = this.damerauLevenshtein(cleanWord, candidate);
1399
+ if (rawDist === 0)
1101
1400
  continue;
1102
1401
  const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
1103
1402
  const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
1403
+ const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
1104
1404
  const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
1105
1405
  if (better) {
1106
- best = { candidate, distance, firstMismatch, lengthMismatch };
1406
+ best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
1107
1407
  }
1108
1408
  }
1109
- if (best && best.distance <= 2) {
1409
+ if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
1110
1410
  return { isCorrect: false, word: word2, suggestion: best.candidate };
1111
1411
  }
1112
1412
  return { isCorrect: false, word: word2 };
@@ -1513,14 +1813,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1513
1813
  meaningCount: meaningsA.length,
1514
1814
  origin: originA,
1515
1815
  syllables: this.syllabicate(a),
1516
- harmony: this.checkVowelHarmony(a)
1816
+ harmony: this.checkVowelHarmony(a),
1817
+ labialHarmony: this.checkLabialHarmony(a)
1517
1818
  },
1518
1819
  b: {
1519
1820
  word: b,
1520
1821
  meaningCount: meaningsB.length,
1521
1822
  origin: originB,
1522
1823
  syllables: this.syllabicate(b),
1523
- harmony: this.checkVowelHarmony(b)
1824
+ harmony: this.checkVowelHarmony(b),
1825
+ labialHarmony: this.checkLabialHarmony(b)
1524
1826
  }
1525
1827
  };
1526
1828
  }
@@ -1651,9 +1953,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1651
1953
  }
1652
1954
  /**
1653
1955
  * Syllabicates a Turkish word based on general grammar rules.
1956
+ * Handles syllable separation for vowels, single consonants, double consonants,
1957
+ * and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
1654
1958
  */
1655
1959
  static syllabicate(word2) {
1656
1960
  const vowels = /[aeıioöuüAEIİOÖUÜ]/;
1961
+ const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
1657
1962
  const result = [];
1658
1963
  let currentSyllable = "";
1659
1964
  for (let i = word2.length - 1; i >= 0; i--) {
@@ -1664,8 +1969,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1664
1969
  currentSyllable = word2[i - 1] + currentSyllable;
1665
1970
  i--;
1666
1971
  } else if (i - 2 >= 0 && !vowels.test(word2[i - 2])) {
1667
- currentSyllable = word2[i - 1] + currentSyllable;
1668
- i--;
1972
+ if (i - 3 >= 0 && !vowels.test(word2[i - 3]) && ONSET_CLUSTERS.has((word2[i - 2] + word2[i - 1]).toLowerCase())) {
1973
+ currentSyllable = word2[i - 2] + word2[i - 1] + currentSyllable;
1974
+ i -= 2;
1975
+ } else {
1976
+ currentSyllable = word2[i - 1] + currentSyllable;
1977
+ i--;
1978
+ }
1669
1979
  }
1670
1980
  }
1671
1981
  result.unshift(currentSyllable);
@@ -1695,12 +2005,375 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1695
2005
  const hasFront = frontVowels.test(lower);
1696
2006
  return !(hasBack && hasFront);
1697
2007
  }
2008
+ /**
2009
+ * Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
2010
+ * Rules:
2011
+ * 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
2012
+ * 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
2013
+ * Single-syllable words and words with <=1 vowel are considered compliant by convention.
2014
+ */
2015
+ static checkLabialHarmony(word2) {
2016
+ const lower = word2.toLocaleLowerCase("tr-TR");
2017
+ const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
2018
+ if (vowels.length <= 1)
2019
+ return true;
2020
+ for (let i = 0; i < vowels.length - 1; i++) {
2021
+ const v1 = vowels[i];
2022
+ const v2 = vowels[i + 1];
2023
+ if ("ae\u0131i".includes(v1)) {
2024
+ if (!"ae\u0131i".includes(v2))
2025
+ return false;
2026
+ } else if ("o\xF6u\xFC".includes(v1)) {
2027
+ if (!"aeu\xFC".includes(v2))
2028
+ return false;
2029
+ }
2030
+ }
2031
+ return true;
2032
+ }
2033
+ /**
2034
+ * Searches TDK headwords using a wildcard / pattern string.
2035
+ * Wildcards:
2036
+ * '_' or '?' matches any single character
2037
+ * '*' matches zero or more characters
2038
+ * Example: "k_l_m" matches "kalem", "kelam", "kilim".
2039
+ * Runs in-memory against TDK's 81k headword list.
2040
+ */
2041
+ static async patternSearch(pattern, options) {
2042
+ if (!pattern || pattern.trim() === "")
2043
+ return [];
2044
+ await this.ensureAutocompleteLoaded();
2045
+ const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
2046
+ const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
2047
+ const regex = new RegExp(`^${escaped}$`, "u");
2048
+ const max = options?.maxResults ?? 50;
2049
+ const matches = [];
2050
+ for (const headword of this.autocompleteCache) {
2051
+ const lower = headword.toLocaleLowerCase("tr-TR");
2052
+ if (regex.test(lower)) {
2053
+ matches.push(headword);
2054
+ if (matches.length >= max)
2055
+ break;
2056
+ }
2057
+ }
2058
+ return matches;
2059
+ }
2060
+ /**
2061
+ * Finds headwords in TDK that can be formed from the given letters (anagrams).
2062
+ * If exact-length anagrams exist, they are returned.
2063
+ * If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
2064
+ * minimum 3 letters) are returned, sorted by length descending.
2065
+ */
2066
+ static async findAnagrams(letters, options) {
2067
+ if (!letters || letters.trim() === "")
2068
+ return [];
2069
+ await this.ensureAutocompleteLoaded();
2070
+ const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
2071
+ if (clean.length === 0)
2072
+ return [];
2073
+ const forceExact = options?.exactLength === true;
2074
+ const max = options?.maxResults ?? 50;
2075
+ const getFrequency = (str) => {
2076
+ const freq = {};
2077
+ for (const ch of str) {
2078
+ freq[ch] = (freq[ch] || 0) + 1;
2079
+ }
2080
+ return freq;
2081
+ };
2082
+ const targetFreq = getFrequency(clean);
2083
+ const exactMatches = [];
2084
+ const subMatches = [];
2085
+ for (const headword of this.autocompleteCache) {
2086
+ const lower = headword.toLocaleLowerCase("tr-TR");
2087
+ if (lower.includes(" ") || lower.includes("-"))
2088
+ continue;
2089
+ if (lower.length > clean.length || lower.length < 3)
2090
+ continue;
2091
+ const wordFreq = getFrequency(lower);
2092
+ let isValid = true;
2093
+ for (const [ch, count] of Object.entries(wordFreq)) {
2094
+ if (!targetFreq[ch] || targetFreq[ch] < count) {
2095
+ isValid = false;
2096
+ break;
2097
+ }
2098
+ }
2099
+ if (isValid && lower !== clean) {
2100
+ if (lower.length === clean.length) {
2101
+ exactMatches.push(headword);
2102
+ } else {
2103
+ subMatches.push(headword);
2104
+ }
2105
+ }
2106
+ }
2107
+ if (exactMatches.length > 0 || forceExact) {
2108
+ return exactMatches.slice(0, max);
2109
+ }
2110
+ subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
2111
+ return subMatches.slice(0, max);
2112
+ }
2113
+ /**
2114
+ * Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
2115
+ * @param word The target word
2116
+ * @param options.minLetters Minimum number of ending characters that must match (default: 3)
2117
+ * @param options.maxResults Maximum number of rhyme results to return (default: 50)
2118
+ */
2119
+ static async findRhymes(word2, options) {
2120
+ if (!word2 || word2.trim() === "")
2121
+ return [];
2122
+ await this.ensureAutocompleteLoaded();
2123
+ const clean = word2.trim().toLocaleLowerCase("tr-TR");
2124
+ const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
2125
+ const max = options?.maxResults ?? 50;
2126
+ const suffix = clean.slice(-minLetters);
2127
+ const results = [];
2128
+ for (const headword of this.autocompleteCache) {
2129
+ const lower = headword.toLocaleLowerCase("tr-TR");
2130
+ if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
2131
+ results.push(headword);
2132
+ if (results.length >= max)
2133
+ break;
2134
+ }
2135
+ }
2136
+ return results;
2137
+ }
2138
+ /**
2139
+ * Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
2140
+ * Detects:
2141
+ * 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
2142
+ * 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
2143
+ * 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
2144
+ * 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
2145
+ */
2146
+ static async proofread(text) {
2147
+ if (!text || text.trim() === "") {
2148
+ return { text: text || "", issues: [], isCorrect: true };
2149
+ }
2150
+ await this.ensureAutocompleteLoaded();
2151
+ const issues = [];
2152
+ const SOMBAHCEMI = /* @__PURE__ */ new Set([
2153
+ "sanki",
2154
+ "oysaki",
2155
+ "mademki",
2156
+ "belki",
2157
+ "halbuki",
2158
+ "\xE7\xFCnk\xFC",
2159
+ "me\u011Ferki",
2160
+ "illaki"
2161
+ ]);
2162
+ const PHRASE_MISTAKES = [
2163
+ {
2164
+ regex: /\bhiç\s+bir\b/gi,
2165
+ suggestion: "hi\xE7bir",
2166
+ message: "'hi\xE7bir' belgisiz s\u0131fat\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2167
+ type: "spelling"
2168
+ },
2169
+ {
2170
+ regex: /\bbir\s+çok\b/gi,
2171
+ suggestion: "bir\xE7ok",
2172
+ message: "'bir\xE7ok' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2173
+ type: "spelling"
2174
+ },
2175
+ {
2176
+ regex: /\bbir\s+kaç\b/gi,
2177
+ suggestion: "birka\xE7",
2178
+ message: "'birka\xE7' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2179
+ type: "spelling"
2180
+ },
2181
+ {
2182
+ regex: /\bbir\s+az\b/gi,
2183
+ suggestion: "biraz",
2184
+ message: "'biraz' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2185
+ type: "spelling"
2186
+ },
2187
+ {
2188
+ regex: /\bher\s+hangi\b/gi,
2189
+ suggestion: "herhangi",
2190
+ message: "'herhangi' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2191
+ type: "spelling"
2192
+ },
2193
+ {
2194
+ regex: /\bgit\s+gide\b/gi,
2195
+ suggestion: "gitgide",
2196
+ message: "'gitgide' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2197
+ type: "spelling"
2198
+ },
2199
+ {
2200
+ regex: /\bbirden\s+bire\b/gi,
2201
+ suggestion: "birdenbire",
2202
+ message: "'birdenbire' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2203
+ type: "spelling"
2204
+ },
2205
+ {
2206
+ regex: /\brast\s+gele\b/gi,
2207
+ suggestion: "rastgele",
2208
+ message: "'rastgele' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2209
+ type: "spelling"
2210
+ }
2211
+ ];
2212
+ const coveredRanges = [];
2213
+ for (const pm of PHRASE_MISTAKES) {
2214
+ let pmMatch;
2215
+ while ((pmMatch = pm.regex.exec(text)) !== null) {
2216
+ const start = pmMatch.index;
2217
+ const end = start + pmMatch[0].length;
2218
+ coveredRanges.push({ start, end });
2219
+ issues.push({
2220
+ type: pm.type,
2221
+ word: pmMatch[0],
2222
+ startIndex: start,
2223
+ endIndex: end,
2224
+ suggestion: pm.suggestion,
2225
+ message: pm.message
2226
+ });
2227
+ }
2228
+ }
2229
+ const tokenRegex = /[\p{L}0-9'’]+/gu;
2230
+ let match;
2231
+ while ((match = tokenRegex.exec(text)) !== null) {
2232
+ const rawWord = match[0];
2233
+ const startIndex = match.index;
2234
+ const endIndex = startIndex + rawWord.length;
2235
+ const lower = rawWord.toLocaleLowerCase("tr-TR");
2236
+ if (/^\d+$/.test(lower))
2237
+ continue;
2238
+ if (coveredRanges.some((r) => startIndex >= r.start && endIndex <= r.end))
2239
+ continue;
2240
+ let flagged = false;
2241
+ 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)?)$/);
2242
+ if (questionMatch) {
2243
+ const base = questionMatch[1];
2244
+ const particle = questionMatch[2];
2245
+ if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
2246
+ if (!await this.isHeadword(lower)) {
2247
+ issues.push({
2248
+ type: "question_particle",
2249
+ word: rawWord,
2250
+ startIndex,
2251
+ endIndex,
2252
+ suggestion: `${base} ${particle}`,
2253
+ message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
2254
+ });
2255
+ flagged = true;
2256
+ }
2257
+ }
2258
+ }
2259
+ 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;
2260
+ if (!flagged && lower.endsWith("ki") && lower.length > 3) {
2261
+ const base = lower.slice(0, -2);
2262
+ if (!SOMBAHCEMI.has(lower)) {
2263
+ if (!await this.isHeadword(lower)) {
2264
+ const root = await this.findRoot(base);
2265
+ const isVerb = (base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base)) && (root ? root.endsWith("mek") || root.endsWith("mak") : true);
2266
+ if (isVerb) {
2267
+ issues.push({
2268
+ type: "conjunction_ki",
2269
+ word: rawWord,
2270
+ startIndex,
2271
+ endIndex,
2272
+ suggestion: `${base} ki`,
2273
+ message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
2274
+ });
2275
+ flagged = true;
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+ if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
2281
+ const base = lower.slice(0, -2);
2282
+ const ending = lower.slice(-2);
2283
+ if (!await this.isHeadword(lower)) {
2284
+ const root = await this.findRoot(base);
2285
+ const isVerb = VERB_CONJUGATION_REGEX.test(base) && (root ? root.endsWith("mek") || root.endsWith("mak") : false);
2286
+ if (isVerb) {
2287
+ const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
2288
+ issues.push({
2289
+ type: "conjunction_da",
2290
+ word: rawWord,
2291
+ startIndex,
2292
+ endIndex,
2293
+ suggestion: `${base} ${correctEnding}`,
2294
+ message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
2295
+ });
2296
+ flagged = true;
2297
+ }
2298
+ }
2299
+ }
2300
+ const seyMatch = lower.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
2301
+ if (!flagged && seyMatch && !SEY_EXCEPTIONS.has(lower)) {
2302
+ let prefix = seyMatch[1];
2303
+ const suffix = seyMatch[2] || "";
2304
+ if (prefix === "hicbir")
2305
+ prefix = "hi\xE7bir";
2306
+ if (prefix === "cok")
2307
+ prefix = "\xE7ok";
2308
+ issues.push({
2309
+ type: "spelling",
2310
+ word: rawWord,
2311
+ startIndex,
2312
+ endIndex,
2313
+ suggestion: `${prefix} \u015Fey${suffix}`,
2314
+ message: "'\u015Fey' s\xF6zc\xFC\u011F\xFC kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r."
2315
+ });
2316
+ flagged = true;
2317
+ }
2318
+ if (!flagged && lower === "yada") {
2319
+ issues.push({
2320
+ type: "spelling",
2321
+ word: rawWord,
2322
+ startIndex,
2323
+ endIndex,
2324
+ suggestion: "ya da",
2325
+ message: "'ya da' ba\u011Flac\u0131 her zaman ayr\u0131 yaz\u0131l\u0131r."
2326
+ });
2327
+ flagged = true;
2328
+ }
2329
+ if (!flagged && (lower === "burda" || lower === "\u015Furda" || lower === "surda" || lower === "orda" || lower === "i\xE7erde" || lower === "icerde" || lower === "d\u0131\u015Farda" || lower === "disarda" || lower === "yukarda")) {
2330
+ const correct = COMMON_MISSPELLINGS[lower] || lower;
2331
+ issues.push({
2332
+ type: "spelling",
2333
+ word: rawWord,
2334
+ startIndex,
2335
+ endIndex,
2336
+ suggestion: correct,
2337
+ message: `'${rawWord}' s\xF6zc\xFC\u011F\xFCnde \xFCnl\xFC d\xFC\u015Fmesi yap\u0131lmaz.`
2338
+ });
2339
+ flagged = true;
2340
+ }
2341
+ if (!flagged) {
2342
+ const check = await this.checkSpelling(rawWord);
2343
+ if (!check.isCorrect) {
2344
+ issues.push({
2345
+ type: "spelling",
2346
+ word: rawWord,
2347
+ startIndex,
2348
+ endIndex,
2349
+ suggestion: check.suggestion,
2350
+ message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir.` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
2351
+ });
2352
+ }
2353
+ }
2354
+ }
2355
+ issues.sort((a, b) => a.startIndex - b.startIndex);
2356
+ return {
2357
+ text,
2358
+ issues,
2359
+ isCorrect: issues.length === 0
2360
+ };
2361
+ }
1698
2362
  };
1699
2363
 
1700
2364
  // src/cli.ts
1701
2365
  var rawArgs = process.argv.slice(2);
1702
2366
  var jsonMode = rawArgs.includes("--json");
1703
2367
  var args = rawArgs.filter((a) => a !== "--json");
2368
+ var isColor = !jsonMode && Boolean(process.stdout.isTTY);
2369
+ var c = {
2370
+ bold: (s) => isColor ? `\x1B[1m${s}\x1B[0m` : s,
2371
+ dim: (s) => isColor ? `\x1B[2m${s}\x1B[0m` : s,
2372
+ green: (s) => isColor ? `\x1B[32m${s}\x1B[0m` : s,
2373
+ yellow: (s) => isColor ? `\x1B[33m${s}\x1B[0m` : s,
2374
+ cyan: (s) => isColor ? `\x1B[36m${s}\x1B[0m` : s,
2375
+ red: (s) => isColor ? `\x1B[31m${s}\x1B[0m` : s
2376
+ };
1704
2377
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1705
2378
  "ara",
1706
2379
  "anlam",
@@ -1708,6 +2381,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1708
2381
  "ornek",
1709
2382
  "hece",
1710
2383
  "uyum",
2384
+ "kucukuyum",
1711
2385
  "yazim",
1712
2386
  "kok",
1713
2387
  "stem",
@@ -1722,13 +2396,21 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1722
2396
  "karsilastir",
1723
2397
  "analiz",
1724
2398
  "oneri",
2399
+ "bulmaca",
2400
+ "pattern",
2401
+ "anagram",
2402
+ "kafiye",
2403
+ "rhyme",
2404
+ "denetle",
2405
+ "proofread",
2406
+ "repl",
1725
2407
  "kubbealti",
1726
2408
  "nisanyan",
1727
2409
  "viki"
1728
2410
  ]);
1729
2411
  var command = args[0];
1730
2412
  var word = args.slice(1).join(" ");
1731
- if (command && !KNOWN_COMMANDS.has(command) && command !== "--help" && command !== "-h") {
2413
+ if (command && !KNOWN_COMMANDS.has(command) && command !== "--help" && command !== "-h" && command !== "--version" && command !== "-v") {
1732
2414
  word = args.join(" ");
1733
2415
  command = "anlam";
1734
2416
  }
@@ -1743,17 +2425,97 @@ function printError(message) {
1743
2425
  if (jsonMode) {
1744
2426
  console.log(JSON.stringify({ error: message }));
1745
2427
  } else {
1746
- console.log(`Hata: ${message}`);
2428
+ console.log(c.red(`Hata: ${message}`));
1747
2429
  }
1748
2430
  }
2431
+ async function startRepl() {
2432
+ const readline = await import("readline");
2433
+ const rl = readline.createInterface({
2434
+ input: process.stdin,
2435
+ output: process.stdout,
2436
+ prompt: c.cyan("tdk> ")
2437
+ });
2438
+ console.log(c.bold("TDK \u0130nteraktif S\xF6zl\xFCk Kabu\u011Fu (\xC7\u0131kmak i\xE7in 'exit' veya Ctrl+C)"));
2439
+ console.log(c.dim("Komutlar: ara <kelime>, hece <kelime>, bulmaca <desen>, denetle <metin> veya do\u011Frudan kelime"));
2440
+ rl.prompt();
2441
+ rl.on("line", async (line) => {
2442
+ const trimmed = line.trim();
2443
+ if (!trimmed) {
2444
+ rl.prompt();
2445
+ return;
2446
+ }
2447
+ if (trimmed === "exit" || trimmed === "quit" || trimmed === ".exit") {
2448
+ rl.close();
2449
+ return;
2450
+ }
2451
+ const parts = trimmed.split(/\s+/);
2452
+ let subCmd = parts[0].toLowerCase();
2453
+ let subArg = parts.slice(1).join(" ");
2454
+ if (!KNOWN_COMMANDS.has(subCmd)) {
2455
+ subArg = trimmed;
2456
+ subCmd = "anlam";
2457
+ }
2458
+ try {
2459
+ if (subCmd === "ara" || subCmd === "anlam") {
2460
+ const meanings = await TDK.getMeanings(subArg);
2461
+ if (meanings.length === 0)
2462
+ console.log(c.dim("Sonu\xE7 bulunamad\u0131."));
2463
+ else
2464
+ meanings.forEach((m, i) => console.log(`${i + 1}. ${c.green(m)}`));
2465
+ } else if (subCmd === "koken") {
2466
+ const origin = await TDK.getOrigin(subArg);
2467
+ console.log(`K\xF6ken: ${c.cyan(origin || "Bilinmiyor")}`);
2468
+ } else if (subCmd === "hece") {
2469
+ const s = TDK.syllabicate(subArg);
2470
+ console.log(`Heceler: ${c.yellow(s.join("-"))}`);
2471
+ } else if (subCmd === "uyum") {
2472
+ const h = TDK.checkVowelHarmony(subArg);
2473
+ console.log(`B\xFCy\xFCk \xDCnl\xFC Uyumu: ${h ? c.green("Uyar") : c.red("Uymaz")}`);
2474
+ } else if (subCmd === "kucukuyum") {
2475
+ const h = TDK.checkLabialHarmony(subArg);
2476
+ console.log(`K\xFC\xE7\xFCk \xDCnl\xFC Uyumu: ${h ? c.green("Uyar") : c.red("Uymaz")}`);
2477
+ } else if (subCmd === "bulmaca" || subCmd === "pattern") {
2478
+ const matches = await TDK.patternSearch(subArg);
2479
+ console.log(matches.slice(0, 15).join(", "));
2480
+ } else if (subCmd === "denetle" || subCmd === "proofread") {
2481
+ const res = await TDK.proofread(subArg);
2482
+ if (res.isCorrect)
2483
+ console.log(c.green("\u2713 Sorun bulunamad\u0131."));
2484
+ else
2485
+ res.issues.forEach((iss) => console.log(`- ${c.yellow(iss.word)}: ${iss.message}${iss.suggestion ? " -> " + c.green(iss.suggestion) : ""}`));
2486
+ } else {
2487
+ console.log(c.dim("\xD6rnek komutlar: 'ara kalem', 'hece elektrik', 'bulmaca k_l_m', 'denetle Bug\xFCn evdeyim'"));
2488
+ }
2489
+ } catch (e) {
2490
+ console.log(c.red(`Hata: ${e?.message || e}`));
2491
+ }
2492
+ rl.prompt();
2493
+ });
2494
+ }
1749
2495
  async function run() {
1750
- if (!command || command === "--help" || command === "-h") {
2496
+ if (!command) {
2497
+ if (process.stdin.isTTY) {
2498
+ await startRepl();
2499
+ return;
2500
+ }
2501
+ console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
2502
+ console.log(
2503
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki"
2504
+ );
2505
+ console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
2506
+ process.exit(1);
2507
+ }
2508
+ if (command === "--version" || command === "-v") {
2509
+ console.log("tdk-api-wrapper v1.5.1");
2510
+ process.exit(0);
2511
+ }
2512
+ if (command === "--help" || command === "-h") {
1751
2513
  console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
1752
2514
  console.log(
1753
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, kubbealti, nisanyan, viki"
2515
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki"
1754
2516
  );
1755
2517
  console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
1756
- process.exit(command ? 0 : 1);
2518
+ process.exit(0);
1757
2519
  }
1758
2520
  TDK.enableCache(false);
1759
2521
  try {
@@ -1812,6 +2574,17 @@ async function run() {
1812
2574
  );
1813
2575
  break;
1814
2576
  }
2577
+ case "kucukuyum":
2578
+ case "labial": {
2579
+ if (!word)
2580
+ throw new Error("Kelime belirtmelisiniz.");
2581
+ const isHarmony = TDK.checkLabialHarmony(word);
2582
+ printResult(
2583
+ { word, labialHarmony: isHarmony },
2584
+ () => console.log(`K\xFC\xE7\xFCk \xDCnl\xFC Uyumu: ${isHarmony ? "Uyar" : "Uymaz"}`)
2585
+ );
2586
+ break;
2587
+ }
1815
2588
  case "yazim": {
1816
2589
  if (!word)
1817
2590
  throw new Error("Kelime belirtmelisiniz.");
@@ -1987,6 +2760,76 @@ async function run() {
1987
2760
  });
1988
2761
  break;
1989
2762
  }
2763
+ case "bulmaca":
2764
+ case "pattern": {
2765
+ if (!word)
2766
+ throw new Error("Desen belirtmelisiniz (\xF6rn: k_l_m).");
2767
+ const matches = await TDK.patternSearch(word);
2768
+ printResult(matches, () => {
2769
+ if (matches.length === 0) {
2770
+ console.log("E\u015Fle\u015Fen kelime bulunamad\u0131.");
2771
+ } else {
2772
+ console.log(c.bold(`Bulunan Kelimeler (${matches.length}):`));
2773
+ matches.forEach((m, i) => console.log(`${i + 1}. ${c.cyan(m)}`));
2774
+ }
2775
+ });
2776
+ break;
2777
+ }
2778
+ case "anagram": {
2779
+ if (!word)
2780
+ throw new Error("Harfler belirtmelisiniz.");
2781
+ const anagrams = await TDK.findAnagrams(word);
2782
+ printResult(anagrams, () => {
2783
+ if (anagrams.length === 0) {
2784
+ console.log("Anagram veya bu harflerle t\xFCretilebilecek kelime bulunamad\u0131.");
2785
+ } else {
2786
+ const clean = word.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
2787
+ const hasExact = anagrams.some((a) => a.length === clean.length);
2788
+ const title = hasExact ? `Anagramlar (${anagrams.length}):` : `Birebir anagram bulunamad\u0131. Bu harflerle t\xFCretilen kelimeler (${anagrams.length}):`;
2789
+ console.log(c.bold(title));
2790
+ anagrams.forEach((a, i) => console.log(`${i + 1}. ${c.green(a)} ${c.dim(`(${a.length} harf)`)}`));
2791
+ }
2792
+ });
2793
+ break;
2794
+ }
2795
+ case "kafiye":
2796
+ case "rhyme": {
2797
+ if (!word)
2798
+ throw new Error("Kelime belirtmelisiniz.");
2799
+ const rhymes = await TDK.findRhymes(word);
2800
+ printResult(rhymes, () => {
2801
+ if (rhymes.length === 0) {
2802
+ console.log("Kafiye bulunamad\u0131.");
2803
+ } else {
2804
+ console.log(c.bold(`Kafiyeli Kelimeler (${rhymes.length}):`));
2805
+ rhymes.forEach((r, i) => console.log(`${i + 1}. ${c.yellow(r)}`));
2806
+ }
2807
+ });
2808
+ break;
2809
+ }
2810
+ case "denetle":
2811
+ case "proofread": {
2812
+ if (!word)
2813
+ throw new Error("Metin belirtmelisiniz.");
2814
+ const result = await TDK.proofread(word);
2815
+ printResult(result, () => {
2816
+ if (result.isCorrect) {
2817
+ console.log(c.green("\u2713 Metinde imla veya ba\u011Fla\xE7 hatas\u0131 tespit edilmedi."));
2818
+ } else {
2819
+ console.log(c.bold(c.red(`Metinde ${result.issues.length} olas\u0131 sorun tespit edildi:`)));
2820
+ result.issues.forEach((issue, i) => {
2821
+ const label = c.yellow(`[${issue.type}]`);
2822
+ const sug = issue.suggestion ? c.green(` -> \xD6neri: ${issue.suggestion}`) : "";
2823
+ console.log(`${i + 1}. ${label} "${c.bold(issue.word)}": ${issue.message}${sug}`);
2824
+ });
2825
+ }
2826
+ });
2827
+ break;
2828
+ }
2829
+ case "repl": {
2830
+ await startRepl();
2831
+ break;
2832
+ }
1990
2833
  case "kubbealti": {
1991
2834
  if (!word)
1992
2835
  throw new Error("Kelime belirtmelisiniz.");