tdk-api-wrapper 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -14
- package/dist/{chunk-5TYJDVHK.mjs → chunk-NEC5MIQM.mjs} +734 -24
- package/dist/cli.js +872 -29
- package/dist/cli.mjs +185 -6
- package/dist/index.d.mts +119 -1
- package/dist/index.d.ts +119 -1
- package/dist/index.js +738 -25
- package/dist/index.mjs +9 -3
- package/package.json +2 -2
- package/src/cli.ts +191 -5
- package/src/index.ts +1 -1
- package/src/morphology.ts +124 -5
- package/src/tdk.ts +762 -39
- 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 +88 -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 [];
|
|
@@ -492,6 +535,13 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
|
492
535
|
seen.add(apostropheStem);
|
|
493
536
|
}
|
|
494
537
|
}
|
|
538
|
+
const bareInfinitives = restoreInfinitive(normalized);
|
|
539
|
+
for (const inf of bareInfinitives) {
|
|
540
|
+
if (!seen.has(inf) && inf !== normalized) {
|
|
541
|
+
seen.add(inf);
|
|
542
|
+
candidatesWithWeight.push({ candidate: inf, baseLength: normalized.length });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
495
545
|
let frontier = [normalized];
|
|
496
546
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
497
547
|
const nextFrontier = [];
|
|
@@ -501,9 +551,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
|
501
551
|
const stem = current.slice(0, -suffix.length);
|
|
502
552
|
const hardened = restoreConsonantSoftening(stem);
|
|
503
553
|
const vowelDropped = restoreVowelDrop(stem);
|
|
504
|
-
const
|
|
554
|
+
const geminated = restoreGemination(stem);
|
|
555
|
+
const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
|
|
556
|
+
const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
|
|
557
|
+
const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
|
|
558
|
+
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");
|
|
559
|
+
const verbalBases = [stem, ...hardened, ...narrowed];
|
|
505
560
|
const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
|
|
506
|
-
const variants = [stem, ...hardened, ...vowelDropped, ...
|
|
561
|
+
const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
|
|
507
562
|
for (const variant of variants) {
|
|
508
563
|
if (!seen.has(variant) && variant !== normalized) {
|
|
509
564
|
seen.add(variant);
|
|
@@ -511,6 +566,14 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
|
511
566
|
candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
|
|
512
567
|
}
|
|
513
568
|
}
|
|
569
|
+
for (const inf of infinitives) {
|
|
570
|
+
if (!seen.has(inf) && inf !== normalized) {
|
|
571
|
+
seen.add(inf);
|
|
572
|
+
nextFrontier.push(inf);
|
|
573
|
+
const weight = isVerbSuffix ? stem.length + 5 : stem.length;
|
|
574
|
+
candidatesWithWeight.push({ candidate: inf, baseLength: weight });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
514
577
|
}
|
|
515
578
|
}
|
|
516
579
|
}
|
|
@@ -528,6 +591,159 @@ import * as path from "path";
|
|
|
528
591
|
import * as os from "os";
|
|
529
592
|
import * as https from "https";
|
|
530
593
|
import * as tls from "tls";
|
|
594
|
+
var COMMON_MISSPELLINGS = {
|
|
595
|
+
// -şey ile biten ve ayrı yazılması zorunlu söz öbekleri
|
|
596
|
+
her\u015Fey: "her \u015Fey",
|
|
597
|
+
hersey: "her \u015Fey",
|
|
598
|
+
bir\u015Fey: "bir \u015Fey",
|
|
599
|
+
birsey: "bir \u015Fey",
|
|
600
|
+
hi\u00E7bir\u015Fey: "hi\xE7bir \u015Fey",
|
|
601
|
+
hicbirsey: "hi\xE7bir \u015Fey",
|
|
602
|
+
\u00E7ok\u015Fey: "\xE7ok \u015Fey",
|
|
603
|
+
coksey: "\xE7ok \u015Fey",
|
|
604
|
+
\u015Feyler: "\u015Feyler",
|
|
605
|
+
seyler: "\u015Feyler",
|
|
606
|
+
herhangibir\u015Fey: "herhangi bir \u015Fey",
|
|
607
|
+
herhangibirsey: "herhangi bir \u015Fey",
|
|
608
|
+
// Sıkça birleşik yazılan ama ayrı yazılması gereken sözler
|
|
609
|
+
herg\u00FCn: "her g\xFCn",
|
|
610
|
+
hergun: "her g\xFCn",
|
|
611
|
+
herzaman: "her zaman",
|
|
612
|
+
heran: "her an",
|
|
613
|
+
heryer: "her yer",
|
|
614
|
+
herbiri: "her biri",
|
|
615
|
+
pek\u00E7ok: "pek \xE7ok",
|
|
616
|
+
pekcok: "pek \xE7ok",
|
|
617
|
+
pekaz: "pek az",
|
|
618
|
+
yada: "ya da",
|
|
619
|
+
tabiki: "tabii ki",
|
|
620
|
+
tabiiki: "tabii ki",
|
|
621
|
+
sa\u011Fol: "sa\u011F ol",
|
|
622
|
+
sagol: "sa\u011F ol",
|
|
623
|
+
sa\u011Folun: "sa\u011F olun",
|
|
624
|
+
sagolun: "sa\u011F olun",
|
|
625
|
+
ho\u015F\u00E7akal: "ho\u015F\xE7a kal",
|
|
626
|
+
hoscakal: "ho\u015F\xE7a kal",
|
|
627
|
+
ho\u015Fgeldin: "ho\u015F geldin",
|
|
628
|
+
hosgeldin: "ho\u015F geldin",
|
|
629
|
+
ho\u015Fgeldiniz: "ho\u015F geldiniz",
|
|
630
|
+
hosgeldiniz: "ho\u015F geldiniz",
|
|
631
|
+
ho\u015Fbulduk: "ho\u015F bulduk",
|
|
632
|
+
hosbulduk: "ho\u015F bulduk",
|
|
633
|
+
yan\u0131s\u0131ra: "yan\u0131 s\u0131ra",
|
|
634
|
+
yanisira: "yan\u0131 s\u0131ra",
|
|
635
|
+
pe\u015Fis\u0131ra: "pe\u015Fi s\u0131ra",
|
|
636
|
+
pesisira: "pe\u015Fi s\u0131ra",
|
|
637
|
+
ard\u0131s\u0131ra: "ard\u0131 s\u0131ra",
|
|
638
|
+
ardisira: "ard\u0131 s\u0131ra",
|
|
639
|
+
artarda: "art arda",
|
|
640
|
+
y\u00FCzy\u00FCze: "y\xFCz y\xFCze",
|
|
641
|
+
yuzyuze: "y\xFCz y\xFCze",
|
|
642
|
+
elele: "el ele",
|
|
643
|
+
g\u00F6zg\u00F6ze: "g\xF6z g\xF6ze",
|
|
644
|
+
ba\u015Fba\u015Fa: "ba\u015F ba\u015Fa",
|
|
645
|
+
basbasa: "ba\u015F ba\u015Fa",
|
|
646
|
+
yanyana: "yan yana",
|
|
647
|
+
i\u00E7i\u00E7e: "i\xE7 i\xE7e",
|
|
648
|
+
icice: "i\xE7 i\xE7e",
|
|
649
|
+
\u00FCst\u00FCste: "\xFCst \xFCste",
|
|
650
|
+
ustuste: "\xFCst \xFCste",
|
|
651
|
+
altalta: "alt alta",
|
|
652
|
+
\u00F6ns\u00F6z: "\xF6n s\xF6z",
|
|
653
|
+
onsoz: "\xF6n s\xF6z",
|
|
654
|
+
\u00F6nyarg\u0131: "\xF6n yarg\u0131",
|
|
655
|
+
onyargi: "\xF6n yarg\u0131",
|
|
656
|
+
farketmek: "fark etmek",
|
|
657
|
+
farketti: "fark etti",
|
|
658
|
+
farkettim: "fark ettim",
|
|
659
|
+
farkeder: "fark eder",
|
|
660
|
+
farketmez: "fark etmez",
|
|
661
|
+
terketmek: "terk etmek",
|
|
662
|
+
terketti: "terk etti",
|
|
663
|
+
ay\u0131rdetmek: "ay\u0131rt etmek",
|
|
664
|
+
ay\u0131rtetmek: "ay\u0131rt etmek",
|
|
665
|
+
arzetmek: "arz etmek",
|
|
666
|
+
arzederim: "arz ederim",
|
|
667
|
+
varolmak: "var olmak",
|
|
668
|
+
yokolmak: "yok olmak",
|
|
669
|
+
haketmek: "hak etmek",
|
|
670
|
+
haketti: "hak etti",
|
|
671
|
+
hakkaten: "hakikaten",
|
|
672
|
+
hi\u00E7kimse: "hi\xE7 kimse",
|
|
673
|
+
hickimse: "hi\xE7 kimse",
|
|
674
|
+
// Ünlü düşmesi yapılmaması gereken yer bildiren sözler (TDK Kural 15)
|
|
675
|
+
burda: "burada",
|
|
676
|
+
burdan: "buradan",
|
|
677
|
+
\u015Furda: "\u015Furada",
|
|
678
|
+
surda: "\u015Furada",
|
|
679
|
+
\u015Furdan: "\u015Furadan",
|
|
680
|
+
surdan: "\u015Furadan",
|
|
681
|
+
orda: "orada",
|
|
682
|
+
ordan: "oradan",
|
|
683
|
+
i\u00E7erde: "i\xE7eride",
|
|
684
|
+
icerde: "i\xE7eride",
|
|
685
|
+
i\u00E7erden: "i\xE7eriden",
|
|
686
|
+
icerden: "i\xE7eriden",
|
|
687
|
+
d\u0131\u015Farda: "d\u0131\u015Far\u0131da",
|
|
688
|
+
disarda: "d\u0131\u015Far\u0131da",
|
|
689
|
+
d\u0131\u015Fardan: "d\u0131\u015Far\u0131dan",
|
|
690
|
+
disardan: "d\u0131\u015Far\u0131dan",
|
|
691
|
+
yukarda: "yukar\u0131da",
|
|
692
|
+
yukardan: "yukar\u0131dan",
|
|
693
|
+
// Sıkça yanlış yazılan sözcükler
|
|
694
|
+
herkez: "herkes",
|
|
695
|
+
yanl\u0131z: "yaln\u0131z",
|
|
696
|
+
yaln\u0131\u015F: "yanl\u0131\u015F",
|
|
697
|
+
orjinal: "orijinal",
|
|
698
|
+
labaratuar: "laboratuvar",
|
|
699
|
+
laboratuar: "laboratuvar",
|
|
700
|
+
\u015F\u00F6f\u00F6r: "\u015Fof\xF6r",
|
|
701
|
+
sofor: "\u015Fof\xF6r",
|
|
702
|
+
egzos: "egzoz",
|
|
703
|
+
eksoz: "egzoz",
|
|
704
|
+
ekzoz: "egzoz",
|
|
705
|
+
kiprik: "kirpik",
|
|
706
|
+
kirbit: "kibrit",
|
|
707
|
+
klavuz: "k\u0131lavuz",
|
|
708
|
+
k\u0131ravat: "kravat",
|
|
709
|
+
s\u00FCpriz: "s\xFCrpriz",
|
|
710
|
+
supriz: "s\xFCrpriz",
|
|
711
|
+
raslant\u0131: "rastlant\u0131",
|
|
712
|
+
hastahane: "hastane",
|
|
713
|
+
pastahane: "pastane",
|
|
714
|
+
postahane: "postane",
|
|
715
|
+
eczahane: "eczane",
|
|
716
|
+
meyva: "meyve",
|
|
717
|
+
sarm\u0131sak: "sar\u0131msak",
|
|
718
|
+
dinazor: "dinozor",
|
|
719
|
+
pantalon: "pantolon",
|
|
720
|
+
tesbih: "tespih",
|
|
721
|
+
ah\u00E7\u0131: "a\u015F\xE7\u0131",
|
|
722
|
+
matba: "matbaa",
|
|
723
|
+
idda: "iddia",
|
|
724
|
+
iddaa: "iddia",
|
|
725
|
+
muhattap: "muhatap",
|
|
726
|
+
tra\u015F: "t\u0131ra\u015F",
|
|
727
|
+
karn\u0131bahar: "karnabahar",
|
|
728
|
+
kareografi: "koreografi",
|
|
729
|
+
poa\u00E7a: "po\u011Fa\xE7a",
|
|
730
|
+
poha\u00E7a: "po\u011Fa\xE7a",
|
|
731
|
+
\u015Farz: "\u015Farj",
|
|
732
|
+
sarj: "\u015Farj",
|
|
733
|
+
makina: "makine",
|
|
734
|
+
m\u00FCsade: "m\xFCsaade",
|
|
735
|
+
entellekt\u00FCel: "entelekt\xFCel",
|
|
736
|
+
inisiyatif: "inisiyatif",
|
|
737
|
+
insiyatif: "inisiyatif",
|
|
738
|
+
sezeryan: "sezaryen",
|
|
739
|
+
dok\u00FCman: "dok\xFCman",
|
|
740
|
+
d\u00F6k\u00FCman: "dok\xFCman",
|
|
741
|
+
erozyon: "erozyon",
|
|
742
|
+
erizyon: "erozyon",
|
|
743
|
+
anane: "anneanne",
|
|
744
|
+
babaanne: "babaanne"
|
|
745
|
+
};
|
|
746
|
+
var SEY_EXCEPTIONS = /* @__PURE__ */ new Set(["d\xFC\u015Fey", "e\u015Fey", "konsey", "jersey", "\u015Fey"]);
|
|
531
747
|
var TDK = class {
|
|
532
748
|
static BASE_URL = "https://sozluk.gov.tr";
|
|
533
749
|
static AUDIO_API_HOST = "api.sozluk.gov.tr";
|
|
@@ -608,6 +824,10 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
|
|
|
608
824
|
yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
609
825
|
-----END CERTIFICATE-----`
|
|
610
826
|
];
|
|
827
|
+
// Configuration
|
|
828
|
+
static defaultTimeoutMs = 8e3;
|
|
829
|
+
static defaultRetries = 1;
|
|
830
|
+
static maxCacheSize = 1e3;
|
|
611
831
|
// Cache Mechanism
|
|
612
832
|
static isCacheEnabled = false;
|
|
613
833
|
static wordCache = /* @__PURE__ */ new Map();
|
|
@@ -615,6 +835,19 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
615
835
|
static autocompleteCache = [];
|
|
616
836
|
static autocompleteSet = /* @__PURE__ */ new Set();
|
|
617
837
|
static stemCache = /* @__PURE__ */ new Map();
|
|
838
|
+
/**
|
|
839
|
+
* Configures global client options such as network timeout, retries, and cache size.
|
|
840
|
+
*/
|
|
841
|
+
static configure(config) {
|
|
842
|
+
if (config.timeoutMs !== void 0)
|
|
843
|
+
this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
|
|
844
|
+
if (config.retries !== void 0)
|
|
845
|
+
this.defaultRetries = Math.max(0, config.retries);
|
|
846
|
+
if (config.cache !== void 0)
|
|
847
|
+
this.enableCache(config.cache);
|
|
848
|
+
if (config.maxCacheSize !== void 0)
|
|
849
|
+
this.maxCacheSize = Math.max(10, config.maxCacheSize);
|
|
850
|
+
}
|
|
618
851
|
/**
|
|
619
852
|
* Enables or disables in-memory caching for API requests.
|
|
620
853
|
*/
|
|
@@ -634,9 +867,50 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
634
867
|
this.autocompleteSet.clear();
|
|
635
868
|
this.stemCache.clear();
|
|
636
869
|
}
|
|
870
|
+
static setBoundedCache(map, key, value) {
|
|
871
|
+
if (map.size >= this.maxCacheSize) {
|
|
872
|
+
const firstKey = map.keys().next().value;
|
|
873
|
+
if (firstKey !== void 0)
|
|
874
|
+
map.delete(firstKey);
|
|
875
|
+
}
|
|
876
|
+
map.set(key, value);
|
|
877
|
+
}
|
|
637
878
|
static delay(ms) {
|
|
638
879
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
639
880
|
}
|
|
881
|
+
/**
|
|
882
|
+
* Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
|
|
883
|
+
*/
|
|
884
|
+
static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
|
|
885
|
+
let lastError;
|
|
886
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
887
|
+
try {
|
|
888
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
889
|
+
const headers = {
|
|
890
|
+
"User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
|
|
891
|
+
...options.headers || {}
|
|
892
|
+
};
|
|
893
|
+
const res = await fetch(url, { ...options, headers, signal });
|
|
894
|
+
if (res.ok || res.status >= 400 && res.status < 500) {
|
|
895
|
+
return res;
|
|
896
|
+
}
|
|
897
|
+
if (attempt < retries) {
|
|
898
|
+
await this.delay(200 * (attempt + 1));
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
return res;
|
|
902
|
+
} catch (err) {
|
|
903
|
+
lastError = err;
|
|
904
|
+
if (attempt < retries) {
|
|
905
|
+
await this.delay(200 * (attempt + 1));
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
|
|
911
|
+
cause: lastError
|
|
912
|
+
});
|
|
913
|
+
}
|
|
640
914
|
/**
|
|
641
915
|
* Fetches detailed information for a given word from the TDK Dictionary.
|
|
642
916
|
*/
|
|
@@ -651,9 +925,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
651
925
|
const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
|
|
652
926
|
let response;
|
|
653
927
|
try {
|
|
654
|
-
response = await
|
|
655
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
656
|
-
});
|
|
928
|
+
response = await this.fetchWithRetry(url);
|
|
657
929
|
} catch (error) {
|
|
658
930
|
throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
|
|
659
931
|
}
|
|
@@ -670,12 +942,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
670
942
|
}
|
|
671
943
|
if (!Array.isArray(data) && data && "error" in data) {
|
|
672
944
|
if (this.isCacheEnabled)
|
|
673
|
-
this.wordCache
|
|
945
|
+
this.setBoundedCache(this.wordCache, cleanWord, []);
|
|
674
946
|
return [];
|
|
675
947
|
}
|
|
676
948
|
const results = data;
|
|
677
949
|
if (this.isCacheEnabled) {
|
|
678
|
-
this.wordCache
|
|
950
|
+
this.setBoundedCache(this.wordCache, cleanWord, results);
|
|
679
951
|
}
|
|
680
952
|
return results;
|
|
681
953
|
}
|
|
@@ -802,17 +1074,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
802
1074
|
return this.stemCache.get(clean);
|
|
803
1075
|
}
|
|
804
1076
|
if (await this.isHeadword(clean)) {
|
|
805
|
-
this.stemCache
|
|
1077
|
+
this.setBoundedCache(this.stemCache, clean, clean);
|
|
806
1078
|
return clean;
|
|
807
1079
|
}
|
|
808
1080
|
const candidates = getStemCandidates(clean);
|
|
809
1081
|
for (const candidate of candidates) {
|
|
810
1082
|
if (await this.isHeadword(candidate)) {
|
|
811
|
-
this.stemCache
|
|
1083
|
+
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
812
1084
|
return candidate;
|
|
813
1085
|
}
|
|
814
1086
|
}
|
|
815
|
-
this.stemCache
|
|
1087
|
+
this.setBoundedCache(this.stemCache, clean, null);
|
|
816
1088
|
return null;
|
|
817
1089
|
}
|
|
818
1090
|
/**
|
|
@@ -1035,25 +1307,45 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1035
1307
|
* Checks spelling and returns suggestions if wrong.
|
|
1036
1308
|
*/
|
|
1037
1309
|
static async checkSpelling(word) {
|
|
1310
|
+
if (!word || word.trim() === "") {
|
|
1311
|
+
return { isCorrect: false, word };
|
|
1312
|
+
}
|
|
1313
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
1038
1314
|
const results = await this.getWord(word);
|
|
1039
1315
|
if (results.length > 0) {
|
|
1040
1316
|
return { isCorrect: true, word };
|
|
1041
1317
|
}
|
|
1318
|
+
if (COMMON_MISSPELLINGS[cleanWord]) {
|
|
1319
|
+
return { isCorrect: false, word, suggestion: COMMON_MISSPELLINGS[cleanWord] };
|
|
1320
|
+
}
|
|
1321
|
+
const seyMatch = cleanWord.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
1322
|
+
if (seyMatch && !SEY_EXCEPTIONS.has(cleanWord)) {
|
|
1323
|
+
let prefix = seyMatch[1];
|
|
1324
|
+
const suffix = seyMatch[2] || "";
|
|
1325
|
+
if (prefix === "hicbir")
|
|
1326
|
+
prefix = "hi\xE7bir";
|
|
1327
|
+
if (prefix === "cok")
|
|
1328
|
+
prefix = "\xE7ok";
|
|
1329
|
+
return {
|
|
1330
|
+
isCorrect: false,
|
|
1331
|
+
word,
|
|
1332
|
+
suggestion: `${prefix} \u015Fey${suffix}`
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1042
1335
|
const daily = await this.getDailyContent();
|
|
1043
1336
|
if (daily) {
|
|
1044
|
-
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") ===
|
|
1337
|
+
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1045
1338
|
if (syydMatch) {
|
|
1046
1339
|
return { isCorrect: false, word, suggestion: syydMatch.dogrukelime };
|
|
1047
1340
|
}
|
|
1048
|
-
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") ===
|
|
1341
|
+
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1049
1342
|
if (mixMatch) {
|
|
1050
1343
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
1051
1344
|
}
|
|
1052
1345
|
}
|
|
1053
1346
|
const root = await this.findRoot(word);
|
|
1054
1347
|
if (root) {
|
|
1055
|
-
const
|
|
1056
|
-
const isInflected = root !== cleanWord2;
|
|
1348
|
+
const isInflected = root !== cleanWord;
|
|
1057
1349
|
return {
|
|
1058
1350
|
isCorrect: true,
|
|
1059
1351
|
word,
|
|
@@ -1064,24 +1356,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1064
1356
|
if (this.autocompleteCache.length === 0) {
|
|
1065
1357
|
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
1066
1358
|
}
|
|
1067
|
-
const
|
|
1359
|
+
for (const candidate of this.autocompleteCache) {
|
|
1360
|
+
if (candidate.includes(" ")) {
|
|
1361
|
+
const candidateNoSpace = candidate.replace(/\s+/g, "").toLocaleLowerCase("tr-TR");
|
|
1362
|
+
if (candidateNoSpace === cleanWord) {
|
|
1363
|
+
return { isCorrect: false, word, suggestion: candidate };
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1068
1367
|
let best = null;
|
|
1069
1368
|
for (const candidate of this.autocompleteCache) {
|
|
1070
1369
|
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
1071
1370
|
continue;
|
|
1072
1371
|
if (Math.abs(candidate.length - cleanWord.length) > 2)
|
|
1073
1372
|
continue;
|
|
1074
|
-
const
|
|
1075
|
-
if (
|
|
1373
|
+
const rawDist = this.damerauLevenshtein(cleanWord, candidate);
|
|
1374
|
+
if (rawDist === 0)
|
|
1076
1375
|
continue;
|
|
1077
1376
|
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
1078
1377
|
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
1378
|
+
const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
|
|
1079
1379
|
const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
|
|
1080
1380
|
if (better) {
|
|
1081
|
-
best = { candidate, distance, firstMismatch, lengthMismatch };
|
|
1381
|
+
best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
|
|
1082
1382
|
}
|
|
1083
1383
|
}
|
|
1084
|
-
if (best && best.
|
|
1384
|
+
if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
|
|
1085
1385
|
return { isCorrect: false, word, suggestion: best.candidate };
|
|
1086
1386
|
}
|
|
1087
1387
|
return { isCorrect: false, word };
|
|
@@ -1488,14 +1788,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1488
1788
|
meaningCount: meaningsA.length,
|
|
1489
1789
|
origin: originA,
|
|
1490
1790
|
syllables: this.syllabicate(a),
|
|
1491
|
-
harmony: this.checkVowelHarmony(a)
|
|
1791
|
+
harmony: this.checkVowelHarmony(a),
|
|
1792
|
+
labialHarmony: this.checkLabialHarmony(a)
|
|
1492
1793
|
},
|
|
1493
1794
|
b: {
|
|
1494
1795
|
word: b,
|
|
1495
1796
|
meaningCount: meaningsB.length,
|
|
1496
1797
|
origin: originB,
|
|
1497
1798
|
syllables: this.syllabicate(b),
|
|
1498
|
-
harmony: this.checkVowelHarmony(b)
|
|
1799
|
+
harmony: this.checkVowelHarmony(b),
|
|
1800
|
+
labialHarmony: this.checkLabialHarmony(b)
|
|
1499
1801
|
}
|
|
1500
1802
|
};
|
|
1501
1803
|
}
|
|
@@ -1626,9 +1928,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1626
1928
|
}
|
|
1627
1929
|
/**
|
|
1628
1930
|
* Syllabicates a Turkish word based on general grammar rules.
|
|
1931
|
+
* Handles syllable separation for vowels, single consonants, double consonants,
|
|
1932
|
+
* and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
|
|
1629
1933
|
*/
|
|
1630
1934
|
static syllabicate(word) {
|
|
1631
1935
|
const vowels = /[aeıioöuüAEIİOÖUÜ]/;
|
|
1936
|
+
const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
|
|
1632
1937
|
const result = [];
|
|
1633
1938
|
let currentSyllable = "";
|
|
1634
1939
|
for (let i = word.length - 1; i >= 0; i--) {
|
|
@@ -1639,8 +1944,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1639
1944
|
currentSyllable = word[i - 1] + currentSyllable;
|
|
1640
1945
|
i--;
|
|
1641
1946
|
} else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
|
|
1642
|
-
|
|
1643
|
-
|
|
1947
|
+
if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
|
|
1948
|
+
currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
|
|
1949
|
+
i -= 2;
|
|
1950
|
+
} else {
|
|
1951
|
+
currentSyllable = word[i - 1] + currentSyllable;
|
|
1952
|
+
i--;
|
|
1953
|
+
}
|
|
1644
1954
|
}
|
|
1645
1955
|
}
|
|
1646
1956
|
result.unshift(currentSyllable);
|
|
@@ -1670,6 +1980,403 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1670
1980
|
const hasFront = frontVowels.test(lower);
|
|
1671
1981
|
return !(hasBack && hasFront);
|
|
1672
1982
|
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
|
|
1985
|
+
* Rules:
|
|
1986
|
+
* 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
|
|
1987
|
+
* 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
|
|
1988
|
+
* Single-syllable words and words with <=1 vowel are considered compliant by convention.
|
|
1989
|
+
*/
|
|
1990
|
+
static checkLabialHarmony(word) {
|
|
1991
|
+
const lower = word.toLocaleLowerCase("tr-TR");
|
|
1992
|
+
const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
|
|
1993
|
+
if (vowels.length <= 1)
|
|
1994
|
+
return true;
|
|
1995
|
+
for (let i = 0; i < vowels.length - 1; i++) {
|
|
1996
|
+
const v1 = vowels[i];
|
|
1997
|
+
const v2 = vowels[i + 1];
|
|
1998
|
+
if ("ae\u0131i".includes(v1)) {
|
|
1999
|
+
if (!"ae\u0131i".includes(v2))
|
|
2000
|
+
return false;
|
|
2001
|
+
} else if ("o\xF6u\xFC".includes(v1)) {
|
|
2002
|
+
if (!"aeu\xFC".includes(v2))
|
|
2003
|
+
return false;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Searches TDK headwords using a wildcard / pattern string.
|
|
2010
|
+
* Wildcards:
|
|
2011
|
+
* '_' or '?' matches any single character
|
|
2012
|
+
* '*' matches zero or more characters
|
|
2013
|
+
* Example: "k_l_m" matches "kalem", "kelam", "kilim".
|
|
2014
|
+
* Runs in-memory against TDK's 81k headword list.
|
|
2015
|
+
*/
|
|
2016
|
+
static async patternSearch(pattern, options) {
|
|
2017
|
+
if (!pattern || pattern.trim() === "")
|
|
2018
|
+
return [];
|
|
2019
|
+
await this.ensureAutocompleteLoaded();
|
|
2020
|
+
const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
|
|
2021
|
+
const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
|
|
2022
|
+
const regex = new RegExp(`^${escaped}$`, "u");
|
|
2023
|
+
const max = options?.maxResults ?? 50;
|
|
2024
|
+
const matches = [];
|
|
2025
|
+
for (const headword of this.autocompleteCache) {
|
|
2026
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2027
|
+
if (regex.test(lower)) {
|
|
2028
|
+
matches.push(headword);
|
|
2029
|
+
if (matches.length >= max)
|
|
2030
|
+
break;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return matches;
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Finds headwords in TDK that can be formed from the given letters (anagrams).
|
|
2037
|
+
* If exact-length anagrams exist, they are returned.
|
|
2038
|
+
* If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
|
|
2039
|
+
* minimum 3 letters) are returned, sorted by length descending.
|
|
2040
|
+
*/
|
|
2041
|
+
static async findAnagrams(letters, options) {
|
|
2042
|
+
if (!letters || letters.trim() === "")
|
|
2043
|
+
return [];
|
|
2044
|
+
await this.ensureAutocompleteLoaded();
|
|
2045
|
+
const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
|
|
2046
|
+
if (clean.length === 0)
|
|
2047
|
+
return [];
|
|
2048
|
+
const forceExact = options?.exactLength === true;
|
|
2049
|
+
const max = options?.maxResults ?? 50;
|
|
2050
|
+
const getFrequency = (str) => {
|
|
2051
|
+
const freq = {};
|
|
2052
|
+
for (const ch of str) {
|
|
2053
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
2054
|
+
}
|
|
2055
|
+
return freq;
|
|
2056
|
+
};
|
|
2057
|
+
const targetFreq = getFrequency(clean);
|
|
2058
|
+
const exactMatches = [];
|
|
2059
|
+
const subMatches = [];
|
|
2060
|
+
for (const headword of this.autocompleteCache) {
|
|
2061
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2062
|
+
if (lower.includes(" ") || lower.includes("-"))
|
|
2063
|
+
continue;
|
|
2064
|
+
if (lower.length > clean.length || lower.length < 3)
|
|
2065
|
+
continue;
|
|
2066
|
+
const wordFreq = getFrequency(lower);
|
|
2067
|
+
let isValid = true;
|
|
2068
|
+
for (const [ch, count] of Object.entries(wordFreq)) {
|
|
2069
|
+
if (!targetFreq[ch] || targetFreq[ch] < count) {
|
|
2070
|
+
isValid = false;
|
|
2071
|
+
break;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
if (isValid && lower !== clean) {
|
|
2075
|
+
if (lower.length === clean.length) {
|
|
2076
|
+
exactMatches.push(headword);
|
|
2077
|
+
} else {
|
|
2078
|
+
subMatches.push(headword);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
if (exactMatches.length > 0 || forceExact) {
|
|
2083
|
+
return exactMatches.slice(0, max);
|
|
2084
|
+
}
|
|
2085
|
+
subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
|
|
2086
|
+
return subMatches.slice(0, max);
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
|
|
2090
|
+
* @param word The target word
|
|
2091
|
+
* @param options.minLetters Minimum number of ending characters that must match (default: 3)
|
|
2092
|
+
* @param options.maxResults Maximum number of rhyme results to return (default: 50)
|
|
2093
|
+
*/
|
|
2094
|
+
static async findRhymes(word, options) {
|
|
2095
|
+
if (!word || word.trim() === "")
|
|
2096
|
+
return [];
|
|
2097
|
+
await this.ensureAutocompleteLoaded();
|
|
2098
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
2099
|
+
const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
|
|
2100
|
+
const max = options?.maxResults ?? 50;
|
|
2101
|
+
const suffix = clean.slice(-minLetters);
|
|
2102
|
+
const results = [];
|
|
2103
|
+
for (const headword of this.autocompleteCache) {
|
|
2104
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2105
|
+
if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
|
|
2106
|
+
results.push(headword);
|
|
2107
|
+
if (results.length >= max)
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
return results;
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
|
|
2115
|
+
* Detects:
|
|
2116
|
+
* 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
|
|
2117
|
+
* 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
|
|
2118
|
+
* 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
|
|
2119
|
+
* 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
|
|
2120
|
+
*/
|
|
2121
|
+
static async proofread(text) {
|
|
2122
|
+
if (!text || text.trim() === "") {
|
|
2123
|
+
return { text: text || "", issues: [], isCorrect: true };
|
|
2124
|
+
}
|
|
2125
|
+
await this.ensureAutocompleteLoaded();
|
|
2126
|
+
const issues = [];
|
|
2127
|
+
const SOMBAHCEMI = /* @__PURE__ */ new Set([
|
|
2128
|
+
"sanki",
|
|
2129
|
+
"oysaki",
|
|
2130
|
+
"mademki",
|
|
2131
|
+
"belki",
|
|
2132
|
+
"halbuki",
|
|
2133
|
+
"\xE7\xFCnk\xFC",
|
|
2134
|
+
"me\u011Ferki",
|
|
2135
|
+
"illaki"
|
|
2136
|
+
]);
|
|
2137
|
+
const PHRASE_MISTAKES = [
|
|
2138
|
+
{
|
|
2139
|
+
regex: /\bhiç\s+bir\b/gi,
|
|
2140
|
+
suggestion: "hi\xE7bir",
|
|
2141
|
+
message: "'hi\xE7bir' belgisiz s\u0131fat\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2142
|
+
type: "spelling"
|
|
2143
|
+
},
|
|
2144
|
+
{
|
|
2145
|
+
regex: /\bbir\s+çok\b/gi,
|
|
2146
|
+
suggestion: "bir\xE7ok",
|
|
2147
|
+
message: "'bir\xE7ok' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2148
|
+
type: "spelling"
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
regex: /\bbir\s+kaç\b/gi,
|
|
2152
|
+
suggestion: "birka\xE7",
|
|
2153
|
+
message: "'birka\xE7' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2154
|
+
type: "spelling"
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
regex: /\bbir\s+az\b/gi,
|
|
2158
|
+
suggestion: "biraz",
|
|
2159
|
+
message: "'biraz' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2160
|
+
type: "spelling"
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
regex: /\bher\s+hangi\b/gi,
|
|
2164
|
+
suggestion: "herhangi",
|
|
2165
|
+
message: "'herhangi' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2166
|
+
type: "spelling"
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
regex: /\bgit\s+gide\b/gi,
|
|
2170
|
+
suggestion: "gitgide",
|
|
2171
|
+
message: "'gitgide' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2172
|
+
type: "spelling"
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
regex: /\bbirden\s+bire\b/gi,
|
|
2176
|
+
suggestion: "birdenbire",
|
|
2177
|
+
message: "'birdenbire' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2178
|
+
type: "spelling"
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
regex: /\brast\s+gele\b/gi,
|
|
2182
|
+
suggestion: "rastgele",
|
|
2183
|
+
message: "'rastgele' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2184
|
+
type: "spelling"
|
|
2185
|
+
}
|
|
2186
|
+
];
|
|
2187
|
+
const coveredRanges = [];
|
|
2188
|
+
for (const pm of PHRASE_MISTAKES) {
|
|
2189
|
+
let pmMatch;
|
|
2190
|
+
while ((pmMatch = pm.regex.exec(text)) !== null) {
|
|
2191
|
+
const start = pmMatch.index;
|
|
2192
|
+
const end = start + pmMatch[0].length;
|
|
2193
|
+
coveredRanges.push({ start, end });
|
|
2194
|
+
issues.push({
|
|
2195
|
+
type: pm.type,
|
|
2196
|
+
word: pmMatch[0],
|
|
2197
|
+
startIndex: start,
|
|
2198
|
+
endIndex: end,
|
|
2199
|
+
suggestion: pm.suggestion,
|
|
2200
|
+
message: pm.message
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
const tokenRegex = /[\p{L}0-9'’]+/gu;
|
|
2205
|
+
let match;
|
|
2206
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
2207
|
+
const rawWord = match[0];
|
|
2208
|
+
const startIndex = match.index;
|
|
2209
|
+
const endIndex = startIndex + rawWord.length;
|
|
2210
|
+
const lower = rawWord.toLocaleLowerCase("tr-TR");
|
|
2211
|
+
if (/^\d+$/.test(lower))
|
|
2212
|
+
continue;
|
|
2213
|
+
if (coveredRanges.some((r) => startIndex >= r.start && endIndex <= r.end))
|
|
2214
|
+
continue;
|
|
2215
|
+
let flagged = false;
|
|
2216
|
+
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)?)$/);
|
|
2217
|
+
if (questionMatch) {
|
|
2218
|
+
const base = questionMatch[1];
|
|
2219
|
+
const particle = questionMatch[2];
|
|
2220
|
+
if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
|
|
2221
|
+
if (!await this.isHeadword(lower)) {
|
|
2222
|
+
issues.push({
|
|
2223
|
+
type: "question_particle",
|
|
2224
|
+
word: rawWord,
|
|
2225
|
+
startIndex,
|
|
2226
|
+
endIndex,
|
|
2227
|
+
suggestion: `${base} ${particle}`,
|
|
2228
|
+
message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
2229
|
+
});
|
|
2230
|
+
flagged = true;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
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;
|
|
2235
|
+
if (!flagged && lower.endsWith("ki") && lower.length > 3) {
|
|
2236
|
+
const base = lower.slice(0, -2);
|
|
2237
|
+
if (!SOMBAHCEMI.has(lower)) {
|
|
2238
|
+
if (!await this.isHeadword(lower)) {
|
|
2239
|
+
const root = await this.findRoot(base);
|
|
2240
|
+
const isVerb = (base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base)) && (root ? root.endsWith("mek") || root.endsWith("mak") : true);
|
|
2241
|
+
if (isVerb) {
|
|
2242
|
+
issues.push({
|
|
2243
|
+
type: "conjunction_ki",
|
|
2244
|
+
word: rawWord,
|
|
2245
|
+
startIndex,
|
|
2246
|
+
endIndex,
|
|
2247
|
+
suggestion: `${base} ki`,
|
|
2248
|
+
message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
2249
|
+
});
|
|
2250
|
+
flagged = true;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
|
|
2256
|
+
const base = lower.slice(0, -2);
|
|
2257
|
+
const ending = lower.slice(-2);
|
|
2258
|
+
if (!await this.isHeadword(lower)) {
|
|
2259
|
+
const root = await this.findRoot(base);
|
|
2260
|
+
const isVerb = VERB_CONJUGATION_REGEX.test(base) && (root ? root.endsWith("mek") || root.endsWith("mak") : false);
|
|
2261
|
+
if (isVerb) {
|
|
2262
|
+
const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
|
|
2263
|
+
issues.push({
|
|
2264
|
+
type: "conjunction_da",
|
|
2265
|
+
word: rawWord,
|
|
2266
|
+
startIndex,
|
|
2267
|
+
endIndex,
|
|
2268
|
+
suggestion: `${base} ${correctEnding}`,
|
|
2269
|
+
message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
|
|
2270
|
+
});
|
|
2271
|
+
flagged = true;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const seyMatch = lower.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
2276
|
+
if (!flagged && seyMatch && !SEY_EXCEPTIONS.has(lower)) {
|
|
2277
|
+
let prefix = seyMatch[1];
|
|
2278
|
+
const suffix = seyMatch[2] || "";
|
|
2279
|
+
if (prefix === "hicbir")
|
|
2280
|
+
prefix = "hi\xE7bir";
|
|
2281
|
+
if (prefix === "cok")
|
|
2282
|
+
prefix = "\xE7ok";
|
|
2283
|
+
issues.push({
|
|
2284
|
+
type: "spelling",
|
|
2285
|
+
word: rawWord,
|
|
2286
|
+
startIndex,
|
|
2287
|
+
endIndex,
|
|
2288
|
+
suggestion: `${prefix} \u015Fey${suffix}`,
|
|
2289
|
+
message: "'\u015Fey' s\xF6zc\xFC\u011F\xFC kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r."
|
|
2290
|
+
});
|
|
2291
|
+
flagged = true;
|
|
2292
|
+
}
|
|
2293
|
+
if (!flagged && lower === "yada") {
|
|
2294
|
+
issues.push({
|
|
2295
|
+
type: "spelling",
|
|
2296
|
+
word: rawWord,
|
|
2297
|
+
startIndex,
|
|
2298
|
+
endIndex,
|
|
2299
|
+
suggestion: "ya da",
|
|
2300
|
+
message: "'ya da' ba\u011Flac\u0131 her zaman ayr\u0131 yaz\u0131l\u0131r."
|
|
2301
|
+
});
|
|
2302
|
+
flagged = true;
|
|
2303
|
+
}
|
|
2304
|
+
if (!flagged && (lower === "burda" || lower === "\u015Furda" || lower === "surda" || lower === "orda" || lower === "i\xE7erde" || lower === "icerde" || lower === "d\u0131\u015Farda" || lower === "disarda" || lower === "yukarda")) {
|
|
2305
|
+
const correct = COMMON_MISSPELLINGS[lower] || lower;
|
|
2306
|
+
issues.push({
|
|
2307
|
+
type: "spelling",
|
|
2308
|
+
word: rawWord,
|
|
2309
|
+
startIndex,
|
|
2310
|
+
endIndex,
|
|
2311
|
+
suggestion: correct,
|
|
2312
|
+
message: `'${rawWord}' s\xF6zc\xFC\u011F\xFCnde \xFCnl\xFC d\xFC\u015Fmesi yap\u0131lmaz.`
|
|
2313
|
+
});
|
|
2314
|
+
flagged = true;
|
|
2315
|
+
}
|
|
2316
|
+
if (!flagged) {
|
|
2317
|
+
const check = await this.checkSpelling(rawWord);
|
|
2318
|
+
if (!check.isCorrect) {
|
|
2319
|
+
issues.push({
|
|
2320
|
+
type: "spelling",
|
|
2321
|
+
word: rawWord,
|
|
2322
|
+
startIndex,
|
|
2323
|
+
endIndex,
|
|
2324
|
+
suggestion: check.suggestion,
|
|
2325
|
+
message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir.` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
issues.sort((a, b) => a.startIndex - b.startIndex);
|
|
2331
|
+
return {
|
|
2332
|
+
text,
|
|
2333
|
+
issues,
|
|
2334
|
+
isCorrect: issues.length === 0
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
var TDKClient = class {
|
|
2339
|
+
constructor(config) {
|
|
2340
|
+
if (config) {
|
|
2341
|
+
TDK.configure(config);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
getWord(word) {
|
|
2345
|
+
return TDK.getWord(word);
|
|
2346
|
+
}
|
|
2347
|
+
getMeanings(word) {
|
|
2348
|
+
return TDK.getMeanings(word);
|
|
2349
|
+
}
|
|
2350
|
+
checkSpelling(word) {
|
|
2351
|
+
return TDK.checkSpelling(word);
|
|
2352
|
+
}
|
|
2353
|
+
findRoot(word) {
|
|
2354
|
+
return TDK.findRoot(word);
|
|
2355
|
+
}
|
|
2356
|
+
stem(word) {
|
|
2357
|
+
return TDK.stem(word);
|
|
2358
|
+
}
|
|
2359
|
+
proofread(text) {
|
|
2360
|
+
return TDK.proofread(text);
|
|
2361
|
+
}
|
|
2362
|
+
patternSearch(pattern, options) {
|
|
2363
|
+
return TDK.patternSearch(pattern, options);
|
|
2364
|
+
}
|
|
2365
|
+
findAnagrams(letters, options) {
|
|
2366
|
+
return TDK.findAnagrams(letters, options);
|
|
2367
|
+
}
|
|
2368
|
+
findRhymes(word, options) {
|
|
2369
|
+
return TDK.findRhymes(word, options);
|
|
2370
|
+
}
|
|
2371
|
+
syllabicate(word) {
|
|
2372
|
+
return TDK.syllabicate(word);
|
|
2373
|
+
}
|
|
2374
|
+
checkVowelHarmony(word) {
|
|
2375
|
+
return TDK.checkVowelHarmony(word);
|
|
2376
|
+
}
|
|
2377
|
+
checkLabialHarmony(word) {
|
|
2378
|
+
return TDK.checkLabialHarmony(word);
|
|
2379
|
+
}
|
|
1673
2380
|
};
|
|
1674
2381
|
|
|
1675
2382
|
export {
|
|
@@ -1681,7 +2388,10 @@ export {
|
|
|
1681
2388
|
TURKISH_SUFFIXES,
|
|
1682
2389
|
restoreConsonantSoftening,
|
|
1683
2390
|
restoreVowelDrop,
|
|
2391
|
+
restoreGemination,
|
|
2392
|
+
restoreVowelNarrowing,
|
|
1684
2393
|
restoreInfinitive,
|
|
1685
2394
|
getStemCandidates,
|
|
1686
|
-
TDK
|
|
2395
|
+
TDK,
|
|
2396
|
+
TDKClient
|
|
1687
2397
|
};
|