tdk-api-wrapper 1.5.0 → 1.6.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 +1 -1
- package/dist/{chunk-7KJHYRJZ.mjs → chunk-ZFWZXNGY.mjs} +389 -13
- package/dist/cli.js +394 -14
- package/dist/cli.mjs +6 -2
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +389 -13
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/cli.ts +6 -1
- package/src/morphology.ts +9 -0
- package/src/tdk.ts +441 -40
- package/test/morphology.test.js +11 -0
- package/test/proofread.test.js +28 -0
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak veya `TDKClient`
|
|
|
90
90
|
- **`TDK.checkVowelHarmony(word)`**: Kelimenin büyük ünlü uyumuna uyup uymadığını (boolean) kontrol eder.
|
|
91
91
|
- **`TDK.checkLabialHarmony(word)`**: Kelimenin küçük ünlü uyumuna (düzlük-yuvarlaklık uyumu) uyup uymadığını (boolean) kontrol eder.
|
|
92
92
|
- **`TDK.getPartOfSpeech(word)`**: Kelimenin sözcük türünü (isim, sıfat, zarf vb.) döndürür.
|
|
93
|
-
- **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'de doğrudan arar; bulamazsa TDK'nin "sık yapılan yanlışlar" listesini kontrol eder. Ardından **morfolojik ek sıyırma (stemming) motoru** devreye girer; kelime çekimli bir biçimse (`halılarımızın`, `kitabımız`, `çocuğa`, `okuyoruz`, `hakkımızda`, `başlıyor`) kökünü tespit edip `{ isCorrect: true, isInflected: true, root: "..." }` döner. Son aşamada ise TDK'nin ~81 bin kelimelik tam madde listesi üzerinde Damerau-Levenshtein edit-distance ile en yakın kelimeyi önerir (`yanlız` → `yalnız`).
|
|
93
|
+
- **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'de doğrudan arar; bulamazsa TDK'nin "sık yapılan yanlışlar" listesini kontrol eder. Ardından **morfolojik ek sıyırma (stemming) motoru** devreye girer; kelime çekimli bir biçimse (`halılarımızın`, `kitabımız`, `çocuğa`, `okuyoruz`, `hakkımızda`, `başlıyor`) kökünü tespit edip `{ isCorrect: true, isInflected: true, root: "..." }` döner. Son aşamada ise TDK'nin ~81 bin kelimelik tam madde listesi üzerinde **klavye ve düzeltme işareti farkındalıklı** bir Damerau-Levenshtein edit-distance ile en yakın kelimeyi önerir: Türkçe Q klavyede yan yana duran tuşlar (`arabs` → `araba`, `swlam` → `selam`) ve ASCII/Türkçe harf ikilileri (`ı/i`, `ş/s`, `ö/o` …) tam bir düzeltme yerine bir düzeltmenin küçük bir kesri kadar sayılır; böylece hem en yakın hem de en olası madde kazanır (`yanlız` → `yalnız`).
|
|
94
94
|
- **`TDK.getCompoundWords(word)`**: Aranan kelime ile oluşturulmuş birleşik kelimeleri (Örn: dolma kalem) listeler.
|
|
95
95
|
|
|
96
96
|
### 3. Morfoloji ve Kök Bulma (Morphology Engine)
|
|
@@ -535,6 +535,13 @@ function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
|
535
535
|
seen.add(apostropheStem);
|
|
536
536
|
}
|
|
537
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
|
+
}
|
|
538
545
|
let frontier = [normalized];
|
|
539
546
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
540
547
|
const nextFrontier = [];
|
|
@@ -584,6 +591,210 @@ import * as path from "path";
|
|
|
584
591
|
import * as os from "os";
|
|
585
592
|
import * as https from "https";
|
|
586
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"]);
|
|
747
|
+
var KEYBOARD_ROWS = [
|
|
748
|
+
["qwertyu\u0131op\u011F\xFC", 0],
|
|
749
|
+
["asdfghjkl\u015Fi", 0.5],
|
|
750
|
+
["zxcvbnm\xF6\xE7", 1]
|
|
751
|
+
];
|
|
752
|
+
var KEYBOARD_COORDS = (() => {
|
|
753
|
+
const coords = {};
|
|
754
|
+
KEYBOARD_ROWS.forEach(([keys, offset], row) => {
|
|
755
|
+
[...keys].forEach((key, col) => {
|
|
756
|
+
coords[key] = [col + offset, row];
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
return coords;
|
|
760
|
+
})();
|
|
761
|
+
var DIACRITIC_SIBLINGS = {
|
|
762
|
+
\u0131: "i",
|
|
763
|
+
i: "\u0131",
|
|
764
|
+
\u00F6: "o",
|
|
765
|
+
o: "\xF6",
|
|
766
|
+
\u00FC: "u",
|
|
767
|
+
u: "\xFC",
|
|
768
|
+
\u015F: "s",
|
|
769
|
+
s: "\u015F",
|
|
770
|
+
\u00E7: "c",
|
|
771
|
+
c: "\xE7",
|
|
772
|
+
\u011F: "g",
|
|
773
|
+
g: "\u011F",
|
|
774
|
+
\u00E2: "a",
|
|
775
|
+
a: "\xE2"
|
|
776
|
+
};
|
|
777
|
+
var KEYBOARD_ROW_SUB_COST = 0.4;
|
|
778
|
+
var KEYBOARD_DIAGONAL_SUB_COST = 0.55;
|
|
779
|
+
var DIACRITIC_SUB_COST = 0.3;
|
|
780
|
+
var TRANSPOSITION_COST = 0.8;
|
|
781
|
+
function keyboardSubCost(a, b) {
|
|
782
|
+
if (a === b)
|
|
783
|
+
return 0;
|
|
784
|
+
if (DIACRITIC_SIBLINGS[a] === b)
|
|
785
|
+
return DIACRITIC_SUB_COST;
|
|
786
|
+
const pa = KEYBOARD_COORDS[a];
|
|
787
|
+
const pb = KEYBOARD_COORDS[b];
|
|
788
|
+
if (!pa || !pb)
|
|
789
|
+
return 1;
|
|
790
|
+
const dx = Math.abs(pa[0] - pb[0]);
|
|
791
|
+
const dy = Math.abs(pa[1] - pb[1]);
|
|
792
|
+
if (dy === 0 && dx <= 1 + 1e-9)
|
|
793
|
+
return KEYBOARD_ROW_SUB_COST;
|
|
794
|
+
if (dy === 1 && dx <= 1 + 1e-9)
|
|
795
|
+
return KEYBOARD_DIAGONAL_SUB_COST;
|
|
796
|
+
return 1;
|
|
797
|
+
}
|
|
587
798
|
var TDK = class {
|
|
588
799
|
static BASE_URL = "https://sozluk.gov.tr";
|
|
589
800
|
static AUDIO_API_HOST = "api.sozluk.gov.tr";
|
|
@@ -1147,25 +1358,45 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1147
1358
|
* Checks spelling and returns suggestions if wrong.
|
|
1148
1359
|
*/
|
|
1149
1360
|
static async checkSpelling(word) {
|
|
1361
|
+
if (!word || word.trim() === "") {
|
|
1362
|
+
return { isCorrect: false, word };
|
|
1363
|
+
}
|
|
1364
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
1150
1365
|
const results = await this.getWord(word);
|
|
1151
1366
|
if (results.length > 0) {
|
|
1152
1367
|
return { isCorrect: true, word };
|
|
1153
1368
|
}
|
|
1369
|
+
if (COMMON_MISSPELLINGS[cleanWord]) {
|
|
1370
|
+
return { isCorrect: false, word, suggestion: COMMON_MISSPELLINGS[cleanWord] };
|
|
1371
|
+
}
|
|
1372
|
+
const seyMatch = cleanWord.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
1373
|
+
if (seyMatch && !SEY_EXCEPTIONS.has(cleanWord)) {
|
|
1374
|
+
let prefix = seyMatch[1];
|
|
1375
|
+
const suffix = seyMatch[2] || "";
|
|
1376
|
+
if (prefix === "hicbir")
|
|
1377
|
+
prefix = "hi\xE7bir";
|
|
1378
|
+
if (prefix === "cok")
|
|
1379
|
+
prefix = "\xE7ok";
|
|
1380
|
+
return {
|
|
1381
|
+
isCorrect: false,
|
|
1382
|
+
word,
|
|
1383
|
+
suggestion: `${prefix} \u015Fey${suffix}`
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1154
1386
|
const daily = await this.getDailyContent();
|
|
1155
1387
|
if (daily) {
|
|
1156
|
-
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") ===
|
|
1388
|
+
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1157
1389
|
if (syydMatch) {
|
|
1158
1390
|
return { isCorrect: false, word, suggestion: syydMatch.dogrukelime };
|
|
1159
1391
|
}
|
|
1160
|
-
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") ===
|
|
1392
|
+
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1161
1393
|
if (mixMatch) {
|
|
1162
1394
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
1163
1395
|
}
|
|
1164
1396
|
}
|
|
1165
1397
|
const root = await this.findRoot(word);
|
|
1166
1398
|
if (root) {
|
|
1167
|
-
const
|
|
1168
|
-
const isInflected = root !== cleanWord2;
|
|
1399
|
+
const isInflected = root !== cleanWord;
|
|
1169
1400
|
return {
|
|
1170
1401
|
isCorrect: true,
|
|
1171
1402
|
word,
|
|
@@ -1176,24 +1407,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1176
1407
|
if (this.autocompleteCache.length === 0) {
|
|
1177
1408
|
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
1178
1409
|
}
|
|
1179
|
-
const
|
|
1410
|
+
for (const candidate of this.autocompleteCache) {
|
|
1411
|
+
if (candidate.includes(" ")) {
|
|
1412
|
+
const candidateNoSpace = candidate.replace(/\s+/g, "").toLocaleLowerCase("tr-TR");
|
|
1413
|
+
if (candidateNoSpace === cleanWord) {
|
|
1414
|
+
return { isCorrect: false, word, suggestion: candidate };
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1180
1418
|
let best = null;
|
|
1181
1419
|
for (const candidate of this.autocompleteCache) {
|
|
1182
1420
|
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
1183
1421
|
continue;
|
|
1184
1422
|
if (Math.abs(candidate.length - cleanWord.length) > 2)
|
|
1185
1423
|
continue;
|
|
1186
|
-
const
|
|
1187
|
-
if (
|
|
1424
|
+
const rawDist = this.damerauLevenshtein(cleanWord, candidate);
|
|
1425
|
+
if (rawDist === 0 || rawDist > 2)
|
|
1188
1426
|
continue;
|
|
1189
1427
|
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
1190
1428
|
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
1191
|
-
const
|
|
1429
|
+
const score = this.keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
|
|
1430
|
+
const better = !best || score < best.score - 1e-9 || Math.abs(score - best.score) < 1e-9 && firstMismatch < best.firstMismatch || Math.abs(score - best.score) < 1e-9 && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
|
|
1192
1431
|
if (better) {
|
|
1193
|
-
best = { candidate,
|
|
1432
|
+
best = { candidate, score, rawDist, firstMismatch, lengthMismatch };
|
|
1194
1433
|
}
|
|
1195
1434
|
}
|
|
1196
|
-
if (best && best.
|
|
1435
|
+
if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
|
|
1197
1436
|
return { isCorrect: false, word, suggestion: best.candidate };
|
|
1198
1437
|
}
|
|
1199
1438
|
return { isCorrect: false, word };
|
|
@@ -1722,6 +1961,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1722
1961
|
}
|
|
1723
1962
|
return dp[a.length][b.length];
|
|
1724
1963
|
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
|
|
1966
|
+
* recurrence as {@link damerauLevenshtein}, but a substitution is charged by
|
|
1967
|
+
* {@link keyboardSubCost} (a fraction of an edit when the two letters are
|
|
1968
|
+
* adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
|
|
1969
|
+
* transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
|
|
1970
|
+
* still cost a full 1. Used only to *rank* spelling candidates; the plain
|
|
1971
|
+
* integer distance still gates whether a suggestion is offered at all.
|
|
1972
|
+
*/
|
|
1973
|
+
static keyboardAwareDistance(a, b) {
|
|
1974
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
1975
|
+
for (let i = 0; i <= a.length; i++)
|
|
1976
|
+
dp[i][0] = i;
|
|
1977
|
+
for (let j = 0; j <= b.length; j++)
|
|
1978
|
+
dp[0][j] = j;
|
|
1979
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1980
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1981
|
+
const cost = keyboardSubCost(a[i - 1], b[j - 1]);
|
|
1982
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
1983
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
1984
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + TRANSPOSITION_COST);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return dp[a.length][b.length];
|
|
1989
|
+
}
|
|
1725
1990
|
/**
|
|
1726
1991
|
* Fetches multiple words concurrently with a small delay to avoid rate limiting.
|
|
1727
1992
|
*/
|
|
@@ -1946,6 +2211,73 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1946
2211
|
"me\u011Ferki",
|
|
1947
2212
|
"illaki"
|
|
1948
2213
|
]);
|
|
2214
|
+
const PHRASE_MISTAKES = [
|
|
2215
|
+
{
|
|
2216
|
+
regex: /\bhiç\s+bir\b/gi,
|
|
2217
|
+
suggestion: "hi\xE7bir",
|
|
2218
|
+
message: "'hi\xE7bir' belgisiz s\u0131fat\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2219
|
+
type: "spelling"
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
regex: /\bbir\s+çok\b/gi,
|
|
2223
|
+
suggestion: "bir\xE7ok",
|
|
2224
|
+
message: "'bir\xE7ok' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2225
|
+
type: "spelling"
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
regex: /\bbir\s+kaç\b/gi,
|
|
2229
|
+
suggestion: "birka\xE7",
|
|
2230
|
+
message: "'birka\xE7' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2231
|
+
type: "spelling"
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
regex: /\bbir\s+az\b/gi,
|
|
2235
|
+
suggestion: "biraz",
|
|
2236
|
+
message: "'biraz' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2237
|
+
type: "spelling"
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
regex: /\bher\s+hangi\b/gi,
|
|
2241
|
+
suggestion: "herhangi",
|
|
2242
|
+
message: "'herhangi' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2243
|
+
type: "spelling"
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
regex: /\bgit\s+gide\b/gi,
|
|
2247
|
+
suggestion: "gitgide",
|
|
2248
|
+
message: "'gitgide' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2249
|
+
type: "spelling"
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
regex: /\bbirden\s+bire\b/gi,
|
|
2253
|
+
suggestion: "birdenbire",
|
|
2254
|
+
message: "'birdenbire' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2255
|
+
type: "spelling"
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
regex: /\brast\s+gele\b/gi,
|
|
2259
|
+
suggestion: "rastgele",
|
|
2260
|
+
message: "'rastgele' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2261
|
+
type: "spelling"
|
|
2262
|
+
}
|
|
2263
|
+
];
|
|
2264
|
+
const coveredRanges = [];
|
|
2265
|
+
for (const pm of PHRASE_MISTAKES) {
|
|
2266
|
+
let pmMatch;
|
|
2267
|
+
while ((pmMatch = pm.regex.exec(text)) !== null) {
|
|
2268
|
+
const start = pmMatch.index;
|
|
2269
|
+
const end = start + pmMatch[0].length;
|
|
2270
|
+
coveredRanges.push({ start, end });
|
|
2271
|
+
issues.push({
|
|
2272
|
+
type: pm.type,
|
|
2273
|
+
word: pmMatch[0],
|
|
2274
|
+
startIndex: start,
|
|
2275
|
+
endIndex: end,
|
|
2276
|
+
suggestion: pm.suggestion,
|
|
2277
|
+
message: pm.message
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
1949
2281
|
const tokenRegex = /[\p{L}0-9'’]+/gu;
|
|
1950
2282
|
let match;
|
|
1951
2283
|
while ((match = tokenRegex.exec(text)) !== null) {
|
|
@@ -1955,6 +2287,8 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1955
2287
|
const lower = rawWord.toLocaleLowerCase("tr-TR");
|
|
1956
2288
|
if (/^\d+$/.test(lower))
|
|
1957
2289
|
continue;
|
|
2290
|
+
if (coveredRanges.some((r) => startIndex >= r.start && endIndex <= r.end))
|
|
2291
|
+
continue;
|
|
1958
2292
|
let flagged = false;
|
|
1959
2293
|
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
2294
|
if (questionMatch) {
|
|
@@ -1980,7 +2314,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1980
2314
|
if (!SOMBAHCEMI.has(lower)) {
|
|
1981
2315
|
if (!await this.isHeadword(lower)) {
|
|
1982
2316
|
const root = await this.findRoot(base);
|
|
1983
|
-
const isVerb =
|
|
2317
|
+
const isVerb = (base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base)) && (root ? root.endsWith("mek") || root.endsWith("mak") : true);
|
|
1984
2318
|
if (isVerb) {
|
|
1985
2319
|
issues.push({
|
|
1986
2320
|
type: "conjunction_ki",
|
|
@@ -2000,7 +2334,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
2000
2334
|
const ending = lower.slice(-2);
|
|
2001
2335
|
if (!await this.isHeadword(lower)) {
|
|
2002
2336
|
const root = await this.findRoot(base);
|
|
2003
|
-
const isVerb =
|
|
2337
|
+
const isVerb = VERB_CONJUGATION_REGEX.test(base) && (root ? root.endsWith("mek") || root.endsWith("mak") : false);
|
|
2004
2338
|
if (isVerb) {
|
|
2005
2339
|
const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
|
|
2006
2340
|
issues.push({
|
|
@@ -2015,6 +2349,47 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
2015
2349
|
}
|
|
2016
2350
|
}
|
|
2017
2351
|
}
|
|
2352
|
+
const seyMatch = lower.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
2353
|
+
if (!flagged && seyMatch && !SEY_EXCEPTIONS.has(lower)) {
|
|
2354
|
+
let prefix = seyMatch[1];
|
|
2355
|
+
const suffix = seyMatch[2] || "";
|
|
2356
|
+
if (prefix === "hicbir")
|
|
2357
|
+
prefix = "hi\xE7bir";
|
|
2358
|
+
if (prefix === "cok")
|
|
2359
|
+
prefix = "\xE7ok";
|
|
2360
|
+
issues.push({
|
|
2361
|
+
type: "spelling",
|
|
2362
|
+
word: rawWord,
|
|
2363
|
+
startIndex,
|
|
2364
|
+
endIndex,
|
|
2365
|
+
suggestion: `${prefix} \u015Fey${suffix}`,
|
|
2366
|
+
message: "'\u015Fey' s\xF6zc\xFC\u011F\xFC kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r."
|
|
2367
|
+
});
|
|
2368
|
+
flagged = true;
|
|
2369
|
+
}
|
|
2370
|
+
if (!flagged && lower === "yada") {
|
|
2371
|
+
issues.push({
|
|
2372
|
+
type: "spelling",
|
|
2373
|
+
word: rawWord,
|
|
2374
|
+
startIndex,
|
|
2375
|
+
endIndex,
|
|
2376
|
+
suggestion: "ya da",
|
|
2377
|
+
message: "'ya da' ba\u011Flac\u0131 her zaman ayr\u0131 yaz\u0131l\u0131r."
|
|
2378
|
+
});
|
|
2379
|
+
flagged = true;
|
|
2380
|
+
}
|
|
2381
|
+
if (!flagged && (lower === "burda" || lower === "\u015Furda" || lower === "surda" || lower === "orda" || lower === "i\xE7erde" || lower === "icerde" || lower === "d\u0131\u015Farda" || lower === "disarda" || lower === "yukarda")) {
|
|
2382
|
+
const correct = COMMON_MISSPELLINGS[lower] || lower;
|
|
2383
|
+
issues.push({
|
|
2384
|
+
type: "spelling",
|
|
2385
|
+
word: rawWord,
|
|
2386
|
+
startIndex,
|
|
2387
|
+
endIndex,
|
|
2388
|
+
suggestion: correct,
|
|
2389
|
+
message: `'${rawWord}' s\xF6zc\xFC\u011F\xFCnde \xFCnl\xFC d\xFC\u015Fmesi yap\u0131lmaz.`
|
|
2390
|
+
});
|
|
2391
|
+
flagged = true;
|
|
2392
|
+
}
|
|
2018
2393
|
if (!flagged) {
|
|
2019
2394
|
const check = await this.checkSpelling(rawWord);
|
|
2020
2395
|
if (!check.isCorrect) {
|
|
@@ -2024,11 +2399,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
2024
2399
|
startIndex,
|
|
2025
2400
|
endIndex,
|
|
2026
2401
|
suggestion: check.suggestion,
|
|
2027
|
-
message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir
|
|
2402
|
+
message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir.` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
|
|
2028
2403
|
});
|
|
2029
2404
|
}
|
|
2030
2405
|
}
|
|
2031
2406
|
}
|
|
2407
|
+
issues.sort((a, b) => a.startIndex - b.startIndex);
|
|
2032
2408
|
return {
|
|
2033
2409
|
text,
|
|
2034
2410
|
issues,
|