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
|
@@ -332,6 +332,12 @@ var TURKISH_SUFFIXES = [
|
|
|
332
332
|
"s\u0131n",
|
|
333
333
|
"sun",
|
|
334
334
|
"s\xFCn",
|
|
335
|
+
"sen",
|
|
336
|
+
"san",
|
|
337
|
+
"sem",
|
|
338
|
+
"sam",
|
|
339
|
+
"sek",
|
|
340
|
+
"sak",
|
|
335
341
|
"siz",
|
|
336
342
|
"s\u0131z",
|
|
337
343
|
"suz",
|
|
@@ -469,6 +475,43 @@ function restoreVowelDrop(stem) {
|
|
|
469
475
|
}
|
|
470
476
|
return [];
|
|
471
477
|
}
|
|
478
|
+
function restoreGemination(stem) {
|
|
479
|
+
if (stem.length < 3)
|
|
480
|
+
return [];
|
|
481
|
+
const c1 = stem[stem.length - 2];
|
|
482
|
+
const c2 = stem[stem.length - 1];
|
|
483
|
+
if (c1 === c2 && !isVowel(c1)) {
|
|
484
|
+
const single = stem.slice(0, -1);
|
|
485
|
+
const hardened = restoreConsonantSoftening(single);
|
|
486
|
+
return [single, ...hardened];
|
|
487
|
+
}
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
function restoreVowelNarrowing(stem) {
|
|
491
|
+
if (stem.length < 2)
|
|
492
|
+
return [];
|
|
493
|
+
if (stem === "di")
|
|
494
|
+
return ["de"];
|
|
495
|
+
if (stem === "yi")
|
|
496
|
+
return ["ye"];
|
|
497
|
+
const lastChar = stem[stem.length - 1];
|
|
498
|
+
const isLastNarrow = "\u0131iu\xFC".includes(lastChar);
|
|
499
|
+
if (isLastNarrow) {
|
|
500
|
+
const vowelsInBase = stem.slice(0, -1).split("").filter(isVowel);
|
|
501
|
+
const lastVowel = vowelsInBase.length > 0 ? vowelsInBase[vowelsInBase.length - 1] : lastChar;
|
|
502
|
+
const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
|
|
503
|
+
return [stem.slice(0, -1) + widened];
|
|
504
|
+
}
|
|
505
|
+
if (!isVowel(lastChar)) {
|
|
506
|
+
const vowelsInBase = stem.split("").filter(isVowel);
|
|
507
|
+
if (vowelsInBase.length > 0) {
|
|
508
|
+
const lastVowel = vowelsInBase[vowelsInBase.length - 1];
|
|
509
|
+
const widened = "a\u0131ou".includes(lastVowel) ? "a" : "e";
|
|
510
|
+
return [stem + widened];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
472
515
|
function restoreInfinitive(stem) {
|
|
473
516
|
if (stem.length < 2)
|
|
474
517
|
return [];
|
|
@@ -501,9 +544,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
|
501
544
|
const stem = current.slice(0, -suffix.length);
|
|
502
545
|
const hardened = restoreConsonantSoftening(stem);
|
|
503
546
|
const vowelDropped = restoreVowelDrop(stem);
|
|
504
|
-
const
|
|
547
|
+
const geminated = restoreGemination(stem);
|
|
548
|
+
const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
|
|
549
|
+
const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
|
|
550
|
+
const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
|
|
551
|
+
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");
|
|
552
|
+
const verbalBases = [stem, ...hardened, ...narrowed];
|
|
505
553
|
const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
|
|
506
|
-
const variants = [stem, ...hardened, ...vowelDropped, ...
|
|
554
|
+
const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
|
|
507
555
|
for (const variant of variants) {
|
|
508
556
|
if (!seen.has(variant) && variant !== normalized) {
|
|
509
557
|
seen.add(variant);
|
|
@@ -511,6 +559,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
|
511
559
|
candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
|
|
512
560
|
}
|
|
513
561
|
}
|
|
562
|
+
for (const inf of infinitives) {
|
|
563
|
+
if (!seen.has(inf) && inf !== normalized) {
|
|
564
|
+
seen.add(inf);
|
|
565
|
+
nextFrontier.push(inf);
|
|
566
|
+
const weight = isVerbSuffix ? stem.length + 5 : stem.length;
|
|
567
|
+
candidatesWithWeight.push({ candidate: inf, baseLength: weight });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
514
570
|
}
|
|
515
571
|
}
|
|
516
572
|
}
|
|
@@ -608,6 +664,10 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
|
|
|
608
664
|
yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
609
665
|
-----END CERTIFICATE-----`
|
|
610
666
|
];
|
|
667
|
+
// Configuration
|
|
668
|
+
static defaultTimeoutMs = 8e3;
|
|
669
|
+
static defaultRetries = 1;
|
|
670
|
+
static maxCacheSize = 1e3;
|
|
611
671
|
// Cache Mechanism
|
|
612
672
|
static isCacheEnabled = false;
|
|
613
673
|
static wordCache = /* @__PURE__ */ new Map();
|
|
@@ -615,6 +675,19 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
615
675
|
static autocompleteCache = [];
|
|
616
676
|
static autocompleteSet = /* @__PURE__ */ new Set();
|
|
617
677
|
static stemCache = /* @__PURE__ */ new Map();
|
|
678
|
+
/**
|
|
679
|
+
* Configures global client options such as network timeout, retries, and cache size.
|
|
680
|
+
*/
|
|
681
|
+
static configure(config) {
|
|
682
|
+
if (config.timeoutMs !== void 0)
|
|
683
|
+
this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
|
|
684
|
+
if (config.retries !== void 0)
|
|
685
|
+
this.defaultRetries = Math.max(0, config.retries);
|
|
686
|
+
if (config.cache !== void 0)
|
|
687
|
+
this.enableCache(config.cache);
|
|
688
|
+
if (config.maxCacheSize !== void 0)
|
|
689
|
+
this.maxCacheSize = Math.max(10, config.maxCacheSize);
|
|
690
|
+
}
|
|
618
691
|
/**
|
|
619
692
|
* Enables or disables in-memory caching for API requests.
|
|
620
693
|
*/
|
|
@@ -634,9 +707,50 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
634
707
|
this.autocompleteSet.clear();
|
|
635
708
|
this.stemCache.clear();
|
|
636
709
|
}
|
|
710
|
+
static setBoundedCache(map, key, value) {
|
|
711
|
+
if (map.size >= this.maxCacheSize) {
|
|
712
|
+
const firstKey = map.keys().next().value;
|
|
713
|
+
if (firstKey !== void 0)
|
|
714
|
+
map.delete(firstKey);
|
|
715
|
+
}
|
|
716
|
+
map.set(key, value);
|
|
717
|
+
}
|
|
637
718
|
static delay(ms) {
|
|
638
719
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
639
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
|
|
723
|
+
*/
|
|
724
|
+
static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
|
|
725
|
+
let lastError;
|
|
726
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
727
|
+
try {
|
|
728
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
729
|
+
const headers = {
|
|
730
|
+
"User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
|
|
731
|
+
...options.headers || {}
|
|
732
|
+
};
|
|
733
|
+
const res = await fetch(url, { ...options, headers, signal });
|
|
734
|
+
if (res.ok || res.status >= 400 && res.status < 500) {
|
|
735
|
+
return res;
|
|
736
|
+
}
|
|
737
|
+
if (attempt < retries) {
|
|
738
|
+
await this.delay(200 * (attempt + 1));
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
return res;
|
|
742
|
+
} catch (err) {
|
|
743
|
+
lastError = err;
|
|
744
|
+
if (attempt < retries) {
|
|
745
|
+
await this.delay(200 * (attempt + 1));
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
|
|
751
|
+
cause: lastError
|
|
752
|
+
});
|
|
753
|
+
}
|
|
640
754
|
/**
|
|
641
755
|
* Fetches detailed information for a given word from the TDK Dictionary.
|
|
642
756
|
*/
|
|
@@ -651,9 +765,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
651
765
|
const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
|
|
652
766
|
let response;
|
|
653
767
|
try {
|
|
654
|
-
response = await
|
|
655
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
656
|
-
});
|
|
768
|
+
response = await this.fetchWithRetry(url);
|
|
657
769
|
} catch (error) {
|
|
658
770
|
throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
|
|
659
771
|
}
|
|
@@ -670,12 +782,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
670
782
|
}
|
|
671
783
|
if (!Array.isArray(data) && data && "error" in data) {
|
|
672
784
|
if (this.isCacheEnabled)
|
|
673
|
-
this.wordCache
|
|
785
|
+
this.setBoundedCache(this.wordCache, cleanWord, []);
|
|
674
786
|
return [];
|
|
675
787
|
}
|
|
676
788
|
const results = data;
|
|
677
789
|
if (this.isCacheEnabled) {
|
|
678
|
-
this.wordCache
|
|
790
|
+
this.setBoundedCache(this.wordCache, cleanWord, results);
|
|
679
791
|
}
|
|
680
792
|
return results;
|
|
681
793
|
}
|
|
@@ -802,17 +914,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
802
914
|
return this.stemCache.get(clean);
|
|
803
915
|
}
|
|
804
916
|
if (await this.isHeadword(clean)) {
|
|
805
|
-
this.stemCache
|
|
917
|
+
this.setBoundedCache(this.stemCache, clean, clean);
|
|
806
918
|
return clean;
|
|
807
919
|
}
|
|
808
920
|
const candidates = getStemCandidates(clean);
|
|
809
921
|
for (const candidate of candidates) {
|
|
810
922
|
if (await this.isHeadword(candidate)) {
|
|
811
|
-
this.stemCache
|
|
923
|
+
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
812
924
|
return candidate;
|
|
813
925
|
}
|
|
814
926
|
}
|
|
815
|
-
this.stemCache
|
|
927
|
+
this.setBoundedCache(this.stemCache, clean, null);
|
|
816
928
|
return null;
|
|
817
929
|
}
|
|
818
930
|
/**
|
|
@@ -1488,14 +1600,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1488
1600
|
meaningCount: meaningsA.length,
|
|
1489
1601
|
origin: originA,
|
|
1490
1602
|
syllables: this.syllabicate(a),
|
|
1491
|
-
harmony: this.checkVowelHarmony(a)
|
|
1603
|
+
harmony: this.checkVowelHarmony(a),
|
|
1604
|
+
labialHarmony: this.checkLabialHarmony(a)
|
|
1492
1605
|
},
|
|
1493
1606
|
b: {
|
|
1494
1607
|
word: b,
|
|
1495
1608
|
meaningCount: meaningsB.length,
|
|
1496
1609
|
origin: originB,
|
|
1497
1610
|
syllables: this.syllabicate(b),
|
|
1498
|
-
harmony: this.checkVowelHarmony(b)
|
|
1611
|
+
harmony: this.checkVowelHarmony(b),
|
|
1612
|
+
labialHarmony: this.checkLabialHarmony(b)
|
|
1499
1613
|
}
|
|
1500
1614
|
};
|
|
1501
1615
|
}
|
|
@@ -1626,9 +1740,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1626
1740
|
}
|
|
1627
1741
|
/**
|
|
1628
1742
|
* Syllabicates a Turkish word based on general grammar rules.
|
|
1743
|
+
* Handles syllable separation for vowels, single consonants, double consonants,
|
|
1744
|
+
* and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
|
|
1629
1745
|
*/
|
|
1630
1746
|
static syllabicate(word) {
|
|
1631
1747
|
const vowels = /[aeıioöuüAEIİOÖUÜ]/;
|
|
1748
|
+
const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
|
|
1632
1749
|
const result = [];
|
|
1633
1750
|
let currentSyllable = "";
|
|
1634
1751
|
for (let i = word.length - 1; i >= 0; i--) {
|
|
@@ -1639,8 +1756,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1639
1756
|
currentSyllable = word[i - 1] + currentSyllable;
|
|
1640
1757
|
i--;
|
|
1641
1758
|
} else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
|
|
1642
|
-
|
|
1643
|
-
|
|
1759
|
+
if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
|
|
1760
|
+
currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
|
|
1761
|
+
i -= 2;
|
|
1762
|
+
} else {
|
|
1763
|
+
currentSyllable = word[i - 1] + currentSyllable;
|
|
1764
|
+
i--;
|
|
1765
|
+
}
|
|
1644
1766
|
}
|
|
1645
1767
|
}
|
|
1646
1768
|
result.unshift(currentSyllable);
|
|
@@ -1670,6 +1792,292 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1670
1792
|
const hasFront = frontVowels.test(lower);
|
|
1671
1793
|
return !(hasBack && hasFront);
|
|
1672
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
|
|
1797
|
+
* Rules:
|
|
1798
|
+
* 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
|
|
1799
|
+
* 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
|
|
1800
|
+
* Single-syllable words and words with <=1 vowel are considered compliant by convention.
|
|
1801
|
+
*/
|
|
1802
|
+
static checkLabialHarmony(word) {
|
|
1803
|
+
const lower = word.toLocaleLowerCase("tr-TR");
|
|
1804
|
+
const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
|
|
1805
|
+
if (vowels.length <= 1)
|
|
1806
|
+
return true;
|
|
1807
|
+
for (let i = 0; i < vowels.length - 1; i++) {
|
|
1808
|
+
const v1 = vowels[i];
|
|
1809
|
+
const v2 = vowels[i + 1];
|
|
1810
|
+
if ("ae\u0131i".includes(v1)) {
|
|
1811
|
+
if (!"ae\u0131i".includes(v2))
|
|
1812
|
+
return false;
|
|
1813
|
+
} else if ("o\xF6u\xFC".includes(v1)) {
|
|
1814
|
+
if (!"aeu\xFC".includes(v2))
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Searches TDK headwords using a wildcard / pattern string.
|
|
1822
|
+
* Wildcards:
|
|
1823
|
+
* '_' or '?' matches any single character
|
|
1824
|
+
* '*' matches zero or more characters
|
|
1825
|
+
* Example: "k_l_m" matches "kalem", "kelam", "kilim".
|
|
1826
|
+
* Runs in-memory against TDK's 81k headword list.
|
|
1827
|
+
*/
|
|
1828
|
+
static async patternSearch(pattern, options) {
|
|
1829
|
+
if (!pattern || pattern.trim() === "")
|
|
1830
|
+
return [];
|
|
1831
|
+
await this.ensureAutocompleteLoaded();
|
|
1832
|
+
const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
|
|
1833
|
+
const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
|
|
1834
|
+
const regex = new RegExp(`^${escaped}$`, "u");
|
|
1835
|
+
const max = options?.maxResults ?? 50;
|
|
1836
|
+
const matches = [];
|
|
1837
|
+
for (const headword of this.autocompleteCache) {
|
|
1838
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1839
|
+
if (regex.test(lower)) {
|
|
1840
|
+
matches.push(headword);
|
|
1841
|
+
if (matches.length >= max)
|
|
1842
|
+
break;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return matches;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Finds headwords in TDK that can be formed from the given letters (anagrams).
|
|
1849
|
+
* If exact-length anagrams exist, they are returned.
|
|
1850
|
+
* If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
|
|
1851
|
+
* minimum 3 letters) are returned, sorted by length descending.
|
|
1852
|
+
*/
|
|
1853
|
+
static async findAnagrams(letters, options) {
|
|
1854
|
+
if (!letters || letters.trim() === "")
|
|
1855
|
+
return [];
|
|
1856
|
+
await this.ensureAutocompleteLoaded();
|
|
1857
|
+
const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
|
|
1858
|
+
if (clean.length === 0)
|
|
1859
|
+
return [];
|
|
1860
|
+
const forceExact = options?.exactLength === true;
|
|
1861
|
+
const max = options?.maxResults ?? 50;
|
|
1862
|
+
const getFrequency = (str) => {
|
|
1863
|
+
const freq = {};
|
|
1864
|
+
for (const ch of str) {
|
|
1865
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
1866
|
+
}
|
|
1867
|
+
return freq;
|
|
1868
|
+
};
|
|
1869
|
+
const targetFreq = getFrequency(clean);
|
|
1870
|
+
const exactMatches = [];
|
|
1871
|
+
const subMatches = [];
|
|
1872
|
+
for (const headword of this.autocompleteCache) {
|
|
1873
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1874
|
+
if (lower.includes(" ") || lower.includes("-"))
|
|
1875
|
+
continue;
|
|
1876
|
+
if (lower.length > clean.length || lower.length < 3)
|
|
1877
|
+
continue;
|
|
1878
|
+
const wordFreq = getFrequency(lower);
|
|
1879
|
+
let isValid = true;
|
|
1880
|
+
for (const [ch, count] of Object.entries(wordFreq)) {
|
|
1881
|
+
if (!targetFreq[ch] || targetFreq[ch] < count) {
|
|
1882
|
+
isValid = false;
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
if (isValid && lower !== clean) {
|
|
1887
|
+
if (lower.length === clean.length) {
|
|
1888
|
+
exactMatches.push(headword);
|
|
1889
|
+
} else {
|
|
1890
|
+
subMatches.push(headword);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (exactMatches.length > 0 || forceExact) {
|
|
1895
|
+
return exactMatches.slice(0, max);
|
|
1896
|
+
}
|
|
1897
|
+
subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
|
|
1898
|
+
return subMatches.slice(0, max);
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
|
|
1902
|
+
* @param word The target word
|
|
1903
|
+
* @param options.minLetters Minimum number of ending characters that must match (default: 3)
|
|
1904
|
+
* @param options.maxResults Maximum number of rhyme results to return (default: 50)
|
|
1905
|
+
*/
|
|
1906
|
+
static async findRhymes(word, options) {
|
|
1907
|
+
if (!word || word.trim() === "")
|
|
1908
|
+
return [];
|
|
1909
|
+
await this.ensureAutocompleteLoaded();
|
|
1910
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
1911
|
+
const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
|
|
1912
|
+
const max = options?.maxResults ?? 50;
|
|
1913
|
+
const suffix = clean.slice(-minLetters);
|
|
1914
|
+
const results = [];
|
|
1915
|
+
for (const headword of this.autocompleteCache) {
|
|
1916
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1917
|
+
if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
|
|
1918
|
+
results.push(headword);
|
|
1919
|
+
if (results.length >= max)
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return results;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
|
|
1927
|
+
* Detects:
|
|
1928
|
+
* 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
|
|
1929
|
+
* 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
|
|
1930
|
+
* 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
|
|
1931
|
+
* 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
|
|
1932
|
+
*/
|
|
1933
|
+
static async proofread(text) {
|
|
1934
|
+
if (!text || text.trim() === "") {
|
|
1935
|
+
return { text: text || "", issues: [], isCorrect: true };
|
|
1936
|
+
}
|
|
1937
|
+
await this.ensureAutocompleteLoaded();
|
|
1938
|
+
const issues = [];
|
|
1939
|
+
const SOMBAHCEMI = /* @__PURE__ */ new Set([
|
|
1940
|
+
"sanki",
|
|
1941
|
+
"oysaki",
|
|
1942
|
+
"mademki",
|
|
1943
|
+
"belki",
|
|
1944
|
+
"halbuki",
|
|
1945
|
+
"\xE7\xFCnk\xFC",
|
|
1946
|
+
"me\u011Ferki",
|
|
1947
|
+
"illaki"
|
|
1948
|
+
]);
|
|
1949
|
+
const tokenRegex = /[\p{L}0-9'’]+/gu;
|
|
1950
|
+
let match;
|
|
1951
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
1952
|
+
const rawWord = match[0];
|
|
1953
|
+
const startIndex = match.index;
|
|
1954
|
+
const endIndex = startIndex + rawWord.length;
|
|
1955
|
+
const lower = rawWord.toLocaleLowerCase("tr-TR");
|
|
1956
|
+
if (/^\d+$/.test(lower))
|
|
1957
|
+
continue;
|
|
1958
|
+
let flagged = false;
|
|
1959
|
+
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)?)$/);
|
|
1960
|
+
if (questionMatch) {
|
|
1961
|
+
const base = questionMatch[1];
|
|
1962
|
+
const particle = questionMatch[2];
|
|
1963
|
+
if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
|
|
1964
|
+
if (!await this.isHeadword(lower)) {
|
|
1965
|
+
issues.push({
|
|
1966
|
+
type: "question_particle",
|
|
1967
|
+
word: rawWord,
|
|
1968
|
+
startIndex,
|
|
1969
|
+
endIndex,
|
|
1970
|
+
suggestion: `${base} ${particle}`,
|
|
1971
|
+
message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
1972
|
+
});
|
|
1973
|
+
flagged = true;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
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;
|
|
1978
|
+
if (!flagged && lower.endsWith("ki") && lower.length > 3) {
|
|
1979
|
+
const base = lower.slice(0, -2);
|
|
1980
|
+
if (!SOMBAHCEMI.has(lower)) {
|
|
1981
|
+
if (!await this.isHeadword(lower)) {
|
|
1982
|
+
const root = await this.findRoot(base);
|
|
1983
|
+
const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base);
|
|
1984
|
+
if (isVerb) {
|
|
1985
|
+
issues.push({
|
|
1986
|
+
type: "conjunction_ki",
|
|
1987
|
+
word: rawWord,
|
|
1988
|
+
startIndex,
|
|
1989
|
+
endIndex,
|
|
1990
|
+
suggestion: `${base} ki`,
|
|
1991
|
+
message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
1992
|
+
});
|
|
1993
|
+
flagged = true;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
|
|
1999
|
+
const base = lower.slice(0, -2);
|
|
2000
|
+
const ending = lower.slice(-2);
|
|
2001
|
+
if (!await this.isHeadword(lower)) {
|
|
2002
|
+
const root = await this.findRoot(base);
|
|
2003
|
+
const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || VERB_CONJUGATION_REGEX.test(base);
|
|
2004
|
+
if (isVerb) {
|
|
2005
|
+
const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
|
|
2006
|
+
issues.push({
|
|
2007
|
+
type: "conjunction_da",
|
|
2008
|
+
word: rawWord,
|
|
2009
|
+
startIndex,
|
|
2010
|
+
endIndex,
|
|
2011
|
+
suggestion: `${base} ${correctEnding}`,
|
|
2012
|
+
message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
|
|
2013
|
+
});
|
|
2014
|
+
flagged = true;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (!flagged) {
|
|
2019
|
+
const check = await this.checkSpelling(rawWord);
|
|
2020
|
+
if (!check.isCorrect) {
|
|
2021
|
+
issues.push({
|
|
2022
|
+
type: "spelling",
|
|
2023
|
+
word: rawWord,
|
|
2024
|
+
startIndex,
|
|
2025
|
+
endIndex,
|
|
2026
|
+
suggestion: check.suggestion,
|
|
2027
|
+
message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir. \xD6neri: '${check.suggestion}'` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
text,
|
|
2034
|
+
issues,
|
|
2035
|
+
isCorrect: issues.length === 0
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
var TDKClient = class {
|
|
2040
|
+
constructor(config) {
|
|
2041
|
+
if (config) {
|
|
2042
|
+
TDK.configure(config);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
getWord(word) {
|
|
2046
|
+
return TDK.getWord(word);
|
|
2047
|
+
}
|
|
2048
|
+
getMeanings(word) {
|
|
2049
|
+
return TDK.getMeanings(word);
|
|
2050
|
+
}
|
|
2051
|
+
checkSpelling(word) {
|
|
2052
|
+
return TDK.checkSpelling(word);
|
|
2053
|
+
}
|
|
2054
|
+
findRoot(word) {
|
|
2055
|
+
return TDK.findRoot(word);
|
|
2056
|
+
}
|
|
2057
|
+
stem(word) {
|
|
2058
|
+
return TDK.stem(word);
|
|
2059
|
+
}
|
|
2060
|
+
proofread(text) {
|
|
2061
|
+
return TDK.proofread(text);
|
|
2062
|
+
}
|
|
2063
|
+
patternSearch(pattern, options) {
|
|
2064
|
+
return TDK.patternSearch(pattern, options);
|
|
2065
|
+
}
|
|
2066
|
+
findAnagrams(letters, options) {
|
|
2067
|
+
return TDK.findAnagrams(letters, options);
|
|
2068
|
+
}
|
|
2069
|
+
findRhymes(word, options) {
|
|
2070
|
+
return TDK.findRhymes(word, options);
|
|
2071
|
+
}
|
|
2072
|
+
syllabicate(word) {
|
|
2073
|
+
return TDK.syllabicate(word);
|
|
2074
|
+
}
|
|
2075
|
+
checkVowelHarmony(word) {
|
|
2076
|
+
return TDK.checkVowelHarmony(word);
|
|
2077
|
+
}
|
|
2078
|
+
checkLabialHarmony(word) {
|
|
2079
|
+
return TDK.checkLabialHarmony(word);
|
|
2080
|
+
}
|
|
1673
2081
|
};
|
|
1674
2082
|
|
|
1675
2083
|
export {
|
|
@@ -1681,7 +2089,10 @@ export {
|
|
|
1681
2089
|
TURKISH_SUFFIXES,
|
|
1682
2090
|
restoreConsonantSoftening,
|
|
1683
2091
|
restoreVowelDrop,
|
|
2092
|
+
restoreGemination,
|
|
2093
|
+
restoreVowelNarrowing,
|
|
1684
2094
|
restoreInfinitive,
|
|
1685
2095
|
getStemCandidates,
|
|
1686
|
-
TDK
|
|
2096
|
+
TDK,
|
|
2097
|
+
TDKClient
|
|
1687
2098
|
};
|