tdk-api-wrapper 1.4.0 → 1.5.0

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 [];
@@ -526,9 +569,14 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
526
569
  const stem = current.slice(0, -suffix.length);
527
570
  const hardened = restoreConsonantSoftening(stem);
528
571
  const vowelDropped = restoreVowelDrop(stem);
529
- const verbalBases = [stem, ...hardened];
572
+ const geminated = restoreGemination(stem);
573
+ const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
574
+ const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
575
+ const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
576
+ 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");
577
+ const verbalBases = [stem, ...hardened, ...narrowed];
530
578
  const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
531
- const variants = [stem, ...hardened, ...vowelDropped, ...infinitives];
579
+ const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
532
580
  for (const variant of variants) {
533
581
  if (!seen.has(variant) && variant !== normalized) {
534
582
  seen.add(variant);
@@ -536,6 +584,14 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
536
584
  candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
537
585
  }
538
586
  }
587
+ for (const inf of infinitives) {
588
+ if (!seen.has(inf) && inf !== normalized) {
589
+ seen.add(inf);
590
+ nextFrontier.push(inf);
591
+ const weight = isVerbSuffix ? stem.length + 5 : stem.length;
592
+ candidatesWithWeight.push({ candidate: inf, baseLength: weight });
593
+ }
594
+ }
539
595
  }
540
596
  }
541
597
  }
@@ -544,7 +600,7 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
544
600
  frontier = nextFrontier;
545
601
  }
546
602
  candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
547
- return [...new Set(candidatesWithWeight.map((c) => c.candidate))];
603
+ return [...new Set(candidatesWithWeight.map((c2) => c2.candidate))];
548
604
  }
549
605
 
550
606
  // src/tdk.ts
@@ -633,6 +689,10 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
633
689
  yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
634
690
  -----END CERTIFICATE-----`
635
691
  ];
692
+ // Configuration
693
+ static defaultTimeoutMs = 8e3;
694
+ static defaultRetries = 1;
695
+ static maxCacheSize = 1e3;
636
696
  // Cache Mechanism
637
697
  static isCacheEnabled = false;
638
698
  static wordCache = /* @__PURE__ */ new Map();
@@ -640,6 +700,19 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
640
700
  static autocompleteCache = [];
641
701
  static autocompleteSet = /* @__PURE__ */ new Set();
642
702
  static stemCache = /* @__PURE__ */ new Map();
703
+ /**
704
+ * Configures global client options such as network timeout, retries, and cache size.
705
+ */
706
+ static configure(config) {
707
+ if (config.timeoutMs !== void 0)
708
+ this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
709
+ if (config.retries !== void 0)
710
+ this.defaultRetries = Math.max(0, config.retries);
711
+ if (config.cache !== void 0)
712
+ this.enableCache(config.cache);
713
+ if (config.maxCacheSize !== void 0)
714
+ this.maxCacheSize = Math.max(10, config.maxCacheSize);
715
+ }
643
716
  /**
644
717
  * Enables or disables in-memory caching for API requests.
645
718
  */
@@ -659,9 +732,50 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
659
732
  this.autocompleteSet.clear();
660
733
  this.stemCache.clear();
661
734
  }
735
+ static setBoundedCache(map, key, value) {
736
+ if (map.size >= this.maxCacheSize) {
737
+ const firstKey = map.keys().next().value;
738
+ if (firstKey !== void 0)
739
+ map.delete(firstKey);
740
+ }
741
+ map.set(key, value);
742
+ }
662
743
  static delay(ms) {
663
744
  return new Promise((resolve) => setTimeout(resolve, ms));
664
745
  }
746
+ /**
747
+ * Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
748
+ */
749
+ static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
750
+ let lastError;
751
+ for (let attempt = 0; attempt <= retries; attempt++) {
752
+ try {
753
+ const signal = AbortSignal.timeout(timeoutMs);
754
+ const headers = {
755
+ "User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
756
+ ...options.headers || {}
757
+ };
758
+ const res = await fetch(url, { ...options, headers, signal });
759
+ if (res.ok || res.status >= 400 && res.status < 500) {
760
+ return res;
761
+ }
762
+ if (attempt < retries) {
763
+ await this.delay(200 * (attempt + 1));
764
+ continue;
765
+ }
766
+ return res;
767
+ } catch (err) {
768
+ lastError = err;
769
+ if (attempt < retries) {
770
+ await this.delay(200 * (attempt + 1));
771
+ continue;
772
+ }
773
+ }
774
+ }
775
+ throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
776
+ cause: lastError
777
+ });
778
+ }
665
779
  /**
666
780
  * Fetches detailed information for a given word from the TDK Dictionary.
667
781
  */
@@ -676,9 +790,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
676
790
  const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
677
791
  let response;
678
792
  try {
679
- response = await fetch(url, {
680
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
681
- });
793
+ response = await this.fetchWithRetry(url);
682
794
  } catch (error) {
683
795
  throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
684
796
  }
@@ -695,12 +807,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
695
807
  }
696
808
  if (!Array.isArray(data) && data && "error" in data) {
697
809
  if (this.isCacheEnabled)
698
- this.wordCache.set(cleanWord, []);
810
+ this.setBoundedCache(this.wordCache, cleanWord, []);
699
811
  return [];
700
812
  }
701
813
  const results = data;
702
814
  if (this.isCacheEnabled) {
703
- this.wordCache.set(cleanWord, results);
815
+ this.setBoundedCache(this.wordCache, cleanWord, results);
704
816
  }
705
817
  return results;
706
818
  }
@@ -827,17 +939,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
827
939
  return this.stemCache.get(clean);
828
940
  }
829
941
  if (await this.isHeadword(clean)) {
830
- this.stemCache.set(clean, clean);
942
+ this.setBoundedCache(this.stemCache, clean, clean);
831
943
  return clean;
832
944
  }
833
945
  const candidates = getStemCandidates(clean);
834
946
  for (const candidate of candidates) {
835
947
  if (await this.isHeadword(candidate)) {
836
- this.stemCache.set(clean, candidate);
948
+ this.setBoundedCache(this.stemCache, clean, candidate);
837
949
  return candidate;
838
950
  }
839
951
  }
840
- this.stemCache.set(clean, null);
952
+ this.setBoundedCache(this.stemCache, clean, null);
841
953
  return null;
842
954
  }
843
955
  /**
@@ -1513,14 +1625,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1513
1625
  meaningCount: meaningsA.length,
1514
1626
  origin: originA,
1515
1627
  syllables: this.syllabicate(a),
1516
- harmony: this.checkVowelHarmony(a)
1628
+ harmony: this.checkVowelHarmony(a),
1629
+ labialHarmony: this.checkLabialHarmony(a)
1517
1630
  },
1518
1631
  b: {
1519
1632
  word: b,
1520
1633
  meaningCount: meaningsB.length,
1521
1634
  origin: originB,
1522
1635
  syllables: this.syllabicate(b),
1523
- harmony: this.checkVowelHarmony(b)
1636
+ harmony: this.checkVowelHarmony(b),
1637
+ labialHarmony: this.checkLabialHarmony(b)
1524
1638
  }
1525
1639
  };
1526
1640
  }
@@ -1651,9 +1765,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1651
1765
  }
1652
1766
  /**
1653
1767
  * Syllabicates a Turkish word based on general grammar rules.
1768
+ * Handles syllable separation for vowels, single consonants, double consonants,
1769
+ * and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
1654
1770
  */
1655
1771
  static syllabicate(word2) {
1656
1772
  const vowels = /[aeıioöuüAEIİOÖUÜ]/;
1773
+ const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
1657
1774
  const result = [];
1658
1775
  let currentSyllable = "";
1659
1776
  for (let i = word2.length - 1; i >= 0; i--) {
@@ -1664,8 +1781,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1664
1781
  currentSyllable = word2[i - 1] + currentSyllable;
1665
1782
  i--;
1666
1783
  } else if (i - 2 >= 0 && !vowels.test(word2[i - 2])) {
1667
- currentSyllable = word2[i - 1] + currentSyllable;
1668
- i--;
1784
+ if (i - 3 >= 0 && !vowels.test(word2[i - 3]) && ONSET_CLUSTERS.has((word2[i - 2] + word2[i - 1]).toLowerCase())) {
1785
+ currentSyllable = word2[i - 2] + word2[i - 1] + currentSyllable;
1786
+ i -= 2;
1787
+ } else {
1788
+ currentSyllable = word2[i - 1] + currentSyllable;
1789
+ i--;
1790
+ }
1669
1791
  }
1670
1792
  }
1671
1793
  result.unshift(currentSyllable);
@@ -1695,12 +1817,264 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1695
1817
  const hasFront = frontVowels.test(lower);
1696
1818
  return !(hasBack && hasFront);
1697
1819
  }
1820
+ /**
1821
+ * Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
1822
+ * Rules:
1823
+ * 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
1824
+ * 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
1825
+ * Single-syllable words and words with <=1 vowel are considered compliant by convention.
1826
+ */
1827
+ static checkLabialHarmony(word2) {
1828
+ const lower = word2.toLocaleLowerCase("tr-TR");
1829
+ const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
1830
+ if (vowels.length <= 1)
1831
+ return true;
1832
+ for (let i = 0; i < vowels.length - 1; i++) {
1833
+ const v1 = vowels[i];
1834
+ const v2 = vowels[i + 1];
1835
+ if ("ae\u0131i".includes(v1)) {
1836
+ if (!"ae\u0131i".includes(v2))
1837
+ return false;
1838
+ } else if ("o\xF6u\xFC".includes(v1)) {
1839
+ if (!"aeu\xFC".includes(v2))
1840
+ return false;
1841
+ }
1842
+ }
1843
+ return true;
1844
+ }
1845
+ /**
1846
+ * Searches TDK headwords using a wildcard / pattern string.
1847
+ * Wildcards:
1848
+ * '_' or '?' matches any single character
1849
+ * '*' matches zero or more characters
1850
+ * Example: "k_l_m" matches "kalem", "kelam", "kilim".
1851
+ * Runs in-memory against TDK's 81k headword list.
1852
+ */
1853
+ static async patternSearch(pattern, options) {
1854
+ if (!pattern || pattern.trim() === "")
1855
+ return [];
1856
+ await this.ensureAutocompleteLoaded();
1857
+ const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
1858
+ const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
1859
+ const regex = new RegExp(`^${escaped}$`, "u");
1860
+ const max = options?.maxResults ?? 50;
1861
+ const matches = [];
1862
+ for (const headword of this.autocompleteCache) {
1863
+ const lower = headword.toLocaleLowerCase("tr-TR");
1864
+ if (regex.test(lower)) {
1865
+ matches.push(headword);
1866
+ if (matches.length >= max)
1867
+ break;
1868
+ }
1869
+ }
1870
+ return matches;
1871
+ }
1872
+ /**
1873
+ * Finds headwords in TDK that can be formed from the given letters (anagrams).
1874
+ * If exact-length anagrams exist, they are returned.
1875
+ * If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
1876
+ * minimum 3 letters) are returned, sorted by length descending.
1877
+ */
1878
+ static async findAnagrams(letters, options) {
1879
+ if (!letters || letters.trim() === "")
1880
+ return [];
1881
+ await this.ensureAutocompleteLoaded();
1882
+ const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
1883
+ if (clean.length === 0)
1884
+ return [];
1885
+ const forceExact = options?.exactLength === true;
1886
+ const max = options?.maxResults ?? 50;
1887
+ const getFrequency = (str) => {
1888
+ const freq = {};
1889
+ for (const ch of str) {
1890
+ freq[ch] = (freq[ch] || 0) + 1;
1891
+ }
1892
+ return freq;
1893
+ };
1894
+ const targetFreq = getFrequency(clean);
1895
+ const exactMatches = [];
1896
+ const subMatches = [];
1897
+ for (const headword of this.autocompleteCache) {
1898
+ const lower = headword.toLocaleLowerCase("tr-TR");
1899
+ if (lower.includes(" ") || lower.includes("-"))
1900
+ continue;
1901
+ if (lower.length > clean.length || lower.length < 3)
1902
+ continue;
1903
+ const wordFreq = getFrequency(lower);
1904
+ let isValid = true;
1905
+ for (const [ch, count] of Object.entries(wordFreq)) {
1906
+ if (!targetFreq[ch] || targetFreq[ch] < count) {
1907
+ isValid = false;
1908
+ break;
1909
+ }
1910
+ }
1911
+ if (isValid && lower !== clean) {
1912
+ if (lower.length === clean.length) {
1913
+ exactMatches.push(headword);
1914
+ } else {
1915
+ subMatches.push(headword);
1916
+ }
1917
+ }
1918
+ }
1919
+ if (exactMatches.length > 0 || forceExact) {
1920
+ return exactMatches.slice(0, max);
1921
+ }
1922
+ subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
1923
+ return subMatches.slice(0, max);
1924
+ }
1925
+ /**
1926
+ * Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
1927
+ * @param word The target word
1928
+ * @param options.minLetters Minimum number of ending characters that must match (default: 3)
1929
+ * @param options.maxResults Maximum number of rhyme results to return (default: 50)
1930
+ */
1931
+ static async findRhymes(word2, options) {
1932
+ if (!word2 || word2.trim() === "")
1933
+ return [];
1934
+ await this.ensureAutocompleteLoaded();
1935
+ const clean = word2.trim().toLocaleLowerCase("tr-TR");
1936
+ const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
1937
+ const max = options?.maxResults ?? 50;
1938
+ const suffix = clean.slice(-minLetters);
1939
+ const results = [];
1940
+ for (const headword of this.autocompleteCache) {
1941
+ const lower = headword.toLocaleLowerCase("tr-TR");
1942
+ if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
1943
+ results.push(headword);
1944
+ if (results.length >= max)
1945
+ break;
1946
+ }
1947
+ }
1948
+ return results;
1949
+ }
1950
+ /**
1951
+ * Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
1952
+ * Detects:
1953
+ * 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
1954
+ * 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
1955
+ * 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
1956
+ * 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
1957
+ */
1958
+ static async proofread(text) {
1959
+ if (!text || text.trim() === "") {
1960
+ return { text: text || "", issues: [], isCorrect: true };
1961
+ }
1962
+ await this.ensureAutocompleteLoaded();
1963
+ const issues = [];
1964
+ const SOMBAHCEMI = /* @__PURE__ */ new Set([
1965
+ "sanki",
1966
+ "oysaki",
1967
+ "mademki",
1968
+ "belki",
1969
+ "halbuki",
1970
+ "\xE7\xFCnk\xFC",
1971
+ "me\u011Ferki",
1972
+ "illaki"
1973
+ ]);
1974
+ const tokenRegex = /[\p{L}0-9'’]+/gu;
1975
+ let match;
1976
+ while ((match = tokenRegex.exec(text)) !== null) {
1977
+ const rawWord = match[0];
1978
+ const startIndex = match.index;
1979
+ const endIndex = startIndex + rawWord.length;
1980
+ const lower = rawWord.toLocaleLowerCase("tr-TR");
1981
+ if (/^\d+$/.test(lower))
1982
+ continue;
1983
+ let flagged = false;
1984
+ 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)?)$/);
1985
+ if (questionMatch) {
1986
+ const base = questionMatch[1];
1987
+ const particle = questionMatch[2];
1988
+ if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
1989
+ if (!await this.isHeadword(lower)) {
1990
+ issues.push({
1991
+ type: "question_particle",
1992
+ word: rawWord,
1993
+ startIndex,
1994
+ endIndex,
1995
+ suggestion: `${base} ${particle}`,
1996
+ message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
1997
+ });
1998
+ flagged = true;
1999
+ }
2000
+ }
2001
+ }
2002
+ 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;
2003
+ if (!flagged && lower.endsWith("ki") && lower.length > 3) {
2004
+ const base = lower.slice(0, -2);
2005
+ if (!SOMBAHCEMI.has(lower)) {
2006
+ if (!await this.isHeadword(lower)) {
2007
+ const root = await this.findRoot(base);
2008
+ const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base);
2009
+ if (isVerb) {
2010
+ issues.push({
2011
+ type: "conjunction_ki",
2012
+ word: rawWord,
2013
+ startIndex,
2014
+ endIndex,
2015
+ suggestion: `${base} ki`,
2016
+ message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
2017
+ });
2018
+ flagged = true;
2019
+ }
2020
+ }
2021
+ }
2022
+ }
2023
+ if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
2024
+ const base = lower.slice(0, -2);
2025
+ const ending = lower.slice(-2);
2026
+ if (!await this.isHeadword(lower)) {
2027
+ const root = await this.findRoot(base);
2028
+ const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || VERB_CONJUGATION_REGEX.test(base);
2029
+ if (isVerb) {
2030
+ const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
2031
+ issues.push({
2032
+ type: "conjunction_da",
2033
+ word: rawWord,
2034
+ startIndex,
2035
+ endIndex,
2036
+ suggestion: `${base} ${correctEnding}`,
2037
+ message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
2038
+ });
2039
+ flagged = true;
2040
+ }
2041
+ }
2042
+ }
2043
+ if (!flagged) {
2044
+ const check = await this.checkSpelling(rawWord);
2045
+ if (!check.isCorrect) {
2046
+ issues.push({
2047
+ type: "spelling",
2048
+ word: rawWord,
2049
+ startIndex,
2050
+ endIndex,
2051
+ suggestion: check.suggestion,
2052
+ message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir. \xD6neri: '${check.suggestion}'` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
2053
+ });
2054
+ }
2055
+ }
2056
+ }
2057
+ return {
2058
+ text,
2059
+ issues,
2060
+ isCorrect: issues.length === 0
2061
+ };
2062
+ }
1698
2063
  };
1699
2064
 
1700
2065
  // src/cli.ts
1701
2066
  var rawArgs = process.argv.slice(2);
1702
2067
  var jsonMode = rawArgs.includes("--json");
1703
2068
  var args = rawArgs.filter((a) => a !== "--json");
2069
+ var isColor = !jsonMode && Boolean(process.stdout.isTTY);
2070
+ var c = {
2071
+ bold: (s) => isColor ? `\x1B[1m${s}\x1B[0m` : s,
2072
+ dim: (s) => isColor ? `\x1B[2m${s}\x1B[0m` : s,
2073
+ green: (s) => isColor ? `\x1B[32m${s}\x1B[0m` : s,
2074
+ yellow: (s) => isColor ? `\x1B[33m${s}\x1B[0m` : s,
2075
+ cyan: (s) => isColor ? `\x1B[36m${s}\x1B[0m` : s,
2076
+ red: (s) => isColor ? `\x1B[31m${s}\x1B[0m` : s
2077
+ };
1704
2078
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1705
2079
  "ara",
1706
2080
  "anlam",
@@ -1708,6 +2082,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1708
2082
  "ornek",
1709
2083
  "hece",
1710
2084
  "uyum",
2085
+ "kucukuyum",
1711
2086
  "yazim",
1712
2087
  "kok",
1713
2088
  "stem",
@@ -1722,6 +2097,14 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1722
2097
  "karsilastir",
1723
2098
  "analiz",
1724
2099
  "oneri",
2100
+ "bulmaca",
2101
+ "pattern",
2102
+ "anagram",
2103
+ "kafiye",
2104
+ "rhyme",
2105
+ "denetle",
2106
+ "proofread",
2107
+ "repl",
1725
2108
  "kubbealti",
1726
2109
  "nisanyan",
1727
2110
  "viki"
@@ -1743,17 +2126,93 @@ function printError(message) {
1743
2126
  if (jsonMode) {
1744
2127
  console.log(JSON.stringify({ error: message }));
1745
2128
  } else {
1746
- console.log(`Hata: ${message}`);
2129
+ console.log(c.red(`Hata: ${message}`));
1747
2130
  }
1748
2131
  }
2132
+ async function startRepl() {
2133
+ const readline = await import("readline");
2134
+ const rl = readline.createInterface({
2135
+ input: process.stdin,
2136
+ output: process.stdout,
2137
+ prompt: c.cyan("tdk> ")
2138
+ });
2139
+ console.log(c.bold("TDK \u0130nteraktif S\xF6zl\xFCk Kabu\u011Fu (\xC7\u0131kmak i\xE7in 'exit' veya Ctrl+C)"));
2140
+ console.log(c.dim("Komutlar: ara <kelime>, hece <kelime>, bulmaca <desen>, denetle <metin> veya do\u011Frudan kelime"));
2141
+ rl.prompt();
2142
+ rl.on("line", async (line) => {
2143
+ const trimmed = line.trim();
2144
+ if (!trimmed) {
2145
+ rl.prompt();
2146
+ return;
2147
+ }
2148
+ if (trimmed === "exit" || trimmed === "quit" || trimmed === ".exit") {
2149
+ rl.close();
2150
+ return;
2151
+ }
2152
+ const parts = trimmed.split(/\s+/);
2153
+ let subCmd = parts[0].toLowerCase();
2154
+ let subArg = parts.slice(1).join(" ");
2155
+ if (!KNOWN_COMMANDS.has(subCmd)) {
2156
+ subArg = trimmed;
2157
+ subCmd = "anlam";
2158
+ }
2159
+ try {
2160
+ if (subCmd === "ara" || subCmd === "anlam") {
2161
+ const meanings = await TDK.getMeanings(subArg);
2162
+ if (meanings.length === 0)
2163
+ console.log(c.dim("Sonu\xE7 bulunamad\u0131."));
2164
+ else
2165
+ meanings.forEach((m, i) => console.log(`${i + 1}. ${c.green(m)}`));
2166
+ } else if (subCmd === "koken") {
2167
+ const origin = await TDK.getOrigin(subArg);
2168
+ console.log(`K\xF6ken: ${c.cyan(origin || "Bilinmiyor")}`);
2169
+ } else if (subCmd === "hece") {
2170
+ const s = TDK.syllabicate(subArg);
2171
+ console.log(`Heceler: ${c.yellow(s.join("-"))}`);
2172
+ } else if (subCmd === "uyum") {
2173
+ const h = TDK.checkVowelHarmony(subArg);
2174
+ console.log(`B\xFCy\xFCk \xDCnl\xFC Uyumu: ${h ? c.green("Uyar") : c.red("Uymaz")}`);
2175
+ } else if (subCmd === "kucukuyum") {
2176
+ const h = TDK.checkLabialHarmony(subArg);
2177
+ console.log(`K\xFC\xE7\xFCk \xDCnl\xFC Uyumu: ${h ? c.green("Uyar") : c.red("Uymaz")}`);
2178
+ } else if (subCmd === "bulmaca" || subCmd === "pattern") {
2179
+ const matches = await TDK.patternSearch(subArg);
2180
+ console.log(matches.slice(0, 15).join(", "));
2181
+ } else if (subCmd === "denetle" || subCmd === "proofread") {
2182
+ const res = await TDK.proofread(subArg);
2183
+ if (res.isCorrect)
2184
+ console.log(c.green("\u2713 Sorun bulunamad\u0131."));
2185
+ else
2186
+ res.issues.forEach((iss) => console.log(`- ${c.yellow(iss.word)}: ${iss.message}${iss.suggestion ? " -> " + c.green(iss.suggestion) : ""}`));
2187
+ } else {
2188
+ console.log(c.dim("\xD6rnek komutlar: 'ara kalem', 'hece elektrik', 'bulmaca k_l_m', 'denetle Bug\xFCn evdeyim'"));
2189
+ }
2190
+ } catch (e) {
2191
+ console.log(c.red(`Hata: ${e?.message || e}`));
2192
+ }
2193
+ rl.prompt();
2194
+ });
2195
+ }
1749
2196
  async function run() {
1750
- if (!command || command === "--help" || command === "-h") {
2197
+ if (!command) {
2198
+ if (process.stdin.isTTY) {
2199
+ await startRepl();
2200
+ return;
2201
+ }
1751
2202
  console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
1752
2203
  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"
2204
+ "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
2205
  );
1755
2206
  console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
1756
- process.exit(command ? 0 : 1);
2207
+ process.exit(1);
2208
+ }
2209
+ if (command === "--help" || command === "-h") {
2210
+ console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
2211
+ console.log(
2212
+ "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"
2213
+ );
2214
+ console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
2215
+ process.exit(0);
1757
2216
  }
1758
2217
  TDK.enableCache(false);
1759
2218
  try {
@@ -1812,6 +2271,17 @@ async function run() {
1812
2271
  );
1813
2272
  break;
1814
2273
  }
2274
+ case "kucukuyum":
2275
+ case "labial": {
2276
+ if (!word)
2277
+ throw new Error("Kelime belirtmelisiniz.");
2278
+ const isHarmony = TDK.checkLabialHarmony(word);
2279
+ printResult(
2280
+ { word, labialHarmony: isHarmony },
2281
+ () => console.log(`K\xFC\xE7\xFCk \xDCnl\xFC Uyumu: ${isHarmony ? "Uyar" : "Uymaz"}`)
2282
+ );
2283
+ break;
2284
+ }
1815
2285
  case "yazim": {
1816
2286
  if (!word)
1817
2287
  throw new Error("Kelime belirtmelisiniz.");
@@ -1987,6 +2457,76 @@ async function run() {
1987
2457
  });
1988
2458
  break;
1989
2459
  }
2460
+ case "bulmaca":
2461
+ case "pattern": {
2462
+ if (!word)
2463
+ throw new Error("Desen belirtmelisiniz (\xF6rn: k_l_m).");
2464
+ const matches = await TDK.patternSearch(word);
2465
+ printResult(matches, () => {
2466
+ if (matches.length === 0) {
2467
+ console.log("E\u015Fle\u015Fen kelime bulunamad\u0131.");
2468
+ } else {
2469
+ console.log(c.bold(`Bulunan Kelimeler (${matches.length}):`));
2470
+ matches.forEach((m, i) => console.log(`${i + 1}. ${c.cyan(m)}`));
2471
+ }
2472
+ });
2473
+ break;
2474
+ }
2475
+ case "anagram": {
2476
+ if (!word)
2477
+ throw new Error("Harfler belirtmelisiniz.");
2478
+ const anagrams = await TDK.findAnagrams(word);
2479
+ printResult(anagrams, () => {
2480
+ if (anagrams.length === 0) {
2481
+ console.log("Anagram veya bu harflerle t\xFCretilebilecek kelime bulunamad\u0131.");
2482
+ } else {
2483
+ const clean = word.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
2484
+ const hasExact = anagrams.some((a) => a.length === clean.length);
2485
+ const title = hasExact ? `Anagramlar (${anagrams.length}):` : `Birebir anagram bulunamad\u0131. Bu harflerle t\xFCretilen kelimeler (${anagrams.length}):`;
2486
+ console.log(c.bold(title));
2487
+ anagrams.forEach((a, i) => console.log(`${i + 1}. ${c.green(a)} ${c.dim(`(${a.length} harf)`)}`));
2488
+ }
2489
+ });
2490
+ break;
2491
+ }
2492
+ case "kafiye":
2493
+ case "rhyme": {
2494
+ if (!word)
2495
+ throw new Error("Kelime belirtmelisiniz.");
2496
+ const rhymes = await TDK.findRhymes(word);
2497
+ printResult(rhymes, () => {
2498
+ if (rhymes.length === 0) {
2499
+ console.log("Kafiye bulunamad\u0131.");
2500
+ } else {
2501
+ console.log(c.bold(`Kafiyeli Kelimeler (${rhymes.length}):`));
2502
+ rhymes.forEach((r, i) => console.log(`${i + 1}. ${c.yellow(r)}`));
2503
+ }
2504
+ });
2505
+ break;
2506
+ }
2507
+ case "denetle":
2508
+ case "proofread": {
2509
+ if (!word)
2510
+ throw new Error("Metin belirtmelisiniz.");
2511
+ const result = await TDK.proofread(word);
2512
+ printResult(result, () => {
2513
+ if (result.isCorrect) {
2514
+ console.log(c.green("\u2713 Metinde imla veya ba\u011Fla\xE7 hatas\u0131 tespit edilmedi."));
2515
+ } else {
2516
+ console.log(c.bold(c.red(`Metinde ${result.issues.length} olas\u0131 sorun tespit edildi:`)));
2517
+ result.issues.forEach((issue, i) => {
2518
+ const label = c.yellow(`[${issue.type}]`);
2519
+ const sug = issue.suggestion ? c.green(` -> \xD6neri: ${issue.suggestion}`) : "";
2520
+ console.log(`${i + 1}. ${label} "${c.bold(issue.word)}": ${issue.message}${sug}`);
2521
+ });
2522
+ }
2523
+ });
2524
+ break;
2525
+ }
2526
+ case "repl": {
2527
+ await startRepl();
2528
+ break;
2529
+ }
1990
2530
  case "kubbealti": {
1991
2531
  if (!word)
1992
2532
  throw new Error("Kelime belirtmelisiniz.");