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
package/src/tdk.ts
CHANGED
|
@@ -10,6 +10,12 @@ import type {
|
|
|
10
10
|
TDKRule,
|
|
11
11
|
KubbealtiEntry,
|
|
12
12
|
WiktionaryEntry,
|
|
13
|
+
ProofreadIssue,
|
|
14
|
+
ProofreadResult,
|
|
15
|
+
PatternSearchOptions,
|
|
16
|
+
AnagramOptions,
|
|
17
|
+
RhymeOptions,
|
|
18
|
+
TDKConfig,
|
|
13
19
|
} from "./types";
|
|
14
20
|
import { TDKValidationError, TDKNetworkError } from "./errors";
|
|
15
21
|
import { getStemCandidates } from "./morphology";
|
|
@@ -19,6 +25,168 @@ import * as os from "node:os";
|
|
|
19
25
|
import * as https from "node:https";
|
|
20
26
|
import * as tls from "node:tls";
|
|
21
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Known frequent Turkish misspellings, erroneously joined compound words,
|
|
30
|
+
* and words where vowel dropping is prohibited by TDK (Yazım Kılavuzu).
|
|
31
|
+
*/
|
|
32
|
+
export const COMMON_MISSPELLINGS: Record<string, string> = {
|
|
33
|
+
// -şey ile biten ve ayrı yazılması zorunlu söz öbekleri
|
|
34
|
+
herşey: "her şey",
|
|
35
|
+
hersey: "her şey",
|
|
36
|
+
birşey: "bir şey",
|
|
37
|
+
birsey: "bir şey",
|
|
38
|
+
hiçbirşey: "hiçbir şey",
|
|
39
|
+
hicbirsey: "hiçbir şey",
|
|
40
|
+
çokşey: "çok şey",
|
|
41
|
+
coksey: "çok şey",
|
|
42
|
+
şeyler: "şeyler",
|
|
43
|
+
seyler: "şeyler",
|
|
44
|
+
herhangibirşey: "herhangi bir şey",
|
|
45
|
+
herhangibirsey: "herhangi bir şey",
|
|
46
|
+
|
|
47
|
+
// Sıkça birleşik yazılan ama ayrı yazılması gereken sözler
|
|
48
|
+
hergün: "her gün",
|
|
49
|
+
hergun: "her gün",
|
|
50
|
+
herzaman: "her zaman",
|
|
51
|
+
heran: "her an",
|
|
52
|
+
heryer: "her yer",
|
|
53
|
+
herbiri: "her biri",
|
|
54
|
+
pekçok: "pek çok",
|
|
55
|
+
pekcok: "pek çok",
|
|
56
|
+
pekaz: "pek az",
|
|
57
|
+
yada: "ya da",
|
|
58
|
+
tabiki: "tabii ki",
|
|
59
|
+
tabiiki: "tabii ki",
|
|
60
|
+
sağol: "sağ ol",
|
|
61
|
+
sagol: "sağ ol",
|
|
62
|
+
sağolun: "sağ olun",
|
|
63
|
+
sagolun: "sağ olun",
|
|
64
|
+
hoşçakal: "hoşça kal",
|
|
65
|
+
hoscakal: "hoşça kal",
|
|
66
|
+
hoşgeldin: "hoş geldin",
|
|
67
|
+
hosgeldin: "hoş geldin",
|
|
68
|
+
hoşgeldiniz: "hoş geldiniz",
|
|
69
|
+
hosgeldiniz: "hoş geldiniz",
|
|
70
|
+
hoşbulduk: "hoş bulduk",
|
|
71
|
+
hosbulduk: "hoş bulduk",
|
|
72
|
+
yanısıra: "yanı sıra",
|
|
73
|
+
yanisira: "yanı sıra",
|
|
74
|
+
peşisıra: "peşi sıra",
|
|
75
|
+
pesisira: "peşi sıra",
|
|
76
|
+
ardısıra: "ardı sıra",
|
|
77
|
+
ardisira: "ardı sıra",
|
|
78
|
+
artarda: "art arda",
|
|
79
|
+
yüzyüze: "yüz yüze",
|
|
80
|
+
yuzyuze: "yüz yüze",
|
|
81
|
+
elele: "el ele",
|
|
82
|
+
gözgöze: "göz göze",
|
|
83
|
+
başbaşa: "baş başa",
|
|
84
|
+
basbasa: "baş başa",
|
|
85
|
+
yanyana: "yan yana",
|
|
86
|
+
içiçe: "iç içe",
|
|
87
|
+
icice: "iç içe",
|
|
88
|
+
üstüste: "üst üste",
|
|
89
|
+
ustuste: "üst üste",
|
|
90
|
+
altalta: "alt alta",
|
|
91
|
+
önsöz: "ön söz",
|
|
92
|
+
onsoz: "ön söz",
|
|
93
|
+
önyargı: "ön yargı",
|
|
94
|
+
onyargi: "ön yargı",
|
|
95
|
+
farketmek: "fark etmek",
|
|
96
|
+
farketti: "fark etti",
|
|
97
|
+
farkettim: "fark ettim",
|
|
98
|
+
farkeder: "fark eder",
|
|
99
|
+
farketmez: "fark etmez",
|
|
100
|
+
terketmek: "terk etmek",
|
|
101
|
+
terketti: "terk etti",
|
|
102
|
+
ayırdetmek: "ayırt etmek",
|
|
103
|
+
ayırtetmek: "ayırt etmek",
|
|
104
|
+
arzetmek: "arz etmek",
|
|
105
|
+
arzederim: "arz ederim",
|
|
106
|
+
varolmak: "var olmak",
|
|
107
|
+
yokolmak: "yok olmak",
|
|
108
|
+
haketmek: "hak etmek",
|
|
109
|
+
haketti: "hak etti",
|
|
110
|
+
hakkaten: "hakikaten",
|
|
111
|
+
hiçkimse: "hiç kimse",
|
|
112
|
+
hickimse: "hiç kimse",
|
|
113
|
+
|
|
114
|
+
// Ünlü düşmesi yapılmaması gereken yer bildiren sözler (TDK Kural 15)
|
|
115
|
+
burda: "burada",
|
|
116
|
+
burdan: "buradan",
|
|
117
|
+
şurda: "şurada",
|
|
118
|
+
surda: "şurada",
|
|
119
|
+
şurdan: "şuradan",
|
|
120
|
+
surdan: "şuradan",
|
|
121
|
+
orda: "orada",
|
|
122
|
+
ordan: "oradan",
|
|
123
|
+
içerde: "içeride",
|
|
124
|
+
icerde: "içeride",
|
|
125
|
+
içerden: "içeriden",
|
|
126
|
+
icerden: "içeriden",
|
|
127
|
+
dışarda: "dışarıda",
|
|
128
|
+
disarda: "dışarıda",
|
|
129
|
+
dışardan: "dışarıdan",
|
|
130
|
+
disardan: "dışarıdan",
|
|
131
|
+
yukarda: "yukarıda",
|
|
132
|
+
yukardan: "yukarıdan",
|
|
133
|
+
|
|
134
|
+
// Sıkça yanlış yazılan sözcükler
|
|
135
|
+
herkez: "herkes",
|
|
136
|
+
yanlız: "yalnız",
|
|
137
|
+
yalnış: "yanlış",
|
|
138
|
+
orjinal: "orijinal",
|
|
139
|
+
labaratuar: "laboratuvar",
|
|
140
|
+
laboratuar: "laboratuvar",
|
|
141
|
+
şöför: "şoför",
|
|
142
|
+
sofor: "şoför",
|
|
143
|
+
egzos: "egzoz",
|
|
144
|
+
eksoz: "egzoz",
|
|
145
|
+
ekzoz: "egzoz",
|
|
146
|
+
kiprik: "kirpik",
|
|
147
|
+
kirbit: "kibrit",
|
|
148
|
+
klavuz: "kılavuz",
|
|
149
|
+
kıravat: "kravat",
|
|
150
|
+
süpriz: "sürpriz",
|
|
151
|
+
supriz: "sürpriz",
|
|
152
|
+
raslantı: "rastlantı",
|
|
153
|
+
hastahane: "hastane",
|
|
154
|
+
pastahane: "pastane",
|
|
155
|
+
postahane: "postane",
|
|
156
|
+
eczahane: "eczane",
|
|
157
|
+
meyva: "meyve",
|
|
158
|
+
sarmısak: "sarımsak",
|
|
159
|
+
dinazor: "dinozor",
|
|
160
|
+
pantalon: "pantolon",
|
|
161
|
+
tesbih: "tespih",
|
|
162
|
+
ahçı: "aşçı",
|
|
163
|
+
matba: "matbaa",
|
|
164
|
+
idda: "iddia",
|
|
165
|
+
iddaa: "iddia",
|
|
166
|
+
muhattap: "muhatap",
|
|
167
|
+
traş: "tıraş",
|
|
168
|
+
karnıbahar: "karnabahar",
|
|
169
|
+
kareografi: "koreografi",
|
|
170
|
+
poaça: "poğaça",
|
|
171
|
+
pohaça: "poğaça",
|
|
172
|
+
şarz: "şarj",
|
|
173
|
+
sarj: "şarj",
|
|
174
|
+
makina: "makine",
|
|
175
|
+
müsade: "müsaade",
|
|
176
|
+
entellektüel: "entelektüel",
|
|
177
|
+
inisiyatif: "inisiyatif",
|
|
178
|
+
insiyatif: "inisiyatif",
|
|
179
|
+
sezeryan: "sezaryen",
|
|
180
|
+
doküman: "doküman",
|
|
181
|
+
döküman: "doküman",
|
|
182
|
+
erozyon: "erozyon",
|
|
183
|
+
erizyon: "erozyon",
|
|
184
|
+
anane: "anneanne",
|
|
185
|
+
babaanne: "babaanne",
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const SEY_EXCEPTIONS = new Set(["düşey", "eşey", "konsey", "jersey", "şey"]);
|
|
189
|
+
|
|
22
190
|
/**
|
|
23
191
|
* TDK (Türk Dil Kurumu) API Wrapper
|
|
24
192
|
*/
|
|
@@ -104,6 +272,11 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
104
272
|
-----END CERTIFICATE-----`,
|
|
105
273
|
];
|
|
106
274
|
|
|
275
|
+
// Configuration
|
|
276
|
+
private static defaultTimeoutMs = 8000;
|
|
277
|
+
private static defaultRetries = 1;
|
|
278
|
+
private static maxCacheSize = 1000;
|
|
279
|
+
|
|
107
280
|
// Cache Mechanism
|
|
108
281
|
private static isCacheEnabled = false;
|
|
109
282
|
private static wordCache = new Map<string, WordInfo[]>();
|
|
@@ -112,6 +285,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
112
285
|
private static autocompleteSet: Set<string> = new Set<string>();
|
|
113
286
|
private static stemCache = new Map<string, string | null>();
|
|
114
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Configures global client options such as network timeout, retries, and cache size.
|
|
290
|
+
*/
|
|
291
|
+
public static configure(config: TDKConfig): void {
|
|
292
|
+
if (config.timeoutMs !== undefined) this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
|
|
293
|
+
if (config.retries !== undefined) this.defaultRetries = Math.max(0, config.retries);
|
|
294
|
+
if (config.cache !== undefined) this.enableCache(config.cache);
|
|
295
|
+
if (config.maxCacheSize !== undefined) this.maxCacheSize = Math.max(10, config.maxCacheSize);
|
|
296
|
+
}
|
|
297
|
+
|
|
115
298
|
/**
|
|
116
299
|
* Enables or disables in-memory caching for API requests.
|
|
117
300
|
*/
|
|
@@ -133,10 +316,58 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
133
316
|
this.stemCache.clear();
|
|
134
317
|
}
|
|
135
318
|
|
|
319
|
+
private static setBoundedCache<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
320
|
+
if (map.size >= this.maxCacheSize) {
|
|
321
|
+
const firstKey = map.keys().next().value;
|
|
322
|
+
if (firstKey !== undefined) map.delete(firstKey);
|
|
323
|
+
}
|
|
324
|
+
map.set(key, value);
|
|
325
|
+
}
|
|
326
|
+
|
|
136
327
|
private static delay(ms: number) {
|
|
137
328
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
138
329
|
}
|
|
139
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
|
|
333
|
+
*/
|
|
334
|
+
private static async fetchWithRetry(
|
|
335
|
+
url: string,
|
|
336
|
+
options: RequestInit = {},
|
|
337
|
+
retries: number = this.defaultRetries,
|
|
338
|
+
timeoutMs: number = this.defaultTimeoutMs
|
|
339
|
+
): Promise<Response> {
|
|
340
|
+
let lastError: unknown;
|
|
341
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
342
|
+
try {
|
|
343
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
344
|
+
const headers = {
|
|
345
|
+
"User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
|
|
346
|
+
...((options.headers as Record<string, string>) || {}),
|
|
347
|
+
};
|
|
348
|
+
const res = await fetch(url, { ...options, headers, signal });
|
|
349
|
+
if (res.ok || (res.status >= 400 && res.status < 500)) {
|
|
350
|
+
return res;
|
|
351
|
+
}
|
|
352
|
+
// If 5xx server error, retry
|
|
353
|
+
if (attempt < retries) {
|
|
354
|
+
await this.delay(200 * (attempt + 1));
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
return res;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
lastError = err;
|
|
360
|
+
if (attempt < retries) {
|
|
361
|
+
await this.delay(200 * (attempt + 1));
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
|
|
367
|
+
cause: lastError,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
140
371
|
/**
|
|
141
372
|
* Fetches detailed information for a given word from the TDK Dictionary.
|
|
142
373
|
*/
|
|
@@ -155,9 +386,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
155
386
|
|
|
156
387
|
let response: Response;
|
|
157
388
|
try {
|
|
158
|
-
response = await
|
|
159
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
|
|
160
|
-
});
|
|
389
|
+
response = await this.fetchWithRetry(url);
|
|
161
390
|
} catch (error) {
|
|
162
391
|
throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
|
|
163
392
|
}
|
|
@@ -176,13 +405,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
176
405
|
}
|
|
177
406
|
|
|
178
407
|
if (!Array.isArray(data) && data && "error" in (data as Record<string, unknown>)) {
|
|
179
|
-
if (this.isCacheEnabled) this.wordCache
|
|
408
|
+
if (this.isCacheEnabled) this.setBoundedCache(this.wordCache, cleanWord, []);
|
|
180
409
|
return [];
|
|
181
410
|
}
|
|
182
411
|
|
|
183
412
|
const results = data as WordInfo[];
|
|
184
413
|
if (this.isCacheEnabled) {
|
|
185
|
-
this.wordCache
|
|
414
|
+
this.setBoundedCache(this.wordCache, cleanWord, results);
|
|
186
415
|
}
|
|
187
416
|
return results;
|
|
188
417
|
}
|
|
@@ -320,7 +549,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
320
549
|
|
|
321
550
|
// 1. If the word itself is an exact headword, it is its own root
|
|
322
551
|
if (await this.isHeadword(clean)) {
|
|
323
|
-
this.stemCache
|
|
552
|
+
this.setBoundedCache(this.stemCache, clean, clean);
|
|
324
553
|
return clean;
|
|
325
554
|
}
|
|
326
555
|
|
|
@@ -328,12 +557,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
328
557
|
const candidates = getStemCandidates(clean);
|
|
329
558
|
for (const candidate of candidates) {
|
|
330
559
|
if (await this.isHeadword(candidate)) {
|
|
331
|
-
this.stemCache
|
|
560
|
+
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
332
561
|
return candidate;
|
|
333
562
|
}
|
|
334
563
|
}
|
|
335
564
|
|
|
336
|
-
this.stemCache
|
|
565
|
+
this.setBoundedCache(this.stemCache, clean, null);
|
|
337
566
|
return null;
|
|
338
567
|
}
|
|
339
568
|
|
|
@@ -563,32 +792,56 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
563
792
|
* Checks spelling and returns suggestions if wrong.
|
|
564
793
|
*/
|
|
565
794
|
public static async checkSpelling(word: string): Promise<SpellCheckResult> {
|
|
566
|
-
|
|
795
|
+
if (!word || word.trim() === "") {
|
|
796
|
+
return { isCorrect: false, word };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
800
|
+
|
|
801
|
+
// 1. Check if word exists in TDK dictionary
|
|
567
802
|
const results = await this.getWord(word);
|
|
568
803
|
if (results.length > 0) {
|
|
569
804
|
return { isCorrect: true, word };
|
|
570
805
|
}
|
|
571
806
|
|
|
572
|
-
// 2.
|
|
573
|
-
|
|
574
|
-
|
|
807
|
+
// 2. Common Turkish misspellings, erroneously joined compounds, and vowel drop errors
|
|
808
|
+
if (COMMON_MISSPELLINGS[cleanWord]) {
|
|
809
|
+
return { isCorrect: false, word, suggestion: COMMON_MISSPELLINGS[cleanWord] };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// 3. Dynamic -şey / -sey attached check:
|
|
813
|
+
// In Turkish, 'şey' is an indefinite pronoun and is ALWAYS written separately from the preceding word
|
|
814
|
+
// (e.g. her şey, bir şey, hiçbir şey, çok şey, her şeyi, bir şeyler).
|
|
815
|
+
const seyMatch = cleanWord.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
816
|
+
if (seyMatch && !SEY_EXCEPTIONS.has(cleanWord)) {
|
|
817
|
+
let prefix = seyMatch[1];
|
|
818
|
+
const suffix = seyMatch[2] || "";
|
|
819
|
+
if (prefix === "hicbir") prefix = "hiçbir";
|
|
820
|
+
if (prefix === "cok") prefix = "çok";
|
|
821
|
+
return {
|
|
822
|
+
isCorrect: false,
|
|
823
|
+
word,
|
|
824
|
+
suggestion: `${prefix} şey${suffix}`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// 4. "Sıkça yapılan yanlışlar" from DailyContent
|
|
575
829
|
const daily = await this.getDailyContent();
|
|
576
830
|
if (daily) {
|
|
577
|
-
const syydMatch = daily.syyd.find(s => s.yanliskelime.toLocaleLowerCase("tr-TR") ===
|
|
831
|
+
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
578
832
|
if (syydMatch) {
|
|
579
833
|
return { isCorrect: false, word, suggestion: syydMatch.dogrukelime };
|
|
580
834
|
}
|
|
581
|
-
const mixMatch = daily.karistirma.find(s => s.yanlis.toLocaleLowerCase("tr-TR") ===
|
|
835
|
+
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
582
836
|
if (mixMatch) {
|
|
583
837
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
584
838
|
}
|
|
585
839
|
}
|
|
586
840
|
|
|
587
|
-
//
|
|
588
|
-
// (e.g., "halılarımızın" -> "halı", "kitabımız" -> "kitap", "çocuğa" -> "çocuk")
|
|
841
|
+
// 5. Morphology Fallback: Check if the word is an inflected form or bare verb imperative of a known headword
|
|
842
|
+
// (e.g., "halılarımızın" -> "halı", "kitabımız" -> "kitap", "çocuğa" -> "çocuk", "söyle" -> "söylemek")
|
|
589
843
|
const root = await this.findRoot(word);
|
|
590
844
|
if (root) {
|
|
591
|
-
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
592
845
|
const isInflected = root !== cleanWord;
|
|
593
846
|
return {
|
|
594
847
|
isCorrect: true,
|
|
@@ -598,42 +851,44 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
598
851
|
};
|
|
599
852
|
}
|
|
600
853
|
|
|
601
|
-
//
|
|
602
|
-
// headword (by edit distance) across TDK's full ~81k-word list (the same
|
|
603
|
-
// data `getSuggestions()` uses). Restricted to single-token, lowercase
|
|
604
|
-
// headwords so it doesn't suggest compounds/phrases or proper nouns.
|
|
605
|
-
// Candidates whose length differs too much are skipped before running
|
|
606
|
-
// the O(n*m) distance calculation, both for speed and because a huge
|
|
607
|
-
// length gap can't be within the distance threshold anyway. Ties (same
|
|
608
|
-
// distance) prefer a matching first letter, then a matching length —
|
|
609
|
-
// typos rarely change the first letter, and this avoids picking
|
|
610
|
-
// whatever happens to sort alphabetically first. There's no word
|
|
611
|
-
// frequency data available, so a genuine tie can still land on a
|
|
612
|
-
// technically-correct but less commonly intended word.
|
|
854
|
+
// 6. Check if headwords with spaces match when space is removed (e.g. "ön yargı" for "önyargı")
|
|
613
855
|
if (this.autocompleteCache.length === 0) {
|
|
614
856
|
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
615
857
|
}
|
|
616
|
-
const
|
|
617
|
-
|
|
858
|
+
for (const candidate of this.autocompleteCache) {
|
|
859
|
+
if (candidate.includes(" ")) {
|
|
860
|
+
const candidateNoSpace = candidate.replace(/\s+/g, "").toLocaleLowerCase("tr-TR");
|
|
861
|
+
if (candidateNoSpace === cleanWord) {
|
|
862
|
+
return { isCorrect: false, word, suggestion: candidate };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// 7. No exact match or morphology root: fall back to closest headword by edit distance.
|
|
868
|
+
// Ties prefer matching first letter, and initial character mismatches are penalized
|
|
869
|
+
// so irrelevant foreign loanwords (like 'jersey') do not beat Turkish roots.
|
|
870
|
+
let best: { candidate: string; distance: number; rawDist: number; firstMismatch: number; lengthMismatch: number } | null = null;
|
|
618
871
|
for (const candidate of this.autocompleteCache) {
|
|
619
872
|
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
|
|
620
873
|
if (Math.abs(candidate.length - cleanWord.length) > 2) continue;
|
|
621
874
|
|
|
622
|
-
const
|
|
623
|
-
if (
|
|
875
|
+
const rawDist = this.damerauLevenshtein(cleanWord, candidate);
|
|
876
|
+
if (rawDist === 0) continue;
|
|
624
877
|
|
|
625
878
|
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
626
879
|
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
880
|
+
const distance = rawDist + (firstMismatch > 0 ? 1.2 : 0);
|
|
881
|
+
|
|
627
882
|
const better =
|
|
628
883
|
!best ||
|
|
629
884
|
distance < best.distance ||
|
|
630
885
|
(distance === best.distance && firstMismatch < best.firstMismatch) ||
|
|
631
886
|
(distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch);
|
|
632
887
|
if (better) {
|
|
633
|
-
best = { candidate, distance, firstMismatch, lengthMismatch };
|
|
888
|
+
best = { candidate, distance, rawDist, firstMismatch, lengthMismatch };
|
|
634
889
|
}
|
|
635
890
|
}
|
|
636
|
-
if (best && best.
|
|
891
|
+
if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
|
|
637
892
|
return { isCorrect: false, word, suggestion: best.candidate };
|
|
638
893
|
}
|
|
639
894
|
return { isCorrect: false, word };
|
|
@@ -1063,6 +1318,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1063
1318
|
origin: originA,
|
|
1064
1319
|
syllables: this.syllabicate(a),
|
|
1065
1320
|
harmony: this.checkVowelHarmony(a),
|
|
1321
|
+
labialHarmony: this.checkLabialHarmony(a),
|
|
1066
1322
|
},
|
|
1067
1323
|
b: {
|
|
1068
1324
|
word: b,
|
|
@@ -1070,6 +1326,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1070
1326
|
origin: originB,
|
|
1071
1327
|
syllables: this.syllabicate(b),
|
|
1072
1328
|
harmony: this.checkVowelHarmony(b),
|
|
1329
|
+
labialHarmony: this.checkLabialHarmony(b),
|
|
1073
1330
|
},
|
|
1074
1331
|
};
|
|
1075
1332
|
}
|
|
@@ -1181,13 +1438,15 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1181
1438
|
|
|
1182
1439
|
/**
|
|
1183
1440
|
* Syllabicates a Turkish word based on general grammar rules.
|
|
1441
|
+
* Handles syllable separation for vowels, single consonants, double consonants,
|
|
1442
|
+
* and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
|
|
1184
1443
|
*/
|
|
1185
1444
|
public static syllabicate(word: string): string[] {
|
|
1186
1445
|
const vowels = /[aeıioöuüAEIİOÖUÜ]/;
|
|
1446
|
+
const ONSET_CLUSTERS = new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
|
|
1187
1447
|
const result: string[] = [];
|
|
1188
1448
|
let currentSyllable = "";
|
|
1189
1449
|
|
|
1190
|
-
// Better basic syllabification:
|
|
1191
1450
|
// Go from right to left.
|
|
1192
1451
|
for (let i = word.length - 1; i >= 0; i--) {
|
|
1193
1452
|
currentSyllable = word[i] + currentSyllable;
|
|
@@ -1200,9 +1459,14 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1200
1459
|
currentSyllable = word[i - 1] + currentSyllable;
|
|
1201
1460
|
i--; // skip the consonant
|
|
1202
1461
|
} else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
|
|
1203
|
-
//
|
|
1204
|
-
|
|
1205
|
-
|
|
1462
|
+
// Two consonants before this vowel. Check if three consonants exist and end in an onset cluster
|
|
1463
|
+
if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
|
|
1464
|
+
currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
|
|
1465
|
+
i -= 2;
|
|
1466
|
+
} else {
|
|
1467
|
+
currentSyllable = word[i - 1] + currentSyllable;
|
|
1468
|
+
i--;
|
|
1469
|
+
}
|
|
1206
1470
|
}
|
|
1207
1471
|
}
|
|
1208
1472
|
result.unshift(currentSyllable);
|
|
@@ -1236,4 +1500,463 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1236
1500
|
// If it has both front and back vowels, it breaks harmony.
|
|
1237
1501
|
return !(hasBack && hasFront);
|
|
1238
1502
|
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
|
|
1506
|
+
* Rules:
|
|
1507
|
+
* 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
|
|
1508
|
+
* 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
|
|
1509
|
+
* Single-syllable words and words with <=1 vowel are considered compliant by convention.
|
|
1510
|
+
*/
|
|
1511
|
+
public static checkLabialHarmony(word: string): boolean {
|
|
1512
|
+
const lower = word.toLocaleLowerCase("tr-TR");
|
|
1513
|
+
const vowels = lower.split("").filter((ch) => "aeıioöuü".includes(ch));
|
|
1514
|
+
if (vowels.length <= 1) return true;
|
|
1515
|
+
|
|
1516
|
+
for (let i = 0; i < vowels.length - 1; i++) {
|
|
1517
|
+
const v1 = vowels[i];
|
|
1518
|
+
const v2 = vowels[i + 1];
|
|
1519
|
+
|
|
1520
|
+
if ("aeıi".includes(v1)) {
|
|
1521
|
+
if (!"aeıi".includes(v2)) return false;
|
|
1522
|
+
} else if ("oöuü".includes(v1)) {
|
|
1523
|
+
if (!"aeuü".includes(v2)) return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Searches TDK headwords using a wildcard / pattern string.
|
|
1531
|
+
* Wildcards:
|
|
1532
|
+
* '_' or '?' matches any single character
|
|
1533
|
+
* '*' matches zero or more characters
|
|
1534
|
+
* Example: "k_l_m" matches "kalem", "kelam", "kilim".
|
|
1535
|
+
* Runs in-memory against TDK's 81k headword list.
|
|
1536
|
+
*/
|
|
1537
|
+
public static async patternSearch(pattern: string, options?: PatternSearchOptions): Promise<string[]> {
|
|
1538
|
+
if (!pattern || pattern.trim() === "") return [];
|
|
1539
|
+
await this.ensureAutocompleteLoaded();
|
|
1540
|
+
|
|
1541
|
+
const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
|
|
1542
|
+
const escaped = cleanPattern
|
|
1543
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
1544
|
+
.replace(/[_?]/g, "[\\p{L}]")
|
|
1545
|
+
.replace(/\*/g, "[\\p{L}]*");
|
|
1546
|
+
const regex = new RegExp(`^${escaped}$`, "u");
|
|
1547
|
+
|
|
1548
|
+
const max = options?.maxResults ?? 50;
|
|
1549
|
+
const matches: string[] = [];
|
|
1550
|
+
|
|
1551
|
+
for (const headword of this.autocompleteCache) {
|
|
1552
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1553
|
+
if (regex.test(lower)) {
|
|
1554
|
+
matches.push(headword);
|
|
1555
|
+
if (matches.length >= max) break;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return matches;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Finds headwords in TDK that can be formed from the given letters (anagrams).
|
|
1563
|
+
* If exact-length anagrams exist, they are returned.
|
|
1564
|
+
* If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
|
|
1565
|
+
* minimum 3 letters) are returned, sorted by length descending.
|
|
1566
|
+
*/
|
|
1567
|
+
public static async findAnagrams(letters: string, options?: AnagramOptions): Promise<string[]> {
|
|
1568
|
+
if (!letters || letters.trim() === "") return [];
|
|
1569
|
+
await this.ensureAutocompleteLoaded();
|
|
1570
|
+
|
|
1571
|
+
const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
|
|
1572
|
+
if (clean.length === 0) return [];
|
|
1573
|
+
|
|
1574
|
+
const forceExact = options?.exactLength === true;
|
|
1575
|
+
const max = options?.maxResults ?? 50;
|
|
1576
|
+
|
|
1577
|
+
const getFrequency = (str: string): Record<string, number> => {
|
|
1578
|
+
const freq: Record<string, number> = {};
|
|
1579
|
+
for (const ch of str) {
|
|
1580
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
1581
|
+
}
|
|
1582
|
+
return freq;
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
const targetFreq = getFrequency(clean);
|
|
1586
|
+
const exactMatches: string[] = [];
|
|
1587
|
+
const subMatches: string[] = [];
|
|
1588
|
+
|
|
1589
|
+
for (const headword of this.autocompleteCache) {
|
|
1590
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1591
|
+
if (lower.includes(" ") || lower.includes("-")) continue;
|
|
1592
|
+
if (lower.length > clean.length || lower.length < 3) continue;
|
|
1593
|
+
|
|
1594
|
+
const wordFreq = getFrequency(lower);
|
|
1595
|
+
let isValid = true;
|
|
1596
|
+
for (const [ch, count] of Object.entries(wordFreq)) {
|
|
1597
|
+
if (!targetFreq[ch] || targetFreq[ch] < count) {
|
|
1598
|
+
isValid = false;
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (isValid && lower !== clean) {
|
|
1604
|
+
if (lower.length === clean.length) {
|
|
1605
|
+
exactMatches.push(headword);
|
|
1606
|
+
} else {
|
|
1607
|
+
subMatches.push(headword);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
if (exactMatches.length > 0 || forceExact) {
|
|
1613
|
+
return exactMatches.slice(0, max);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
|
|
1617
|
+
return subMatches.slice(0, max);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
|
|
1622
|
+
* @param word The target word
|
|
1623
|
+
* @param options.minLetters Minimum number of ending characters that must match (default: 3)
|
|
1624
|
+
* @param options.maxResults Maximum number of rhyme results to return (default: 50)
|
|
1625
|
+
*/
|
|
1626
|
+
public static async findRhymes(word: string, options?: RhymeOptions): Promise<string[]> {
|
|
1627
|
+
if (!word || word.trim() === "") return [];
|
|
1628
|
+
await this.ensureAutocompleteLoaded();
|
|
1629
|
+
|
|
1630
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
1631
|
+
const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
|
|
1632
|
+
const max = options?.maxResults ?? 50;
|
|
1633
|
+
|
|
1634
|
+
const suffix = clean.slice(-minLetters);
|
|
1635
|
+
const results: string[] = [];
|
|
1636
|
+
|
|
1637
|
+
for (const headword of this.autocompleteCache) {
|
|
1638
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1639
|
+
if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
|
|
1640
|
+
results.push(headword);
|
|
1641
|
+
if (results.length >= max) break;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return results;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
|
|
1650
|
+
* Detects:
|
|
1651
|
+
* 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
|
|
1652
|
+
* 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
|
|
1653
|
+
* 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
|
|
1654
|
+
* 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
|
|
1655
|
+
*/
|
|
1656
|
+
public static async proofread(text: string): Promise<ProofreadResult> {
|
|
1657
|
+
if (!text || text.trim() === "") {
|
|
1658
|
+
return { text: text || "", issues: [], isCorrect: true };
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
await this.ensureAutocompleteLoaded();
|
|
1662
|
+
const issues: ProofreadIssue[] = [];
|
|
1663
|
+
|
|
1664
|
+
const SOMBAHCEMI = new Set([
|
|
1665
|
+
"sanki", "oysaki", "mademki", "belki", "halbuki", "çünkü", "meğerki", "illaki"
|
|
1666
|
+
]);
|
|
1667
|
+
|
|
1668
|
+
// 1. Detect multi-word phrases that should be written as single compound words
|
|
1669
|
+
const PHRASE_MISTAKES: {
|
|
1670
|
+
regex: RegExp;
|
|
1671
|
+
suggestion: string;
|
|
1672
|
+
message: string;
|
|
1673
|
+
type: ProofreadIssue["type"];
|
|
1674
|
+
}[] = [
|
|
1675
|
+
{
|
|
1676
|
+
regex: /\bhiç\s+bir\b/gi,
|
|
1677
|
+
suggestion: "hiçbir",
|
|
1678
|
+
message: "'hiçbir' belgisiz sıfatı bitişik yazılmalıdır.",
|
|
1679
|
+
type: "spelling",
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
regex: /\bbir\s+çok\b/gi,
|
|
1683
|
+
suggestion: "birçok",
|
|
1684
|
+
message: "'birçok' belgisiz sıfatı/zamiri bitişik yazılmalıdır.",
|
|
1685
|
+
type: "spelling",
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
regex: /\bbir\s+kaç\b/gi,
|
|
1689
|
+
suggestion: "birkaç",
|
|
1690
|
+
message: "'birkaç' belgisiz sıfatı/zamiri bitişik yazılmalıdır.",
|
|
1691
|
+
type: "spelling",
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
regex: /\bbir\s+az\b/gi,
|
|
1695
|
+
suggestion: "biraz",
|
|
1696
|
+
message: "'biraz' sözcüğü bitişik yazılmalıdır.",
|
|
1697
|
+
type: "spelling",
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
regex: /\bher\s+hangi\b/gi,
|
|
1701
|
+
suggestion: "herhangi",
|
|
1702
|
+
message: "'herhangi' sözcüğü bitişik yazılmalıdır.",
|
|
1703
|
+
type: "spelling",
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
regex: /\bgit\s+gide\b/gi,
|
|
1707
|
+
suggestion: "gitgide",
|
|
1708
|
+
message: "'gitgide' zarfı bitişik yazılmalıdır.",
|
|
1709
|
+
type: "spelling",
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
regex: /\bbirden\s+bire\b/gi,
|
|
1713
|
+
suggestion: "birdenbire",
|
|
1714
|
+
message: "'birdenbire' zarfı bitişik yazılmalıdır.",
|
|
1715
|
+
type: "spelling",
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
regex: /\brast\s+gele\b/gi,
|
|
1719
|
+
suggestion: "rastgele",
|
|
1720
|
+
message: "'rastgele' zarfı bitişik yazılmalıdır.",
|
|
1721
|
+
type: "spelling",
|
|
1722
|
+
},
|
|
1723
|
+
];
|
|
1724
|
+
|
|
1725
|
+
const coveredRanges: { start: number; end: number }[] = [];
|
|
1726
|
+
for (const pm of PHRASE_MISTAKES) {
|
|
1727
|
+
let pmMatch: RegExpExecArray | null;
|
|
1728
|
+
while ((pmMatch = pm.regex.exec(text)) !== null) {
|
|
1729
|
+
const start = pmMatch.index;
|
|
1730
|
+
const end = start + pmMatch[0].length;
|
|
1731
|
+
coveredRanges.push({ start, end });
|
|
1732
|
+
issues.push({
|
|
1733
|
+
type: pm.type,
|
|
1734
|
+
word: pmMatch[0],
|
|
1735
|
+
startIndex: start,
|
|
1736
|
+
endIndex: end,
|
|
1737
|
+
suggestion: pm.suggestion,
|
|
1738
|
+
message: pm.message,
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const tokenRegex = /[\p{L}0-9'’]+/gu;
|
|
1744
|
+
let match: RegExpExecArray | null;
|
|
1745
|
+
|
|
1746
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
1747
|
+
const rawWord = match[0];
|
|
1748
|
+
const startIndex = match.index;
|
|
1749
|
+
const endIndex = startIndex + rawWord.length;
|
|
1750
|
+
const lower = rawWord.toLocaleLowerCase("tr-TR");
|
|
1751
|
+
|
|
1752
|
+
if (/^\d+$/.test(lower)) continue;
|
|
1753
|
+
if (coveredRanges.some((r) => startIndex >= r.start && endIndex <= r.end)) continue;
|
|
1754
|
+
|
|
1755
|
+
let flagged = false;
|
|
1756
|
+
|
|
1757
|
+
// 1. Check Question Particle (mı, mi, mu, mü) erroneously attached
|
|
1758
|
+
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)?)$/);
|
|
1759
|
+
if (questionMatch) {
|
|
1760
|
+
const base = questionMatch[1];
|
|
1761
|
+
const particle = questionMatch[2];
|
|
1762
|
+
if (base.length >= 2 && (await this.isHeadword(base) || (await this.findRoot(base)) !== null)) {
|
|
1763
|
+
if (!(await this.isHeadword(lower))) {
|
|
1764
|
+
issues.push({
|
|
1765
|
+
type: "question_particle",
|
|
1766
|
+
word: rawWord,
|
|
1767
|
+
startIndex,
|
|
1768
|
+
endIndex,
|
|
1769
|
+
suggestion: `${base} ${particle}`,
|
|
1770
|
+
message: `'${particle}' soru eki kendinden önceki kelimeden ayrı yazılmalıdır.`,
|
|
1771
|
+
});
|
|
1772
|
+
flagged = true;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const VERB_CONJUGATION_REGEX =
|
|
1778
|
+
/(?: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;
|
|
1779
|
+
|
|
1780
|
+
// 2. Check Conjunction 'ki' erroneously attached to verbs
|
|
1781
|
+
if (!flagged && lower.endsWith("ki") && lower.length > 3) {
|
|
1782
|
+
const base = lower.slice(0, -2);
|
|
1783
|
+
if (!SOMBAHCEMI.has(lower)) {
|
|
1784
|
+
if (!(await this.isHeadword(lower))) {
|
|
1785
|
+
const root = await this.findRoot(base);
|
|
1786
|
+
const isVerb =
|
|
1787
|
+
(base === "demek" || base === "kaldı" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base)) &&
|
|
1788
|
+
(root ? root.endsWith("mek") || root.endsWith("mak") : true);
|
|
1789
|
+
|
|
1790
|
+
if (isVerb) {
|
|
1791
|
+
issues.push({
|
|
1792
|
+
type: "conjunction_ki",
|
|
1793
|
+
word: rawWord,
|
|
1794
|
+
startIndex,
|
|
1795
|
+
endIndex,
|
|
1796
|
+
suggestion: `${base} ki`,
|
|
1797
|
+
message: `'ki' bağlacı ayrı yazılmalıdır.`,
|
|
1798
|
+
});
|
|
1799
|
+
flagged = true;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// 3. Check Conjunction 'da/de/ta/te' erroneously attached to verbs
|
|
1806
|
+
if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
|
|
1807
|
+
const base = lower.slice(0, -2);
|
|
1808
|
+
const ending = lower.slice(-2);
|
|
1809
|
+
if (!(await this.isHeadword(lower))) {
|
|
1810
|
+
const root = await this.findRoot(base);
|
|
1811
|
+
const isVerb =
|
|
1812
|
+
VERB_CONJUGATION_REGEX.test(base) &&
|
|
1813
|
+
(root ? root.endsWith("mek") || root.endsWith("mak") : false);
|
|
1814
|
+
|
|
1815
|
+
if (isVerb) {
|
|
1816
|
+
const correctEnding = ending.startsWith("t") ? (ending === "te" ? "de" : "da") : ending;
|
|
1817
|
+
issues.push({
|
|
1818
|
+
type: "conjunction_da",
|
|
1819
|
+
word: rawWord,
|
|
1820
|
+
startIndex,
|
|
1821
|
+
endIndex,
|
|
1822
|
+
suggestion: `${base} ${correctEnding}`,
|
|
1823
|
+
message: `'da/de' bağlacı fiillerden sonra her zaman ayrı yazılır (bağlaç olan da/de sertleşmez).`,
|
|
1824
|
+
});
|
|
1825
|
+
flagged = true;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// 4. Check -şey / -sey erroneously attached to preceding word
|
|
1831
|
+
const seyMatch = lower.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
1832
|
+
if (!flagged && seyMatch && !SEY_EXCEPTIONS.has(lower)) {
|
|
1833
|
+
let prefix = seyMatch[1];
|
|
1834
|
+
const suffix = seyMatch[2] || "";
|
|
1835
|
+
if (prefix === "hicbir") prefix = "hiçbir";
|
|
1836
|
+
if (prefix === "cok") prefix = "çok";
|
|
1837
|
+
issues.push({
|
|
1838
|
+
type: "spelling",
|
|
1839
|
+
word: rawWord,
|
|
1840
|
+
startIndex,
|
|
1841
|
+
endIndex,
|
|
1842
|
+
suggestion: `${prefix} şey${suffix}`,
|
|
1843
|
+
message: "'şey' sözcüğü kendinden önceki kelimeden ayrı yazılmalıdır.",
|
|
1844
|
+
});
|
|
1845
|
+
flagged = true;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// 5. Check 'yada' conjunction mistake
|
|
1849
|
+
if (!flagged && lower === "yada") {
|
|
1850
|
+
issues.push({
|
|
1851
|
+
type: "spelling",
|
|
1852
|
+
word: rawWord,
|
|
1853
|
+
startIndex,
|
|
1854
|
+
endIndex,
|
|
1855
|
+
suggestion: "ya da",
|
|
1856
|
+
message: "'ya da' bağlacı her zaman ayrı yazılır.",
|
|
1857
|
+
});
|
|
1858
|
+
flagged = true;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// 6. Check common vowel drop mistakes: burda, şurda, orda, vb. (TDK Kural 15)
|
|
1862
|
+
if (!flagged && (lower === "burda" || lower === "şurda" || lower === "surda" || lower === "orda" || lower === "içerde" || lower === "icerde" || lower === "dışarda" || lower === "disarda" || lower === "yukarda")) {
|
|
1863
|
+
const correct = COMMON_MISSPELLINGS[lower] || lower;
|
|
1864
|
+
issues.push({
|
|
1865
|
+
type: "spelling",
|
|
1866
|
+
word: rawWord,
|
|
1867
|
+
startIndex,
|
|
1868
|
+
endIndex,
|
|
1869
|
+
suggestion: correct,
|
|
1870
|
+
message: `'${rawWord}' sözcüğünde ünlü düşmesi yapılmaz.`,
|
|
1871
|
+
});
|
|
1872
|
+
flagged = true;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// 7. General Spell Check
|
|
1876
|
+
if (!flagged) {
|
|
1877
|
+
const check = await this.checkSpelling(rawWord);
|
|
1878
|
+
if (!check.isCorrect) {
|
|
1879
|
+
issues.push({
|
|
1880
|
+
type: "spelling",
|
|
1881
|
+
word: rawWord,
|
|
1882
|
+
startIndex,
|
|
1883
|
+
endIndex,
|
|
1884
|
+
suggestion: check.suggestion,
|
|
1885
|
+
message: check.suggestion
|
|
1886
|
+
? `'${rawWord}' yanlış yazılmış olabilir.`
|
|
1887
|
+
: `'${rawWord}' sözlükte bulunamadı.`,
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
issues.sort((a, b) => a.startIndex - b.startIndex);
|
|
1894
|
+
|
|
1895
|
+
return {
|
|
1896
|
+
text,
|
|
1897
|
+
issues,
|
|
1898
|
+
isCorrect: issues.length === 0,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Configurable instance-based client for TDK API.
|
|
1905
|
+
* Useful for multi-tenant applications or backend services requiring isolated configurations.
|
|
1906
|
+
*/
|
|
1907
|
+
export class TDKClient {
|
|
1908
|
+
constructor(config?: TDKConfig) {
|
|
1909
|
+
if (config) {
|
|
1910
|
+
TDK.configure(config);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
public getWord(word: string): Promise<WordInfo[]> {
|
|
1915
|
+
return TDK.getWord(word);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
public getMeanings(word: string): Promise<string[]> {
|
|
1919
|
+
return TDK.getMeanings(word);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
public checkSpelling(word: string): Promise<SpellCheckResult> {
|
|
1923
|
+
return TDK.checkSpelling(word);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
public findRoot(word: string): Promise<string | null> {
|
|
1927
|
+
return TDK.findRoot(word);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
public stem(word: string): Promise<StemResult | null> {
|
|
1931
|
+
return TDK.stem(word);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
public proofread(text: string): Promise<ProofreadResult> {
|
|
1935
|
+
return TDK.proofread(text);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
public patternSearch(pattern: string, options?: PatternSearchOptions): Promise<string[]> {
|
|
1939
|
+
return TDK.patternSearch(pattern, options);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
public findAnagrams(letters: string, options?: AnagramOptions): Promise<string[]> {
|
|
1943
|
+
return TDK.findAnagrams(letters, options);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
public findRhymes(word: string, options?: RhymeOptions): Promise<string[]> {
|
|
1947
|
+
return TDK.findRhymes(word, options);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
public syllabicate(word: string): string[] {
|
|
1951
|
+
return TDK.syllabicate(word);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
public checkVowelHarmony(word: string): boolean {
|
|
1955
|
+
return TDK.checkVowelHarmony(word);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
public checkLabialHarmony(word: string): boolean {
|
|
1959
|
+
return TDK.checkLabialHarmony(word);
|
|
1960
|
+
}
|
|
1239
1961
|
}
|
|
1962
|
+
|