tdk-api-wrapper 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -15
- package/dist/{chunk-ACMGCL7T.mjs → chunk-7KJHYRJZ.mjs} +1041 -16
- package/dist/cli.js +1205 -21
- package/dist/cli.mjs +219 -7
- package/dist/index.d.mts +206 -1
- package/dist/index.d.ts +206 -1
- package/dist/index.js +1052 -17
- package/dist/index.mjs +23 -3
- package/package.json +3 -2
- package/src/cli.ts +224 -6
- package/src/index.ts +2 -1
- package/src/morphology.ts +324 -0
- package/src/tdk.ts +560 -15
- package/src/types.ts +49 -0
- package/test/grammar.test.js +52 -0
- package/test/morphology.test.js +129 -0
- package/test/proofread.test.js +60 -0
- package/test/tools.test.js +51 -0
|
@@ -25,6 +25,559 @@ var TDKNetworkError = class extends TDKError {
|
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
// src/morphology.ts
|
|
29
|
+
var TURKISH_VOWELS = "ae\u0131io\xF6u\xFC";
|
|
30
|
+
function isVowel(ch) {
|
|
31
|
+
return TURKISH_VOWELS.includes(ch);
|
|
32
|
+
}
|
|
33
|
+
var TURKISH_SUFFIXES = [
|
|
34
|
+
// 9-letter composite suffixes
|
|
35
|
+
"lerimizden",
|
|
36
|
+
"lar\u0131m\u0131zdan",
|
|
37
|
+
"lerinizden",
|
|
38
|
+
"lar\u0131n\u0131zdan",
|
|
39
|
+
// 8-letter composite suffixes
|
|
40
|
+
"lerinin",
|
|
41
|
+
"lar\u0131n\u0131n",
|
|
42
|
+
"lerinde",
|
|
43
|
+
"lar\u0131nda",
|
|
44
|
+
"lerinden",
|
|
45
|
+
"lar\u0131ndan",
|
|
46
|
+
"leriyle",
|
|
47
|
+
"lar\u0131yla",
|
|
48
|
+
"lerini",
|
|
49
|
+
"lar\u0131n\u0131",
|
|
50
|
+
"lerimize",
|
|
51
|
+
"lar\u0131m\u0131za",
|
|
52
|
+
"lerimizle",
|
|
53
|
+
"lar\u0131m\u0131zla",
|
|
54
|
+
"lerinizin",
|
|
55
|
+
"lar\u0131n\u0131z\u0131n",
|
|
56
|
+
"lerinizde",
|
|
57
|
+
"lar\u0131n\u0131zda",
|
|
58
|
+
"d\u0131\u011F\u0131ndan",
|
|
59
|
+
"di\u011Finden",
|
|
60
|
+
"du\u011Fundan",
|
|
61
|
+
"d\xFC\u011F\xFCnden",
|
|
62
|
+
"t\u0131\u011F\u0131ndan",
|
|
63
|
+
"ti\u011Finden",
|
|
64
|
+
"tu\u011Fundan",
|
|
65
|
+
"t\xFC\u011F\xFCnden",
|
|
66
|
+
// 7-letter composite suffixes
|
|
67
|
+
"ecektir",
|
|
68
|
+
"acakt\u0131r",
|
|
69
|
+
"ece\u011Fim",
|
|
70
|
+
"aca\u011F\u0131m",
|
|
71
|
+
"eceksin",
|
|
72
|
+
"acaks\u0131n",
|
|
73
|
+
"ece\u011Fiz",
|
|
74
|
+
"aca\u011F\u0131z",
|
|
75
|
+
"lerimiz",
|
|
76
|
+
"lar\u0131m\u0131z",
|
|
77
|
+
"leriniz",
|
|
78
|
+
"lar\u0131n\u0131z",
|
|
79
|
+
"umuzdan",
|
|
80
|
+
"\xFCm\xFCzden",
|
|
81
|
+
"inizden",
|
|
82
|
+
"\u0131n\u0131zdan",
|
|
83
|
+
"\xFCn\xFCzden",
|
|
84
|
+
"d\u0131\u011F\u0131nda",
|
|
85
|
+
"di\u011Finde",
|
|
86
|
+
"du\u011Funda",
|
|
87
|
+
"d\xFC\u011F\xFCnde",
|
|
88
|
+
"t\u0131\u011F\u0131nda",
|
|
89
|
+
"ti\u011Finde",
|
|
90
|
+
"tu\u011Funda",
|
|
91
|
+
"t\xFC\u011F\xFCnde",
|
|
92
|
+
"mas\u0131na",
|
|
93
|
+
"mesine",
|
|
94
|
+
"\u0131yorsunuz",
|
|
95
|
+
"iyorsunuz",
|
|
96
|
+
"uyorsunuz",
|
|
97
|
+
"\xFCyorsunuz",
|
|
98
|
+
"yorsunuz",
|
|
99
|
+
// 6-letter composite suffixes
|
|
100
|
+
"iyorsa",
|
|
101
|
+
"iyorduk",
|
|
102
|
+
"iyordu",
|
|
103
|
+
"iyormu\u015F",
|
|
104
|
+
"\u0131yorsa",
|
|
105
|
+
"\u0131yorduk",
|
|
106
|
+
"\u0131yordu",
|
|
107
|
+
"\u0131yormu\u015F",
|
|
108
|
+
"uyorsa",
|
|
109
|
+
"uyorduk",
|
|
110
|
+
"uyordu",
|
|
111
|
+
"uyormu\u015F",
|
|
112
|
+
"\xFCyorsa",
|
|
113
|
+
"\xFCyorduk",
|
|
114
|
+
"\xFCyordu",
|
|
115
|
+
"\xFCyormu\u015F",
|
|
116
|
+
"\u0131yorsun",
|
|
117
|
+
"iyorsun",
|
|
118
|
+
"uyorsun",
|
|
119
|
+
"\xFCyorsun",
|
|
120
|
+
"\u0131yorlar",
|
|
121
|
+
"iyorlar",
|
|
122
|
+
"uyorlar",
|
|
123
|
+
"\xFCyorlar",
|
|
124
|
+
"iyoruz",
|
|
125
|
+
"\u0131yoruz",
|
|
126
|
+
"uyoruz",
|
|
127
|
+
"\xFCyoruz",
|
|
128
|
+
"imizin",
|
|
129
|
+
"\u0131m\u0131z\u0131n",
|
|
130
|
+
"umuzun",
|
|
131
|
+
"\xFCm\xFCz\xFCn",
|
|
132
|
+
"imizde",
|
|
133
|
+
"\u0131m\u0131zda",
|
|
134
|
+
"umuzda",
|
|
135
|
+
"\xFCm\xFCzde",
|
|
136
|
+
"imizden",
|
|
137
|
+
"\u0131m\u0131zdan",
|
|
138
|
+
"imizle",
|
|
139
|
+
"\u0131m\u0131zla",
|
|
140
|
+
"umuzla",
|
|
141
|
+
"\xFCm\xFCzle",
|
|
142
|
+
"lerdir",
|
|
143
|
+
"lard\u0131r",
|
|
144
|
+
"mu\u015Ftur",
|
|
145
|
+
"mi\u015Ftir",
|
|
146
|
+
"mu\u015Ftur",
|
|
147
|
+
"m\xFC\u015Ft\xFCr",
|
|
148
|
+
"lerden",
|
|
149
|
+
"lardan",
|
|
150
|
+
"lerine",
|
|
151
|
+
"lar\u0131na",
|
|
152
|
+
"leriyle",
|
|
153
|
+
"lar\u0131yla",
|
|
154
|
+
"seniz",
|
|
155
|
+
"san\u0131z",
|
|
156
|
+
"diniz",
|
|
157
|
+
"d\u0131n\u0131z",
|
|
158
|
+
"dunuz",
|
|
159
|
+
"d\xFCn\xFCz",
|
|
160
|
+
"tiniz",
|
|
161
|
+
"t\u0131n\u0131z",
|
|
162
|
+
"tunuz",
|
|
163
|
+
"t\xFCn\xFCz",
|
|
164
|
+
"siniz",
|
|
165
|
+
"s\u0131n\u0131z",
|
|
166
|
+
"sunuz",
|
|
167
|
+
"s\xFCn\xFCz",
|
|
168
|
+
"d\u0131k\xE7a",
|
|
169
|
+
"dik\xE7e",
|
|
170
|
+
"duk\xE7a",
|
|
171
|
+
"d\xFCk\xE7e",
|
|
172
|
+
"t\u0131k\xE7a",
|
|
173
|
+
"tik\xE7e",
|
|
174
|
+
"tuk\xE7a",
|
|
175
|
+
"t\xFCk\xE7e",
|
|
176
|
+
"\u0131rken",
|
|
177
|
+
"irken",
|
|
178
|
+
"urken",
|
|
179
|
+
"\xFCrken",
|
|
180
|
+
"arken",
|
|
181
|
+
"erken",
|
|
182
|
+
// 5-letter suffixes
|
|
183
|
+
"lerde",
|
|
184
|
+
"larda",
|
|
185
|
+
"lerle",
|
|
186
|
+
"larla",
|
|
187
|
+
"lerin",
|
|
188
|
+
"lar\u0131n",
|
|
189
|
+
"lerim",
|
|
190
|
+
"lar\u0131m",
|
|
191
|
+
"dirler",
|
|
192
|
+
"d\u0131rlar",
|
|
193
|
+
"d\xFCrler",
|
|
194
|
+
"durlar",
|
|
195
|
+
"tirler",
|
|
196
|
+
"t\u0131rlar",
|
|
197
|
+
"t\xFCrler",
|
|
198
|
+
"turlar",
|
|
199
|
+
"siniz",
|
|
200
|
+
"s\u0131n\u0131z",
|
|
201
|
+
"sunuz",
|
|
202
|
+
"s\xFCn\xFCz",
|
|
203
|
+
"yorum",
|
|
204
|
+
"yorsun",
|
|
205
|
+
"uyoruz",
|
|
206
|
+
"yorsunuz",
|
|
207
|
+
"yorlar",
|
|
208
|
+
"eceks",
|
|
209
|
+
"acaks",
|
|
210
|
+
"eyim",
|
|
211
|
+
"ay\u0131m",
|
|
212
|
+
"indik",
|
|
213
|
+
"\u0131nd\u0131k",
|
|
214
|
+
"unduk",
|
|
215
|
+
"\xFCnd\xFCk",
|
|
216
|
+
"ildik",
|
|
217
|
+
"\u0131ld\u0131k",
|
|
218
|
+
"ulduk",
|
|
219
|
+
"\xFCld\xFCk",
|
|
220
|
+
"meden",
|
|
221
|
+
"madan",
|
|
222
|
+
"y\u0131n\u0131z",
|
|
223
|
+
"yiniz",
|
|
224
|
+
"yunuz",
|
|
225
|
+
"y\xFCn\xFCz",
|
|
226
|
+
// 4-letter suffixes
|
|
227
|
+
"imiz",
|
|
228
|
+
"\u0131m\u0131z",
|
|
229
|
+
"umuz",
|
|
230
|
+
"\xFCm\xFCz",
|
|
231
|
+
"iniz",
|
|
232
|
+
"\u0131n\u0131z",
|
|
233
|
+
"unuz",
|
|
234
|
+
"\xFCn\xFCz",
|
|
235
|
+
"leri",
|
|
236
|
+
"lar\u0131",
|
|
237
|
+
"idir",
|
|
238
|
+
"\u0131d\u0131r",
|
|
239
|
+
"udur",
|
|
240
|
+
"\xFCd\xFCr",
|
|
241
|
+
"ecek",
|
|
242
|
+
"acak",
|
|
243
|
+
"erek",
|
|
244
|
+
"arak",
|
|
245
|
+
"ince",
|
|
246
|
+
"\u0131nca",
|
|
247
|
+
"unca",
|
|
248
|
+
"\xFCnce",
|
|
249
|
+
"ken",
|
|
250
|
+
"meli",
|
|
251
|
+
"mal\u0131",
|
|
252
|
+
"iyor",
|
|
253
|
+
"\u0131yor",
|
|
254
|
+
"uyor",
|
|
255
|
+
"\xFCyor",
|
|
256
|
+
"mi\u015Fti",
|
|
257
|
+
"m\u0131\u015Ft\u0131",
|
|
258
|
+
"mu\u015Ftu",
|
|
259
|
+
"m\xFC\u015Ft\xFC",
|
|
260
|
+
"seydi",
|
|
261
|
+
"sayd\u0131",
|
|
262
|
+
"ydim",
|
|
263
|
+
"yd\u0131m",
|
|
264
|
+
"ydum",
|
|
265
|
+
"yd\xFCm",
|
|
266
|
+
"tiler",
|
|
267
|
+
"t\u0131lar",
|
|
268
|
+
"diler",
|
|
269
|
+
"d\u0131lar",
|
|
270
|
+
"ikten",
|
|
271
|
+
"\u0131ktan",
|
|
272
|
+
"uktan",
|
|
273
|
+
"\xFCkten",
|
|
274
|
+
// 3-letter suffixes
|
|
275
|
+
"ler",
|
|
276
|
+
"lar",
|
|
277
|
+
"den",
|
|
278
|
+
"dan",
|
|
279
|
+
"ten",
|
|
280
|
+
"tan",
|
|
281
|
+
"dir",
|
|
282
|
+
"d\u0131r",
|
|
283
|
+
"dur",
|
|
284
|
+
"d\xFCr",
|
|
285
|
+
"tir",
|
|
286
|
+
"t\u0131r",
|
|
287
|
+
"tur",
|
|
288
|
+
"t\xFCr",
|
|
289
|
+
"nin",
|
|
290
|
+
"n\u0131n",
|
|
291
|
+
"nun",
|
|
292
|
+
"n\xFCn",
|
|
293
|
+
"yle",
|
|
294
|
+
"yla",
|
|
295
|
+
"mi\u015F",
|
|
296
|
+
"m\u0131\u015F",
|
|
297
|
+
"mu\u015F",
|
|
298
|
+
"m\xFC\u015F",
|
|
299
|
+
"dim",
|
|
300
|
+
"d\u0131m",
|
|
301
|
+
"dum",
|
|
302
|
+
"d\xFCm",
|
|
303
|
+
"tim",
|
|
304
|
+
"t\u0131m",
|
|
305
|
+
"tum",
|
|
306
|
+
"t\xFCm",
|
|
307
|
+
"din",
|
|
308
|
+
"d\u0131n",
|
|
309
|
+
"dun",
|
|
310
|
+
"d\xFCn",
|
|
311
|
+
"tin",
|
|
312
|
+
"t\u0131n",
|
|
313
|
+
"tun",
|
|
314
|
+
"t\xFCn",
|
|
315
|
+
"dik",
|
|
316
|
+
"d\u0131k",
|
|
317
|
+
"duk",
|
|
318
|
+
"d\xFCk",
|
|
319
|
+
"tik",
|
|
320
|
+
"t\u0131k",
|
|
321
|
+
"tuk",
|
|
322
|
+
"t\xFCk",
|
|
323
|
+
"ydi",
|
|
324
|
+
"yd\u0131",
|
|
325
|
+
"ydu",
|
|
326
|
+
"yd\xFC",
|
|
327
|
+
"yim",
|
|
328
|
+
"y\u0131m",
|
|
329
|
+
"yum",
|
|
330
|
+
"y\xFCm",
|
|
331
|
+
"sin",
|
|
332
|
+
"s\u0131n",
|
|
333
|
+
"sun",
|
|
334
|
+
"s\xFCn",
|
|
335
|
+
"sen",
|
|
336
|
+
"san",
|
|
337
|
+
"sem",
|
|
338
|
+
"sam",
|
|
339
|
+
"sek",
|
|
340
|
+
"sak",
|
|
341
|
+
"siz",
|
|
342
|
+
"s\u0131z",
|
|
343
|
+
"suz",
|
|
344
|
+
"s\xFCz",
|
|
345
|
+
"lik",
|
|
346
|
+
"l\u0131k",
|
|
347
|
+
"luk",
|
|
348
|
+
"l\xFCk",
|
|
349
|
+
"ici",
|
|
350
|
+
"\u0131c\u0131",
|
|
351
|
+
"ucu",
|
|
352
|
+
"\xFCc\xFC",
|
|
353
|
+
"gen",
|
|
354
|
+
"gan",
|
|
355
|
+
"ken",
|
|
356
|
+
"kan",
|
|
357
|
+
"len",
|
|
358
|
+
"lan",
|
|
359
|
+
"le\u015F",
|
|
360
|
+
"la\u015F",
|
|
361
|
+
"mek",
|
|
362
|
+
"mak",
|
|
363
|
+
"yor",
|
|
364
|
+
// 2-letter suffixes
|
|
365
|
+
"de",
|
|
366
|
+
"da",
|
|
367
|
+
"te",
|
|
368
|
+
"ta",
|
|
369
|
+
"im",
|
|
370
|
+
"\u0131m",
|
|
371
|
+
"um",
|
|
372
|
+
"\xFCm",
|
|
373
|
+
"in",
|
|
374
|
+
"\u0131n",
|
|
375
|
+
"un",
|
|
376
|
+
"\xFCn",
|
|
377
|
+
"iz",
|
|
378
|
+
"\u0131z",
|
|
379
|
+
"uz",
|
|
380
|
+
"\xFCz",
|
|
381
|
+
"si",
|
|
382
|
+
"s\u0131",
|
|
383
|
+
"su",
|
|
384
|
+
"s\xFC",
|
|
385
|
+
"ye",
|
|
386
|
+
"ya",
|
|
387
|
+
"le",
|
|
388
|
+
"la",
|
|
389
|
+
"di",
|
|
390
|
+
"d\u0131",
|
|
391
|
+
"du",
|
|
392
|
+
"d\xFC",
|
|
393
|
+
"ti",
|
|
394
|
+
"t\u0131",
|
|
395
|
+
"tu",
|
|
396
|
+
"t\xFC",
|
|
397
|
+
"se",
|
|
398
|
+
"sa",
|
|
399
|
+
"ce",
|
|
400
|
+
"ca",
|
|
401
|
+
"\xE7e",
|
|
402
|
+
"\xE7a",
|
|
403
|
+
"me",
|
|
404
|
+
"ma",
|
|
405
|
+
"ip",
|
|
406
|
+
"\u0131p",
|
|
407
|
+
"up",
|
|
408
|
+
"\xFCp",
|
|
409
|
+
"en",
|
|
410
|
+
"an",
|
|
411
|
+
"i\u015F",
|
|
412
|
+
"\u0131\u015F",
|
|
413
|
+
"u\u015F",
|
|
414
|
+
"\xFC\u015F",
|
|
415
|
+
"li",
|
|
416
|
+
"l\u0131",
|
|
417
|
+
"lu",
|
|
418
|
+
"l\xFC",
|
|
419
|
+
"ci",
|
|
420
|
+
"c\u0131",
|
|
421
|
+
"cu",
|
|
422
|
+
"c\xFC",
|
|
423
|
+
"\xE7i",
|
|
424
|
+
"\xE7\u0131",
|
|
425
|
+
"\xE7u",
|
|
426
|
+
"\xE7\xFC",
|
|
427
|
+
// 1-letter suffixes (vowels / basic case endings)
|
|
428
|
+
"e",
|
|
429
|
+
"a",
|
|
430
|
+
"i",
|
|
431
|
+
"\u0131",
|
|
432
|
+
"u",
|
|
433
|
+
"\xFC"
|
|
434
|
+
];
|
|
435
|
+
function restoreConsonantSoftening(stem) {
|
|
436
|
+
if (stem.length < 2)
|
|
437
|
+
return [];
|
|
438
|
+
const last = stem.slice(-1);
|
|
439
|
+
const base = stem.slice(0, -1);
|
|
440
|
+
switch (last) {
|
|
441
|
+
case "b":
|
|
442
|
+
return [base + "p"];
|
|
443
|
+
case "c":
|
|
444
|
+
return [base + "\xE7"];
|
|
445
|
+
case "d":
|
|
446
|
+
return [base + "t"];
|
|
447
|
+
case "\u011F":
|
|
448
|
+
return [base + "k"];
|
|
449
|
+
case "g":
|
|
450
|
+
return [base + "k"];
|
|
451
|
+
default:
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function restoreVowelDrop(stem) {
|
|
456
|
+
if (stem.length < 3)
|
|
457
|
+
return [];
|
|
458
|
+
const c1 = stem[stem.length - 2];
|
|
459
|
+
const c2 = stem[stem.length - 1];
|
|
460
|
+
if (!isVowel(c1) && !isVowel(c2)) {
|
|
461
|
+
const vowelsInBase = stem.slice(0, -2).split("").filter(isVowel);
|
|
462
|
+
if (vowelsInBase.length > 0) {
|
|
463
|
+
const lastVowel = vowelsInBase[vowelsInBase.length - 1];
|
|
464
|
+
let inserted = "i";
|
|
465
|
+
if ("a\u0131".includes(lastVowel))
|
|
466
|
+
inserted = "\u0131";
|
|
467
|
+
else if ("ei".includes(lastVowel))
|
|
468
|
+
inserted = "i";
|
|
469
|
+
else if ("ou".includes(lastVowel))
|
|
470
|
+
inserted = "u";
|
|
471
|
+
else if ("\xF6\xFC".includes(lastVowel))
|
|
472
|
+
inserted = "\xFC";
|
|
473
|
+
return [stem.slice(0, -1) + inserted + c2];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return [];
|
|
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
|
+
}
|
|
515
|
+
function restoreInfinitive(stem) {
|
|
516
|
+
if (stem.length < 2)
|
|
517
|
+
return [];
|
|
518
|
+
const vowelsInBase = stem.split("").filter(isVowel);
|
|
519
|
+
if (vowelsInBase.length === 0)
|
|
520
|
+
return [];
|
|
521
|
+
const lastVowel = vowelsInBase[vowelsInBase.length - 1];
|
|
522
|
+
return "a\u0131ou".includes(lastVowel) ? [stem + "mak"] : [stem + "mek"];
|
|
523
|
+
}
|
|
524
|
+
function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
525
|
+
if (!word || word.trim().length === 0)
|
|
526
|
+
return [];
|
|
527
|
+
const raw = word.trim();
|
|
528
|
+
const normalized = raw.toLocaleLowerCase("tr-TR");
|
|
529
|
+
const candidatesWithWeight = [];
|
|
530
|
+
const seen = /* @__PURE__ */ new Set();
|
|
531
|
+
if (raw.includes("'") || raw.includes("\u2019")) {
|
|
532
|
+
const apostropheStem = normalized.split(/['’]/)[0];
|
|
533
|
+
if (apostropheStem.length >= minStemLength) {
|
|
534
|
+
candidatesWithWeight.push({ candidate: apostropheStem, baseLength: apostropheStem.length + 10 });
|
|
535
|
+
seen.add(apostropheStem);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
let frontier = [normalized];
|
|
539
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
540
|
+
const nextFrontier = [];
|
|
541
|
+
for (const current of frontier) {
|
|
542
|
+
for (const suffix of TURKISH_SUFFIXES) {
|
|
543
|
+
if (current.length - suffix.length >= minStemLength && current.endsWith(suffix)) {
|
|
544
|
+
const stem = current.slice(0, -suffix.length);
|
|
545
|
+
const hardened = restoreConsonantSoftening(stem);
|
|
546
|
+
const vowelDropped = restoreVowelDrop(stem);
|
|
547
|
+
const geminated = restoreGemination(stem);
|
|
548
|
+
const isNarrowingSuffix = suffix.startsWith("yor") || suffix.includes("iyor") || suffix.includes("\u0131yor") || suffix.includes("uyor") || suffix.includes("\xFCyor");
|
|
549
|
+
const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
|
|
550
|
+
const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
|
|
551
|
+
const isVerbSuffix = isNarrowingSuffix || suffix.includes("ecek") || suffix.includes("acak") || suffix.includes("mi\u015F") || suffix.includes("m\u0131\u015F") || suffix.includes("m\xFC\u015F") || suffix.includes("mu\u015F") || suffix.includes("mek") || suffix.includes("mak") || suffix.includes("erek") || suffix.includes("arak") || suffix.includes("dik") || suffix.includes("d\u0131k") || suffix.includes("duk") || suffix.includes("d\xFCk") || suffix.includes("tik") || suffix.includes("t\u0131k") || suffix.includes("tuk") || suffix.includes("t\xFCk") || suffix.includes("sen") || suffix.includes("san") || suffix.includes("sem") || suffix.includes("sam") || suffix.includes("sek") || suffix.includes("sak");
|
|
552
|
+
const verbalBases = [stem, ...hardened, ...narrowed];
|
|
553
|
+
const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
|
|
554
|
+
const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
|
|
555
|
+
for (const variant of variants) {
|
|
556
|
+
if (!seen.has(variant) && variant !== normalized) {
|
|
557
|
+
seen.add(variant);
|
|
558
|
+
nextFrontier.push(variant);
|
|
559
|
+
candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
for (const inf of infinitives) {
|
|
563
|
+
if (!seen.has(inf) && inf !== normalized) {
|
|
564
|
+
seen.add(inf);
|
|
565
|
+
nextFrontier.push(inf);
|
|
566
|
+
const weight = isVerbSuffix ? stem.length + 5 : stem.length;
|
|
567
|
+
candidatesWithWeight.push({ candidate: inf, baseLength: weight });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (nextFrontier.length === 0)
|
|
574
|
+
break;
|
|
575
|
+
frontier = nextFrontier;
|
|
576
|
+
}
|
|
577
|
+
candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
|
|
578
|
+
return [...new Set(candidatesWithWeight.map((c) => c.candidate))];
|
|
579
|
+
}
|
|
580
|
+
|
|
28
581
|
// src/tdk.ts
|
|
29
582
|
import * as fs from "fs";
|
|
30
583
|
import * as path from "path";
|
|
@@ -111,11 +664,30 @@ M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
|
|
|
111
664
|
yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
112
665
|
-----END CERTIFICATE-----`
|
|
113
666
|
];
|
|
667
|
+
// Configuration
|
|
668
|
+
static defaultTimeoutMs = 8e3;
|
|
669
|
+
static defaultRetries = 1;
|
|
670
|
+
static maxCacheSize = 1e3;
|
|
114
671
|
// Cache Mechanism
|
|
115
672
|
static isCacheEnabled = false;
|
|
116
673
|
static wordCache = /* @__PURE__ */ new Map();
|
|
117
674
|
static dailyContentCache = null;
|
|
118
675
|
static autocompleteCache = [];
|
|
676
|
+
static autocompleteSet = /* @__PURE__ */ new Set();
|
|
677
|
+
static stemCache = /* @__PURE__ */ new Map();
|
|
678
|
+
/**
|
|
679
|
+
* Configures global client options such as network timeout, retries, and cache size.
|
|
680
|
+
*/
|
|
681
|
+
static configure(config) {
|
|
682
|
+
if (config.timeoutMs !== void 0)
|
|
683
|
+
this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
|
|
684
|
+
if (config.retries !== void 0)
|
|
685
|
+
this.defaultRetries = Math.max(0, config.retries);
|
|
686
|
+
if (config.cache !== void 0)
|
|
687
|
+
this.enableCache(config.cache);
|
|
688
|
+
if (config.maxCacheSize !== void 0)
|
|
689
|
+
this.maxCacheSize = Math.max(10, config.maxCacheSize);
|
|
690
|
+
}
|
|
119
691
|
/**
|
|
120
692
|
* Enables or disables in-memory caching for API requests.
|
|
121
693
|
*/
|
|
@@ -132,10 +704,53 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
132
704
|
this.wordCache.clear();
|
|
133
705
|
this.dailyContentCache = null;
|
|
134
706
|
this.autocompleteCache = [];
|
|
707
|
+
this.autocompleteSet.clear();
|
|
708
|
+
this.stemCache.clear();
|
|
709
|
+
}
|
|
710
|
+
static setBoundedCache(map, key, value) {
|
|
711
|
+
if (map.size >= this.maxCacheSize) {
|
|
712
|
+
const firstKey = map.keys().next().value;
|
|
713
|
+
if (firstKey !== void 0)
|
|
714
|
+
map.delete(firstKey);
|
|
715
|
+
}
|
|
716
|
+
map.set(key, value);
|
|
135
717
|
}
|
|
136
718
|
static delay(ms) {
|
|
137
719
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
138
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
|
|
723
|
+
*/
|
|
724
|
+
static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
|
|
725
|
+
let lastError;
|
|
726
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
727
|
+
try {
|
|
728
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
729
|
+
const headers = {
|
|
730
|
+
"User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
|
|
731
|
+
...options.headers || {}
|
|
732
|
+
};
|
|
733
|
+
const res = await fetch(url, { ...options, headers, signal });
|
|
734
|
+
if (res.ok || res.status >= 400 && res.status < 500) {
|
|
735
|
+
return res;
|
|
736
|
+
}
|
|
737
|
+
if (attempt < retries) {
|
|
738
|
+
await this.delay(200 * (attempt + 1));
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
return res;
|
|
742
|
+
} catch (err) {
|
|
743
|
+
lastError = err;
|
|
744
|
+
if (attempt < retries) {
|
|
745
|
+
await this.delay(200 * (attempt + 1));
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
|
|
751
|
+
cause: lastError
|
|
752
|
+
});
|
|
753
|
+
}
|
|
139
754
|
/**
|
|
140
755
|
* Fetches detailed information for a given word from the TDK Dictionary.
|
|
141
756
|
*/
|
|
@@ -150,9 +765,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
150
765
|
const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
|
|
151
766
|
let response;
|
|
152
767
|
try {
|
|
153
|
-
response = await
|
|
154
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
155
|
-
});
|
|
768
|
+
response = await this.fetchWithRetry(url);
|
|
156
769
|
} catch (error) {
|
|
157
770
|
throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
|
|
158
771
|
}
|
|
@@ -169,12 +782,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
169
782
|
}
|
|
170
783
|
if (!Array.isArray(data) && data && "error" in data) {
|
|
171
784
|
if (this.isCacheEnabled)
|
|
172
|
-
this.wordCache
|
|
785
|
+
this.setBoundedCache(this.wordCache, cleanWord, []);
|
|
173
786
|
return [];
|
|
174
787
|
}
|
|
175
788
|
const results = data;
|
|
176
789
|
if (this.isCacheEnabled) {
|
|
177
|
-
this.wordCache
|
|
790
|
+
this.setBoundedCache(this.wordCache, cleanWord, results);
|
|
178
791
|
}
|
|
179
792
|
return results;
|
|
180
793
|
}
|
|
@@ -238,6 +851,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
238
851
|
return [];
|
|
239
852
|
}
|
|
240
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Ensures TDK's ~81k headword list is loaded in memory for fast O(1) set operations.
|
|
856
|
+
*/
|
|
857
|
+
static async ensureAutocompleteLoaded() {
|
|
858
|
+
if (this.autocompleteCache.length === 0) {
|
|
859
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
860
|
+
this.autocompleteSet = new Set(
|
|
861
|
+
this.autocompleteCache.map((w) => w.toLocaleLowerCase("tr-TR"))
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
241
865
|
/**
|
|
242
866
|
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
243
867
|
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
@@ -247,12 +871,81 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
247
871
|
static async getSuggestions(prefix) {
|
|
248
872
|
if (!prefix || prefix.trim() === "")
|
|
249
873
|
return [];
|
|
250
|
-
|
|
251
|
-
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
252
|
-
}
|
|
874
|
+
await this.ensureAutocompleteLoaded();
|
|
253
875
|
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
254
876
|
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
255
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Checks whether a word exists as a known headword in TDK dictionary.
|
|
880
|
+
* Checks in-memory autocompleteSet (81k headwords) if loaded, or queries TDK API.
|
|
881
|
+
*/
|
|
882
|
+
static async isHeadword(word) {
|
|
883
|
+
if (!word || word.trim() === "")
|
|
884
|
+
return false;
|
|
885
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
886
|
+
await this.ensureAutocompleteLoaded();
|
|
887
|
+
if (this.autocompleteSet.size > 0) {
|
|
888
|
+
return this.autocompleteSet.has(clean);
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const results = await this.getWord(clean);
|
|
892
|
+
return results.length > 0;
|
|
893
|
+
} catch {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Generates candidate roots for a given Turkish word using progressive BFS suffix stripping,
|
|
899
|
+
* consonant mutation restoration, and vowel drop restoration.
|
|
900
|
+
*/
|
|
901
|
+
static getStemCandidates(word) {
|
|
902
|
+
return getStemCandidates(word);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Finds the dictionary root (headword) of a word by checking direct existence
|
|
906
|
+
* and evaluating candidate stems generated by morphological analysis.
|
|
907
|
+
* Returns the root headword string if found, or null if no match in TDK.
|
|
908
|
+
*/
|
|
909
|
+
static async findRoot(word) {
|
|
910
|
+
if (!word || word.trim() === "")
|
|
911
|
+
return null;
|
|
912
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
913
|
+
if (this.stemCache.has(clean)) {
|
|
914
|
+
return this.stemCache.get(clean);
|
|
915
|
+
}
|
|
916
|
+
if (await this.isHeadword(clean)) {
|
|
917
|
+
this.setBoundedCache(this.stemCache, clean, clean);
|
|
918
|
+
return clean;
|
|
919
|
+
}
|
|
920
|
+
const candidates = getStemCandidates(clean);
|
|
921
|
+
for (const candidate of candidates) {
|
|
922
|
+
if (await this.isHeadword(candidate)) {
|
|
923
|
+
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
924
|
+
return candidate;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
this.setBoundedCache(this.stemCache, clean, null);
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Performs morphological stemming on a Turkish word.
|
|
932
|
+
* Returns a StemResult containing the original word, resolved root, and whether it is inflected.
|
|
933
|
+
*/
|
|
934
|
+
static async stem(word) {
|
|
935
|
+
if (!word || word.trim() === "")
|
|
936
|
+
return null;
|
|
937
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
938
|
+
const root = await this.findRoot(word);
|
|
939
|
+
if (!root) {
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
word,
|
|
944
|
+
root,
|
|
945
|
+
isInflected: root !== clean,
|
|
946
|
+
candidates: getStemCandidates(word)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
256
949
|
/**
|
|
257
950
|
* Returns a list of proverbs and idioms containing the word.
|
|
258
951
|
*/
|
|
@@ -469,6 +1162,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
469
1162
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
470
1163
|
}
|
|
471
1164
|
}
|
|
1165
|
+
const root = await this.findRoot(word);
|
|
1166
|
+
if (root) {
|
|
1167
|
+
const cleanWord2 = word.trim().toLocaleLowerCase("tr-TR");
|
|
1168
|
+
const isInflected = root !== cleanWord2;
|
|
1169
|
+
return {
|
|
1170
|
+
isCorrect: true,
|
|
1171
|
+
word,
|
|
1172
|
+
isInflected,
|
|
1173
|
+
root
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
472
1176
|
if (this.autocompleteCache.length === 0) {
|
|
473
1177
|
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
474
1178
|
}
|
|
@@ -896,14 +1600,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
896
1600
|
meaningCount: meaningsA.length,
|
|
897
1601
|
origin: originA,
|
|
898
1602
|
syllables: this.syllabicate(a),
|
|
899
|
-
harmony: this.checkVowelHarmony(a)
|
|
1603
|
+
harmony: this.checkVowelHarmony(a),
|
|
1604
|
+
labialHarmony: this.checkLabialHarmony(a)
|
|
900
1605
|
},
|
|
901
1606
|
b: {
|
|
902
1607
|
word: b,
|
|
903
1608
|
meaningCount: meaningsB.length,
|
|
904
1609
|
origin: originB,
|
|
905
1610
|
syllables: this.syllabicate(b),
|
|
906
|
-
harmony: this.checkVowelHarmony(b)
|
|
1611
|
+
harmony: this.checkVowelHarmony(b),
|
|
1612
|
+
labialHarmony: this.checkLabialHarmony(b)
|
|
907
1613
|
}
|
|
908
1614
|
};
|
|
909
1615
|
}
|
|
@@ -965,13 +1671,28 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
965
1671
|
const unique = [...new Set(words)];
|
|
966
1672
|
const analyses = [];
|
|
967
1673
|
for (const word of unique) {
|
|
968
|
-
|
|
969
|
-
|
|
1674
|
+
let results = await this.getWord(word);
|
|
1675
|
+
let found = results.length > 0;
|
|
1676
|
+
let root;
|
|
1677
|
+
let isInflected;
|
|
1678
|
+
if (!found) {
|
|
1679
|
+
const resolvedRoot = await this.findRoot(word);
|
|
1680
|
+
if (resolvedRoot) {
|
|
1681
|
+
results = await this.getWord(resolvedRoot);
|
|
1682
|
+
if (results.length > 0) {
|
|
1683
|
+
found = true;
|
|
1684
|
+
root = resolvedRoot;
|
|
1685
|
+
isInflected = true;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
970
1689
|
analyses.push({
|
|
971
1690
|
word,
|
|
972
1691
|
found,
|
|
973
1692
|
meaning: found ? this.firstMeaning(results) : null,
|
|
974
|
-
origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null
|
|
1693
|
+
origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null,
|
|
1694
|
+
root,
|
|
1695
|
+
isInflected
|
|
975
1696
|
});
|
|
976
1697
|
await this.delay(200);
|
|
977
1698
|
}
|
|
@@ -1019,9 +1740,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1019
1740
|
}
|
|
1020
1741
|
/**
|
|
1021
1742
|
* Syllabicates a Turkish word based on general grammar rules.
|
|
1743
|
+
* Handles syllable separation for vowels, single consonants, double consonants,
|
|
1744
|
+
* and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
|
|
1022
1745
|
*/
|
|
1023
1746
|
static syllabicate(word) {
|
|
1024
1747
|
const vowels = /[aeıioöuüAEIİOÖUÜ]/;
|
|
1748
|
+
const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
|
|
1025
1749
|
const result = [];
|
|
1026
1750
|
let currentSyllable = "";
|
|
1027
1751
|
for (let i = word.length - 1; i >= 0; i--) {
|
|
@@ -1032,8 +1756,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1032
1756
|
currentSyllable = word[i - 1] + currentSyllable;
|
|
1033
1757
|
i--;
|
|
1034
1758
|
} else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1759
|
+
if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
|
|
1760
|
+
currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
|
|
1761
|
+
i -= 2;
|
|
1762
|
+
} else {
|
|
1763
|
+
currentSyllable = word[i - 1] + currentSyllable;
|
|
1764
|
+
i--;
|
|
1765
|
+
}
|
|
1037
1766
|
}
|
|
1038
1767
|
}
|
|
1039
1768
|
result.unshift(currentSyllable);
|
|
@@ -1063,11 +1792,307 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1063
1792
|
const hasFront = frontVowels.test(lower);
|
|
1064
1793
|
return !(hasBack && hasFront);
|
|
1065
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
|
|
1797
|
+
* Rules:
|
|
1798
|
+
* 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
|
|
1799
|
+
* 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
|
|
1800
|
+
* Single-syllable words and words with <=1 vowel are considered compliant by convention.
|
|
1801
|
+
*/
|
|
1802
|
+
static checkLabialHarmony(word) {
|
|
1803
|
+
const lower = word.toLocaleLowerCase("tr-TR");
|
|
1804
|
+
const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
|
|
1805
|
+
if (vowels.length <= 1)
|
|
1806
|
+
return true;
|
|
1807
|
+
for (let i = 0; i < vowels.length - 1; i++) {
|
|
1808
|
+
const v1 = vowels[i];
|
|
1809
|
+
const v2 = vowels[i + 1];
|
|
1810
|
+
if ("ae\u0131i".includes(v1)) {
|
|
1811
|
+
if (!"ae\u0131i".includes(v2))
|
|
1812
|
+
return false;
|
|
1813
|
+
} else if ("o\xF6u\xFC".includes(v1)) {
|
|
1814
|
+
if (!"aeu\xFC".includes(v2))
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Searches TDK headwords using a wildcard / pattern string.
|
|
1822
|
+
* Wildcards:
|
|
1823
|
+
* '_' or '?' matches any single character
|
|
1824
|
+
* '*' matches zero or more characters
|
|
1825
|
+
* Example: "k_l_m" matches "kalem", "kelam", "kilim".
|
|
1826
|
+
* Runs in-memory against TDK's 81k headword list.
|
|
1827
|
+
*/
|
|
1828
|
+
static async patternSearch(pattern, options) {
|
|
1829
|
+
if (!pattern || pattern.trim() === "")
|
|
1830
|
+
return [];
|
|
1831
|
+
await this.ensureAutocompleteLoaded();
|
|
1832
|
+
const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
|
|
1833
|
+
const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
|
|
1834
|
+
const regex = new RegExp(`^${escaped}$`, "u");
|
|
1835
|
+
const max = options?.maxResults ?? 50;
|
|
1836
|
+
const matches = [];
|
|
1837
|
+
for (const headword of this.autocompleteCache) {
|
|
1838
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1839
|
+
if (regex.test(lower)) {
|
|
1840
|
+
matches.push(headword);
|
|
1841
|
+
if (matches.length >= max)
|
|
1842
|
+
break;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return matches;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Finds headwords in TDK that can be formed from the given letters (anagrams).
|
|
1849
|
+
* If exact-length anagrams exist, they are returned.
|
|
1850
|
+
* If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
|
|
1851
|
+
* minimum 3 letters) are returned, sorted by length descending.
|
|
1852
|
+
*/
|
|
1853
|
+
static async findAnagrams(letters, options) {
|
|
1854
|
+
if (!letters || letters.trim() === "")
|
|
1855
|
+
return [];
|
|
1856
|
+
await this.ensureAutocompleteLoaded();
|
|
1857
|
+
const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
|
|
1858
|
+
if (clean.length === 0)
|
|
1859
|
+
return [];
|
|
1860
|
+
const forceExact = options?.exactLength === true;
|
|
1861
|
+
const max = options?.maxResults ?? 50;
|
|
1862
|
+
const getFrequency = (str) => {
|
|
1863
|
+
const freq = {};
|
|
1864
|
+
for (const ch of str) {
|
|
1865
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
1866
|
+
}
|
|
1867
|
+
return freq;
|
|
1868
|
+
};
|
|
1869
|
+
const targetFreq = getFrequency(clean);
|
|
1870
|
+
const exactMatches = [];
|
|
1871
|
+
const subMatches = [];
|
|
1872
|
+
for (const headword of this.autocompleteCache) {
|
|
1873
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1874
|
+
if (lower.includes(" ") || lower.includes("-"))
|
|
1875
|
+
continue;
|
|
1876
|
+
if (lower.length > clean.length || lower.length < 3)
|
|
1877
|
+
continue;
|
|
1878
|
+
const wordFreq = getFrequency(lower);
|
|
1879
|
+
let isValid = true;
|
|
1880
|
+
for (const [ch, count] of Object.entries(wordFreq)) {
|
|
1881
|
+
if (!targetFreq[ch] || targetFreq[ch] < count) {
|
|
1882
|
+
isValid = false;
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
if (isValid && lower !== clean) {
|
|
1887
|
+
if (lower.length === clean.length) {
|
|
1888
|
+
exactMatches.push(headword);
|
|
1889
|
+
} else {
|
|
1890
|
+
subMatches.push(headword);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (exactMatches.length > 0 || forceExact) {
|
|
1895
|
+
return exactMatches.slice(0, max);
|
|
1896
|
+
}
|
|
1897
|
+
subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
|
|
1898
|
+
return subMatches.slice(0, max);
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
|
|
1902
|
+
* @param word The target word
|
|
1903
|
+
* @param options.minLetters Minimum number of ending characters that must match (default: 3)
|
|
1904
|
+
* @param options.maxResults Maximum number of rhyme results to return (default: 50)
|
|
1905
|
+
*/
|
|
1906
|
+
static async findRhymes(word, options) {
|
|
1907
|
+
if (!word || word.trim() === "")
|
|
1908
|
+
return [];
|
|
1909
|
+
await this.ensureAutocompleteLoaded();
|
|
1910
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
1911
|
+
const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
|
|
1912
|
+
const max = options?.maxResults ?? 50;
|
|
1913
|
+
const suffix = clean.slice(-minLetters);
|
|
1914
|
+
const results = [];
|
|
1915
|
+
for (const headword of this.autocompleteCache) {
|
|
1916
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
1917
|
+
if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
|
|
1918
|
+
results.push(headword);
|
|
1919
|
+
if (results.length >= max)
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return results;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
|
|
1927
|
+
* Detects:
|
|
1928
|
+
* 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
|
|
1929
|
+
* 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
|
|
1930
|
+
* 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
|
|
1931
|
+
* 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
|
|
1932
|
+
*/
|
|
1933
|
+
static async proofread(text) {
|
|
1934
|
+
if (!text || text.trim() === "") {
|
|
1935
|
+
return { text: text || "", issues: [], isCorrect: true };
|
|
1936
|
+
}
|
|
1937
|
+
await this.ensureAutocompleteLoaded();
|
|
1938
|
+
const issues = [];
|
|
1939
|
+
const SOMBAHCEMI = /* @__PURE__ */ new Set([
|
|
1940
|
+
"sanki",
|
|
1941
|
+
"oysaki",
|
|
1942
|
+
"mademki",
|
|
1943
|
+
"belki",
|
|
1944
|
+
"halbuki",
|
|
1945
|
+
"\xE7\xFCnk\xFC",
|
|
1946
|
+
"me\u011Ferki",
|
|
1947
|
+
"illaki"
|
|
1948
|
+
]);
|
|
1949
|
+
const tokenRegex = /[\p{L}0-9'’]+/gu;
|
|
1950
|
+
let match;
|
|
1951
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
1952
|
+
const rawWord = match[0];
|
|
1953
|
+
const startIndex = match.index;
|
|
1954
|
+
const endIndex = startIndex + rawWord.length;
|
|
1955
|
+
const lower = rawWord.toLocaleLowerCase("tr-TR");
|
|
1956
|
+
if (/^\d+$/.test(lower))
|
|
1957
|
+
continue;
|
|
1958
|
+
let flagged = false;
|
|
1959
|
+
const questionMatch = lower.match(/^(.+?)(m[ıiuü](?:sin|sın|sun|sün|siniz|sınız|sunuz|sünüz|yiz|yız|yuz|yüz|m|k)?)$/);
|
|
1960
|
+
if (questionMatch) {
|
|
1961
|
+
const base = questionMatch[1];
|
|
1962
|
+
const particle = questionMatch[2];
|
|
1963
|
+
if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
|
|
1964
|
+
if (!await this.isHeadword(lower)) {
|
|
1965
|
+
issues.push({
|
|
1966
|
+
type: "question_particle",
|
|
1967
|
+
word: rawWord,
|
|
1968
|
+
startIndex,
|
|
1969
|
+
endIndex,
|
|
1970
|
+
suggestion: `${base} ${particle}`,
|
|
1971
|
+
message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
1972
|
+
});
|
|
1973
|
+
flagged = true;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const VERB_CONJUGATION_REGEX = /(?:d[ıiuü][kmmn]?|t[ıiuü][kmmn]?|d[ıiuü]n[ıiuü]z?|t[ıiuü]n[ıiuü]z?|m[ıiuü]ş(?:[szn][ıiuü]z?|lar)?|yor(?:um|sun|uz|lar)?|ecek(?:sin|iz|ler)?|acak(?:sın|ız|lar)?|s[ae][mnk]|s[ae]n[ıiz]?|meli|malı|me[mz]|ma[mz])$/i;
|
|
1978
|
+
if (!flagged && lower.endsWith("ki") && lower.length > 3) {
|
|
1979
|
+
const base = lower.slice(0, -2);
|
|
1980
|
+
if (!SOMBAHCEMI.has(lower)) {
|
|
1981
|
+
if (!await this.isHeadword(lower)) {
|
|
1982
|
+
const root = await this.findRoot(base);
|
|
1983
|
+
const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base);
|
|
1984
|
+
if (isVerb) {
|
|
1985
|
+
issues.push({
|
|
1986
|
+
type: "conjunction_ki",
|
|
1987
|
+
word: rawWord,
|
|
1988
|
+
startIndex,
|
|
1989
|
+
endIndex,
|
|
1990
|
+
suggestion: `${base} ki`,
|
|
1991
|
+
message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
1992
|
+
});
|
|
1993
|
+
flagged = true;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
|
|
1999
|
+
const base = lower.slice(0, -2);
|
|
2000
|
+
const ending = lower.slice(-2);
|
|
2001
|
+
if (!await this.isHeadword(lower)) {
|
|
2002
|
+
const root = await this.findRoot(base);
|
|
2003
|
+
const isVerb = root && (root.endsWith("mek") || root.endsWith("mak")) || VERB_CONJUGATION_REGEX.test(base);
|
|
2004
|
+
if (isVerb) {
|
|
2005
|
+
const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
|
|
2006
|
+
issues.push({
|
|
2007
|
+
type: "conjunction_da",
|
|
2008
|
+
word: rawWord,
|
|
2009
|
+
startIndex,
|
|
2010
|
+
endIndex,
|
|
2011
|
+
suggestion: `${base} ${correctEnding}`,
|
|
2012
|
+
message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
|
|
2013
|
+
});
|
|
2014
|
+
flagged = true;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (!flagged) {
|
|
2019
|
+
const check = await this.checkSpelling(rawWord);
|
|
2020
|
+
if (!check.isCorrect) {
|
|
2021
|
+
issues.push({
|
|
2022
|
+
type: "spelling",
|
|
2023
|
+
word: rawWord,
|
|
2024
|
+
startIndex,
|
|
2025
|
+
endIndex,
|
|
2026
|
+
suggestion: check.suggestion,
|
|
2027
|
+
message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir. \xD6neri: '${check.suggestion}'` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
text,
|
|
2034
|
+
issues,
|
|
2035
|
+
isCorrect: issues.length === 0
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
var TDKClient = class {
|
|
2040
|
+
constructor(config) {
|
|
2041
|
+
if (config) {
|
|
2042
|
+
TDK.configure(config);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
getWord(word) {
|
|
2046
|
+
return TDK.getWord(word);
|
|
2047
|
+
}
|
|
2048
|
+
getMeanings(word) {
|
|
2049
|
+
return TDK.getMeanings(word);
|
|
2050
|
+
}
|
|
2051
|
+
checkSpelling(word) {
|
|
2052
|
+
return TDK.checkSpelling(word);
|
|
2053
|
+
}
|
|
2054
|
+
findRoot(word) {
|
|
2055
|
+
return TDK.findRoot(word);
|
|
2056
|
+
}
|
|
2057
|
+
stem(word) {
|
|
2058
|
+
return TDK.stem(word);
|
|
2059
|
+
}
|
|
2060
|
+
proofread(text) {
|
|
2061
|
+
return TDK.proofread(text);
|
|
2062
|
+
}
|
|
2063
|
+
patternSearch(pattern, options) {
|
|
2064
|
+
return TDK.patternSearch(pattern, options);
|
|
2065
|
+
}
|
|
2066
|
+
findAnagrams(letters, options) {
|
|
2067
|
+
return TDK.findAnagrams(letters, options);
|
|
2068
|
+
}
|
|
2069
|
+
findRhymes(word, options) {
|
|
2070
|
+
return TDK.findRhymes(word, options);
|
|
2071
|
+
}
|
|
2072
|
+
syllabicate(word) {
|
|
2073
|
+
return TDK.syllabicate(word);
|
|
2074
|
+
}
|
|
2075
|
+
checkVowelHarmony(word) {
|
|
2076
|
+
return TDK.checkVowelHarmony(word);
|
|
2077
|
+
}
|
|
2078
|
+
checkLabialHarmony(word) {
|
|
2079
|
+
return TDK.checkLabialHarmony(word);
|
|
2080
|
+
}
|
|
1066
2081
|
};
|
|
1067
2082
|
|
|
1068
2083
|
export {
|
|
1069
2084
|
TDKError,
|
|
1070
2085
|
TDKValidationError,
|
|
1071
2086
|
TDKNetworkError,
|
|
1072
|
-
|
|
2087
|
+
TURKISH_VOWELS,
|
|
2088
|
+
isVowel,
|
|
2089
|
+
TURKISH_SUFFIXES,
|
|
2090
|
+
restoreConsonantSoftening,
|
|
2091
|
+
restoreVowelDrop,
|
|
2092
|
+
restoreGemination,
|
|
2093
|
+
restoreVowelNarrowing,
|
|
2094
|
+
restoreInfinitive,
|
|
2095
|
+
getStemCandidates,
|
|
2096
|
+
TDK,
|
|
2097
|
+
TDKClient
|
|
1073
2098
|
};
|