tdk-api-wrapper 1.3.0 → 1.4.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 +15 -7
- package/dist/{chunk-SLNXKZKR.mjs → chunk-5TYJDVHK.mjs} +695 -25
- package/dist/cli.js +728 -28
- package/dist/cli.mjs +41 -4
- package/dist/index.d.mts +113 -5
- package/dist/index.d.ts +113 -5
- package/dist/index.js +704 -27
- package/dist/index.mjs +17 -3
- package/package.json +3 -2
- package/src/cli.ts +40 -3
- package/src/index.ts +1 -0
- package/src/morphology.ts +214 -0
- package/src/tdk.ts +211 -21
- package/src/types.ts +11 -0
- package/test/morphology.test.js +105 -0
package/dist/index.js
CHANGED
|
@@ -33,7 +33,14 @@ __export(src_exports, {
|
|
|
33
33
|
TDK: () => TDK,
|
|
34
34
|
TDKError: () => TDKError,
|
|
35
35
|
TDKNetworkError: () => TDKNetworkError,
|
|
36
|
-
TDKValidationError: () => TDKValidationError
|
|
36
|
+
TDKValidationError: () => TDKValidationError,
|
|
37
|
+
TURKISH_SUFFIXES: () => TURKISH_SUFFIXES,
|
|
38
|
+
TURKISH_VOWELS: () => TURKISH_VOWELS,
|
|
39
|
+
getStemCandidates: () => getStemCandidates,
|
|
40
|
+
isVowel: () => isVowel,
|
|
41
|
+
restoreConsonantSoftening: () => restoreConsonantSoftening,
|
|
42
|
+
restoreInfinitive: () => restoreInfinitive,
|
|
43
|
+
restoreVowelDrop: () => restoreVowelDrop
|
|
37
44
|
});
|
|
38
45
|
module.exports = __toCommonJS(src_exports);
|
|
39
46
|
|
|
@@ -64,6 +71,503 @@ var TDKNetworkError = class extends TDKError {
|
|
|
64
71
|
}
|
|
65
72
|
};
|
|
66
73
|
|
|
74
|
+
// src/morphology.ts
|
|
75
|
+
var TURKISH_VOWELS = "ae\u0131io\xF6u\xFC";
|
|
76
|
+
function isVowel(ch) {
|
|
77
|
+
return TURKISH_VOWELS.includes(ch);
|
|
78
|
+
}
|
|
79
|
+
var TURKISH_SUFFIXES = [
|
|
80
|
+
// 9-letter composite suffixes
|
|
81
|
+
"lerimizden",
|
|
82
|
+
"lar\u0131m\u0131zdan",
|
|
83
|
+
"lerinizden",
|
|
84
|
+
"lar\u0131n\u0131zdan",
|
|
85
|
+
// 8-letter composite suffixes
|
|
86
|
+
"lerinin",
|
|
87
|
+
"lar\u0131n\u0131n",
|
|
88
|
+
"lerinde",
|
|
89
|
+
"lar\u0131nda",
|
|
90
|
+
"lerinden",
|
|
91
|
+
"lar\u0131ndan",
|
|
92
|
+
"leriyle",
|
|
93
|
+
"lar\u0131yla",
|
|
94
|
+
"lerini",
|
|
95
|
+
"lar\u0131n\u0131",
|
|
96
|
+
"lerimize",
|
|
97
|
+
"lar\u0131m\u0131za",
|
|
98
|
+
"lerimizle",
|
|
99
|
+
"lar\u0131m\u0131zla",
|
|
100
|
+
"lerinizin",
|
|
101
|
+
"lar\u0131n\u0131z\u0131n",
|
|
102
|
+
"lerinizde",
|
|
103
|
+
"lar\u0131n\u0131zda",
|
|
104
|
+
"d\u0131\u011F\u0131ndan",
|
|
105
|
+
"di\u011Finden",
|
|
106
|
+
"du\u011Fundan",
|
|
107
|
+
"d\xFC\u011F\xFCnden",
|
|
108
|
+
"t\u0131\u011F\u0131ndan",
|
|
109
|
+
"ti\u011Finden",
|
|
110
|
+
"tu\u011Fundan",
|
|
111
|
+
"t\xFC\u011F\xFCnden",
|
|
112
|
+
// 7-letter composite suffixes
|
|
113
|
+
"ecektir",
|
|
114
|
+
"acakt\u0131r",
|
|
115
|
+
"ece\u011Fim",
|
|
116
|
+
"aca\u011F\u0131m",
|
|
117
|
+
"eceksin",
|
|
118
|
+
"acaks\u0131n",
|
|
119
|
+
"ece\u011Fiz",
|
|
120
|
+
"aca\u011F\u0131z",
|
|
121
|
+
"lerimiz",
|
|
122
|
+
"lar\u0131m\u0131z",
|
|
123
|
+
"leriniz",
|
|
124
|
+
"lar\u0131n\u0131z",
|
|
125
|
+
"umuzdan",
|
|
126
|
+
"\xFCm\xFCzden",
|
|
127
|
+
"inizden",
|
|
128
|
+
"\u0131n\u0131zdan",
|
|
129
|
+
"\xFCn\xFCzden",
|
|
130
|
+
"d\u0131\u011F\u0131nda",
|
|
131
|
+
"di\u011Finde",
|
|
132
|
+
"du\u011Funda",
|
|
133
|
+
"d\xFC\u011F\xFCnde",
|
|
134
|
+
"t\u0131\u011F\u0131nda",
|
|
135
|
+
"ti\u011Finde",
|
|
136
|
+
"tu\u011Funda",
|
|
137
|
+
"t\xFC\u011F\xFCnde",
|
|
138
|
+
"mas\u0131na",
|
|
139
|
+
"mesine",
|
|
140
|
+
"\u0131yorsunuz",
|
|
141
|
+
"iyorsunuz",
|
|
142
|
+
"uyorsunuz",
|
|
143
|
+
"\xFCyorsunuz",
|
|
144
|
+
"yorsunuz",
|
|
145
|
+
// 6-letter composite suffixes
|
|
146
|
+
"iyorsa",
|
|
147
|
+
"iyorduk",
|
|
148
|
+
"iyordu",
|
|
149
|
+
"iyormu\u015F",
|
|
150
|
+
"\u0131yorsa",
|
|
151
|
+
"\u0131yorduk",
|
|
152
|
+
"\u0131yordu",
|
|
153
|
+
"\u0131yormu\u015F",
|
|
154
|
+
"uyorsa",
|
|
155
|
+
"uyorduk",
|
|
156
|
+
"uyordu",
|
|
157
|
+
"uyormu\u015F",
|
|
158
|
+
"\xFCyorsa",
|
|
159
|
+
"\xFCyorduk",
|
|
160
|
+
"\xFCyordu",
|
|
161
|
+
"\xFCyormu\u015F",
|
|
162
|
+
"\u0131yorsun",
|
|
163
|
+
"iyorsun",
|
|
164
|
+
"uyorsun",
|
|
165
|
+
"\xFCyorsun",
|
|
166
|
+
"\u0131yorlar",
|
|
167
|
+
"iyorlar",
|
|
168
|
+
"uyorlar",
|
|
169
|
+
"\xFCyorlar",
|
|
170
|
+
"iyoruz",
|
|
171
|
+
"\u0131yoruz",
|
|
172
|
+
"uyoruz",
|
|
173
|
+
"\xFCyoruz",
|
|
174
|
+
"imizin",
|
|
175
|
+
"\u0131m\u0131z\u0131n",
|
|
176
|
+
"umuzun",
|
|
177
|
+
"\xFCm\xFCz\xFCn",
|
|
178
|
+
"imizde",
|
|
179
|
+
"\u0131m\u0131zda",
|
|
180
|
+
"umuzda",
|
|
181
|
+
"\xFCm\xFCzde",
|
|
182
|
+
"imizden",
|
|
183
|
+
"\u0131m\u0131zdan",
|
|
184
|
+
"imizle",
|
|
185
|
+
"\u0131m\u0131zla",
|
|
186
|
+
"umuzla",
|
|
187
|
+
"\xFCm\xFCzle",
|
|
188
|
+
"lerdir",
|
|
189
|
+
"lard\u0131r",
|
|
190
|
+
"mu\u015Ftur",
|
|
191
|
+
"mi\u015Ftir",
|
|
192
|
+
"mu\u015Ftur",
|
|
193
|
+
"m\xFC\u015Ft\xFCr",
|
|
194
|
+
"lerden",
|
|
195
|
+
"lardan",
|
|
196
|
+
"lerine",
|
|
197
|
+
"lar\u0131na",
|
|
198
|
+
"leriyle",
|
|
199
|
+
"lar\u0131yla",
|
|
200
|
+
"seniz",
|
|
201
|
+
"san\u0131z",
|
|
202
|
+
"diniz",
|
|
203
|
+
"d\u0131n\u0131z",
|
|
204
|
+
"dunuz",
|
|
205
|
+
"d\xFCn\xFCz",
|
|
206
|
+
"tiniz",
|
|
207
|
+
"t\u0131n\u0131z",
|
|
208
|
+
"tunuz",
|
|
209
|
+
"t\xFCn\xFCz",
|
|
210
|
+
"siniz",
|
|
211
|
+
"s\u0131n\u0131z",
|
|
212
|
+
"sunuz",
|
|
213
|
+
"s\xFCn\xFCz",
|
|
214
|
+
"d\u0131k\xE7a",
|
|
215
|
+
"dik\xE7e",
|
|
216
|
+
"duk\xE7a",
|
|
217
|
+
"d\xFCk\xE7e",
|
|
218
|
+
"t\u0131k\xE7a",
|
|
219
|
+
"tik\xE7e",
|
|
220
|
+
"tuk\xE7a",
|
|
221
|
+
"t\xFCk\xE7e",
|
|
222
|
+
"\u0131rken",
|
|
223
|
+
"irken",
|
|
224
|
+
"urken",
|
|
225
|
+
"\xFCrken",
|
|
226
|
+
"arken",
|
|
227
|
+
"erken",
|
|
228
|
+
// 5-letter suffixes
|
|
229
|
+
"lerde",
|
|
230
|
+
"larda",
|
|
231
|
+
"lerle",
|
|
232
|
+
"larla",
|
|
233
|
+
"lerin",
|
|
234
|
+
"lar\u0131n",
|
|
235
|
+
"lerim",
|
|
236
|
+
"lar\u0131m",
|
|
237
|
+
"dirler",
|
|
238
|
+
"d\u0131rlar",
|
|
239
|
+
"d\xFCrler",
|
|
240
|
+
"durlar",
|
|
241
|
+
"tirler",
|
|
242
|
+
"t\u0131rlar",
|
|
243
|
+
"t\xFCrler",
|
|
244
|
+
"turlar",
|
|
245
|
+
"siniz",
|
|
246
|
+
"s\u0131n\u0131z",
|
|
247
|
+
"sunuz",
|
|
248
|
+
"s\xFCn\xFCz",
|
|
249
|
+
"yorum",
|
|
250
|
+
"yorsun",
|
|
251
|
+
"uyoruz",
|
|
252
|
+
"yorsunuz",
|
|
253
|
+
"yorlar",
|
|
254
|
+
"eceks",
|
|
255
|
+
"acaks",
|
|
256
|
+
"eyim",
|
|
257
|
+
"ay\u0131m",
|
|
258
|
+
"indik",
|
|
259
|
+
"\u0131nd\u0131k",
|
|
260
|
+
"unduk",
|
|
261
|
+
"\xFCnd\xFCk",
|
|
262
|
+
"ildik",
|
|
263
|
+
"\u0131ld\u0131k",
|
|
264
|
+
"ulduk",
|
|
265
|
+
"\xFCld\xFCk",
|
|
266
|
+
"meden",
|
|
267
|
+
"madan",
|
|
268
|
+
"y\u0131n\u0131z",
|
|
269
|
+
"yiniz",
|
|
270
|
+
"yunuz",
|
|
271
|
+
"y\xFCn\xFCz",
|
|
272
|
+
// 4-letter suffixes
|
|
273
|
+
"imiz",
|
|
274
|
+
"\u0131m\u0131z",
|
|
275
|
+
"umuz",
|
|
276
|
+
"\xFCm\xFCz",
|
|
277
|
+
"iniz",
|
|
278
|
+
"\u0131n\u0131z",
|
|
279
|
+
"unuz",
|
|
280
|
+
"\xFCn\xFCz",
|
|
281
|
+
"leri",
|
|
282
|
+
"lar\u0131",
|
|
283
|
+
"idir",
|
|
284
|
+
"\u0131d\u0131r",
|
|
285
|
+
"udur",
|
|
286
|
+
"\xFCd\xFCr",
|
|
287
|
+
"ecek",
|
|
288
|
+
"acak",
|
|
289
|
+
"erek",
|
|
290
|
+
"arak",
|
|
291
|
+
"ince",
|
|
292
|
+
"\u0131nca",
|
|
293
|
+
"unca",
|
|
294
|
+
"\xFCnce",
|
|
295
|
+
"ken",
|
|
296
|
+
"meli",
|
|
297
|
+
"mal\u0131",
|
|
298
|
+
"iyor",
|
|
299
|
+
"\u0131yor",
|
|
300
|
+
"uyor",
|
|
301
|
+
"\xFCyor",
|
|
302
|
+
"mi\u015Fti",
|
|
303
|
+
"m\u0131\u015Ft\u0131",
|
|
304
|
+
"mu\u015Ftu",
|
|
305
|
+
"m\xFC\u015Ft\xFC",
|
|
306
|
+
"seydi",
|
|
307
|
+
"sayd\u0131",
|
|
308
|
+
"ydim",
|
|
309
|
+
"yd\u0131m",
|
|
310
|
+
"ydum",
|
|
311
|
+
"yd\xFCm",
|
|
312
|
+
"tiler",
|
|
313
|
+
"t\u0131lar",
|
|
314
|
+
"diler",
|
|
315
|
+
"d\u0131lar",
|
|
316
|
+
"ikten",
|
|
317
|
+
"\u0131ktan",
|
|
318
|
+
"uktan",
|
|
319
|
+
"\xFCkten",
|
|
320
|
+
// 3-letter suffixes
|
|
321
|
+
"ler",
|
|
322
|
+
"lar",
|
|
323
|
+
"den",
|
|
324
|
+
"dan",
|
|
325
|
+
"ten",
|
|
326
|
+
"tan",
|
|
327
|
+
"dir",
|
|
328
|
+
"d\u0131r",
|
|
329
|
+
"dur",
|
|
330
|
+
"d\xFCr",
|
|
331
|
+
"tir",
|
|
332
|
+
"t\u0131r",
|
|
333
|
+
"tur",
|
|
334
|
+
"t\xFCr",
|
|
335
|
+
"nin",
|
|
336
|
+
"n\u0131n",
|
|
337
|
+
"nun",
|
|
338
|
+
"n\xFCn",
|
|
339
|
+
"yle",
|
|
340
|
+
"yla",
|
|
341
|
+
"mi\u015F",
|
|
342
|
+
"m\u0131\u015F",
|
|
343
|
+
"mu\u015F",
|
|
344
|
+
"m\xFC\u015F",
|
|
345
|
+
"dim",
|
|
346
|
+
"d\u0131m",
|
|
347
|
+
"dum",
|
|
348
|
+
"d\xFCm",
|
|
349
|
+
"tim",
|
|
350
|
+
"t\u0131m",
|
|
351
|
+
"tum",
|
|
352
|
+
"t\xFCm",
|
|
353
|
+
"din",
|
|
354
|
+
"d\u0131n",
|
|
355
|
+
"dun",
|
|
356
|
+
"d\xFCn",
|
|
357
|
+
"tin",
|
|
358
|
+
"t\u0131n",
|
|
359
|
+
"tun",
|
|
360
|
+
"t\xFCn",
|
|
361
|
+
"dik",
|
|
362
|
+
"d\u0131k",
|
|
363
|
+
"duk",
|
|
364
|
+
"d\xFCk",
|
|
365
|
+
"tik",
|
|
366
|
+
"t\u0131k",
|
|
367
|
+
"tuk",
|
|
368
|
+
"t\xFCk",
|
|
369
|
+
"ydi",
|
|
370
|
+
"yd\u0131",
|
|
371
|
+
"ydu",
|
|
372
|
+
"yd\xFC",
|
|
373
|
+
"yim",
|
|
374
|
+
"y\u0131m",
|
|
375
|
+
"yum",
|
|
376
|
+
"y\xFCm",
|
|
377
|
+
"sin",
|
|
378
|
+
"s\u0131n",
|
|
379
|
+
"sun",
|
|
380
|
+
"s\xFCn",
|
|
381
|
+
"siz",
|
|
382
|
+
"s\u0131z",
|
|
383
|
+
"suz",
|
|
384
|
+
"s\xFCz",
|
|
385
|
+
"lik",
|
|
386
|
+
"l\u0131k",
|
|
387
|
+
"luk",
|
|
388
|
+
"l\xFCk",
|
|
389
|
+
"ici",
|
|
390
|
+
"\u0131c\u0131",
|
|
391
|
+
"ucu",
|
|
392
|
+
"\xFCc\xFC",
|
|
393
|
+
"gen",
|
|
394
|
+
"gan",
|
|
395
|
+
"ken",
|
|
396
|
+
"kan",
|
|
397
|
+
"len",
|
|
398
|
+
"lan",
|
|
399
|
+
"le\u015F",
|
|
400
|
+
"la\u015F",
|
|
401
|
+
"mek",
|
|
402
|
+
"mak",
|
|
403
|
+
"yor",
|
|
404
|
+
// 2-letter suffixes
|
|
405
|
+
"de",
|
|
406
|
+
"da",
|
|
407
|
+
"te",
|
|
408
|
+
"ta",
|
|
409
|
+
"im",
|
|
410
|
+
"\u0131m",
|
|
411
|
+
"um",
|
|
412
|
+
"\xFCm",
|
|
413
|
+
"in",
|
|
414
|
+
"\u0131n",
|
|
415
|
+
"un",
|
|
416
|
+
"\xFCn",
|
|
417
|
+
"iz",
|
|
418
|
+
"\u0131z",
|
|
419
|
+
"uz",
|
|
420
|
+
"\xFCz",
|
|
421
|
+
"si",
|
|
422
|
+
"s\u0131",
|
|
423
|
+
"su",
|
|
424
|
+
"s\xFC",
|
|
425
|
+
"ye",
|
|
426
|
+
"ya",
|
|
427
|
+
"le",
|
|
428
|
+
"la",
|
|
429
|
+
"di",
|
|
430
|
+
"d\u0131",
|
|
431
|
+
"du",
|
|
432
|
+
"d\xFC",
|
|
433
|
+
"ti",
|
|
434
|
+
"t\u0131",
|
|
435
|
+
"tu",
|
|
436
|
+
"t\xFC",
|
|
437
|
+
"se",
|
|
438
|
+
"sa",
|
|
439
|
+
"ce",
|
|
440
|
+
"ca",
|
|
441
|
+
"\xE7e",
|
|
442
|
+
"\xE7a",
|
|
443
|
+
"me",
|
|
444
|
+
"ma",
|
|
445
|
+
"ip",
|
|
446
|
+
"\u0131p",
|
|
447
|
+
"up",
|
|
448
|
+
"\xFCp",
|
|
449
|
+
"en",
|
|
450
|
+
"an",
|
|
451
|
+
"i\u015F",
|
|
452
|
+
"\u0131\u015F",
|
|
453
|
+
"u\u015F",
|
|
454
|
+
"\xFC\u015F",
|
|
455
|
+
"li",
|
|
456
|
+
"l\u0131",
|
|
457
|
+
"lu",
|
|
458
|
+
"l\xFC",
|
|
459
|
+
"ci",
|
|
460
|
+
"c\u0131",
|
|
461
|
+
"cu",
|
|
462
|
+
"c\xFC",
|
|
463
|
+
"\xE7i",
|
|
464
|
+
"\xE7\u0131",
|
|
465
|
+
"\xE7u",
|
|
466
|
+
"\xE7\xFC",
|
|
467
|
+
// 1-letter suffixes (vowels / basic case endings)
|
|
468
|
+
"e",
|
|
469
|
+
"a",
|
|
470
|
+
"i",
|
|
471
|
+
"\u0131",
|
|
472
|
+
"u",
|
|
473
|
+
"\xFC"
|
|
474
|
+
];
|
|
475
|
+
function restoreConsonantSoftening(stem) {
|
|
476
|
+
if (stem.length < 2)
|
|
477
|
+
return [];
|
|
478
|
+
const last = stem.slice(-1);
|
|
479
|
+
const base = stem.slice(0, -1);
|
|
480
|
+
switch (last) {
|
|
481
|
+
case "b":
|
|
482
|
+
return [base + "p"];
|
|
483
|
+
case "c":
|
|
484
|
+
return [base + "\xE7"];
|
|
485
|
+
case "d":
|
|
486
|
+
return [base + "t"];
|
|
487
|
+
case "\u011F":
|
|
488
|
+
return [base + "k"];
|
|
489
|
+
case "g":
|
|
490
|
+
return [base + "k"];
|
|
491
|
+
default:
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function restoreVowelDrop(stem) {
|
|
496
|
+
if (stem.length < 3)
|
|
497
|
+
return [];
|
|
498
|
+
const c1 = stem[stem.length - 2];
|
|
499
|
+
const c2 = stem[stem.length - 1];
|
|
500
|
+
if (!isVowel(c1) && !isVowel(c2)) {
|
|
501
|
+
const vowelsInBase = stem.slice(0, -2).split("").filter(isVowel);
|
|
502
|
+
if (vowelsInBase.length > 0) {
|
|
503
|
+
const lastVowel = vowelsInBase[vowelsInBase.length - 1];
|
|
504
|
+
let inserted = "i";
|
|
505
|
+
if ("a\u0131".includes(lastVowel))
|
|
506
|
+
inserted = "\u0131";
|
|
507
|
+
else if ("ei".includes(lastVowel))
|
|
508
|
+
inserted = "i";
|
|
509
|
+
else if ("ou".includes(lastVowel))
|
|
510
|
+
inserted = "u";
|
|
511
|
+
else if ("\xF6\xFC".includes(lastVowel))
|
|
512
|
+
inserted = "\xFC";
|
|
513
|
+
return [stem.slice(0, -1) + inserted + c2];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
function restoreInfinitive(stem) {
|
|
519
|
+
if (stem.length < 2)
|
|
520
|
+
return [];
|
|
521
|
+
const vowelsInBase = stem.split("").filter(isVowel);
|
|
522
|
+
if (vowelsInBase.length === 0)
|
|
523
|
+
return [];
|
|
524
|
+
const lastVowel = vowelsInBase[vowelsInBase.length - 1];
|
|
525
|
+
return "a\u0131ou".includes(lastVowel) ? [stem + "mak"] : [stem + "mek"];
|
|
526
|
+
}
|
|
527
|
+
function getStemCandidates(word, minStemLength = 2, maxDepth = 4) {
|
|
528
|
+
if (!word || word.trim().length === 0)
|
|
529
|
+
return [];
|
|
530
|
+
const raw = word.trim();
|
|
531
|
+
const normalized = raw.toLocaleLowerCase("tr-TR");
|
|
532
|
+
const candidatesWithWeight = [];
|
|
533
|
+
const seen = /* @__PURE__ */ new Set();
|
|
534
|
+
if (raw.includes("'") || raw.includes("\u2019")) {
|
|
535
|
+
const apostropheStem = normalized.split(/['’]/)[0];
|
|
536
|
+
if (apostropheStem.length >= minStemLength) {
|
|
537
|
+
candidatesWithWeight.push({ candidate: apostropheStem, baseLength: apostropheStem.length + 10 });
|
|
538
|
+
seen.add(apostropheStem);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
let frontier = [normalized];
|
|
542
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
543
|
+
const nextFrontier = [];
|
|
544
|
+
for (const current of frontier) {
|
|
545
|
+
for (const suffix of TURKISH_SUFFIXES) {
|
|
546
|
+
if (current.length - suffix.length >= minStemLength && current.endsWith(suffix)) {
|
|
547
|
+
const stem = current.slice(0, -suffix.length);
|
|
548
|
+
const hardened = restoreConsonantSoftening(stem);
|
|
549
|
+
const vowelDropped = restoreVowelDrop(stem);
|
|
550
|
+
const verbalBases = [stem, ...hardened];
|
|
551
|
+
const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
|
|
552
|
+
const variants = [stem, ...hardened, ...vowelDropped, ...infinitives];
|
|
553
|
+
for (const variant of variants) {
|
|
554
|
+
if (!seen.has(variant) && variant !== normalized) {
|
|
555
|
+
seen.add(variant);
|
|
556
|
+
nextFrontier.push(variant);
|
|
557
|
+
candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (nextFrontier.length === 0)
|
|
564
|
+
break;
|
|
565
|
+
frontier = nextFrontier;
|
|
566
|
+
}
|
|
567
|
+
candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
|
|
568
|
+
return [...new Set(candidatesWithWeight.map((c) => c.candidate))];
|
|
569
|
+
}
|
|
570
|
+
|
|
67
571
|
// src/tdk.ts
|
|
68
572
|
var fs = __toESM(require("fs"));
|
|
69
573
|
var path = __toESM(require("path"));
|
|
@@ -155,6 +659,8 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
155
659
|
static wordCache = /* @__PURE__ */ new Map();
|
|
156
660
|
static dailyContentCache = null;
|
|
157
661
|
static autocompleteCache = [];
|
|
662
|
+
static autocompleteSet = /* @__PURE__ */ new Set();
|
|
663
|
+
static stemCache = /* @__PURE__ */ new Map();
|
|
158
664
|
/**
|
|
159
665
|
* Enables or disables in-memory caching for API requests.
|
|
160
666
|
*/
|
|
@@ -171,6 +677,8 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
171
677
|
this.wordCache.clear();
|
|
172
678
|
this.dailyContentCache = null;
|
|
173
679
|
this.autocompleteCache = [];
|
|
680
|
+
this.autocompleteSet.clear();
|
|
681
|
+
this.stemCache.clear();
|
|
174
682
|
}
|
|
175
683
|
static delay(ms) {
|
|
176
684
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -277,6 +785,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
277
785
|
return [];
|
|
278
786
|
}
|
|
279
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Ensures TDK's ~81k headword list is loaded in memory for fast O(1) set operations.
|
|
790
|
+
*/
|
|
791
|
+
static async ensureAutocompleteLoaded() {
|
|
792
|
+
if (this.autocompleteCache.length === 0) {
|
|
793
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
794
|
+
this.autocompleteSet = new Set(
|
|
795
|
+
this.autocompleteCache.map((w) => w.toLocaleLowerCase("tr-TR"))
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
280
799
|
/**
|
|
281
800
|
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
282
801
|
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
@@ -286,12 +805,81 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
286
805
|
static async getSuggestions(prefix) {
|
|
287
806
|
if (!prefix || prefix.trim() === "")
|
|
288
807
|
return [];
|
|
289
|
-
|
|
290
|
-
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
291
|
-
}
|
|
808
|
+
await this.ensureAutocompleteLoaded();
|
|
292
809
|
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
293
810
|
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
294
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* Checks whether a word exists as a known headword in TDK dictionary.
|
|
814
|
+
* Checks in-memory autocompleteSet (81k headwords) if loaded, or queries TDK API.
|
|
815
|
+
*/
|
|
816
|
+
static async isHeadword(word) {
|
|
817
|
+
if (!word || word.trim() === "")
|
|
818
|
+
return false;
|
|
819
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
820
|
+
await this.ensureAutocompleteLoaded();
|
|
821
|
+
if (this.autocompleteSet.size > 0) {
|
|
822
|
+
return this.autocompleteSet.has(clean);
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
const results = await this.getWord(clean);
|
|
826
|
+
return results.length > 0;
|
|
827
|
+
} catch {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Generates candidate roots for a given Turkish word using progressive BFS suffix stripping,
|
|
833
|
+
* consonant mutation restoration, and vowel drop restoration.
|
|
834
|
+
*/
|
|
835
|
+
static getStemCandidates(word) {
|
|
836
|
+
return getStemCandidates(word);
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Finds the dictionary root (headword) of a word by checking direct existence
|
|
840
|
+
* and evaluating candidate stems generated by morphological analysis.
|
|
841
|
+
* Returns the root headword string if found, or null if no match in TDK.
|
|
842
|
+
*/
|
|
843
|
+
static async findRoot(word) {
|
|
844
|
+
if (!word || word.trim() === "")
|
|
845
|
+
return null;
|
|
846
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
847
|
+
if (this.stemCache.has(clean)) {
|
|
848
|
+
return this.stemCache.get(clean);
|
|
849
|
+
}
|
|
850
|
+
if (await this.isHeadword(clean)) {
|
|
851
|
+
this.stemCache.set(clean, clean);
|
|
852
|
+
return clean;
|
|
853
|
+
}
|
|
854
|
+
const candidates = getStemCandidates(clean);
|
|
855
|
+
for (const candidate of candidates) {
|
|
856
|
+
if (await this.isHeadword(candidate)) {
|
|
857
|
+
this.stemCache.set(clean, candidate);
|
|
858
|
+
return candidate;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
this.stemCache.set(clean, null);
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Performs morphological stemming on a Turkish word.
|
|
866
|
+
* Returns a StemResult containing the original word, resolved root, and whether it is inflected.
|
|
867
|
+
*/
|
|
868
|
+
static async stem(word) {
|
|
869
|
+
if (!word || word.trim() === "")
|
|
870
|
+
return null;
|
|
871
|
+
const clean = word.trim().toLocaleLowerCase("tr-TR");
|
|
872
|
+
const root = await this.findRoot(word);
|
|
873
|
+
if (!root) {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
word,
|
|
878
|
+
root,
|
|
879
|
+
isInflected: root !== clean,
|
|
880
|
+
candidates: getStemCandidates(word)
|
|
881
|
+
};
|
|
882
|
+
}
|
|
295
883
|
/**
|
|
296
884
|
* Returns a list of proverbs and idioms containing the word.
|
|
297
885
|
*/
|
|
@@ -508,6 +1096,17 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
508
1096
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
509
1097
|
}
|
|
510
1098
|
}
|
|
1099
|
+
const root = await this.findRoot(word);
|
|
1100
|
+
if (root) {
|
|
1101
|
+
const cleanWord2 = word.trim().toLocaleLowerCase("tr-TR");
|
|
1102
|
+
const isInflected = root !== cleanWord2;
|
|
1103
|
+
return {
|
|
1104
|
+
isCorrect: true,
|
|
1105
|
+
word,
|
|
1106
|
+
isInflected,
|
|
1107
|
+
root
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
511
1110
|
if (this.autocompleteCache.length === 0) {
|
|
512
1111
|
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
513
1112
|
}
|
|
@@ -700,14 +1299,46 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
700
1299
|
req.end();
|
|
701
1300
|
});
|
|
702
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Kubbealtı indexes headwords with full classical Turkish orthography,
|
|
1304
|
+
* including letters that a plain-ASCII-ish query tends to drop — most
|
|
1305
|
+
* commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
|
|
1306
|
+
* Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
|
|
1307
|
+
* entirely (verified: even "ruzgâr" alone still misses — it's the missing
|
|
1308
|
+
* ü, not the missing â, that actually breaks the match). This generates
|
|
1309
|
+
* single-letter-substitution variants to retry, one substitution per
|
|
1310
|
+
* variant (not combinatorial) — covers the overwhelmingly common case of
|
|
1311
|
+
* one "de-Turkished" letter without an explosion of API calls for words
|
|
1312
|
+
* with several.
|
|
1313
|
+
*/
|
|
1314
|
+
static TURKISH_DEASCII_MAP = {
|
|
1315
|
+
a: ["\xE2"],
|
|
1316
|
+
i: ["\u0131", "\xEE"],
|
|
1317
|
+
o: ["\xF6"],
|
|
1318
|
+
u: ["\xFC", "\xFB"],
|
|
1319
|
+
c: ["\xE7"],
|
|
1320
|
+
g: ["\u011F"],
|
|
1321
|
+
s: ["\u015F"]
|
|
1322
|
+
};
|
|
1323
|
+
static generateTurkishVariants(word) {
|
|
1324
|
+
const lower = word.trim().toLocaleLowerCase("tr-TR");
|
|
1325
|
+
const variants = [];
|
|
1326
|
+
for (let i = 0; i < lower.length; i++) {
|
|
1327
|
+
for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
|
|
1328
|
+
variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return variants;
|
|
1332
|
+
}
|
|
703
1333
|
/**
|
|
704
1334
|
* Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
|
|
705
1335
|
* word, scraped from the site's own data API — undocumented, and Kubbealtı
|
|
706
1336
|
* Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
|
|
707
1337
|
* openly-published data, so use this in line with their terms. `anlam` is
|
|
708
1338
|
* raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
|
|
709
|
-
* plain text.
|
|
710
|
-
*
|
|
1339
|
+
* plain text. Falls back to `generateTurkishVariants()` if the exact query
|
|
1340
|
+
* comes up empty (see its doc comment). Returns `null` on any fetch/parse
|
|
1341
|
+
* failure, `[]` if no variant matches either.
|
|
711
1342
|
*/
|
|
712
1343
|
static async getKubbealti(word) {
|
|
713
1344
|
if (!word || word.trim() === "")
|
|
@@ -715,7 +1346,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
715
1346
|
const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word.trim())}/`);
|
|
716
1347
|
if (!data || !Array.isArray(data.content))
|
|
717
1348
|
return null;
|
|
718
|
-
|
|
1349
|
+
if (data.content.length > 0) {
|
|
1350
|
+
return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
|
|
1351
|
+
}
|
|
1352
|
+
for (const variant of this.generateTurkishVariants(word)) {
|
|
1353
|
+
const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
|
|
1354
|
+
if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
|
|
1355
|
+
return variantData.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return [];
|
|
719
1359
|
}
|
|
720
1360
|
/**
|
|
721
1361
|
* Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
|
|
@@ -769,21 +1409,10 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
769
1409
|
return null;
|
|
770
1410
|
}
|
|
771
1411
|
}
|
|
772
|
-
|
|
773
|
-
* Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
|
|
774
|
-
* via MediaWiki's official Action API (`action=query&prop=extracts`) — no
|
|
775
|
-
* scraping involved, this is a stable, documented public API. `sections`
|
|
776
|
-
* splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
|
|
777
|
-
* markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
|
|
778
|
-
* unsplit text. Returns `null` if the page doesn't exist or the request
|
|
779
|
-
* fails.
|
|
780
|
-
*/
|
|
781
|
-
static async getWiktionary(word) {
|
|
782
|
-
if (!word || word.trim() === "")
|
|
783
|
-
return null;
|
|
1412
|
+
static async fetchWiktionaryEntry(title) {
|
|
784
1413
|
try {
|
|
785
1414
|
const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
|
|
786
|
-
|
|
1415
|
+
title
|
|
787
1416
|
)}&format=json&explaintext=1&formatversion=2`;
|
|
788
1417
|
const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
|
|
789
1418
|
if (!response.ok)
|
|
@@ -796,16 +1425,42 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
796
1425
|
const sections = {};
|
|
797
1426
|
const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
|
|
798
1427
|
for (let i = 1; i < parts.length; i += 3) {
|
|
799
|
-
const
|
|
1428
|
+
const title2 = parts[i + 1]?.trim();
|
|
800
1429
|
const content = parts[i + 2]?.trim();
|
|
801
|
-
if (
|
|
802
|
-
sections[
|
|
1430
|
+
if (title2)
|
|
1431
|
+
sections[title2] = content ?? "";
|
|
803
1432
|
}
|
|
804
1433
|
return { raw, sections };
|
|
805
1434
|
} catch {
|
|
806
1435
|
return null;
|
|
807
1436
|
}
|
|
808
1437
|
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
|
|
1440
|
+
* via MediaWiki's official Action API (`action=query&prop=extracts`) — no
|
|
1441
|
+
* scraping involved, this is a stable, documented public API. `sections`
|
|
1442
|
+
* splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
|
|
1443
|
+
* markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
|
|
1444
|
+
* unsplit text. This wiki has title capitalization turned off
|
|
1445
|
+
* ($wgCapitalLinks=false — common for Wiktionaries, since case is
|
|
1446
|
+
* meaningful for a dictionary: "Türkiye" the country vs. a lowercase
|
|
1447
|
+
* common word), so an exact-case miss retries with the first letter
|
|
1448
|
+
* uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
|
|
1449
|
+
* "Istanbul") before giving up. Returns `null` if neither is found or the
|
|
1450
|
+
* request fails.
|
|
1451
|
+
*/
|
|
1452
|
+
static async getWiktionary(word) {
|
|
1453
|
+
if (!word || word.trim() === "")
|
|
1454
|
+
return null;
|
|
1455
|
+
const trimmed = word.trim();
|
|
1456
|
+
const direct = await this.fetchWiktionaryEntry(trimmed);
|
|
1457
|
+
if (direct)
|
|
1458
|
+
return direct;
|
|
1459
|
+
const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
|
|
1460
|
+
if (capitalized === trimmed)
|
|
1461
|
+
return null;
|
|
1462
|
+
return this.fetchWiktionaryEntry(capitalized);
|
|
1463
|
+
}
|
|
809
1464
|
/**
|
|
810
1465
|
* Convenience filter over `getWiktionary()`: returns just one section's
|
|
811
1466
|
* text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
|
|
@@ -948,13 +1603,28 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
948
1603
|
const unique = [...new Set(words)];
|
|
949
1604
|
const analyses = [];
|
|
950
1605
|
for (const word of unique) {
|
|
951
|
-
|
|
952
|
-
|
|
1606
|
+
let results = await this.getWord(word);
|
|
1607
|
+
let found = results.length > 0;
|
|
1608
|
+
let root;
|
|
1609
|
+
let isInflected;
|
|
1610
|
+
if (!found) {
|
|
1611
|
+
const resolvedRoot = await this.findRoot(word);
|
|
1612
|
+
if (resolvedRoot) {
|
|
1613
|
+
results = await this.getWord(resolvedRoot);
|
|
1614
|
+
if (results.length > 0) {
|
|
1615
|
+
found = true;
|
|
1616
|
+
root = resolvedRoot;
|
|
1617
|
+
isInflected = true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
953
1621
|
analyses.push({
|
|
954
1622
|
word,
|
|
955
1623
|
found,
|
|
956
1624
|
meaning: found ? this.firstMeaning(results) : null,
|
|
957
|
-
origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null
|
|
1625
|
+
origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null,
|
|
1626
|
+
root,
|
|
1627
|
+
isInflected
|
|
958
1628
|
});
|
|
959
1629
|
await this.delay(200);
|
|
960
1630
|
}
|
|
@@ -1052,5 +1722,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
|
1052
1722
|
TDK,
|
|
1053
1723
|
TDKError,
|
|
1054
1724
|
TDKNetworkError,
|
|
1055
|
-
TDKValidationError
|
|
1725
|
+
TDKValidationError,
|
|
1726
|
+
TURKISH_SUFFIXES,
|
|
1727
|
+
TURKISH_VOWELS,
|
|
1728
|
+
getStemCandidates,
|
|
1729
|
+
isVowel,
|
|
1730
|
+
restoreConsonantSoftening,
|
|
1731
|
+
restoreInfinitive,
|
|
1732
|
+
restoreVowelDrop
|
|
1056
1733
|
});
|