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/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  TDK: () => TDK,
34
+ TDKClient: () => TDKClient,
34
35
  TDKError: () => TDKError,
35
36
  TDKNetworkError: () => TDKNetworkError,
36
37
  TDKValidationError: () => TDKValidationError,
@@ -39,8 +40,10 @@ __export(src_exports, {
39
40
  getStemCandidates: () => getStemCandidates,
40
41
  isVowel: () => isVowel,
41
42
  restoreConsonantSoftening: () => restoreConsonantSoftening,
43
+ restoreGemination: () => restoreGemination,
42
44
  restoreInfinitive: () => restoreInfinitive,
43
- restoreVowelDrop: () => restoreVowelDrop
45
+ restoreVowelDrop: () => restoreVowelDrop,
46
+ restoreVowelNarrowing: () => restoreVowelNarrowing
44
47
  });
45
48
  module.exports = __toCommonJS(src_exports);
46
49
 
@@ -378,6 +381,12 @@ var TURKISH_SUFFIXES = [
378
381
  "s\u0131n",
379
382
  "sun",
380
383
  "s\xFCn",
384
+ "sen",
385
+ "san",
386
+ "sem",
387
+ "sam",
388
+ "sek",
389
+ "sak",
381
390
  "siz",
382
391
  "s\u0131z",
383
392
  "suz",
@@ -515,6 +524,43 @@ function restoreVowelDrop(stem) {
515
524
  }
516
525
  return [];
517
526
  }
527
+ function restoreGemination(stem) {
528
+ if (stem.length < 3)
529
+ return [];
530
+ const c1 = stem[stem.length - 2];
531
+ const c2 = stem[stem.length - 1];
532
+ if (c1 === c2 && !isVowel(c1)) {
533
+ const single = stem.slice(0, -1);
534
+ const hardened = restoreConsonantSoftening(single);
535
+ return [single, ...hardened];
536
+ }
537
+ return [];
538
+ }
539
+ function restoreVowelNarrowing(stem) {
540
+ if (stem.length < 2)
541
+ return [];
542
+ if (stem === "di")
543
+ return ["de"];
544
+ if (stem === "yi")
545
+ return ["ye"];
546
+ const lastChar = stem[stem.length - 1];
547
+ const isLastNarrow = "\u0131iu\xFC".includes(lastChar);
548
+ if (isLastNarrow) {
549
+ const vowelsInBase = stem.slice(0, -1).split("").filter(isVowel);
550
+ const lastVowel = vowelsInBase.length > 0 ? vowelsInBase[vowelsInBase.length - 1] : lastChar;
551
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
552
+ return [stem.slice(0, -1) + widened];
553
+ }
554
+ if (!isVowel(lastChar)) {
555
+ const vowelsInBase = stem.split("").filter(isVowel);
556
+ if (vowelsInBase.length > 0) {
557
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
558
+ const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
559
+ return [stem + widened];
560
+ }
561
+ }
562
+ return [];
563
+ }
518
564
  function restoreInfinitive(stem) {
519
565
  if (stem.length < 2)
520
566
  return [];
@@ -547,9 +593,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
547
593
  const stem = current.slice(0, -suffix.length);
548
594
  const hardened = restoreConsonantSoftening(stem);
549
595
  const vowelDropped = restoreVowelDrop(stem);
550
- const verbalBases = [stem, ...hardened];
596
+ const geminated = restoreGemination(stem);
597
+ const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
598
+ const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
599
+ const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
600
+ 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");
601
+ const verbalBases = [stem, ...hardened, ...narrowed];
551
602
  const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
552
- const variants = [stem, ...hardened, ...vowelDropped, ...infinitives];
603
+ const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
553
604
  for (const variant of variants) {
554
605
  if (!seen.has(variant) && variant !== normalized) {
555
606
  seen.add(variant);
@@ -557,6 +608,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
557
608
  candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
558
609
  }
559
610
  }
611
+ for (const inf of infinitives) {
612
+ if (!seen.has(inf) && inf !== normalized) {
613
+ seen.add(inf);
614
+ nextFrontier.push(inf);
615
+ const weight = isVerbSuffix ? stem.length + 5 : stem.length;
616
+ candidatesWithWeight.push({ candidate: inf, baseLength: weight });
617
+ }
618
+ }
560
619
  }
561
620
  }
562
621
  }
@@ -654,6 +713,10 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
654
713
  yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
655
714
  -----END CERTIFICATE-----`
656
715
  ];
716
+ // Configuration
717
+ static defaultTimeoutMs = 8e3;
718
+ static defaultRetries = 1;
719
+ static maxCacheSize = 1e3;
657
720
  // Cache Mechanism
658
721
  static isCacheEnabled = false;
659
722
  static wordCache = /* @__PURE__ */ new Map();
@@ -661,6 +724,19 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
661
724
  static autocompleteCache = [];
662
725
  static autocompleteSet = /* @__PURE__ */ new Set();
663
726
  static stemCache = /* @__PURE__ */ new Map();
727
+ /**
728
+ * Configures global client options such as network timeout, retries, and cache size.
729
+ */
730
+ static configure(config) {
731
+ if (config.timeoutMs !== void 0)
732
+ this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
733
+ if (config.retries !== void 0)
734
+ this.defaultRetries = Math.max(0, config.retries);
735
+ if (config.cache !== void 0)
736
+ this.enableCache(config.cache);
737
+ if (config.maxCacheSize !== void 0)
738
+ this.maxCacheSize = Math.max(10, config.maxCacheSize);
739
+ }
664
740
  /**
665
741
  * Enables or disables in-memory caching for API requests.
666
742
  */
@@ -680,9 +756,50 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
680
756
  this.autocompleteSet.clear();
681
757
  this.stemCache.clear();
682
758
  }
759
+ static setBoundedCache(map, key, value) {
760
+ if (map.size >= this.maxCacheSize) {
761
+ const firstKey = map.keys().next().value;
762
+ if (firstKey !== void 0)
763
+ map.delete(firstKey);
764
+ }
765
+ map.set(key, value);
766
+ }
683
767
  static delay(ms) {
684
768
  return new Promise((resolve) => setTimeout(resolve, ms));
685
769
  }
770
+ /**
771
+ * Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
772
+ */
773
+ static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
774
+ let lastError;
775
+ for (let attempt = 0; attempt <= retries; attempt++) {
776
+ try {
777
+ const signal = AbortSignal.timeout(timeoutMs);
778
+ const headers = {
779
+ "User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
780
+ ...options.headers || {}
781
+ };
782
+ const res = await fetch(url, { ...options, headers, signal });
783
+ if (res.ok || res.status >= 400 && res.status < 500) {
784
+ return res;
785
+ }
786
+ if (attempt < retries) {
787
+ await this.delay(200 * (attempt + 1));
788
+ continue;
789
+ }
790
+ return res;
791
+ } catch (err) {
792
+ lastError = err;
793
+ if (attempt < retries) {
794
+ await this.delay(200 * (attempt + 1));
795
+ continue;
796
+ }
797
+ }
798
+ }
799
+ throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
800
+ cause: lastError
801
+ });
802
+ }
686
803
  /**
687
804
  * Fetches detailed information for a given word from the TDK Dictionary.
688
805
  */
@@ -697,9 +814,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
697
814
  const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
698
815
  let response;
699
816
  try {
700
- response = await fetch(url, {
701
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
702
- });
817
+ response = await this.fetchWithRetry(url);
703
818
  } catch (error) {
704
819
  throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
705
820
  }
@@ -716,12 +831,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
716
831
  }
717
832
  if (!Array.isArray(data) && data && "error" in data) {
718
833
  if (this.isCacheEnabled)
719
- this.wordCache.set(cleanWord, []);
834
+ this.setBoundedCache(this.wordCache, cleanWord, []);
720
835
  return [];
721
836
  }
722
837
  const results = data;
723
838
  if (this.isCacheEnabled) {
724
- this.wordCache.set(cleanWord, results);
839
+ this.setBoundedCache(this.wordCache, cleanWord, results);
725
840
  }
726
841
  return results;
727
842
  }
@@ -848,17 +963,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
848
963
  return this.stemCache.get(clean);
849
964
  }
850
965
  if (await this.isHeadword(clean)) {
851
- this.stemCache.set(clean, clean);
966
+ this.setBoundedCache(this.stemCache, clean, clean);
852
967
  return clean;
853
968
  }
854
969
  const candidates = getStemCandidates(clean);
855
970
  for (const candidate of candidates) {
856
971
  if (await this.isHeadword(candidate)) {
857
- this.stemCache.set(clean, candidate);
972
+ this.setBoundedCache(this.stemCache, clean, candidate);
858
973
  return candidate;
859
974
  }
860
975
  }
861
- this.stemCache.set(clean, null);
976
+ this.setBoundedCache(this.stemCache, clean, null);
862
977
  return null;
863
978
  }
864
979
  /**
@@ -1534,14 +1649,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1534
1649
  meaningCount: meaningsA.length,
1535
1650
  origin: originA,
1536
1651
  syllables: this.syllabicate(a),
1537
- harmony: this.checkVowelHarmony(a)
1652
+ harmony: this.checkVowelHarmony(a),
1653
+ labialHarmony: this.checkLabialHarmony(a)
1538
1654
  },
1539
1655
  b: {
1540
1656
  word: b,
1541
1657
  meaningCount: meaningsB.length,
1542
1658
  origin: originB,
1543
1659
  syllables: this.syllabicate(b),
1544
- harmony: this.checkVowelHarmony(b)
1660
+ harmony: this.checkVowelHarmony(b),
1661
+ labialHarmony: this.checkLabialHarmony(b)
1545
1662
  }
1546
1663
  };
1547
1664
  }
@@ -1672,9 +1789,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1672
1789
  }
1673
1790
  /**
1674
1791
  * Syllabicates a Turkish word based on general grammar rules.
1792
+ * Handles syllable separation for vowels, single consonants, double consonants,
1793
+ * and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
1675
1794
  */
1676
1795
  static syllabicate(word) {
1677
1796
  const vowels = /[aeıioöuüAEIİOÖUÜ]/;
1797
+ const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
1678
1798
  const result = [];
1679
1799
  let currentSyllable = "";
1680
1800
  for (let i = word.length - 1; i >= 0; i--) {
@@ -1685,8 +1805,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1685
1805
  currentSyllable = word[i - 1] + currentSyllable;
1686
1806
  i--;
1687
1807
  } else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
1688
- currentSyllable = word[i - 1] + currentSyllable;
1689
- i--;
1808
+ if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
1809
+ currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
1810
+ i -= 2;
1811
+ } else {
1812
+ currentSyllable = word[i - 1] + currentSyllable;
1813
+ i--;
1814
+ }
1690
1815
  }
1691
1816
  }
1692
1817
  result.unshift(currentSyllable);
@@ -1716,10 +1841,297 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1716
1841
  const hasFront = frontVowels.test(lower);
1717
1842
  return !(hasBack && hasFront);
1718
1843
  }
1844
+ /**
1845
+ * Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
1846
+ * Rules:
1847
+ * 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
1848
+ * 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
1849
+ * Single-syllable words and words with <=1 vowel are considered compliant by convention.
1850
+ */
1851
+ static checkLabialHarmony(word) {
1852
+ const lower = word.toLocaleLowerCase("tr-TR");
1853
+ const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
1854
+ if (vowels.length <= 1)
1855
+ return true;
1856
+ for (let i = 0; i < vowels.length - 1; i++) {
1857
+ const v1 = vowels[i];
1858
+ const v2 = vowels[i + 1];
1859
+ if ("ae\u0131i".includes(v1)) {
1860
+ if (!"ae\u0131i".includes(v2))
1861
+ return false;
1862
+ } else if ("o\xF6u\xFC".includes(v1)) {
1863
+ if (!"aeu\xFC".includes(v2))
1864
+ return false;
1865
+ }
1866
+ }
1867
+ return true;
1868
+ }
1869
+ /**
1870
+ * Searches TDK headwords using a wildcard / pattern string.
1871
+ * Wildcards:
1872
+ * '_' or '?' matches any single character
1873
+ * '*' matches zero or more characters
1874
+ * Example: "k_l_m" matches "kalem", "kelam", "kilim".
1875
+ * Runs in-memory against TDK's 81k headword list.
1876
+ */
1877
+ static async patternSearch(pattern, options) {
1878
+ if (!pattern || pattern.trim() === "")
1879
+ return [];
1880
+ await this.ensureAutocompleteLoaded();
1881
+ const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
1882
+ const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
1883
+ const regex = new RegExp(`^${escaped}$`, "u");
1884
+ const max = options?.maxResults ?? 50;
1885
+ const matches = [];
1886
+ for (const headword of this.autocompleteCache) {
1887
+ const lower = headword.toLocaleLowerCase("tr-TR");
1888
+ if (regex.test(lower)) {
1889
+ matches.push(headword);
1890
+ if (matches.length >= max)
1891
+ break;
1892
+ }
1893
+ }
1894
+ return matches;
1895
+ }
1896
+ /**
1897
+ * Finds headwords in TDK that can be formed from the given letters (anagrams).
1898
+ * If exact-length anagrams exist, they are returned.
1899
+ * If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
1900
+ * minimum 3 letters) are returned, sorted by length descending.
1901
+ */
1902
+ static async findAnagrams(letters, options) {
1903
+ if (!letters || letters.trim() === "")
1904
+ return [];
1905
+ await this.ensureAutocompleteLoaded();
1906
+ const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
1907
+ if (clean.length === 0)
1908
+ return [];
1909
+ const forceExact = options?.exactLength === true;
1910
+ const max = options?.maxResults ?? 50;
1911
+ const getFrequency = (str) => {
1912
+ const freq = {};
1913
+ for (const ch of str) {
1914
+ freq[ch] = (freq[ch] || 0) + 1;
1915
+ }
1916
+ return freq;
1917
+ };
1918
+ const targetFreq = getFrequency(clean);
1919
+ const exactMatches = [];
1920
+ const subMatches = [];
1921
+ for (const headword of this.autocompleteCache) {
1922
+ const lower = headword.toLocaleLowerCase("tr-TR");
1923
+ if (lower.includes(" ") || lower.includes("-"))
1924
+ continue;
1925
+ if (lower.length > clean.length || lower.length < 3)
1926
+ continue;
1927
+ const wordFreq = getFrequency(lower);
1928
+ let isValid = true;
1929
+ for (const [ch, count] of Object.entries(wordFreq)) {
1930
+ if (!targetFreq[ch] || targetFreq[ch] < count) {
1931
+ isValid = false;
1932
+ break;
1933
+ }
1934
+ }
1935
+ if (isValid && lower !== clean) {
1936
+ if (lower.length === clean.length) {
1937
+ exactMatches.push(headword);
1938
+ } else {
1939
+ subMatches.push(headword);
1940
+ }
1941
+ }
1942
+ }
1943
+ if (exactMatches.length > 0 || forceExact) {
1944
+ return exactMatches.slice(0, max);
1945
+ }
1946
+ subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
1947
+ return subMatches.slice(0, max);
1948
+ }
1949
+ /**
1950
+ * Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
1951
+ * @param word The target word
1952
+ * @param options.minLetters Minimum number of ending characters that must match (default: 3)
1953
+ * @param options.maxResults Maximum number of rhyme results to return (default: 50)
1954
+ */
1955
+ static async findRhymes(word, options) {
1956
+ if (!word || word.trim() === "")
1957
+ return [];
1958
+ await this.ensureAutocompleteLoaded();
1959
+ const clean = word.trim().toLocaleLowerCase("tr-TR");
1960
+ const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
1961
+ const max = options?.maxResults ?? 50;
1962
+ const suffix = clean.slice(-minLetters);
1963
+ const results = [];
1964
+ for (const headword of this.autocompleteCache) {
1965
+ const lower = headword.toLocaleLowerCase("tr-TR");
1966
+ if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
1967
+ results.push(headword);
1968
+ if (results.length >= max)
1969
+ break;
1970
+ }
1971
+ }
1972
+ return results;
1973
+ }
1974
+ /**
1975
+ * Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
1976
+ * Detects:
1977
+ * 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
1978
+ * 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
1979
+ * 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
1980
+ * 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
1981
+ */
1982
+ static async proofread(text) {
1983
+ if (!text || text.trim() === "") {
1984
+ return { text: text || "", issues: [], isCorrect: true };
1985
+ }
1986
+ await this.ensureAutocompleteLoaded();
1987
+ const issues = [];
1988
+ const SOMBAHCEMI = /* @__PURE__ */ new Set([
1989
+ "sanki",
1990
+ "oysaki",
1991
+ "mademki",
1992
+ "belki",
1993
+ "halbuki",
1994
+ "\xE7\xFCnk\xFC",
1995
+ "me\u011Ferki",
1996
+ "illaki"
1997
+ ]);
1998
+ const tokenRegex = /[\p{L}0-9'’]+/gu;
1999
+ let match;
2000
+ while ((match = tokenRegex.exec(text)) !== null) {
2001
+ const rawWord = match[0];
2002
+ const startIndex = match.index;
2003
+ const endIndex = startIndex + rawWord.length;
2004
+ const lower = rawWord.toLocaleLowerCase("tr-TR");
2005
+ if (/^\d+$/.test(lower))
2006
+ continue;
2007
+ let flagged = false;
2008
+ 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)?)$/);
2009
+ if (questionMatch) {
2010
+ const base = questionMatch[1];
2011
+ const particle = questionMatch[2];
2012
+ if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
2013
+ if (!await this.isHeadword(lower)) {
2014
+ issues.push({
2015
+ type: "question_particle",
2016
+ word: rawWord,
2017
+ startIndex,
2018
+ endIndex,
2019
+ suggestion: `${base} ${particle}`,
2020
+ message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
2021
+ });
2022
+ flagged = true;
2023
+ }
2024
+ }
2025
+ }
2026
+ 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;
2027
+ if (!flagged && lower.endsWith("ki") && lower.length > 3) {
2028
+ const base = lower.slice(0, -2);
2029
+ if (!SOMBAHCEMI.has(lower)) {
2030
+ if (!await this.isHeadword(lower)) {
2031
+ const root = await this.findRoot(base);
2032
+ const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base);
2033
+ if (isVerb) {
2034
+ issues.push({
2035
+ type: "conjunction_ki",
2036
+ word: rawWord,
2037
+ startIndex,
2038
+ endIndex,
2039
+ suggestion: `${base} ki`,
2040
+ message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
2041
+ });
2042
+ flagged = true;
2043
+ }
2044
+ }
2045
+ }
2046
+ }
2047
+ if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
2048
+ const base = lower.slice(0, -2);
2049
+ const ending = lower.slice(-2);
2050
+ if (!await this.isHeadword(lower)) {
2051
+ const root = await this.findRoot(base);
2052
+ const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || VERB_CONJUGATION_REGEX.test(base);
2053
+ if (isVerb) {
2054
+ const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
2055
+ issues.push({
2056
+ type: "conjunction_da",
2057
+ word: rawWord,
2058
+ startIndex,
2059
+ endIndex,
2060
+ suggestion: `${base} ${correctEnding}`,
2061
+ message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
2062
+ });
2063
+ flagged = true;
2064
+ }
2065
+ }
2066
+ }
2067
+ if (!flagged) {
2068
+ const check = await this.checkSpelling(rawWord);
2069
+ if (!check.isCorrect) {
2070
+ issues.push({
2071
+ type: "spelling",
2072
+ word: rawWord,
2073
+ startIndex,
2074
+ endIndex,
2075
+ suggestion: check.suggestion,
2076
+ message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir. \xD6neri: '${check.suggestion}'` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
2077
+ });
2078
+ }
2079
+ }
2080
+ }
2081
+ return {
2082
+ text,
2083
+ issues,
2084
+ isCorrect: issues.length === 0
2085
+ };
2086
+ }
2087
+ };
2088
+ var TDKClient = class {
2089
+ constructor(config) {
2090
+ if (config) {
2091
+ TDK.configure(config);
2092
+ }
2093
+ }
2094
+ getWord(word) {
2095
+ return TDK.getWord(word);
2096
+ }
2097
+ getMeanings(word) {
2098
+ return TDK.getMeanings(word);
2099
+ }
2100
+ checkSpelling(word) {
2101
+ return TDK.checkSpelling(word);
2102
+ }
2103
+ findRoot(word) {
2104
+ return TDK.findRoot(word);
2105
+ }
2106
+ stem(word) {
2107
+ return TDK.stem(word);
2108
+ }
2109
+ proofread(text) {
2110
+ return TDK.proofread(text);
2111
+ }
2112
+ patternSearch(pattern, options) {
2113
+ return TDK.patternSearch(pattern, options);
2114
+ }
2115
+ findAnagrams(letters, options) {
2116
+ return TDK.findAnagrams(letters, options);
2117
+ }
2118
+ findRhymes(word, options) {
2119
+ return TDK.findRhymes(word, options);
2120
+ }
2121
+ syllabicate(word) {
2122
+ return TDK.syllabicate(word);
2123
+ }
2124
+ checkVowelHarmony(word) {
2125
+ return TDK.checkVowelHarmony(word);
2126
+ }
2127
+ checkLabialHarmony(word) {
2128
+ return TDK.checkLabialHarmony(word);
2129
+ }
1719
2130
  };
1720
2131
  // Annotate the CommonJS export names for ESM import in node:
1721
2132
  0 && (module.exports = {
1722
2133
  TDK,
2134
+ TDKClient,
1723
2135
  TDKError,
1724
2136
  TDKNetworkError,
1725
2137
  TDKValidationError,
@@ -1728,6 +2140,8 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1728
2140
  getStemCandidates,
1729
2141
  isVowel,
1730
2142
  restoreConsonantSoftening,
2143
+ restoreGemination,
1731
2144
  restoreInfinitive,
1732
- restoreVowelDrop
2145
+ restoreVowelDrop,
2146
+ restoreVowelNarrowing
1733
2147
  });
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  TDK,
3
+ TDKClient,
3
4
  TDKError,
4
5
  TDKNetworkError,
5
6
  TDKValidationError,
@@ -8,11 +9,14 @@ import {
8
9
  getStemCandidates,
9
10
  isVowel,
10
11
  restoreConsonantSoftening,
12
+ restoreGemination,
11
13
  restoreInfinitive,
12
- restoreVowelDrop
13
- } from "./chunk-5TYJDVHK.mjs";
14
+ restoreVowelDrop,
15
+ restoreVowelNarrowing
16
+ } from "./chunk-7KJHYRJZ.mjs";
14
17
  export {
15
18
  TDK,
19
+ TDKClient,
16
20
  TDKError,
17
21
  TDKNetworkError,
18
22
  TDKValidationError,
@@ -21,6 +25,8 @@ export {
21
25
  getStemCandidates,
22
26
  isVowel,
23
27
  restoreConsonantSoftening,
28
+ restoreGemination,
24
29
  restoreInfinitive,
25
- restoreVowelDrop
30
+ restoreVowelDrop,
31
+ restoreVowelNarrowing
26
32
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdk-api-wrapper",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "TDK (Türk Dil Kurumu) unofficial live data API wrapper for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean --shims",
20
- "test": "node test/morphology.test.js"
20
+ "test": "node test/morphology.test.js && node test/grammar.test.js && node test/proofread.test.js && node test/tools.test.js"
21
21
  },
22
22
  "keywords": [
23
23
  "tdk",