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/README.md +43 -14
- package/dist/{chunk-5TYJDVHK.mjs → chunk-7KJHYRJZ.mjs} +426 -15
- package/dist/cli.js +559 -19
- package/dist/cli.mjs +180 -5
- package/dist/index.d.mts +119 -1
- package/dist/index.d.ts +119 -1
- package/dist/index.js +430 -16
- package/dist/index.mjs +9 -3
- package/package.json +2 -2
- package/src/cli.ts +185 -4
- package/src/index.ts +1 -1
- package/src/morphology.ts +115 -5
- package/src/tdk.ts +428 -12
- package/src/types.ts +38 -0
- package/test/grammar.test.js +52 -0
- package/test/morphology.test.js +25 -1
- package/test/proofread.test.js +60 -0
- package/test/tools.test.js +51 -0
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
|
|
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, ...
|
|
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
|
|
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
|
|
834
|
+
this.setBoundedCache(this.wordCache, cleanWord, []);
|
|
720
835
|
return [];
|
|
721
836
|
}
|
|
722
837
|
const results = data;
|
|
723
838
|
if (this.isCacheEnabled) {
|
|
724
|
-
this.wordCache
|
|
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
|
|
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
|
|
972
|
+
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
858
973
|
return candidate;
|
|
859
974
|
}
|
|
860
975
|
}
|
|
861
|
-
this.stemCache
|
|
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
|
-
|
|
1689
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|