tdk-api-wrapper 1.6.1 → 1.7.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/LICENSE +674 -21
- package/README.md +23 -3
- package/dist/{chunk-ZFWZXNGY.mjs → chunk-YY4WA2AO.mjs} +126 -142
- package/dist/chunk-ZG7YRT7V.mjs +279 -0
- package/dist/cli.js +2489 -2127
- package/dist/cli.mjs +12 -5
- package/dist/index.d.mts +23 -38
- package/dist/index.d.ts +23 -38
- package/dist/index.js +402 -143
- package/dist/index.mjs +8 -2
- package/dist/mcp-GBTXQJLW.mjs +9 -0
- package/package.json +13 -5
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,440 +30,462 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
26
|
-
//
|
|
27
|
-
var
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.name = "TDKError";
|
|
31
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
33
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var init_cjs_shims = __esm({
|
|
35
|
+
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
36
|
+
"use strict";
|
|
32
37
|
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/errors.ts
|
|
41
|
+
var TDKError, TDKValidationError, TDKNetworkError;
|
|
42
|
+
var init_errors = __esm({
|
|
43
|
+
"src/errors.ts"() {
|
|
44
|
+
"use strict";
|
|
45
|
+
init_cjs_shims();
|
|
46
|
+
TDKError = class extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "TDKError";
|
|
50
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
TDKValidationError = class extends TDKError {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "TDKValidationError";
|
|
57
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
TDKNetworkError = class extends TDKError {
|
|
61
|
+
status;
|
|
62
|
+
cause;
|
|
63
|
+
constructor(message, options) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "TDKNetworkError";
|
|
66
|
+
this.status = options?.status;
|
|
67
|
+
this.cause = options?.cause;
|
|
68
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
39
71
|
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// src/data/suffixes.ts
|
|
75
|
+
var TURKISH_SUFFIXES;
|
|
76
|
+
var init_suffixes = __esm({
|
|
77
|
+
"src/data/suffixes.ts"() {
|
|
78
|
+
"use strict";
|
|
79
|
+
init_cjs_shims();
|
|
80
|
+
TURKISH_SUFFIXES = [
|
|
81
|
+
// 9-letter composite suffixes
|
|
82
|
+
"lerimizden",
|
|
83
|
+
"lar\u0131m\u0131zdan",
|
|
84
|
+
"lerinizden",
|
|
85
|
+
"lar\u0131n\u0131zdan",
|
|
86
|
+
// 8-letter composite suffixes
|
|
87
|
+
"lerinin",
|
|
88
|
+
"lar\u0131n\u0131n",
|
|
89
|
+
"lerinde",
|
|
90
|
+
"lar\u0131nda",
|
|
91
|
+
"lerinden",
|
|
92
|
+
"lar\u0131ndan",
|
|
93
|
+
"leriyle",
|
|
94
|
+
"lar\u0131yla",
|
|
95
|
+
"lerini",
|
|
96
|
+
"lar\u0131n\u0131",
|
|
97
|
+
"lerimize",
|
|
98
|
+
"lar\u0131m\u0131za",
|
|
99
|
+
"lerimizle",
|
|
100
|
+
"lar\u0131m\u0131zla",
|
|
101
|
+
"lerinizin",
|
|
102
|
+
"lar\u0131n\u0131z\u0131n",
|
|
103
|
+
"lerinizde",
|
|
104
|
+
"lar\u0131n\u0131zda",
|
|
105
|
+
"d\u0131\u011F\u0131ndan",
|
|
106
|
+
"di\u011Finden",
|
|
107
|
+
"du\u011Fundan",
|
|
108
|
+
"d\xFC\u011F\xFCnden",
|
|
109
|
+
"t\u0131\u011F\u0131ndan",
|
|
110
|
+
"ti\u011Finden",
|
|
111
|
+
"tu\u011Fundan",
|
|
112
|
+
"t\xFC\u011F\xFCnden",
|
|
113
|
+
// 7-letter composite suffixes
|
|
114
|
+
"ecektir",
|
|
115
|
+
"acakt\u0131r",
|
|
116
|
+
"ece\u011Fim",
|
|
117
|
+
"aca\u011F\u0131m",
|
|
118
|
+
"eceksin",
|
|
119
|
+
"acaks\u0131n",
|
|
120
|
+
"ece\u011Fiz",
|
|
121
|
+
"aca\u011F\u0131z",
|
|
122
|
+
"lerimiz",
|
|
123
|
+
"lar\u0131m\u0131z",
|
|
124
|
+
"leriniz",
|
|
125
|
+
"lar\u0131n\u0131z",
|
|
126
|
+
"umuzdan",
|
|
127
|
+
"\xFCm\xFCzden",
|
|
128
|
+
"inizden",
|
|
129
|
+
"\u0131n\u0131zdan",
|
|
130
|
+
"\xFCn\xFCzden",
|
|
131
|
+
"d\u0131\u011F\u0131nda",
|
|
132
|
+
"di\u011Finde",
|
|
133
|
+
"du\u011Funda",
|
|
134
|
+
"d\xFC\u011F\xFCnde",
|
|
135
|
+
"t\u0131\u011F\u0131nda",
|
|
136
|
+
"ti\u011Finde",
|
|
137
|
+
"tu\u011Funda",
|
|
138
|
+
"t\xFC\u011F\xFCnde",
|
|
139
|
+
"mas\u0131na",
|
|
140
|
+
"mesine",
|
|
141
|
+
"\u0131yorsunuz",
|
|
142
|
+
"iyorsunuz",
|
|
143
|
+
"uyorsunuz",
|
|
144
|
+
"\xFCyorsunuz",
|
|
145
|
+
"yorsunuz",
|
|
146
|
+
// 6-letter composite suffixes
|
|
147
|
+
"iyorsa",
|
|
148
|
+
"iyorduk",
|
|
149
|
+
"iyordu",
|
|
150
|
+
"iyormu\u015F",
|
|
151
|
+
"\u0131yorsa",
|
|
152
|
+
"\u0131yorduk",
|
|
153
|
+
"\u0131yordu",
|
|
154
|
+
"\u0131yormu\u015F",
|
|
155
|
+
"uyorsa",
|
|
156
|
+
"uyorduk",
|
|
157
|
+
"uyordu",
|
|
158
|
+
"uyormu\u015F",
|
|
159
|
+
"\xFCyorsa",
|
|
160
|
+
"\xFCyorduk",
|
|
161
|
+
"\xFCyordu",
|
|
162
|
+
"\xFCyormu\u015F",
|
|
163
|
+
"\u0131yorsun",
|
|
164
|
+
"iyorsun",
|
|
165
|
+
"uyorsun",
|
|
166
|
+
"\xFCyorsun",
|
|
167
|
+
"\u0131yorlar",
|
|
168
|
+
"iyorlar",
|
|
169
|
+
"uyorlar",
|
|
170
|
+
"\xFCyorlar",
|
|
171
|
+
"iyoruz",
|
|
172
|
+
"\u0131yoruz",
|
|
173
|
+
"uyoruz",
|
|
174
|
+
"\xFCyoruz",
|
|
175
|
+
"imizin",
|
|
176
|
+
"\u0131m\u0131z\u0131n",
|
|
177
|
+
"umuzun",
|
|
178
|
+
"\xFCm\xFCz\xFCn",
|
|
179
|
+
"imizde",
|
|
180
|
+
"\u0131m\u0131zda",
|
|
181
|
+
"umuzda",
|
|
182
|
+
"\xFCm\xFCzde",
|
|
183
|
+
"imizden",
|
|
184
|
+
"\u0131m\u0131zdan",
|
|
185
|
+
"imizle",
|
|
186
|
+
"\u0131m\u0131zla",
|
|
187
|
+
"umuzla",
|
|
188
|
+
"\xFCm\xFCzle",
|
|
189
|
+
"lerdir",
|
|
190
|
+
"lard\u0131r",
|
|
191
|
+
"mu\u015Ftur",
|
|
192
|
+
"mi\u015Ftir",
|
|
193
|
+
"mu\u015Ftur",
|
|
194
|
+
"m\xFC\u015Ft\xFCr",
|
|
195
|
+
"lerden",
|
|
196
|
+
"lardan",
|
|
197
|
+
"lerine",
|
|
198
|
+
"lar\u0131na",
|
|
199
|
+
"leriyle",
|
|
200
|
+
"lar\u0131yla",
|
|
201
|
+
"seniz",
|
|
202
|
+
"san\u0131z",
|
|
203
|
+
"diniz",
|
|
204
|
+
"d\u0131n\u0131z",
|
|
205
|
+
"dunuz",
|
|
206
|
+
"d\xFCn\xFCz",
|
|
207
|
+
"tiniz",
|
|
208
|
+
"t\u0131n\u0131z",
|
|
209
|
+
"tunuz",
|
|
210
|
+
"t\xFCn\xFCz",
|
|
211
|
+
"siniz",
|
|
212
|
+
"s\u0131n\u0131z",
|
|
213
|
+
"sunuz",
|
|
214
|
+
"s\xFCn\xFCz",
|
|
215
|
+
"d\u0131k\xE7a",
|
|
216
|
+
"dik\xE7e",
|
|
217
|
+
"duk\xE7a",
|
|
218
|
+
"d\xFCk\xE7e",
|
|
219
|
+
"t\u0131k\xE7a",
|
|
220
|
+
"tik\xE7e",
|
|
221
|
+
"tuk\xE7a",
|
|
222
|
+
"t\xFCk\xE7e",
|
|
223
|
+
"\u0131rken",
|
|
224
|
+
"irken",
|
|
225
|
+
"urken",
|
|
226
|
+
"\xFCrken",
|
|
227
|
+
"arken",
|
|
228
|
+
"erken",
|
|
229
|
+
// 5-letter suffixes
|
|
230
|
+
"lerde",
|
|
231
|
+
"larda",
|
|
232
|
+
"lerle",
|
|
233
|
+
"larla",
|
|
234
|
+
"lerin",
|
|
235
|
+
"lar\u0131n",
|
|
236
|
+
"lerim",
|
|
237
|
+
"lar\u0131m",
|
|
238
|
+
"dirler",
|
|
239
|
+
"d\u0131rlar",
|
|
240
|
+
"d\xFCrler",
|
|
241
|
+
"durlar",
|
|
242
|
+
"tirler",
|
|
243
|
+
"t\u0131rlar",
|
|
244
|
+
"t\xFCrler",
|
|
245
|
+
"turlar",
|
|
246
|
+
"siniz",
|
|
247
|
+
"s\u0131n\u0131z",
|
|
248
|
+
"sunuz",
|
|
249
|
+
"s\xFCn\xFCz",
|
|
250
|
+
"yorum",
|
|
251
|
+
"yorsun",
|
|
252
|
+
"uyoruz",
|
|
253
|
+
"yorsunuz",
|
|
254
|
+
"yorlar",
|
|
255
|
+
"eceks",
|
|
256
|
+
"acaks",
|
|
257
|
+
"eyim",
|
|
258
|
+
"ay\u0131m",
|
|
259
|
+
"indik",
|
|
260
|
+
"\u0131nd\u0131k",
|
|
261
|
+
"unduk",
|
|
262
|
+
"\xFCnd\xFCk",
|
|
263
|
+
"ildik",
|
|
264
|
+
"\u0131ld\u0131k",
|
|
265
|
+
"ulduk",
|
|
266
|
+
"\xFCld\xFCk",
|
|
267
|
+
"meden",
|
|
268
|
+
"madan",
|
|
269
|
+
"y\u0131n\u0131z",
|
|
270
|
+
"yiniz",
|
|
271
|
+
"yunuz",
|
|
272
|
+
"y\xFCn\xFCz",
|
|
273
|
+
// 4-letter suffixes
|
|
274
|
+
"imiz",
|
|
275
|
+
"\u0131m\u0131z",
|
|
276
|
+
"umuz",
|
|
277
|
+
"\xFCm\xFCz",
|
|
278
|
+
"iniz",
|
|
279
|
+
"\u0131n\u0131z",
|
|
280
|
+
"unuz",
|
|
281
|
+
"\xFCn\xFCz",
|
|
282
|
+
"leri",
|
|
283
|
+
"lar\u0131",
|
|
284
|
+
"idir",
|
|
285
|
+
"\u0131d\u0131r",
|
|
286
|
+
"udur",
|
|
287
|
+
"\xFCd\xFCr",
|
|
288
|
+
"ecek",
|
|
289
|
+
"acak",
|
|
290
|
+
"erek",
|
|
291
|
+
"arak",
|
|
292
|
+
"ince",
|
|
293
|
+
"\u0131nca",
|
|
294
|
+
"unca",
|
|
295
|
+
"\xFCnce",
|
|
296
|
+
"ken",
|
|
297
|
+
"meli",
|
|
298
|
+
"mal\u0131",
|
|
299
|
+
"iyor",
|
|
300
|
+
"\u0131yor",
|
|
301
|
+
"uyor",
|
|
302
|
+
"\xFCyor",
|
|
303
|
+
"mi\u015Fti",
|
|
304
|
+
"m\u0131\u015Ft\u0131",
|
|
305
|
+
"mu\u015Ftu",
|
|
306
|
+
"m\xFC\u015Ft\xFC",
|
|
307
|
+
"seydi",
|
|
308
|
+
"sayd\u0131",
|
|
309
|
+
"ydim",
|
|
310
|
+
"yd\u0131m",
|
|
311
|
+
"ydum",
|
|
312
|
+
"yd\xFCm",
|
|
313
|
+
"tiler",
|
|
314
|
+
"t\u0131lar",
|
|
315
|
+
"diler",
|
|
316
|
+
"d\u0131lar",
|
|
317
|
+
"ikten",
|
|
318
|
+
"\u0131ktan",
|
|
319
|
+
"uktan",
|
|
320
|
+
"\xFCkten",
|
|
321
|
+
// 3-letter suffixes
|
|
322
|
+
"ler",
|
|
323
|
+
"lar",
|
|
324
|
+
"den",
|
|
325
|
+
"dan",
|
|
326
|
+
"ten",
|
|
327
|
+
"tan",
|
|
328
|
+
"dir",
|
|
329
|
+
"d\u0131r",
|
|
330
|
+
"dur",
|
|
331
|
+
"d\xFCr",
|
|
332
|
+
"tir",
|
|
333
|
+
"t\u0131r",
|
|
334
|
+
"tur",
|
|
335
|
+
"t\xFCr",
|
|
336
|
+
"nin",
|
|
337
|
+
"n\u0131n",
|
|
338
|
+
"nun",
|
|
339
|
+
"n\xFCn",
|
|
340
|
+
"yle",
|
|
341
|
+
"yla",
|
|
342
|
+
"mi\u015F",
|
|
343
|
+
"m\u0131\u015F",
|
|
344
|
+
"mu\u015F",
|
|
345
|
+
"m\xFC\u015F",
|
|
346
|
+
"dim",
|
|
347
|
+
"d\u0131m",
|
|
348
|
+
"dum",
|
|
349
|
+
"d\xFCm",
|
|
350
|
+
"tim",
|
|
351
|
+
"t\u0131m",
|
|
352
|
+
"tum",
|
|
353
|
+
"t\xFCm",
|
|
354
|
+
"din",
|
|
355
|
+
"d\u0131n",
|
|
356
|
+
"dun",
|
|
357
|
+
"d\xFCn",
|
|
358
|
+
"tin",
|
|
359
|
+
"t\u0131n",
|
|
360
|
+
"tun",
|
|
361
|
+
"t\xFCn",
|
|
362
|
+
"dik",
|
|
363
|
+
"d\u0131k",
|
|
364
|
+
"duk",
|
|
365
|
+
"d\xFCk",
|
|
366
|
+
"tik",
|
|
367
|
+
"t\u0131k",
|
|
368
|
+
"tuk",
|
|
369
|
+
"t\xFCk",
|
|
370
|
+
"ydi",
|
|
371
|
+
"yd\u0131",
|
|
372
|
+
"ydu",
|
|
373
|
+
"yd\xFC",
|
|
374
|
+
"yim",
|
|
375
|
+
"y\u0131m",
|
|
376
|
+
"yum",
|
|
377
|
+
"y\xFCm",
|
|
378
|
+
"sin",
|
|
379
|
+
"s\u0131n",
|
|
380
|
+
"sun",
|
|
381
|
+
"s\xFCn",
|
|
382
|
+
"sen",
|
|
383
|
+
"san",
|
|
384
|
+
"sem",
|
|
385
|
+
"sam",
|
|
386
|
+
"sek",
|
|
387
|
+
"sak",
|
|
388
|
+
"siz",
|
|
389
|
+
"s\u0131z",
|
|
390
|
+
"suz",
|
|
391
|
+
"s\xFCz",
|
|
392
|
+
"lik",
|
|
393
|
+
"l\u0131k",
|
|
394
|
+
"luk",
|
|
395
|
+
"l\xFCk",
|
|
396
|
+
"ici",
|
|
397
|
+
"\u0131c\u0131",
|
|
398
|
+
"ucu",
|
|
399
|
+
"\xFCc\xFC",
|
|
400
|
+
"gen",
|
|
401
|
+
"gan",
|
|
402
|
+
"ken",
|
|
403
|
+
"kan",
|
|
404
|
+
"len",
|
|
405
|
+
"lan",
|
|
406
|
+
"le\u015F",
|
|
407
|
+
"la\u015F",
|
|
408
|
+
"mek",
|
|
409
|
+
"mak",
|
|
410
|
+
"yor",
|
|
411
|
+
// 2-letter suffixes
|
|
412
|
+
"de",
|
|
413
|
+
"da",
|
|
414
|
+
"te",
|
|
415
|
+
"ta",
|
|
416
|
+
"im",
|
|
417
|
+
"\u0131m",
|
|
418
|
+
"um",
|
|
419
|
+
"\xFCm",
|
|
420
|
+
"in",
|
|
421
|
+
"\u0131n",
|
|
422
|
+
"un",
|
|
423
|
+
"\xFCn",
|
|
424
|
+
"iz",
|
|
425
|
+
"\u0131z",
|
|
426
|
+
"uz",
|
|
427
|
+
"\xFCz",
|
|
428
|
+
"si",
|
|
429
|
+
"s\u0131",
|
|
430
|
+
"su",
|
|
431
|
+
"s\xFC",
|
|
432
|
+
"ye",
|
|
433
|
+
"ya",
|
|
434
|
+
"le",
|
|
435
|
+
"la",
|
|
436
|
+
"di",
|
|
437
|
+
"d\u0131",
|
|
438
|
+
"du",
|
|
439
|
+
"d\xFC",
|
|
440
|
+
"ti",
|
|
441
|
+
"t\u0131",
|
|
442
|
+
"tu",
|
|
443
|
+
"t\xFC",
|
|
444
|
+
"se",
|
|
445
|
+
"sa",
|
|
446
|
+
"ce",
|
|
447
|
+
"ca",
|
|
448
|
+
"\xE7e",
|
|
449
|
+
"\xE7a",
|
|
450
|
+
"me",
|
|
451
|
+
"ma",
|
|
452
|
+
"ip",
|
|
453
|
+
"\u0131p",
|
|
454
|
+
"up",
|
|
455
|
+
"\xFCp",
|
|
456
|
+
"en",
|
|
457
|
+
"an",
|
|
458
|
+
"i\u015F",
|
|
459
|
+
"\u0131\u015F",
|
|
460
|
+
"u\u015F",
|
|
461
|
+
"\xFC\u015F",
|
|
462
|
+
"li",
|
|
463
|
+
"l\u0131",
|
|
464
|
+
"lu",
|
|
465
|
+
"l\xFC",
|
|
466
|
+
"ci",
|
|
467
|
+
"c\u0131",
|
|
468
|
+
"cu",
|
|
469
|
+
"c\xFC",
|
|
470
|
+
"\xE7i",
|
|
471
|
+
"\xE7\u0131",
|
|
472
|
+
"\xE7u",
|
|
473
|
+
"\xE7\xFC",
|
|
474
|
+
// 1-letter suffixes (vowels / basic case endings)
|
|
475
|
+
"e",
|
|
476
|
+
"a",
|
|
477
|
+
"i",
|
|
478
|
+
"\u0131",
|
|
479
|
+
"u",
|
|
480
|
+
"\xFC"
|
|
481
|
+
];
|
|
50
482
|
}
|
|
51
|
-
};
|
|
483
|
+
});
|
|
52
484
|
|
|
53
485
|
// src/morphology.ts
|
|
54
|
-
var TURKISH_VOWELS = "ae\u0131io\xF6u\xFC";
|
|
55
486
|
function isVowel(ch) {
|
|
56
487
|
return TURKISH_VOWELS.includes(ch);
|
|
57
488
|
}
|
|
58
|
-
var TURKISH_SUFFIXES = [
|
|
59
|
-
// 9-letter composite suffixes
|
|
60
|
-
"lerimizden",
|
|
61
|
-
"lar\u0131m\u0131zdan",
|
|
62
|
-
"lerinizden",
|
|
63
|
-
"lar\u0131n\u0131zdan",
|
|
64
|
-
// 8-letter composite suffixes
|
|
65
|
-
"lerinin",
|
|
66
|
-
"lar\u0131n\u0131n",
|
|
67
|
-
"lerinde",
|
|
68
|
-
"lar\u0131nda",
|
|
69
|
-
"lerinden",
|
|
70
|
-
"lar\u0131ndan",
|
|
71
|
-
"leriyle",
|
|
72
|
-
"lar\u0131yla",
|
|
73
|
-
"lerini",
|
|
74
|
-
"lar\u0131n\u0131",
|
|
75
|
-
"lerimize",
|
|
76
|
-
"lar\u0131m\u0131za",
|
|
77
|
-
"lerimizle",
|
|
78
|
-
"lar\u0131m\u0131zla",
|
|
79
|
-
"lerinizin",
|
|
80
|
-
"lar\u0131n\u0131z\u0131n",
|
|
81
|
-
"lerinizde",
|
|
82
|
-
"lar\u0131n\u0131zda",
|
|
83
|
-
"d\u0131\u011F\u0131ndan",
|
|
84
|
-
"di\u011Finden",
|
|
85
|
-
"du\u011Fundan",
|
|
86
|
-
"d\xFC\u011F\xFCnden",
|
|
87
|
-
"t\u0131\u011F\u0131ndan",
|
|
88
|
-
"ti\u011Finden",
|
|
89
|
-
"tu\u011Fundan",
|
|
90
|
-
"t\xFC\u011F\xFCnden",
|
|
91
|
-
// 7-letter composite suffixes
|
|
92
|
-
"ecektir",
|
|
93
|
-
"acakt\u0131r",
|
|
94
|
-
"ece\u011Fim",
|
|
95
|
-
"aca\u011F\u0131m",
|
|
96
|
-
"eceksin",
|
|
97
|
-
"acaks\u0131n",
|
|
98
|
-
"ece\u011Fiz",
|
|
99
|
-
"aca\u011F\u0131z",
|
|
100
|
-
"lerimiz",
|
|
101
|
-
"lar\u0131m\u0131z",
|
|
102
|
-
"leriniz",
|
|
103
|
-
"lar\u0131n\u0131z",
|
|
104
|
-
"umuzdan",
|
|
105
|
-
"\xFCm\xFCzden",
|
|
106
|
-
"inizden",
|
|
107
|
-
"\u0131n\u0131zdan",
|
|
108
|
-
"\xFCn\xFCzden",
|
|
109
|
-
"d\u0131\u011F\u0131nda",
|
|
110
|
-
"di\u011Finde",
|
|
111
|
-
"du\u011Funda",
|
|
112
|
-
"d\xFC\u011F\xFCnde",
|
|
113
|
-
"t\u0131\u011F\u0131nda",
|
|
114
|
-
"ti\u011Finde",
|
|
115
|
-
"tu\u011Funda",
|
|
116
|
-
"t\xFC\u011F\xFCnde",
|
|
117
|
-
"mas\u0131na",
|
|
118
|
-
"mesine",
|
|
119
|
-
"\u0131yorsunuz",
|
|
120
|
-
"iyorsunuz",
|
|
121
|
-
"uyorsunuz",
|
|
122
|
-
"\xFCyorsunuz",
|
|
123
|
-
"yorsunuz",
|
|
124
|
-
// 6-letter composite suffixes
|
|
125
|
-
"iyorsa",
|
|
126
|
-
"iyorduk",
|
|
127
|
-
"iyordu",
|
|
128
|
-
"iyormu\u015F",
|
|
129
|
-
"\u0131yorsa",
|
|
130
|
-
"\u0131yorduk",
|
|
131
|
-
"\u0131yordu",
|
|
132
|
-
"\u0131yormu\u015F",
|
|
133
|
-
"uyorsa",
|
|
134
|
-
"uyorduk",
|
|
135
|
-
"uyordu",
|
|
136
|
-
"uyormu\u015F",
|
|
137
|
-
"\xFCyorsa",
|
|
138
|
-
"\xFCyorduk",
|
|
139
|
-
"\xFCyordu",
|
|
140
|
-
"\xFCyormu\u015F",
|
|
141
|
-
"\u0131yorsun",
|
|
142
|
-
"iyorsun",
|
|
143
|
-
"uyorsun",
|
|
144
|
-
"\xFCyorsun",
|
|
145
|
-
"\u0131yorlar",
|
|
146
|
-
"iyorlar",
|
|
147
|
-
"uyorlar",
|
|
148
|
-
"\xFCyorlar",
|
|
149
|
-
"iyoruz",
|
|
150
|
-
"\u0131yoruz",
|
|
151
|
-
"uyoruz",
|
|
152
|
-
"\xFCyoruz",
|
|
153
|
-
"imizin",
|
|
154
|
-
"\u0131m\u0131z\u0131n",
|
|
155
|
-
"umuzun",
|
|
156
|
-
"\xFCm\xFCz\xFCn",
|
|
157
|
-
"imizde",
|
|
158
|
-
"\u0131m\u0131zda",
|
|
159
|
-
"umuzda",
|
|
160
|
-
"\xFCm\xFCzde",
|
|
161
|
-
"imizden",
|
|
162
|
-
"\u0131m\u0131zdan",
|
|
163
|
-
"imizle",
|
|
164
|
-
"\u0131m\u0131zla",
|
|
165
|
-
"umuzla",
|
|
166
|
-
"\xFCm\xFCzle",
|
|
167
|
-
"lerdir",
|
|
168
|
-
"lard\u0131r",
|
|
169
|
-
"mu\u015Ftur",
|
|
170
|
-
"mi\u015Ftir",
|
|
171
|
-
"mu\u015Ftur",
|
|
172
|
-
"m\xFC\u015Ft\xFCr",
|
|
173
|
-
"lerden",
|
|
174
|
-
"lardan",
|
|
175
|
-
"lerine",
|
|
176
|
-
"lar\u0131na",
|
|
177
|
-
"leriyle",
|
|
178
|
-
"lar\u0131yla",
|
|
179
|
-
"seniz",
|
|
180
|
-
"san\u0131z",
|
|
181
|
-
"diniz",
|
|
182
|
-
"d\u0131n\u0131z",
|
|
183
|
-
"dunuz",
|
|
184
|
-
"d\xFCn\xFCz",
|
|
185
|
-
"tiniz",
|
|
186
|
-
"t\u0131n\u0131z",
|
|
187
|
-
"tunuz",
|
|
188
|
-
"t\xFCn\xFCz",
|
|
189
|
-
"siniz",
|
|
190
|
-
"s\u0131n\u0131z",
|
|
191
|
-
"sunuz",
|
|
192
|
-
"s\xFCn\xFCz",
|
|
193
|
-
"d\u0131k\xE7a",
|
|
194
|
-
"dik\xE7e",
|
|
195
|
-
"duk\xE7a",
|
|
196
|
-
"d\xFCk\xE7e",
|
|
197
|
-
"t\u0131k\xE7a",
|
|
198
|
-
"tik\xE7e",
|
|
199
|
-
"tuk\xE7a",
|
|
200
|
-
"t\xFCk\xE7e",
|
|
201
|
-
"\u0131rken",
|
|
202
|
-
"irken",
|
|
203
|
-
"urken",
|
|
204
|
-
"\xFCrken",
|
|
205
|
-
"arken",
|
|
206
|
-
"erken",
|
|
207
|
-
// 5-letter suffixes
|
|
208
|
-
"lerde",
|
|
209
|
-
"larda",
|
|
210
|
-
"lerle",
|
|
211
|
-
"larla",
|
|
212
|
-
"lerin",
|
|
213
|
-
"lar\u0131n",
|
|
214
|
-
"lerim",
|
|
215
|
-
"lar\u0131m",
|
|
216
|
-
"dirler",
|
|
217
|
-
"d\u0131rlar",
|
|
218
|
-
"d\xFCrler",
|
|
219
|
-
"durlar",
|
|
220
|
-
"tirler",
|
|
221
|
-
"t\u0131rlar",
|
|
222
|
-
"t\xFCrler",
|
|
223
|
-
"turlar",
|
|
224
|
-
"siniz",
|
|
225
|
-
"s\u0131n\u0131z",
|
|
226
|
-
"sunuz",
|
|
227
|
-
"s\xFCn\xFCz",
|
|
228
|
-
"yorum",
|
|
229
|
-
"yorsun",
|
|
230
|
-
"uyoruz",
|
|
231
|
-
"yorsunuz",
|
|
232
|
-
"yorlar",
|
|
233
|
-
"eceks",
|
|
234
|
-
"acaks",
|
|
235
|
-
"eyim",
|
|
236
|
-
"ay\u0131m",
|
|
237
|
-
"indik",
|
|
238
|
-
"\u0131nd\u0131k",
|
|
239
|
-
"unduk",
|
|
240
|
-
"\xFCnd\xFCk",
|
|
241
|
-
"ildik",
|
|
242
|
-
"\u0131ld\u0131k",
|
|
243
|
-
"ulduk",
|
|
244
|
-
"\xFCld\xFCk",
|
|
245
|
-
"meden",
|
|
246
|
-
"madan",
|
|
247
|
-
"y\u0131n\u0131z",
|
|
248
|
-
"yiniz",
|
|
249
|
-
"yunuz",
|
|
250
|
-
"y\xFCn\xFCz",
|
|
251
|
-
// 4-letter suffixes
|
|
252
|
-
"imiz",
|
|
253
|
-
"\u0131m\u0131z",
|
|
254
|
-
"umuz",
|
|
255
|
-
"\xFCm\xFCz",
|
|
256
|
-
"iniz",
|
|
257
|
-
"\u0131n\u0131z",
|
|
258
|
-
"unuz",
|
|
259
|
-
"\xFCn\xFCz",
|
|
260
|
-
"leri",
|
|
261
|
-
"lar\u0131",
|
|
262
|
-
"idir",
|
|
263
|
-
"\u0131d\u0131r",
|
|
264
|
-
"udur",
|
|
265
|
-
"\xFCd\xFCr",
|
|
266
|
-
"ecek",
|
|
267
|
-
"acak",
|
|
268
|
-
"erek",
|
|
269
|
-
"arak",
|
|
270
|
-
"ince",
|
|
271
|
-
"\u0131nca",
|
|
272
|
-
"unca",
|
|
273
|
-
"\xFCnce",
|
|
274
|
-
"ken",
|
|
275
|
-
"meli",
|
|
276
|
-
"mal\u0131",
|
|
277
|
-
"iyor",
|
|
278
|
-
"\u0131yor",
|
|
279
|
-
"uyor",
|
|
280
|
-
"\xFCyor",
|
|
281
|
-
"mi\u015Fti",
|
|
282
|
-
"m\u0131\u015Ft\u0131",
|
|
283
|
-
"mu\u015Ftu",
|
|
284
|
-
"m\xFC\u015Ft\xFC",
|
|
285
|
-
"seydi",
|
|
286
|
-
"sayd\u0131",
|
|
287
|
-
"ydim",
|
|
288
|
-
"yd\u0131m",
|
|
289
|
-
"ydum",
|
|
290
|
-
"yd\xFCm",
|
|
291
|
-
"tiler",
|
|
292
|
-
"t\u0131lar",
|
|
293
|
-
"diler",
|
|
294
|
-
"d\u0131lar",
|
|
295
|
-
"ikten",
|
|
296
|
-
"\u0131ktan",
|
|
297
|
-
"uktan",
|
|
298
|
-
"\xFCkten",
|
|
299
|
-
// 3-letter suffixes
|
|
300
|
-
"ler",
|
|
301
|
-
"lar",
|
|
302
|
-
"den",
|
|
303
|
-
"dan",
|
|
304
|
-
"ten",
|
|
305
|
-
"tan",
|
|
306
|
-
"dir",
|
|
307
|
-
"d\u0131r",
|
|
308
|
-
"dur",
|
|
309
|
-
"d\xFCr",
|
|
310
|
-
"tir",
|
|
311
|
-
"t\u0131r",
|
|
312
|
-
"tur",
|
|
313
|
-
"t\xFCr",
|
|
314
|
-
"nin",
|
|
315
|
-
"n\u0131n",
|
|
316
|
-
"nun",
|
|
317
|
-
"n\xFCn",
|
|
318
|
-
"yle",
|
|
319
|
-
"yla",
|
|
320
|
-
"mi\u015F",
|
|
321
|
-
"m\u0131\u015F",
|
|
322
|
-
"mu\u015F",
|
|
323
|
-
"m\xFC\u015F",
|
|
324
|
-
"dim",
|
|
325
|
-
"d\u0131m",
|
|
326
|
-
"dum",
|
|
327
|
-
"d\xFCm",
|
|
328
|
-
"tim",
|
|
329
|
-
"t\u0131m",
|
|
330
|
-
"tum",
|
|
331
|
-
"t\xFCm",
|
|
332
|
-
"din",
|
|
333
|
-
"d\u0131n",
|
|
334
|
-
"dun",
|
|
335
|
-
"d\xFCn",
|
|
336
|
-
"tin",
|
|
337
|
-
"t\u0131n",
|
|
338
|
-
"tun",
|
|
339
|
-
"t\xFCn",
|
|
340
|
-
"dik",
|
|
341
|
-
"d\u0131k",
|
|
342
|
-
"duk",
|
|
343
|
-
"d\xFCk",
|
|
344
|
-
"tik",
|
|
345
|
-
"t\u0131k",
|
|
346
|
-
"tuk",
|
|
347
|
-
"t\xFCk",
|
|
348
|
-
"ydi",
|
|
349
|
-
"yd\u0131",
|
|
350
|
-
"ydu",
|
|
351
|
-
"yd\xFC",
|
|
352
|
-
"yim",
|
|
353
|
-
"y\u0131m",
|
|
354
|
-
"yum",
|
|
355
|
-
"y\xFCm",
|
|
356
|
-
"sin",
|
|
357
|
-
"s\u0131n",
|
|
358
|
-
"sun",
|
|
359
|
-
"s\xFCn",
|
|
360
|
-
"sen",
|
|
361
|
-
"san",
|
|
362
|
-
"sem",
|
|
363
|
-
"sam",
|
|
364
|
-
"sek",
|
|
365
|
-
"sak",
|
|
366
|
-
"siz",
|
|
367
|
-
"s\u0131z",
|
|
368
|
-
"suz",
|
|
369
|
-
"s\xFCz",
|
|
370
|
-
"lik",
|
|
371
|
-
"l\u0131k",
|
|
372
|
-
"luk",
|
|
373
|
-
"l\xFCk",
|
|
374
|
-
"ici",
|
|
375
|
-
"\u0131c\u0131",
|
|
376
|
-
"ucu",
|
|
377
|
-
"\xFCc\xFC",
|
|
378
|
-
"gen",
|
|
379
|
-
"gan",
|
|
380
|
-
"ken",
|
|
381
|
-
"kan",
|
|
382
|
-
"len",
|
|
383
|
-
"lan",
|
|
384
|
-
"le\u015F",
|
|
385
|
-
"la\u015F",
|
|
386
|
-
"mek",
|
|
387
|
-
"mak",
|
|
388
|
-
"yor",
|
|
389
|
-
// 2-letter suffixes
|
|
390
|
-
"de",
|
|
391
|
-
"da",
|
|
392
|
-
"te",
|
|
393
|
-
"ta",
|
|
394
|
-
"im",
|
|
395
|
-
"\u0131m",
|
|
396
|
-
"um",
|
|
397
|
-
"\xFCm",
|
|
398
|
-
"in",
|
|
399
|
-
"\u0131n",
|
|
400
|
-
"un",
|
|
401
|
-
"\xFCn",
|
|
402
|
-
"iz",
|
|
403
|
-
"\u0131z",
|
|
404
|
-
"uz",
|
|
405
|
-
"\xFCz",
|
|
406
|
-
"si",
|
|
407
|
-
"s\u0131",
|
|
408
|
-
"su",
|
|
409
|
-
"s\xFC",
|
|
410
|
-
"ye",
|
|
411
|
-
"ya",
|
|
412
|
-
"le",
|
|
413
|
-
"la",
|
|
414
|
-
"di",
|
|
415
|
-
"d\u0131",
|
|
416
|
-
"du",
|
|
417
|
-
"d\xFC",
|
|
418
|
-
"ti",
|
|
419
|
-
"t\u0131",
|
|
420
|
-
"tu",
|
|
421
|
-
"t\xFC",
|
|
422
|
-
"se",
|
|
423
|
-
"sa",
|
|
424
|
-
"ce",
|
|
425
|
-
"ca",
|
|
426
|
-
"\xE7e",
|
|
427
|
-
"\xE7a",
|
|
428
|
-
"me",
|
|
429
|
-
"ma",
|
|
430
|
-
"ip",
|
|
431
|
-
"\u0131p",
|
|
432
|
-
"up",
|
|
433
|
-
"\xFCp",
|
|
434
|
-
"en",
|
|
435
|
-
"an",
|
|
436
|
-
"i\u015F",
|
|
437
|
-
"\u0131\u015F",
|
|
438
|
-
"u\u015F",
|
|
439
|
-
"\xFC\u015F",
|
|
440
|
-
"li",
|
|
441
|
-
"l\u0131",
|
|
442
|
-
"lu",
|
|
443
|
-
"l\xFC",
|
|
444
|
-
"ci",
|
|
445
|
-
"c\u0131",
|
|
446
|
-
"cu",
|
|
447
|
-
"c\xFC",
|
|
448
|
-
"\xE7i",
|
|
449
|
-
"\xE7\u0131",
|
|
450
|
-
"\xE7u",
|
|
451
|
-
"\xE7\xFC",
|
|
452
|
-
// 1-letter suffixes (vowels / basic case endings)
|
|
453
|
-
"e",
|
|
454
|
-
"a",
|
|
455
|
-
"i",
|
|
456
|
-
"\u0131",
|
|
457
|
-
"u",
|
|
458
|
-
"\xFC"
|
|
459
|
-
];
|
|
460
489
|
function restoreConsonantSoftening(stem) {
|
|
461
490
|
if (stem.length < 2)
|
|
462
491
|
return [];
|
|
@@ -609,235 +638,187 @@ function getStemCandidates(word2, minStemLength = 2, maxDepth = 4) {
|
|
|
609
638
|
candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
|
|
610
639
|
return [...new Set(candidatesWithWeight.map((c2) => c2.candidate))];
|
|
611
640
|
}
|
|
641
|
+
var TURKISH_VOWELS;
|
|
642
|
+
var init_morphology = __esm({
|
|
643
|
+
"src/morphology.ts"() {
|
|
644
|
+
"use strict";
|
|
645
|
+
init_cjs_shims();
|
|
646
|
+
init_suffixes();
|
|
647
|
+
init_suffixes();
|
|
648
|
+
TURKISH_VOWELS = "ae\u0131io\xF6u\xFC";
|
|
649
|
+
}
|
|
650
|
+
});
|
|
612
651
|
|
|
613
|
-
// src/
|
|
614
|
-
var
|
|
615
|
-
var
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
var
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
});
|
|
784
|
-
return coords;
|
|
785
|
-
})();
|
|
786
|
-
var DIACRITIC_SIBLINGS = {
|
|
787
|
-
\u0131: "i",
|
|
788
|
-
i: "\u0131",
|
|
789
|
-
\u00F6: "o",
|
|
790
|
-
o: "\xF6",
|
|
791
|
-
\u00FC: "u",
|
|
792
|
-
u: "\xFC",
|
|
793
|
-
\u015F: "s",
|
|
794
|
-
s: "\u015F",
|
|
795
|
-
\u00E7: "c",
|
|
796
|
-
c: "\xE7",
|
|
797
|
-
\u011F: "g",
|
|
798
|
-
g: "\u011F",
|
|
799
|
-
\u00E2: "a",
|
|
800
|
-
a: "\xE2"
|
|
801
|
-
};
|
|
802
|
-
var KEYBOARD_ROW_SUB_COST = 0.4;
|
|
803
|
-
var KEYBOARD_DIAGONAL_SUB_COST = 0.55;
|
|
804
|
-
var DIACRITIC_SUB_COST = 0.3;
|
|
805
|
-
var TRANSPOSITION_COST = 0.8;
|
|
806
|
-
function keyboardSubCost(a, b) {
|
|
807
|
-
if (a === b)
|
|
808
|
-
return 0;
|
|
809
|
-
if (DIACRITIC_SIBLINGS[a] === b)
|
|
810
|
-
return DIACRITIC_SUB_COST;
|
|
811
|
-
const pa = KEYBOARD_COORDS[a];
|
|
812
|
-
const pb = KEYBOARD_COORDS[b];
|
|
813
|
-
if (!pa || !pb)
|
|
814
|
-
return 1;
|
|
815
|
-
const dx = Math.abs(pa[0] - pb[0]);
|
|
816
|
-
const dy = Math.abs(pa[1] - pb[1]);
|
|
817
|
-
if (dy === 0 && dx <= 1 + 1e-9)
|
|
818
|
-
return KEYBOARD_ROW_SUB_COST;
|
|
819
|
-
if (dy === 1 && dx <= 1 + 1e-9)
|
|
820
|
-
return KEYBOARD_DIAGONAL_SUB_COST;
|
|
821
|
-
return 1;
|
|
822
|
-
}
|
|
823
|
-
var TDK = class {
|
|
824
|
-
static BASE_URL = "https://sozluk.gov.tr";
|
|
825
|
-
static AUDIO_API_HOST = "api.sozluk.gov.tr";
|
|
826
|
-
static KUBBEALTI_HOST = "eski.lugatim.com";
|
|
827
|
-
/**
|
|
828
|
-
* `eski.lugatim.com` (Kubbealtı Lugatı's data API) sends only its leaf
|
|
829
|
-
* certificate during the TLS handshake, omitting the intermediates a
|
|
830
|
-
* correctly configured server would include — a server-side misconfiguration,
|
|
831
|
-
* not something we should paper over by disabling verification. These are
|
|
832
|
-
* the two certificates the server *should* be sending (fetched from the
|
|
833
|
-
* leaf's own Authority Information Access URLs), supplied here so Node can
|
|
834
|
-
* still build a full, properly verified chain up to a root it already
|
|
835
|
-
* trusts (ISRG Root X1). If Let's Encrypt rotates this intermediate, this
|
|
836
|
-
* stops working and every Kubbealtı call fails closed to `null` — same
|
|
837
|
-
* fail-closed contract as the rest of this file's fragile integrations.
|
|
838
|
-
*/
|
|
839
|
-
static KUBBEALTI_EXTRA_CA = [
|
|
840
|
-
`-----BEGIN CERTIFICATE-----
|
|
652
|
+
// src/data/misspellings.ts
|
|
653
|
+
var COMMON_MISSPELLINGS, SEY_EXCEPTIONS;
|
|
654
|
+
var init_misspellings = __esm({
|
|
655
|
+
"src/data/misspellings.ts"() {
|
|
656
|
+
"use strict";
|
|
657
|
+
init_cjs_shims();
|
|
658
|
+
COMMON_MISSPELLINGS = {
|
|
659
|
+
// -şey ile biten ve ayrı yazılması zorunlu söz öbekleri
|
|
660
|
+
her\u015Fey: "her \u015Fey",
|
|
661
|
+
hersey: "her \u015Fey",
|
|
662
|
+
bir\u015Fey: "bir \u015Fey",
|
|
663
|
+
birsey: "bir \u015Fey",
|
|
664
|
+
hi\u00E7bir\u015Fey: "hi\xE7bir \u015Fey",
|
|
665
|
+
hicbirsey: "hi\xE7bir \u015Fey",
|
|
666
|
+
\u00E7ok\u015Fey: "\xE7ok \u015Fey",
|
|
667
|
+
coksey: "\xE7ok \u015Fey",
|
|
668
|
+
\u015Feyler: "\u015Feyler",
|
|
669
|
+
seyler: "\u015Feyler",
|
|
670
|
+
herhangibir\u015Fey: "herhangi bir \u015Fey",
|
|
671
|
+
herhangibirsey: "herhangi bir \u015Fey",
|
|
672
|
+
// Sıkça birleşik yazılan ama ayrı yazılması gereken sözler
|
|
673
|
+
herg\u00FCn: "her g\xFCn",
|
|
674
|
+
hergun: "her g\xFCn",
|
|
675
|
+
herzaman: "her zaman",
|
|
676
|
+
heran: "her an",
|
|
677
|
+
heryer: "her yer",
|
|
678
|
+
herbiri: "her biri",
|
|
679
|
+
pek\u00E7ok: "pek \xE7ok",
|
|
680
|
+
pekcok: "pek \xE7ok",
|
|
681
|
+
pekaz: "pek az",
|
|
682
|
+
yada: "ya da",
|
|
683
|
+
tabiki: "tabii ki",
|
|
684
|
+
tabiiki: "tabii ki",
|
|
685
|
+
sa\u011Fol: "sa\u011F ol",
|
|
686
|
+
sagol: "sa\u011F ol",
|
|
687
|
+
sa\u011Folun: "sa\u011F olun",
|
|
688
|
+
sagolun: "sa\u011F olun",
|
|
689
|
+
ho\u015F\u00E7akal: "ho\u015F\xE7a kal",
|
|
690
|
+
hoscakal: "ho\u015F\xE7a kal",
|
|
691
|
+
ho\u015Fgeldin: "ho\u015F geldin",
|
|
692
|
+
hosgeldin: "ho\u015F geldin",
|
|
693
|
+
ho\u015Fgeldiniz: "ho\u015F geldiniz",
|
|
694
|
+
hosgeldiniz: "ho\u015F geldiniz",
|
|
695
|
+
ho\u015Fbulduk: "ho\u015F bulduk",
|
|
696
|
+
hosbulduk: "ho\u015F bulduk",
|
|
697
|
+
yan\u0131s\u0131ra: "yan\u0131 s\u0131ra",
|
|
698
|
+
yanisira: "yan\u0131 s\u0131ra",
|
|
699
|
+
pe\u015Fis\u0131ra: "pe\u015Fi s\u0131ra",
|
|
700
|
+
pesisira: "pe\u015Fi s\u0131ra",
|
|
701
|
+
ard\u0131s\u0131ra: "ard\u0131 s\u0131ra",
|
|
702
|
+
ardisira: "ard\u0131 s\u0131ra",
|
|
703
|
+
artarda: "art arda",
|
|
704
|
+
y\u00FCzy\u00FCze: "y\xFCz y\xFCze",
|
|
705
|
+
yuzyuze: "y\xFCz y\xFCze",
|
|
706
|
+
elele: "el ele",
|
|
707
|
+
g\u00F6zg\u00F6ze: "g\xF6z g\xF6ze",
|
|
708
|
+
ba\u015Fba\u015Fa: "ba\u015F ba\u015Fa",
|
|
709
|
+
basbasa: "ba\u015F ba\u015Fa",
|
|
710
|
+
yanyana: "yan yana",
|
|
711
|
+
i\u00E7i\u00E7e: "i\xE7 i\xE7e",
|
|
712
|
+
icice: "i\xE7 i\xE7e",
|
|
713
|
+
\u00FCst\u00FCste: "\xFCst \xFCste",
|
|
714
|
+
ustuste: "\xFCst \xFCste",
|
|
715
|
+
altalta: "alt alta",
|
|
716
|
+
\u00F6ns\u00F6z: "\xF6n s\xF6z",
|
|
717
|
+
onsoz: "\xF6n s\xF6z",
|
|
718
|
+
\u00F6nyarg\u0131: "\xF6n yarg\u0131",
|
|
719
|
+
onyargi: "\xF6n yarg\u0131",
|
|
720
|
+
farketmek: "fark etmek",
|
|
721
|
+
farketti: "fark etti",
|
|
722
|
+
farkettim: "fark ettim",
|
|
723
|
+
farkeder: "fark eder",
|
|
724
|
+
farketmez: "fark etmez",
|
|
725
|
+
terketmek: "terk etmek",
|
|
726
|
+
terketti: "terk etti",
|
|
727
|
+
ay\u0131rdetmek: "ay\u0131rt etmek",
|
|
728
|
+
ay\u0131rtetmek: "ay\u0131rt etmek",
|
|
729
|
+
arzetmek: "arz etmek",
|
|
730
|
+
arzederim: "arz ederim",
|
|
731
|
+
varolmak: "var olmak",
|
|
732
|
+
yokolmak: "yok olmak",
|
|
733
|
+
haketmek: "hak etmek",
|
|
734
|
+
haketti: "hak etti",
|
|
735
|
+
hakkaten: "hakikaten",
|
|
736
|
+
hi\u00E7kimse: "hi\xE7 kimse",
|
|
737
|
+
hickimse: "hi\xE7 kimse",
|
|
738
|
+
// Ünlü düşmesi yapılmaması gereken yer bildiren sözler (TDK Kural 15)
|
|
739
|
+
burda: "burada",
|
|
740
|
+
burdan: "buradan",
|
|
741
|
+
\u015Furda: "\u015Furada",
|
|
742
|
+
surda: "\u015Furada",
|
|
743
|
+
\u015Furdan: "\u015Furadan",
|
|
744
|
+
surdan: "\u015Furadan",
|
|
745
|
+
orda: "orada",
|
|
746
|
+
ordan: "oradan",
|
|
747
|
+
i\u00E7erde: "i\xE7eride",
|
|
748
|
+
icerde: "i\xE7eride",
|
|
749
|
+
i\u00E7erden: "i\xE7eriden",
|
|
750
|
+
icerden: "i\xE7eriden",
|
|
751
|
+
d\u0131\u015Farda: "d\u0131\u015Far\u0131da",
|
|
752
|
+
disarda: "d\u0131\u015Far\u0131da",
|
|
753
|
+
d\u0131\u015Fardan: "d\u0131\u015Far\u0131dan",
|
|
754
|
+
disardan: "d\u0131\u015Far\u0131dan",
|
|
755
|
+
yukarda: "yukar\u0131da",
|
|
756
|
+
yukardan: "yukar\u0131dan",
|
|
757
|
+
// Sıkça yanlış yazılan sözcükler
|
|
758
|
+
herkez: "herkes",
|
|
759
|
+
yanl\u0131z: "yaln\u0131z",
|
|
760
|
+
yaln\u0131\u015F: "yanl\u0131\u015F",
|
|
761
|
+
orjinal: "orijinal",
|
|
762
|
+
labaratuar: "laboratuvar",
|
|
763
|
+
laboratuar: "laboratuvar",
|
|
764
|
+
\u015F\u00F6f\u00F6r: "\u015Fof\xF6r",
|
|
765
|
+
sofor: "\u015Fof\xF6r",
|
|
766
|
+
egzos: "egzoz",
|
|
767
|
+
eksoz: "egzoz",
|
|
768
|
+
ekzoz: "egzoz",
|
|
769
|
+
kiprik: "kirpik",
|
|
770
|
+
kirbit: "kibrit",
|
|
771
|
+
klavuz: "k\u0131lavuz",
|
|
772
|
+
k\u0131ravat: "kravat",
|
|
773
|
+
s\u00FCpriz: "s\xFCrpriz",
|
|
774
|
+
supriz: "s\xFCrpriz",
|
|
775
|
+
raslant\u0131: "rastlant\u0131",
|
|
776
|
+
hastahane: "hastane",
|
|
777
|
+
pastahane: "pastane",
|
|
778
|
+
postahane: "postane",
|
|
779
|
+
eczahane: "eczane",
|
|
780
|
+
meyva: "meyve",
|
|
781
|
+
sarm\u0131sak: "sar\u0131msak",
|
|
782
|
+
dinazor: "dinozor",
|
|
783
|
+
pantalon: "pantolon",
|
|
784
|
+
tesbih: "tespih",
|
|
785
|
+
ah\u00E7\u0131: "a\u015F\xE7\u0131",
|
|
786
|
+
matba: "matbaa",
|
|
787
|
+
idda: "iddia",
|
|
788
|
+
iddaa: "iddia",
|
|
789
|
+
muhattap: "muhatap",
|
|
790
|
+
tra\u015F: "t\u0131ra\u015F",
|
|
791
|
+
karn\u0131bahar: "karnabahar",
|
|
792
|
+
kareografi: "koreografi",
|
|
793
|
+
poa\u00E7a: "po\u011Fa\xE7a",
|
|
794
|
+
poha\u00E7a: "po\u011Fa\xE7a",
|
|
795
|
+
\u015Farz: "\u015Farj",
|
|
796
|
+
sarj: "\u015Farj",
|
|
797
|
+
makina: "makine",
|
|
798
|
+
m\u00FCsade: "m\xFCsaade",
|
|
799
|
+
entellekt\u00FCel: "entelekt\xFCel",
|
|
800
|
+
inisiyatif: "inisiyatif",
|
|
801
|
+
insiyatif: "inisiyatif",
|
|
802
|
+
sezeryan: "sezaryen",
|
|
803
|
+
dok\u00FCman: "dok\xFCman",
|
|
804
|
+
d\u00F6k\u00FCman: "dok\xFCman",
|
|
805
|
+
erozyon: "erozyon",
|
|
806
|
+
erizyon: "erozyon",
|
|
807
|
+
anane: "anneanne",
|
|
808
|
+
babaanne: "babaanne"
|
|
809
|
+
};
|
|
810
|
+
SEY_EXCEPTIONS = /* @__PURE__ */ new Set(["d\xFC\u015Fey", "e\u015Fey", "konsey", "jersey", "\u015Fey"]);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// src/data/kubbealti-ca.ts
|
|
815
|
+
var KUBBEALTI_EXTRA_CA;
|
|
816
|
+
var init_kubbealti_ca = __esm({
|
|
817
|
+
"src/data/kubbealti-ca.ts"() {
|
|
818
|
+
"use strict";
|
|
819
|
+
init_cjs_shims();
|
|
820
|
+
KUBBEALTI_EXTRA_CA = [
|
|
821
|
+
`-----BEGIN CERTIFICATE-----
|
|
841
822
|
MIIE2jCCAsKgAwIBAgIQTr0klH4k05SALYSlL9WzGTANBgkqhkiG9w0BAQsFADAu
|
|
842
823
|
MQswCQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZUjAe
|
|
843
824
|
Fw0yNTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYw
|
|
@@ -865,7 +846,7 @@ NA3wJdl4DDUuQSV8hBgx6zoI1ZSGORprDFux7c6rhc77QZMSRrEgomBeklervEve
|
|
|
865
846
|
86ylWmZ3WWHV6RLMi8xNvjd71r4EPIGgY7BZU/VPBkq+uA7Gb6mbJnFgV43uh3xy
|
|
866
847
|
LRFgxIAphIukwTGSMZZR+AI+Qnp0BYTWovHXozOf3H8r6hozEoT02JHn0AeTfA==
|
|
867
848
|
-----END CERTIFICATE-----`,
|
|
868
|
-
|
|
849
|
+
`-----BEGIN CERTIFICATE-----
|
|
869
850
|
MIIF9DCCA9ygAwIBAgIRAPJLbRf52a18scn+p4eCaZ8wDQYJKoZIhvcNAQELBQAw
|
|
870
851
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
|
871
852
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjYwNTEzMDAwMDAw
|
|
@@ -899,1546 +880,1920 @@ RGMuHGnzS3hFIrRTfKxrzUZ9RzQWzEG3K6fJ3r2nqSltkeytis9DIBoFY9VmVyjL
|
|
|
899
880
|
M71DMi+y1+TRSJVClEMwvA4yL++7q9XZx5r5wBRWB4kQTKH5qyoZnDw7iiuh1lID
|
|
900
881
|
yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
|
|
901
882
|
-----END CERTIFICATE-----`
|
|
902
|
-
|
|
903
|
-
// Configuration
|
|
904
|
-
static defaultTimeoutMs = 8e3;
|
|
905
|
-
static defaultRetries = 1;
|
|
906
|
-
static maxCacheSize = 1e3;
|
|
907
|
-
// Cache Mechanism
|
|
908
|
-
static isCacheEnabled = false;
|
|
909
|
-
static wordCache = /* @__PURE__ */ new Map();
|
|
910
|
-
static dailyContentCache = null;
|
|
911
|
-
static autocompleteCache = [];
|
|
912
|
-
static autocompleteSet = /* @__PURE__ */ new Set();
|
|
913
|
-
static stemCache = /* @__PURE__ */ new Map();
|
|
914
|
-
/**
|
|
915
|
-
* Configures global client options such as network timeout, retries, and cache size.
|
|
916
|
-
*/
|
|
917
|
-
static configure(config) {
|
|
918
|
-
if (config.timeoutMs !== void 0)
|
|
919
|
-
this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
|
|
920
|
-
if (config.retries !== void 0)
|
|
921
|
-
this.defaultRetries = Math.max(0, config.retries);
|
|
922
|
-
if (config.cache !== void 0)
|
|
923
|
-
this.enableCache(config.cache);
|
|
924
|
-
if (config.maxCacheSize !== void 0)
|
|
925
|
-
this.maxCacheSize = Math.max(10, config.maxCacheSize);
|
|
926
|
-
}
|
|
927
|
-
/**
|
|
928
|
-
* Enables or disables in-memory caching for API requests.
|
|
929
|
-
*/
|
|
930
|
-
static enableCache(status = true) {
|
|
931
|
-
this.isCacheEnabled = status;
|
|
932
|
-
if (!status) {
|
|
933
|
-
this.clearCache();
|
|
934
|
-
}
|
|
883
|
+
];
|
|
935
884
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// src/data/keyboard-layout.ts
|
|
888
|
+
function keyboardSubCost(a, b) {
|
|
889
|
+
if (a === b)
|
|
890
|
+
return 0;
|
|
891
|
+
if (DIACRITIC_SIBLINGS[a] === b)
|
|
892
|
+
return DIACRITIC_SUB_COST;
|
|
893
|
+
const pa = KEYBOARD_COORDS[a];
|
|
894
|
+
const pb = KEYBOARD_COORDS[b];
|
|
895
|
+
if (!pa || !pb)
|
|
896
|
+
return 1;
|
|
897
|
+
const dx = Math.abs(pa[0] - pb[0]);
|
|
898
|
+
const dy = Math.abs(pa[1] - pb[1]);
|
|
899
|
+
if (dy === 0 && dx <= 1 + 1e-9)
|
|
900
|
+
return KEYBOARD_ROW_SUB_COST;
|
|
901
|
+
if (dy === 1 && dx <= 1 + 1e-9)
|
|
902
|
+
return KEYBOARD_DIAGONAL_SUB_COST;
|
|
903
|
+
return 1;
|
|
904
|
+
}
|
|
905
|
+
var KEYBOARD_ROWS, KEYBOARD_COORDS, DIACRITIC_SIBLINGS, KEYBOARD_ROW_SUB_COST, KEYBOARD_DIAGONAL_SUB_COST, DIACRITIC_SUB_COST, TRANSPOSITION_COST;
|
|
906
|
+
var init_keyboard_layout = __esm({
|
|
907
|
+
"src/data/keyboard-layout.ts"() {
|
|
908
|
+
"use strict";
|
|
909
|
+
init_cjs_shims();
|
|
910
|
+
KEYBOARD_ROWS = [
|
|
911
|
+
["qwertyu\u0131op\u011F\xFC", 0],
|
|
912
|
+
["asdfghjkl\u015Fi", 0.5],
|
|
913
|
+
["zxcvbnm\xF6\xE7", 1]
|
|
914
|
+
];
|
|
915
|
+
KEYBOARD_COORDS = (() => {
|
|
916
|
+
const coords = {};
|
|
917
|
+
KEYBOARD_ROWS.forEach(([keys, offset], row) => {
|
|
918
|
+
[...keys].forEach((key, col) => {
|
|
919
|
+
coords[key] = [col + offset, row];
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
return coords;
|
|
923
|
+
})();
|
|
924
|
+
DIACRITIC_SIBLINGS = {
|
|
925
|
+
\u0131: "i",
|
|
926
|
+
i: "\u0131",
|
|
927
|
+
\u00F6: "o",
|
|
928
|
+
o: "\xF6",
|
|
929
|
+
\u00FC: "u",
|
|
930
|
+
u: "\xFC",
|
|
931
|
+
\u015F: "s",
|
|
932
|
+
s: "\u015F",
|
|
933
|
+
\u00E7: "c",
|
|
934
|
+
c: "\xE7",
|
|
935
|
+
\u011F: "g",
|
|
936
|
+
g: "\u011F",
|
|
937
|
+
\u00E2: "a",
|
|
938
|
+
a: "\xE2"
|
|
939
|
+
};
|
|
940
|
+
KEYBOARD_ROW_SUB_COST = 0.4;
|
|
941
|
+
KEYBOARD_DIAGONAL_SUB_COST = 0.55;
|
|
942
|
+
DIACRITIC_SUB_COST = 0.3;
|
|
943
|
+
TRANSPOSITION_COST = 0.8;
|
|
945
944
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// src/lib/edit-distance.ts
|
|
948
|
+
function damerauLevenshtein(a, b) {
|
|
949
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
950
|
+
for (let i = 0; i <= a.length; i++)
|
|
951
|
+
dp[i][0] = i;
|
|
952
|
+
for (let j = 0; j <= b.length; j++)
|
|
953
|
+
dp[0][j] = j;
|
|
954
|
+
for (let i = 1; i <= a.length; i++) {
|
|
955
|
+
for (let j = 1; j <= b.length; j++) {
|
|
956
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
957
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
958
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
959
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return dp[a.length][b.length];
|
|
964
|
+
}
|
|
965
|
+
function keyboardAwareDistance(a, b) {
|
|
966
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
967
|
+
for (let i = 0; i <= a.length; i++)
|
|
968
|
+
dp[i][0] = i;
|
|
969
|
+
for (let j = 0; j <= b.length; j++)
|
|
970
|
+
dp[0][j] = j;
|
|
971
|
+
for (let i = 1; i <= a.length; i++) {
|
|
972
|
+
for (let j = 1; j <= b.length; j++) {
|
|
973
|
+
const cost = keyboardSubCost(a[i - 1], b[j - 1]);
|
|
974
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
975
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
976
|
+
dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + TRANSPOSITION_COST);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return dp[a.length][b.length];
|
|
981
|
+
}
|
|
982
|
+
var init_edit_distance = __esm({
|
|
983
|
+
"src/lib/edit-distance.ts"() {
|
|
984
|
+
"use strict";
|
|
985
|
+
init_cjs_shims();
|
|
986
|
+
init_keyboard_layout();
|
|
953
987
|
}
|
|
954
|
-
|
|
955
|
-
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// src/lib/html.ts
|
|
991
|
+
function htmlToPlainText(html) {
|
|
992
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/ /gi, " ").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'|’/gi, "'").replace(/&/gi, "&").replace(/[ \t]+/g, " ").replace(/[ \t]*\n[ \t]*/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
993
|
+
}
|
|
994
|
+
var init_html = __esm({
|
|
995
|
+
"src/lib/html.ts"() {
|
|
996
|
+
"use strict";
|
|
997
|
+
init_cjs_shims();
|
|
956
998
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// src/tdk.ts
|
|
1002
|
+
var fs, path, os, https, tls, TDK;
|
|
1003
|
+
var init_tdk = __esm({
|
|
1004
|
+
"src/tdk.ts"() {
|
|
1005
|
+
"use strict";
|
|
1006
|
+
init_cjs_shims();
|
|
1007
|
+
init_errors();
|
|
1008
|
+
init_morphology();
|
|
1009
|
+
init_misspellings();
|
|
1010
|
+
init_kubbealti_ca();
|
|
1011
|
+
init_edit_distance();
|
|
1012
|
+
init_html();
|
|
1013
|
+
fs = __toESM(require("fs"));
|
|
1014
|
+
path = __toESM(require("path"));
|
|
1015
|
+
os = __toESM(require("os"));
|
|
1016
|
+
https = __toESM(require("https"));
|
|
1017
|
+
tls = __toESM(require("tls"));
|
|
1018
|
+
init_misspellings();
|
|
1019
|
+
TDK = class {
|
|
1020
|
+
static BASE_URL = "https://sozluk.gov.tr";
|
|
1021
|
+
static AUDIO_API_HOST = "api.sozluk.gov.tr";
|
|
1022
|
+
static KUBBEALTI_HOST = "eski.lugatim.com";
|
|
1023
|
+
// Configuration
|
|
1024
|
+
static defaultTimeoutMs = 8e3;
|
|
1025
|
+
static defaultRetries = 1;
|
|
1026
|
+
static maxCacheSize = 1e3;
|
|
1027
|
+
// Cache Mechanism
|
|
1028
|
+
static isCacheEnabled = false;
|
|
1029
|
+
static wordCache = /* @__PURE__ */ new Map();
|
|
1030
|
+
static dailyContentCache = null;
|
|
1031
|
+
static autocompleteCache = [];
|
|
1032
|
+
static autocompleteSet = /* @__PURE__ */ new Set();
|
|
1033
|
+
static stemCache = /* @__PURE__ */ new Map();
|
|
1034
|
+
/**
|
|
1035
|
+
* Configures global client options such as network timeout, retries, and cache size.
|
|
1036
|
+
*/
|
|
1037
|
+
static configure(config) {
|
|
1038
|
+
if (config.timeoutMs !== void 0)
|
|
1039
|
+
this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
|
|
1040
|
+
if (config.retries !== void 0)
|
|
1041
|
+
this.defaultRetries = Math.max(0, config.retries);
|
|
1042
|
+
if (config.cache !== void 0)
|
|
1043
|
+
this.enableCache(config.cache);
|
|
1044
|
+
if (config.maxCacheSize !== void 0)
|
|
1045
|
+
this.maxCacheSize = Math.max(10, config.maxCacheSize);
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Enables or disables in-memory caching for API requests.
|
|
1049
|
+
*/
|
|
1050
|
+
static enableCache(status = true) {
|
|
1051
|
+
this.isCacheEnabled = status;
|
|
1052
|
+
if (!status) {
|
|
1053
|
+
this.clearCache();
|
|
972
1054
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Clears the internal cache.
|
|
1058
|
+
*/
|
|
1059
|
+
static clearCache() {
|
|
1060
|
+
this.wordCache.clear();
|
|
1061
|
+
this.dailyContentCache = null;
|
|
1062
|
+
this.autocompleteCache = [];
|
|
1063
|
+
this.autocompleteSet.clear();
|
|
1064
|
+
this.stemCache.clear();
|
|
1065
|
+
}
|
|
1066
|
+
static setBoundedCache(map, key, value) {
|
|
1067
|
+
if (map.size >= this.maxCacheSize) {
|
|
1068
|
+
const firstKey = map.keys().next().value;
|
|
1069
|
+
if (firstKey !== void 0)
|
|
1070
|
+
map.delete(firstKey);
|
|
976
1071
|
}
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1072
|
+
map.set(key, value);
|
|
1073
|
+
}
|
|
1074
|
+
static delay(ms) {
|
|
1075
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
|
|
1079
|
+
*/
|
|
1080
|
+
static async fetchWithRetry(url, options = {}, retries = this.defaultRetries, timeoutMs = this.defaultTimeoutMs) {
|
|
1081
|
+
let lastError;
|
|
1082
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
1083
|
+
try {
|
|
1084
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
1085
|
+
const headers = {
|
|
1086
|
+
"User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
|
|
1087
|
+
...options.headers || {}
|
|
1088
|
+
};
|
|
1089
|
+
const res = await fetch(url, { ...options, headers, signal });
|
|
1090
|
+
if (res.ok || res.status >= 400 && res.status < 500) {
|
|
1091
|
+
return res;
|
|
1092
|
+
}
|
|
1093
|
+
if (attempt < retries) {
|
|
1094
|
+
await this.delay(200 * (attempt + 1));
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
return res;
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
lastError = err;
|
|
1100
|
+
if (attempt < retries) {
|
|
1101
|
+
await this.delay(200 * (attempt + 1));
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
983
1105
|
}
|
|
1106
|
+
throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
|
|
1107
|
+
cause: lastError
|
|
1108
|
+
});
|
|
984
1109
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
* Fetches detailed information for a given word from the TDK Dictionary.
|
|
992
|
-
*/
|
|
993
|
-
static async getWord(word2) {
|
|
994
|
-
if (!word2 || word2.trim() === "") {
|
|
995
|
-
throw new TDKValidationError("Word parameter cannot be empty.");
|
|
996
|
-
}
|
|
997
|
-
const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
|
|
998
|
-
if (this.isCacheEnabled && this.wordCache.has(cleanWord)) {
|
|
999
|
-
return this.wordCache.get(cleanWord);
|
|
1000
|
-
}
|
|
1001
|
-
const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
|
|
1002
|
-
let response;
|
|
1003
|
-
try {
|
|
1004
|
-
response = await this.fetchWithRetry(url);
|
|
1005
|
-
} catch (error) {
|
|
1006
|
-
throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
|
|
1007
|
-
}
|
|
1008
|
-
if (!response.ok) {
|
|
1009
|
-
throw new TDKNetworkError(`Failed to fetch word from TDK: HTTP ${response.status}.`, {
|
|
1010
|
-
status: response.status
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
let data;
|
|
1014
|
-
try {
|
|
1015
|
-
data = await response.json();
|
|
1016
|
-
} catch (error) {
|
|
1017
|
-
throw new TDKNetworkError("Failed to fetch word from TDK: invalid JSON response.", { cause: error });
|
|
1018
|
-
}
|
|
1019
|
-
if (!Array.isArray(data) && data && "error" in data) {
|
|
1020
|
-
if (this.isCacheEnabled)
|
|
1021
|
-
this.setBoundedCache(this.wordCache, cleanWord, []);
|
|
1022
|
-
return [];
|
|
1023
|
-
}
|
|
1024
|
-
const results = data;
|
|
1025
|
-
if (this.isCacheEnabled) {
|
|
1026
|
-
this.setBoundedCache(this.wordCache, cleanWord, results);
|
|
1027
|
-
}
|
|
1028
|
-
return results;
|
|
1029
|
-
}
|
|
1030
|
-
/**
|
|
1031
|
-
* Helper method to get only the meanings (definitions) of a word as a string array.
|
|
1032
|
-
*/
|
|
1033
|
-
static async getMeanings(word2) {
|
|
1034
|
-
const results = await this.getWord(word2);
|
|
1035
|
-
if (results.length === 0)
|
|
1036
|
-
return [];
|
|
1037
|
-
const meanings = [];
|
|
1038
|
-
for (const result of results) {
|
|
1039
|
-
if (result.anlamlarListe) {
|
|
1040
|
-
for (const anlam of result.anlamlarListe) {
|
|
1041
|
-
if (anlam.anlam)
|
|
1042
|
-
meanings.push(anlam.anlam);
|
|
1110
|
+
/**
|
|
1111
|
+
* Fetches detailed information for a given word from the TDK Dictionary.
|
|
1112
|
+
*/
|
|
1113
|
+
static async getWord(word2) {
|
|
1114
|
+
if (!word2 || word2.trim() === "") {
|
|
1115
|
+
throw new TDKValidationError("Word parameter cannot be empty.");
|
|
1043
1116
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
1050
|
-
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
1051
|
-
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
1052
|
-
* instead bundled directly into its main JS asset as a
|
|
1053
|
-
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
1054
|
-
* page to find that asset's current hashed filename, downloads it (a few
|
|
1055
|
-
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
1056
|
-
* scraping of an implementation detail — if TDK's build stops embedding
|
|
1057
|
-
* this, this fails closed to `[]` rather than throwing.
|
|
1058
|
-
*/
|
|
1059
|
-
static async fetchAutocompleteData() {
|
|
1060
|
-
try {
|
|
1061
|
-
const homeResponse = await fetch(`${this.BASE_URL}/`, {
|
|
1062
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
1063
|
-
});
|
|
1064
|
-
if (!homeResponse.ok)
|
|
1065
|
-
return [];
|
|
1066
|
-
const html = await homeResponse.text();
|
|
1067
|
-
const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
|
|
1068
|
-
if (!scriptMatch)
|
|
1069
|
-
return [];
|
|
1070
|
-
const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
|
|
1071
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
1072
|
-
});
|
|
1073
|
-
if (!bundleResponse.ok)
|
|
1074
|
-
return [];
|
|
1075
|
-
const bundleJs = await bundleResponse.text();
|
|
1076
|
-
const startMarker = 'JSON.parse(`[{"madde":';
|
|
1077
|
-
const startIdx = bundleJs.indexOf(startMarker);
|
|
1078
|
-
if (startIdx === -1)
|
|
1079
|
-
return [];
|
|
1080
|
-
const jsonStart = startIdx + "JSON.parse(".length + 1;
|
|
1081
|
-
const jsonEnd = bundleJs.indexOf("`)", jsonStart);
|
|
1082
|
-
if (jsonEnd === -1)
|
|
1083
|
-
return [];
|
|
1084
|
-
const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
|
|
1085
|
-
return data.map((item) => item.madde).filter(Boolean);
|
|
1086
|
-
} catch {
|
|
1087
|
-
return [];
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Ensures TDK's ~81k headword list is loaded in memory for fast O(1) set operations.
|
|
1092
|
-
*/
|
|
1093
|
-
static async ensureAutocompleteLoaded() {
|
|
1094
|
-
if (this.autocompleteCache.length === 0) {
|
|
1095
|
-
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
1096
|
-
this.autocompleteSet = new Set(
|
|
1097
|
-
this.autocompleteCache.map((w) => w.toLocaleLowerCase("tr-TR"))
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
/**
|
|
1102
|
-
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
1103
|
-
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
1104
|
-
* and cached once per process regardless of `enableCache()` — the same
|
|
1105
|
-
* caching behavior as before — and only cleared by `clearCache()`.
|
|
1106
|
-
*/
|
|
1107
|
-
static async getSuggestions(prefix) {
|
|
1108
|
-
if (!prefix || prefix.trim() === "")
|
|
1109
|
-
return [];
|
|
1110
|
-
await this.ensureAutocompleteLoaded();
|
|
1111
|
-
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
1112
|
-
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Checks whether a word exists as a known headword in TDK dictionary.
|
|
1116
|
-
* Checks in-memory autocompleteSet (81k headwords) if loaded, or queries TDK API.
|
|
1117
|
-
*/
|
|
1118
|
-
static async isHeadword(word2) {
|
|
1119
|
-
if (!word2 || word2.trim() === "")
|
|
1120
|
-
return false;
|
|
1121
|
-
const clean = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1122
|
-
await this.ensureAutocompleteLoaded();
|
|
1123
|
-
if (this.autocompleteSet.size > 0) {
|
|
1124
|
-
return this.autocompleteSet.has(clean);
|
|
1125
|
-
}
|
|
1126
|
-
try {
|
|
1127
|
-
const results = await this.getWord(clean);
|
|
1128
|
-
return results.length > 0;
|
|
1129
|
-
} catch {
|
|
1130
|
-
return false;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* Generates candidate roots for a given Turkish word using progressive BFS suffix stripping,
|
|
1135
|
-
* consonant mutation restoration, and vowel drop restoration.
|
|
1136
|
-
*/
|
|
1137
|
-
static getStemCandidates(word2) {
|
|
1138
|
-
return getStemCandidates(word2);
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Finds the dictionary root (headword) of a word by checking direct existence
|
|
1142
|
-
* and evaluating candidate stems generated by morphological analysis.
|
|
1143
|
-
* Returns the root headword string if found, or null if no match in TDK.
|
|
1144
|
-
*/
|
|
1145
|
-
static async findRoot(word2) {
|
|
1146
|
-
if (!word2 || word2.trim() === "")
|
|
1147
|
-
return null;
|
|
1148
|
-
const clean = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1149
|
-
if (this.stemCache.has(clean)) {
|
|
1150
|
-
return this.stemCache.get(clean);
|
|
1151
|
-
}
|
|
1152
|
-
if (await this.isHeadword(clean)) {
|
|
1153
|
-
this.setBoundedCache(this.stemCache, clean, clean);
|
|
1154
|
-
return clean;
|
|
1155
|
-
}
|
|
1156
|
-
const candidates = getStemCandidates(clean);
|
|
1157
|
-
for (const candidate of candidates) {
|
|
1158
|
-
if (await this.isHeadword(candidate)) {
|
|
1159
|
-
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
1160
|
-
return candidate;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
this.setBoundedCache(this.stemCache, clean, null);
|
|
1164
|
-
return null;
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Performs morphological stemming on a Turkish word.
|
|
1168
|
-
* Returns a StemResult containing the original word, resolved root, and whether it is inflected.
|
|
1169
|
-
*/
|
|
1170
|
-
static async stem(word2) {
|
|
1171
|
-
if (!word2 || word2.trim() === "")
|
|
1172
|
-
return null;
|
|
1173
|
-
const clean = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1174
|
-
const root = await this.findRoot(word2);
|
|
1175
|
-
if (!root) {
|
|
1176
|
-
return null;
|
|
1177
|
-
}
|
|
1178
|
-
return {
|
|
1179
|
-
word: word2,
|
|
1180
|
-
root,
|
|
1181
|
-
isInflected: root !== clean,
|
|
1182
|
-
candidates: getStemCandidates(word2)
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Returns a list of proverbs and idioms containing the word.
|
|
1187
|
-
*/
|
|
1188
|
-
static async getProverbs(word2) {
|
|
1189
|
-
const results = await this.getWord(word2);
|
|
1190
|
-
if (results.length === 0)
|
|
1191
|
-
return [];
|
|
1192
|
-
const proverbs = [];
|
|
1193
|
-
for (const result of results) {
|
|
1194
|
-
if (result.atasozu) {
|
|
1195
|
-
for (const atasoz of result.atasozu) {
|
|
1196
|
-
if (atasoz.madde)
|
|
1197
|
-
proverbs.push(atasoz.madde);
|
|
1117
|
+
const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1118
|
+
if (this.isCacheEnabled && this.wordCache.has(cleanWord)) {
|
|
1119
|
+
return this.wordCache.get(cleanWord);
|
|
1198
1120
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
*/
|
|
1242
|
-
static async getExamples(word2) {
|
|
1243
|
-
const results = await this.getWord(word2);
|
|
1244
|
-
const examples = [];
|
|
1245
|
-
for (const result of results) {
|
|
1246
|
-
if (result.anlamlarListe) {
|
|
1247
|
-
for (const anlam of result.anlamlarListe) {
|
|
1248
|
-
if (anlam.orneklerListe) {
|
|
1249
|
-
for (const ornek of anlam.orneklerListe) {
|
|
1250
|
-
const author = ornek.yazar && ornek.yazar.length > 0 ? ornek.yazar[0].tam_adi : null;
|
|
1251
|
-
examples.push({ sentence: ornek.ornek, author });
|
|
1121
|
+
const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
|
|
1122
|
+
let response;
|
|
1123
|
+
try {
|
|
1124
|
+
response = await this.fetchWithRetry(url);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
|
|
1127
|
+
}
|
|
1128
|
+
if (!response.ok) {
|
|
1129
|
+
throw new TDKNetworkError(`Failed to fetch word from TDK: HTTP ${response.status}.`, {
|
|
1130
|
+
status: response.status
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
let data;
|
|
1134
|
+
try {
|
|
1135
|
+
data = await response.json();
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
throw new TDKNetworkError("Failed to fetch word from TDK: invalid JSON response.", { cause: error });
|
|
1138
|
+
}
|
|
1139
|
+
if (!Array.isArray(data) && data && "error" in data) {
|
|
1140
|
+
if (this.isCacheEnabled)
|
|
1141
|
+
this.setBoundedCache(this.wordCache, cleanWord, []);
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
const results = data;
|
|
1145
|
+
if (this.isCacheEnabled) {
|
|
1146
|
+
this.setBoundedCache(this.wordCache, cleanWord, results);
|
|
1147
|
+
}
|
|
1148
|
+
return results;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Helper method to get only the meanings (definitions) of a word as a string array.
|
|
1152
|
+
*/
|
|
1153
|
+
static async getMeanings(word2) {
|
|
1154
|
+
const results = await this.getWord(word2);
|
|
1155
|
+
if (results.length === 0)
|
|
1156
|
+
return [];
|
|
1157
|
+
const meanings = [];
|
|
1158
|
+
for (const result of results) {
|
|
1159
|
+
if (result.anlamlarListe) {
|
|
1160
|
+
for (const anlam of result.anlamlarListe) {
|
|
1161
|
+
if (anlam.anlam)
|
|
1162
|
+
meanings.push(anlam.anlam);
|
|
1252
1163
|
}
|
|
1253
1164
|
}
|
|
1254
1165
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
"User-Agent": "
|
|
1281
|
-
Origin: this.BASE_URL,
|
|
1282
|
-
Referer: `${this.BASE_URL}/`
|
|
1283
|
-
}
|
|
1284
|
-
},
|
|
1285
|
-
(res) => {
|
|
1286
|
-
let body = "";
|
|
1287
|
-
res.on("data", (chunk) => body += chunk);
|
|
1288
|
-
res.on("end", () => {
|
|
1289
|
-
try {
|
|
1290
|
-
const data = JSON.parse(body);
|
|
1291
|
-
resolve(Array.isArray(data) ? data : null);
|
|
1292
|
-
} catch {
|
|
1293
|
-
resolve(null);
|
|
1294
|
-
}
|
|
1166
|
+
return meanings;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
1170
|
+
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
1171
|
+
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
1172
|
+
* instead bundled directly into its main JS asset as a
|
|
1173
|
+
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
1174
|
+
* page to find that asset's current hashed filename, downloads it (a few
|
|
1175
|
+
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
1176
|
+
* scraping of an implementation detail — if TDK's build stops embedding
|
|
1177
|
+
* this, this fails closed to `[]` rather than throwing.
|
|
1178
|
+
*/
|
|
1179
|
+
static async fetchAutocompleteData() {
|
|
1180
|
+
try {
|
|
1181
|
+
const homeResponse = await fetch(`${this.BASE_URL}/`, {
|
|
1182
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
1183
|
+
});
|
|
1184
|
+
if (!homeResponse.ok)
|
|
1185
|
+
return [];
|
|
1186
|
+
const html = await homeResponse.text();
|
|
1187
|
+
const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
|
|
1188
|
+
if (!scriptMatch)
|
|
1189
|
+
return [];
|
|
1190
|
+
const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
|
|
1191
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
1295
1192
|
});
|
|
1193
|
+
if (!bundleResponse.ok)
|
|
1194
|
+
return [];
|
|
1195
|
+
const bundleJs = await bundleResponse.text();
|
|
1196
|
+
const startMarker = 'JSON.parse(`[{"madde":';
|
|
1197
|
+
const startIdx = bundleJs.indexOf(startMarker);
|
|
1198
|
+
if (startIdx === -1)
|
|
1199
|
+
return [];
|
|
1200
|
+
const jsonStart = startIdx + "JSON.parse(".length + 1;
|
|
1201
|
+
const jsonEnd = bundleJs.indexOf("`)", jsonStart);
|
|
1202
|
+
if (jsonEnd === -1)
|
|
1203
|
+
return [];
|
|
1204
|
+
const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
|
|
1205
|
+
return data.map((item) => item.madde).filter(Boolean);
|
|
1206
|
+
} catch {
|
|
1207
|
+
return [];
|
|
1296
1208
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* Returns synonyms ("eş anlamlı kelimeler") recorded for the word, pooled
|
|
1309
|
-
* across all of its meanings. Uses the same undocumented `gts-yeni`
|
|
1310
|
-
* endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
|
|
1311
|
-
*/
|
|
1312
|
-
static async getSynonyms(word2) {
|
|
1313
|
-
if (!word2 || word2.trim() === "")
|
|
1314
|
-
return [];
|
|
1315
|
-
const data = await this.fetchGtsYeni(word2.trim().toLocaleLowerCase("tr-TR"));
|
|
1316
|
-
if (!data)
|
|
1317
|
-
return [];
|
|
1318
|
-
const synonyms = [];
|
|
1319
|
-
for (const entry of data) {
|
|
1320
|
-
for (const anlam of entry.anlamlarListe ?? []) {
|
|
1321
|
-
for (const es of anlam.anlamEsAnlam ?? []) {
|
|
1322
|
-
if (es.deger)
|
|
1323
|
-
synonyms.push(es.deger);
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Ensures TDK's ~81k headword list is loaded in memory for fast O(1) set operations.
|
|
1212
|
+
*/
|
|
1213
|
+
static async ensureAutocompleteLoaded() {
|
|
1214
|
+
if (this.autocompleteCache.length === 0) {
|
|
1215
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
1216
|
+
this.autocompleteSet = new Set(
|
|
1217
|
+
this.autocompleteCache.map((w) => w.toLocaleLowerCase("tr-TR"))
|
|
1218
|
+
);
|
|
1324
1219
|
}
|
|
1325
1220
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1221
|
+
/**
|
|
1222
|
+
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
1223
|
+
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
1224
|
+
* and cached once per process regardless of `enableCache()` — the same
|
|
1225
|
+
* caching behavior as before — and only cleared by `clearCache()`.
|
|
1226
|
+
*/
|
|
1227
|
+
static async getSuggestions(prefix) {
|
|
1228
|
+
if (!prefix || prefix.trim() === "")
|
|
1229
|
+
return [];
|
|
1230
|
+
await this.ensureAutocompleteLoaded();
|
|
1231
|
+
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
1232
|
+
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Checks whether a word exists as a known headword in TDK dictionary.
|
|
1236
|
+
* Checks in-memory autocompleteSet (81k headwords) if loaded, or queries TDK API.
|
|
1237
|
+
*/
|
|
1238
|
+
static async isHeadword(word2) {
|
|
1239
|
+
if (!word2 || word2.trim() === "")
|
|
1240
|
+
return false;
|
|
1241
|
+
const clean = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1242
|
+
await this.ensureAutocompleteLoaded();
|
|
1243
|
+
if (this.autocompleteSet.size > 0) {
|
|
1244
|
+
return this.autocompleteSet.has(clean);
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
const results = await this.getWord(clean);
|
|
1248
|
+
return results.length > 0;
|
|
1249
|
+
} catch {
|
|
1250
|
+
return false;
|
|
1346
1251
|
}
|
|
1347
1252
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1253
|
+
/**
|
|
1254
|
+
* Generates candidate roots for a given Turkish word using progressive BFS suffix stripping,
|
|
1255
|
+
* consonant mutation restoration, and vowel drop restoration.
|
|
1256
|
+
*/
|
|
1257
|
+
static getStemCandidates(word2) {
|
|
1258
|
+
return getStemCandidates(word2);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Finds the dictionary root (headword) of a word by checking direct existence
|
|
1262
|
+
* and evaluating candidate stems generated by morphological analysis.
|
|
1263
|
+
* Returns the root headword string if found, or null if no match in TDK.
|
|
1264
|
+
*/
|
|
1265
|
+
static async findRoot(word2) {
|
|
1266
|
+
if (!word2 || word2.trim() === "")
|
|
1267
|
+
return null;
|
|
1268
|
+
const clean = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1269
|
+
if (this.stemCache.has(clean)) {
|
|
1270
|
+
return this.stemCache.get(clean);
|
|
1271
|
+
}
|
|
1272
|
+
if (await this.isHeadword(clean)) {
|
|
1273
|
+
this.setBoundedCache(this.stemCache, clean, clean);
|
|
1274
|
+
return clean;
|
|
1275
|
+
}
|
|
1276
|
+
const candidates = getStemCandidates(clean);
|
|
1277
|
+
for (const candidate of candidates) {
|
|
1278
|
+
if (await this.isHeadword(candidate)) {
|
|
1279
|
+
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
1280
|
+
return candidate;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
this.setBoundedCache(this.stemCache, clean, null);
|
|
1374
1284
|
return null;
|
|
1375
|
-
const buffer = await res.arrayBuffer();
|
|
1376
|
-
fs.writeFileSync(finalPath, Buffer.from(buffer));
|
|
1377
|
-
return finalPath;
|
|
1378
|
-
} catch {
|
|
1379
|
-
return null;
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* Checks spelling and returns suggestions if wrong.
|
|
1384
|
-
*/
|
|
1385
|
-
static async checkSpelling(word2) {
|
|
1386
|
-
if (!word2 || word2.trim() === "") {
|
|
1387
|
-
return { isCorrect: false, word: word2 };
|
|
1388
|
-
}
|
|
1389
|
-
const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1390
|
-
const results = await this.getWord(word2);
|
|
1391
|
-
if (results.length > 0) {
|
|
1392
|
-
return { isCorrect: true, word: word2 };
|
|
1393
|
-
}
|
|
1394
|
-
if (COMMON_MISSPELLINGS[cleanWord]) {
|
|
1395
|
-
return { isCorrect: false, word: word2, suggestion: COMMON_MISSPELLINGS[cleanWord] };
|
|
1396
|
-
}
|
|
1397
|
-
const seyMatch = cleanWord.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
1398
|
-
if (seyMatch && !SEY_EXCEPTIONS.has(cleanWord)) {
|
|
1399
|
-
let prefix = seyMatch[1];
|
|
1400
|
-
const suffix = seyMatch[2] || "";
|
|
1401
|
-
if (prefix === "hicbir")
|
|
1402
|
-
prefix = "hi\xE7bir";
|
|
1403
|
-
if (prefix === "cok")
|
|
1404
|
-
prefix = "\xE7ok";
|
|
1405
|
-
return {
|
|
1406
|
-
isCorrect: false,
|
|
1407
|
-
word: word2,
|
|
1408
|
-
suggestion: `${prefix} \u015Fey${suffix}`
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
const daily = await this.getDailyContent();
|
|
1412
|
-
if (daily) {
|
|
1413
|
-
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1414
|
-
if (syydMatch) {
|
|
1415
|
-
return { isCorrect: false, word: word2, suggestion: syydMatch.dogrukelime };
|
|
1416
|
-
}
|
|
1417
|
-
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1418
|
-
if (mixMatch) {
|
|
1419
|
-
return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
|
|
1420
1285
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
root
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
if (this.autocompleteCache.length === 0) {
|
|
1433
|
-
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
1434
|
-
}
|
|
1435
|
-
for (const candidate of this.autocompleteCache) {
|
|
1436
|
-
if (candidate.includes(" ")) {
|
|
1437
|
-
const candidateNoSpace = candidate.replace(/\s+/g, "").toLocaleLowerCase("tr-TR");
|
|
1438
|
-
if (candidateNoSpace === cleanWord) {
|
|
1439
|
-
return { isCorrect: false, word: word2, suggestion: candidate };
|
|
1286
|
+
/**
|
|
1287
|
+
* Performs morphological stemming on a Turkish word.
|
|
1288
|
+
* Returns a StemResult containing the original word, resolved root, and whether it is inflected.
|
|
1289
|
+
*/
|
|
1290
|
+
static async stem(word2) {
|
|
1291
|
+
if (!word2 || word2.trim() === "")
|
|
1292
|
+
return null;
|
|
1293
|
+
const clean = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1294
|
+
const root = await this.findRoot(word2);
|
|
1295
|
+
if (!root) {
|
|
1296
|
+
return null;
|
|
1440
1297
|
}
|
|
1298
|
+
return {
|
|
1299
|
+
word: word2,
|
|
1300
|
+
root,
|
|
1301
|
+
isInflected: root !== clean,
|
|
1302
|
+
candidates: getStemCandidates(word2)
|
|
1303
|
+
};
|
|
1441
1304
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1305
|
+
/**
|
|
1306
|
+
* Returns a list of proverbs and idioms containing the word.
|
|
1307
|
+
*/
|
|
1308
|
+
static async getProverbs(word2) {
|
|
1309
|
+
const results = await this.getWord(word2);
|
|
1310
|
+
if (results.length === 0)
|
|
1311
|
+
return [];
|
|
1312
|
+
const proverbs = [];
|
|
1313
|
+
for (const result of results) {
|
|
1314
|
+
if (result.atasozu) {
|
|
1315
|
+
for (const atasoz of result.atasozu) {
|
|
1316
|
+
if (atasoz.madde)
|
|
1317
|
+
proverbs.push(atasoz.madde);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return proverbs;
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Returns the etymological origin of the word, or "Türkçe" if TDK doesn't
|
|
1325
|
+
* record a foreign origin for it. Returns `null` only when the word itself
|
|
1326
|
+
* isn't found in the dictionary at all.
|
|
1327
|
+
*/
|
|
1328
|
+
static async getOrigin(word2) {
|
|
1329
|
+
const results = await this.getWord(word2);
|
|
1330
|
+
if (results.length === 0)
|
|
1331
|
+
return null;
|
|
1332
|
+
return results[0].lisan || "T\xFCrk\xE7e";
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Returns whether the word has a recorded foreign etymological origin.
|
|
1336
|
+
* Returns `null` (instead of a boolean) when the word isn't found at all.
|
|
1337
|
+
*/
|
|
1338
|
+
static async isForeignWord(word2) {
|
|
1339
|
+
const origin = await this.getOrigin(word2);
|
|
1340
|
+
if (origin === null)
|
|
1341
|
+
return null;
|
|
1342
|
+
return origin !== "T\xFCrk\xE7e";
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Groups a list of words by their etymological origin. Words not found in
|
|
1346
|
+
* the dictionary are grouped under "Bilinmiyor". Throttled like getWordsBatch.
|
|
1347
|
+
*/
|
|
1348
|
+
static async groupByOrigin(words) {
|
|
1349
|
+
const groups = {};
|
|
1350
|
+
for (const word2 of words) {
|
|
1351
|
+
const origin = await this.getOrigin(word2) ?? "Bilinmiyor";
|
|
1352
|
+
if (!groups[origin])
|
|
1353
|
+
groups[origin] = [];
|
|
1354
|
+
groups[origin].push(word2);
|
|
1355
|
+
await this.delay(200);
|
|
1356
|
+
}
|
|
1357
|
+
return groups;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Returns literature examples containing the word.
|
|
1361
|
+
*/
|
|
1362
|
+
static async getExamples(word2) {
|
|
1363
|
+
const results = await this.getWord(word2);
|
|
1364
|
+
const examples = [];
|
|
1365
|
+
for (const result of results) {
|
|
1366
|
+
if (result.anlamlarListe) {
|
|
1367
|
+
for (const anlam of result.anlamlarListe) {
|
|
1368
|
+
if (anlam.orneklerListe) {
|
|
1369
|
+
for (const ornek of anlam.orneklerListe) {
|
|
1370
|
+
const author = ornek.yazar && ornek.yazar.length > 0 ? ornek.yazar[0].tam_adi : null;
|
|
1371
|
+
examples.push({ sentence: ornek.ornek, author });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return examples;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Calls the `api.sozluk.gov.tr/gts-yeni` endpoint the official web UI uses
|
|
1381
|
+
* internally (richer than the public `/gts`: includes `seskod`,
|
|
1382
|
+
* `anlamEsAnlam`/`anlamKarsitAnlam`, etc). That endpoint 403s unless the
|
|
1383
|
+
* request looks like it came from a browser tab on sozluk.gov.tr: it needs
|
|
1384
|
+
* an `Origin`/`Referer` pair matching that site AND a browser-like
|
|
1385
|
+
* `User-Agent` (our usual `TDK-API-Nodejs-Wrapper/…` UA gets rejected).
|
|
1386
|
+
* `fetch` (undici) also strips a manually-set `Origin` header as a
|
|
1387
|
+
* forbidden header name, so this uses `node:https` directly instead.
|
|
1388
|
+
* This is inherently fragile scraping of an undocumented endpoint — if
|
|
1389
|
+
* TDK tightens this check further, this should fail closed to `null`
|
|
1390
|
+
* rather than throw.
|
|
1391
|
+
*/
|
|
1392
|
+
static fetchGtsYeni(word2) {
|
|
1393
|
+
return new Promise((resolve) => {
|
|
1394
|
+
const req = https.request(
|
|
1395
|
+
{
|
|
1396
|
+
hostname: this.AUDIO_API_HOST,
|
|
1397
|
+
path: `/gts-yeni?ara=${encodeURIComponent(word2)}`,
|
|
1398
|
+
method: "GET",
|
|
1399
|
+
headers: {
|
|
1400
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
|
1401
|
+
Origin: this.BASE_URL,
|
|
1402
|
+
Referer: `${this.BASE_URL}/`
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
(res) => {
|
|
1406
|
+
let body = "";
|
|
1407
|
+
res.on("data", (chunk) => body += chunk);
|
|
1408
|
+
res.on("end", () => {
|
|
1409
|
+
try {
|
|
1410
|
+
const data = JSON.parse(body);
|
|
1411
|
+
resolve(Array.isArray(data) ? data : null);
|
|
1412
|
+
} catch {
|
|
1413
|
+
resolve(null);
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
req.on("error", () => resolve(null));
|
|
1419
|
+
req.end();
|
|
1420
|
+
});
|
|
1458
1421
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
return
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
* request is independent and stateless) rather than one-at-a-time with a
|
|
1539
|
-
* delay: same total sample size (25) and hit probability as a sequential
|
|
1540
|
-
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
1541
|
-
* a miss resolves in roughly one round-trip time instead of several
|
|
1542
|
-
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
1543
|
-
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
1544
|
-
* cached `/icerik` response and could never find a rule outside whatever
|
|
1545
|
-
* the first draw happened to be. Returns `null` if no match turns up
|
|
1546
|
-
* within the attempt budget or the matched page can't be parsed.
|
|
1547
|
-
*/
|
|
1548
|
-
static async getRule(name) {
|
|
1549
|
-
if (!name || name.trim() === "")
|
|
1550
|
-
return null;
|
|
1551
|
-
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
1552
|
-
const BATCH_SIZE = 5;
|
|
1553
|
-
const ROUNDS = 5;
|
|
1554
|
-
for (let round = 0; round < ROUNDS; round++) {
|
|
1555
|
-
const batches = await Promise.all(
|
|
1556
|
-
Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
|
|
1557
|
-
);
|
|
1558
|
-
for (const rules of batches) {
|
|
1559
|
-
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
1560
|
-
if (match)
|
|
1561
|
-
return this.fetchRuleText(match.url);
|
|
1422
|
+
static async fetchSeskod(word2) {
|
|
1423
|
+
const data = await this.fetchGtsYeni(word2);
|
|
1424
|
+
const seskod = data?.[0]?.seskod;
|
|
1425
|
+
return seskod ? String(seskod) : null;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Returns synonyms ("eş anlamlı kelimeler") recorded for the word, pooled
|
|
1429
|
+
* across all of its meanings. Uses the same undocumented `gts-yeni`
|
|
1430
|
+
* endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
|
|
1431
|
+
*/
|
|
1432
|
+
static async getSynonyms(word2) {
|
|
1433
|
+
if (!word2 || word2.trim() === "")
|
|
1434
|
+
return [];
|
|
1435
|
+
const data = await this.fetchGtsYeni(word2.trim().toLocaleLowerCase("tr-TR"));
|
|
1436
|
+
if (!data)
|
|
1437
|
+
return [];
|
|
1438
|
+
const synonyms = [];
|
|
1439
|
+
for (const entry of data) {
|
|
1440
|
+
for (const anlam of entry.anlamlarListe ?? []) {
|
|
1441
|
+
for (const es of anlam.anlamEsAnlam ?? []) {
|
|
1442
|
+
if (es.deger)
|
|
1443
|
+
synonyms.push(es.deger);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return [...new Set(synonyms)];
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Returns antonyms ("zıt anlamlı kelimeler") recorded for the word, pooled
|
|
1451
|
+
* across all of its meanings. Uses the same undocumented `gts-yeni`
|
|
1452
|
+
* endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
|
|
1453
|
+
*/
|
|
1454
|
+
static async getAntonyms(word2) {
|
|
1455
|
+
if (!word2 || word2.trim() === "")
|
|
1456
|
+
return [];
|
|
1457
|
+
const data = await this.fetchGtsYeni(word2.trim().toLocaleLowerCase("tr-TR"));
|
|
1458
|
+
if (!data)
|
|
1459
|
+
return [];
|
|
1460
|
+
const antonyms = [];
|
|
1461
|
+
for (const entry of data) {
|
|
1462
|
+
for (const anlam of entry.anlamlarListe ?? []) {
|
|
1463
|
+
for (const ka of anlam.anlamKarsitAnlam ?? []) {
|
|
1464
|
+
if (ka.deger)
|
|
1465
|
+
antonyms.push(ka.deger);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return [...new Set(antonyms)];
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Returns the direct URL of the audio pronunciation, if TDK has one recorded for this word.
|
|
1473
|
+
*/
|
|
1474
|
+
static async getAudioUrl(word2) {
|
|
1475
|
+
if (!word2 || word2.trim() === "") {
|
|
1476
|
+
throw new TDKValidationError("Word parameter cannot be empty.");
|
|
1477
|
+
}
|
|
1478
|
+
const seskod = await this.fetchSeskod(word2.trim().toLocaleLowerCase("tr-TR"));
|
|
1479
|
+
if (!seskod)
|
|
1480
|
+
return null;
|
|
1481
|
+
return `https://${this.AUDIO_API_HOST}/ses/${encodeURIComponent(seskod)}.wav`;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Downloads the audio pronunciation to the specified path.
|
|
1485
|
+
*/
|
|
1486
|
+
static async downloadAudio(word2, destPath) {
|
|
1487
|
+
const url = await this.getAudioUrl(word2);
|
|
1488
|
+
if (!url)
|
|
1489
|
+
return null;
|
|
1490
|
+
const finalPath = destPath || path.join(os.tmpdir(), `${word2}.wav`);
|
|
1491
|
+
try {
|
|
1492
|
+
const res = await fetch(url);
|
|
1493
|
+
if (!res.ok)
|
|
1494
|
+
return null;
|
|
1495
|
+
const buffer = await res.arrayBuffer();
|
|
1496
|
+
fs.writeFileSync(finalPath, Buffer.from(buffer));
|
|
1497
|
+
return finalPath;
|
|
1498
|
+
} catch {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1562
1501
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
* `null` on any error — network, TLS, HTTP, or JSON parse.
|
|
1598
|
-
*/
|
|
1599
|
-
static fetchKubbealtiJson(path2) {
|
|
1600
|
-
return new Promise((resolve) => {
|
|
1601
|
-
const req = https.request(
|
|
1602
|
-
{
|
|
1603
|
-
hostname: this.KUBBEALTI_HOST,
|
|
1604
|
-
path: path2,
|
|
1605
|
-
method: "GET",
|
|
1606
|
-
ca: [...tls.rootCertificates, ...this.KUBBEALTI_EXTRA_CA],
|
|
1607
|
-
headers: {
|
|
1608
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
1502
|
+
/**
|
|
1503
|
+
* Checks spelling and returns suggestions if wrong.
|
|
1504
|
+
*/
|
|
1505
|
+
static async checkSpelling(word2) {
|
|
1506
|
+
if (!word2 || word2.trim() === "") {
|
|
1507
|
+
return { isCorrect: false, word: word2 };
|
|
1508
|
+
}
|
|
1509
|
+
const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1510
|
+
const results = await this.getWord(word2);
|
|
1511
|
+
if (results.length > 0) {
|
|
1512
|
+
return { isCorrect: true, word: word2 };
|
|
1513
|
+
}
|
|
1514
|
+
if (COMMON_MISSPELLINGS[cleanWord]) {
|
|
1515
|
+
return { isCorrect: false, word: word2, suggestion: COMMON_MISSPELLINGS[cleanWord] };
|
|
1516
|
+
}
|
|
1517
|
+
const seyMatch = cleanWord.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
1518
|
+
if (seyMatch && !SEY_EXCEPTIONS.has(cleanWord)) {
|
|
1519
|
+
let prefix = seyMatch[1];
|
|
1520
|
+
const suffix = seyMatch[2] || "";
|
|
1521
|
+
if (prefix === "hicbir")
|
|
1522
|
+
prefix = "hi\xE7bir";
|
|
1523
|
+
if (prefix === "cok")
|
|
1524
|
+
prefix = "\xE7ok";
|
|
1525
|
+
return {
|
|
1526
|
+
isCorrect: false,
|
|
1527
|
+
word: word2,
|
|
1528
|
+
suggestion: `${prefix} \u015Fey${suffix}`
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
const daily = await this.getDailyContent();
|
|
1532
|
+
if (daily) {
|
|
1533
|
+
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1534
|
+
if (syydMatch) {
|
|
1535
|
+
return { isCorrect: false, word: word2, suggestion: syydMatch.dogrukelime };
|
|
1609
1536
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
res.resume();
|
|
1614
|
-
resolve(null);
|
|
1615
|
-
return;
|
|
1537
|
+
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === cleanWord);
|
|
1538
|
+
if (mixMatch) {
|
|
1539
|
+
return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
|
|
1616
1540
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1541
|
+
}
|
|
1542
|
+
const root = await this.findRoot(word2);
|
|
1543
|
+
if (root) {
|
|
1544
|
+
const isInflected = root !== cleanWord;
|
|
1545
|
+
return {
|
|
1546
|
+
isCorrect: true,
|
|
1547
|
+
word: word2,
|
|
1548
|
+
isInflected,
|
|
1549
|
+
root
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
if (this.autocompleteCache.length === 0) {
|
|
1553
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
1554
|
+
}
|
|
1555
|
+
for (const candidate of this.autocompleteCache) {
|
|
1556
|
+
if (candidate.includes(" ")) {
|
|
1557
|
+
const candidateNoSpace = candidate.replace(/\s+/g, "").toLocaleLowerCase("tr-TR");
|
|
1558
|
+
if (candidateNoSpace === cleanWord) {
|
|
1559
|
+
return { isCorrect: false, word: word2, suggestion: candidate };
|
|
1624
1560
|
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
let best = null;
|
|
1564
|
+
for (const candidate of this.autocompleteCache) {
|
|
1565
|
+
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
1566
|
+
continue;
|
|
1567
|
+
if (Math.abs(candidate.length - cleanWord.length) > 2)
|
|
1568
|
+
continue;
|
|
1569
|
+
const rawDist = damerauLevenshtein(cleanWord, candidate);
|
|
1570
|
+
if (rawDist === 0 || rawDist > 2)
|
|
1571
|
+
continue;
|
|
1572
|
+
const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
|
|
1573
|
+
const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
|
|
1574
|
+
const score = keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
|
|
1575
|
+
const better = !best || score < best.score - 1e-9 || Math.abs(score - best.score) < 1e-9 && firstMismatch < best.firstMismatch || Math.abs(score - best.score) < 1e-9 && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
|
|
1576
|
+
if (better) {
|
|
1577
|
+
best = { candidate, score, rawDist, firstMismatch, lengthMismatch };
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
|
|
1581
|
+
return { isCorrect: false, word: word2, suggestion: best.candidate };
|
|
1582
|
+
}
|
|
1583
|
+
return { isCorrect: false, word: word2 };
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
1587
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
1588
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
1589
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
1590
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
1591
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
1592
|
+
*/
|
|
1593
|
+
static async getDailyContent(bypassCache = false) {
|
|
1594
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
|
|
1595
|
+
return this.dailyContentCache;
|
|
1596
|
+
try {
|
|
1597
|
+
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
1598
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
1625
1599
|
});
|
|
1600
|
+
if (response.ok) {
|
|
1601
|
+
const data = await response.json();
|
|
1602
|
+
if (!bypassCache && this.isCacheEnabled)
|
|
1603
|
+
this.dailyContentCache = data;
|
|
1604
|
+
return data;
|
|
1605
|
+
}
|
|
1606
|
+
} catch {
|
|
1607
|
+
return null;
|
|
1626
1608
|
}
|
|
1627
|
-
);
|
|
1628
|
-
req.on("error", () => resolve(null));
|
|
1629
|
-
req.end();
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
/**
|
|
1633
|
-
* Kubbealtı indexes headwords with full classical Turkish orthography,
|
|
1634
|
-
* including letters that a plain-ASCII-ish query tends to drop — most
|
|
1635
|
-
* commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
|
|
1636
|
-
* Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
|
|
1637
|
-
* entirely (verified: even "ruzgâr" alone still misses — it's the missing
|
|
1638
|
-
* ü, not the missing â, that actually breaks the match). This generates
|
|
1639
|
-
* single-letter-substitution variants to retry, one substitution per
|
|
1640
|
-
* variant (not combinatorial) — covers the overwhelmingly common case of
|
|
1641
|
-
* one "de-Turkished" letter without an explosion of API calls for words
|
|
1642
|
-
* with several.
|
|
1643
|
-
*/
|
|
1644
|
-
static TURKISH_DEASCII_MAP = {
|
|
1645
|
-
a: ["\xE2"],
|
|
1646
|
-
i: ["\u0131", "\xEE"],
|
|
1647
|
-
o: ["\xF6"],
|
|
1648
|
-
u: ["\xFC", "\xFB"],
|
|
1649
|
-
c: ["\xE7"],
|
|
1650
|
-
g: ["\u011F"],
|
|
1651
|
-
s: ["\u015F"]
|
|
1652
|
-
};
|
|
1653
|
-
static generateTurkishVariants(word2) {
|
|
1654
|
-
const lower = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1655
|
-
const variants = [];
|
|
1656
|
-
for (let i = 0; i < lower.length; i++) {
|
|
1657
|
-
for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
|
|
1658
|
-
variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
return variants;
|
|
1662
|
-
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
|
|
1665
|
-
* word, scraped from the site's own data API — undocumented, and Kubbealtı
|
|
1666
|
-
* Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
|
|
1667
|
-
* openly-published data, so use this in line with their terms. `anlam` is
|
|
1668
|
-
* raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
|
|
1669
|
-
* plain text. Falls back to `generateTurkishVariants()` if the exact query
|
|
1670
|
-
* comes up empty (see its doc comment). Returns `null` on any fetch/parse
|
|
1671
|
-
* failure, `[]` if no variant matches either.
|
|
1672
|
-
*/
|
|
1673
|
-
static async getKubbealti(word2) {
|
|
1674
|
-
if (!word2 || word2.trim() === "")
|
|
1675
|
-
return null;
|
|
1676
|
-
const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word2.trim())}/`);
|
|
1677
|
-
if (!data || !Array.isArray(data.content))
|
|
1678
|
-
return null;
|
|
1679
|
-
if (data.content.length > 0) {
|
|
1680
|
-
return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
|
|
1681
|
-
}
|
|
1682
|
-
for (const variant of this.generateTurkishVariants(word2)) {
|
|
1683
|
-
const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
|
|
1684
|
-
if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
|
|
1685
|
-
return variantData.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return [];
|
|
1689
|
-
}
|
|
1690
|
-
/**
|
|
1691
|
-
* Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
|
|
1692
|
-
* plain text via `htmlToPlainText()`.
|
|
1693
|
-
*/
|
|
1694
|
-
static async getKubbealtiMeanings(word2) {
|
|
1695
|
-
const entries = await this.getKubbealti(word2);
|
|
1696
|
-
if (!entries)
|
|
1697
|
-
return null;
|
|
1698
|
-
return entries.map((e) => this.htmlToPlainText(e.anlam));
|
|
1699
|
-
}
|
|
1700
|
-
/**
|
|
1701
|
-
* Autocomplete suggestions from Kubbealtı Lugatı's own typeahead endpoint
|
|
1702
|
-
* (separate from `getSuggestions()`, which uses TDK's data).
|
|
1703
|
-
*/
|
|
1704
|
-
static async getKubbealtiSuggestions(prefix) {
|
|
1705
|
-
if (!prefix || prefix.trim() === "")
|
|
1706
|
-
return [];
|
|
1707
|
-
const data = await this.fetchKubbealtiJson(`/rest/word-search/${encodeURIComponent(prefix.trim())}`);
|
|
1708
|
-
if (!Array.isArray(data))
|
|
1709
|
-
return [];
|
|
1710
|
-
return data.map((item) => item.display).filter(Boolean);
|
|
1711
|
-
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Returns the etymology paragraph for a word from Nişanyan Sözlük, scraped
|
|
1714
|
-
* from that page's server-rendered `<meta name="description">` tag (the
|
|
1715
|
-
* page already puts the full etymology text there for SEO, so no need to
|
|
1716
|
-
* parse the site's internal SvelteKit data format). Returns `null` if the
|
|
1717
|
-
* word isn't found (the page falls back to a generic site tagline in that
|
|
1718
|
-
* case) or the request fails.
|
|
1719
|
-
*/
|
|
1720
|
-
static async getNisanyan(word2) {
|
|
1721
|
-
if (!word2 || word2.trim() === "")
|
|
1722
|
-
return null;
|
|
1723
|
-
try {
|
|
1724
|
-
const response = await fetch(
|
|
1725
|
-
`https://www.nisanyansozluk.com/kelime/${encodeURIComponent(word2.trim().toLocaleLowerCase("tr-TR"))}`,
|
|
1726
|
-
{ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } }
|
|
1727
|
-
);
|
|
1728
|
-
if (!response.ok)
|
|
1729
1609
|
return null;
|
|
1730
|
-
const html = await response.text();
|
|
1731
|
-
const match = html.match(/<meta name="description" content="([^"]*)"/);
|
|
1732
|
-
if (!match)
|
|
1733
|
-
return null;
|
|
1734
|
-
const description = this.htmlToPlainText(match[1]);
|
|
1735
|
-
if (description === "\xC7a\u011Fda\u015F T\xFCrk\xE7enin Etimolojisi")
|
|
1736
|
-
return null;
|
|
1737
|
-
return description;
|
|
1738
|
-
} catch {
|
|
1739
|
-
return null;
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
static async fetchWiktionaryEntry(title) {
|
|
1743
|
-
try {
|
|
1744
|
-
const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
|
|
1745
|
-
title
|
|
1746
|
-
)}&format=json&explaintext=1&formatversion=2`;
|
|
1747
|
-
const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
|
|
1748
|
-
if (!response.ok)
|
|
1749
|
-
return null;
|
|
1750
|
-
const data = await response.json();
|
|
1751
|
-
const page = data?.query?.pages?.[0];
|
|
1752
|
-
if (!page || page.missing || !page.extract)
|
|
1753
|
-
return null;
|
|
1754
|
-
const raw = page.extract;
|
|
1755
|
-
const sections = {};
|
|
1756
|
-
const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
|
|
1757
|
-
for (let i = 1; i < parts.length; i += 3) {
|
|
1758
|
-
const title2 = parts[i + 1]?.trim();
|
|
1759
|
-
const content = parts[i + 2]?.trim();
|
|
1760
|
-
if (title2)
|
|
1761
|
-
sections[title2] = content ?? "";
|
|
1762
|
-
}
|
|
1763
|
-
return { raw, sections };
|
|
1764
|
-
} catch {
|
|
1765
|
-
return null;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
/**
|
|
1769
|
-
* Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
|
|
1770
|
-
* via MediaWiki's official Action API (`action=query&prop=extracts`) — no
|
|
1771
|
-
* scraping involved, this is a stable, documented public API. `sections`
|
|
1772
|
-
* splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
|
|
1773
|
-
* markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
|
|
1774
|
-
* unsplit text. This wiki has title capitalization turned off
|
|
1775
|
-
* ($wgCapitalLinks=false — common for Wiktionaries, since case is
|
|
1776
|
-
* meaningful for a dictionary: "Türkiye" the country vs. a lowercase
|
|
1777
|
-
* common word), so an exact-case miss retries with the first letter
|
|
1778
|
-
* uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
|
|
1779
|
-
* "Istanbul") before giving up. Returns `null` if neither is found or the
|
|
1780
|
-
* request fails.
|
|
1781
|
-
*/
|
|
1782
|
-
static async getWiktionary(word2) {
|
|
1783
|
-
if (!word2 || word2.trim() === "")
|
|
1784
|
-
return null;
|
|
1785
|
-
const trimmed = word2.trim();
|
|
1786
|
-
const direct = await this.fetchWiktionaryEntry(trimmed);
|
|
1787
|
-
if (direct)
|
|
1788
|
-
return direct;
|
|
1789
|
-
const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
|
|
1790
|
-
if (capitalized === trimmed)
|
|
1791
|
-
return null;
|
|
1792
|
-
return this.fetchWiktionaryEntry(capitalized);
|
|
1793
|
-
}
|
|
1794
|
-
/**
|
|
1795
|
-
* Convenience filter over `getWiktionary()`: returns just one section's
|
|
1796
|
-
* text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
|
|
1797
|
-
* case-insensitively. Returns `null` if the word or the section isn't found.
|
|
1798
|
-
*/
|
|
1799
|
-
static async getWiktionarySection(word2, sectionName) {
|
|
1800
|
-
const entry = await this.getWiktionary(word2);
|
|
1801
|
-
if (!entry)
|
|
1802
|
-
return null;
|
|
1803
|
-
const key = Object.keys(entry.sections).find(
|
|
1804
|
-
(k) => k.toLocaleLowerCase("tr-TR") === sectionName.trim().toLocaleLowerCase("tr-TR")
|
|
1805
|
-
);
|
|
1806
|
-
return key ? entry.sections[key] : null;
|
|
1807
|
-
}
|
|
1808
|
-
/**
|
|
1809
|
-
* Returns compound words that contain this word.
|
|
1810
|
-
*/
|
|
1811
|
-
static async getCompoundWords(word2) {
|
|
1812
|
-
const results = await this.getWord(word2);
|
|
1813
|
-
if (results.length === 0)
|
|
1814
|
-
return [];
|
|
1815
|
-
const compound = [];
|
|
1816
|
-
for (const result of results) {
|
|
1817
|
-
if (result.birlesikler) {
|
|
1818
|
-
const words = result.birlesikler.split(",").map((w) => w.trim());
|
|
1819
|
-
compound.push(...words);
|
|
1820
1610
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1611
|
+
/**
|
|
1612
|
+
* Returns today's word of the day along with all of its listed meanings.
|
|
1613
|
+
*/
|
|
1614
|
+
static async getWordOfTheDay() {
|
|
1615
|
+
const daily = await this.getDailyContent();
|
|
1616
|
+
if (!daily || daily.kelime.length === 0)
|
|
1617
|
+
return null;
|
|
1618
|
+
const word2 = daily.kelime[0].madde;
|
|
1619
|
+
const meanings = daily.kelime.filter((k) => k.madde === word2).map((k) => k.anlam);
|
|
1620
|
+
return { word: word2, meanings };
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Picks a random entry (word or proverb) from today's daily content.
|
|
1624
|
+
* Note: this samples from today's `getDailyContent()` picks, not the full dictionary.
|
|
1625
|
+
*/
|
|
1626
|
+
static async getRandomWord() {
|
|
1627
|
+
const daily = await this.getDailyContent();
|
|
1628
|
+
if (!daily)
|
|
1629
|
+
return null;
|
|
1630
|
+
const pool = [
|
|
1631
|
+
...daily.kelime.map((k) => ({ type: "kelime", madde: k.madde, anlam: k.anlam })),
|
|
1632
|
+
...daily.atasoz.map((a) => ({ type: "atasoz", madde: a.madde, anlam: a.anlam }))
|
|
1633
|
+
];
|
|
1634
|
+
if (pool.length === 0)
|
|
1635
|
+
return null;
|
|
1636
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Returns the spelling-rule page(s) ("yazım kuralları") linked from TDK's
|
|
1640
|
+
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
1641
|
+
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
1642
|
+
* appears to hand back a single randomly-rotated rule per request, so two
|
|
1643
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
1644
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
1645
|
+
* draw even when `enableCache(true)` is on.
|
|
1646
|
+
*/
|
|
1647
|
+
static async getKurallar(bypassCache = false) {
|
|
1648
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
1649
|
+
return daily?.kural ?? [];
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Fetches the full plain-text content of a named spelling rule (matched
|
|
1653
|
+
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
1654
|
+
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
1655
|
+
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
1656
|
+
* draw would rarely match a given name — this re-draws until it finds a
|
|
1657
|
+
* match or gives up. Draws happen in concurrent batches (each `/icerik`
|
|
1658
|
+
* request is independent and stateless) rather than one-at-a-time with a
|
|
1659
|
+
* delay: same total sample size (25) and hit probability as a sequential
|
|
1660
|
+
* loop, but bounded to a handful of round-trips instead of 25 of them, so
|
|
1661
|
+
* a miss resolves in roughly one round-trip time instead of several
|
|
1662
|
+
* seconds. Every draw bypasses `dailyContentCache` — without that, once
|
|
1663
|
+
* `enableCache(true)` is on, every attempt would just re-read the same
|
|
1664
|
+
* cached `/icerik` response and could never find a rule outside whatever
|
|
1665
|
+
* the first draw happened to be. Returns `null` if no match turns up
|
|
1666
|
+
* within the attempt budget or the matched page can't be parsed.
|
|
1667
|
+
*/
|
|
1668
|
+
static async getRule(name) {
|
|
1669
|
+
if (!name || name.trim() === "")
|
|
1670
|
+
return null;
|
|
1671
|
+
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
1672
|
+
const BATCH_SIZE = 5;
|
|
1673
|
+
const ROUNDS = 5;
|
|
1674
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
1675
|
+
const batches = await Promise.all(
|
|
1676
|
+
Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
|
|
1677
|
+
);
|
|
1678
|
+
for (const rules of batches) {
|
|
1679
|
+
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
1680
|
+
if (match)
|
|
1681
|
+
return this.fetchRuleText(match.url);
|
|
1841
1682
|
}
|
|
1842
1683
|
}
|
|
1684
|
+
return null;
|
|
1843
1685
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
harmony: this.checkVowelHarmony(a),
|
|
1868
|
-
labialHarmony: this.checkLabialHarmony(a)
|
|
1869
|
-
},
|
|
1870
|
-
b: {
|
|
1871
|
-
word: b,
|
|
1872
|
-
meaningCount: meaningsB.length,
|
|
1873
|
-
origin: originB,
|
|
1874
|
-
syllables: this.syllabicate(b),
|
|
1875
|
-
harmony: this.checkVowelHarmony(b),
|
|
1876
|
-
labialHarmony: this.checkLabialHarmony(b)
|
|
1686
|
+
/**
|
|
1687
|
+
* `tdk.gov.tr` rule pages are WordPress/Avada-themed. The actual article
|
|
1688
|
+
* text lives in `<div ... itemprop="text">...</div>` right before a
|
|
1689
|
+
* `<footer class="entry...">` (share buttons, author box, structured-data
|
|
1690
|
+
* spans) — cutting there avoids that trailing cruft.
|
|
1691
|
+
*/
|
|
1692
|
+
static async fetchRuleText(url) {
|
|
1693
|
+
try {
|
|
1694
|
+
const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
|
|
1695
|
+
if (!response.ok)
|
|
1696
|
+
return null;
|
|
1697
|
+
const html = await response.text();
|
|
1698
|
+
const marker = html.indexOf('itemprop="text"');
|
|
1699
|
+
if (marker === -1)
|
|
1700
|
+
return null;
|
|
1701
|
+
const contentStart = html.indexOf(">", marker) + 1;
|
|
1702
|
+
const contentEnd = html.indexOf("<footer", contentStart);
|
|
1703
|
+
if (contentEnd === -1)
|
|
1704
|
+
return null;
|
|
1705
|
+
return htmlToPlainText(html.slice(contentStart, contentEnd));
|
|
1706
|
+
} catch {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1877
1709
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
for (const anlam of result.anlamlarListe ?? []) {
|
|
1917
|
-
if (anlam.anlam)
|
|
1918
|
-
return anlam.anlam;
|
|
1710
|
+
/**
|
|
1711
|
+
* GETs a JSON path from Kubbealtı Lugatı's data API (`eski.lugatim.com`),
|
|
1712
|
+
* supplying `KUBBEALTI_EXTRA_CA` to work around that host's incomplete
|
|
1713
|
+
* certificate chain (see the constant's doc comment). Fails closed to
|
|
1714
|
+
* `null` on any error — network, TLS, HTTP, or JSON parse.
|
|
1715
|
+
*/
|
|
1716
|
+
static fetchKubbealtiJson(path2) {
|
|
1717
|
+
return new Promise((resolve) => {
|
|
1718
|
+
const req = https.request(
|
|
1719
|
+
{
|
|
1720
|
+
hostname: this.KUBBEALTI_HOST,
|
|
1721
|
+
path: path2,
|
|
1722
|
+
method: "GET",
|
|
1723
|
+
ca: [...tls.rootCertificates, ...KUBBEALTI_EXTRA_CA],
|
|
1724
|
+
headers: {
|
|
1725
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
(res) => {
|
|
1729
|
+
if (res.statusCode !== 200) {
|
|
1730
|
+
res.resume();
|
|
1731
|
+
resolve(null);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
let body = "";
|
|
1735
|
+
res.on("data", (chunk) => body += chunk);
|
|
1736
|
+
res.on("end", () => {
|
|
1737
|
+
try {
|
|
1738
|
+
resolve(JSON.parse(body));
|
|
1739
|
+
} catch {
|
|
1740
|
+
resolve(null);
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
);
|
|
1745
|
+
req.on("error", () => resolve(null));
|
|
1746
|
+
req.end();
|
|
1747
|
+
});
|
|
1919
1748
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
const
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1749
|
+
/**
|
|
1750
|
+
* Kubbealtı indexes headwords with full classical Turkish orthography,
|
|
1751
|
+
* including letters that a plain-ASCII-ish query tends to drop — most
|
|
1752
|
+
* commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
|
|
1753
|
+
* Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
|
|
1754
|
+
* entirely (verified: even "ruzgâr" alone still misses — it's the missing
|
|
1755
|
+
* ü, not the missing â, that actually breaks the match). This generates
|
|
1756
|
+
* single-letter-substitution variants to retry, one substitution per
|
|
1757
|
+
* variant (not combinatorial) — covers the overwhelmingly common case of
|
|
1758
|
+
* one "de-Turkished" letter without an explosion of API calls for words
|
|
1759
|
+
* with several.
|
|
1760
|
+
*/
|
|
1761
|
+
static TURKISH_DEASCII_MAP = {
|
|
1762
|
+
a: ["\xE2"],
|
|
1763
|
+
i: ["\u0131", "\xEE"],
|
|
1764
|
+
o: ["\xF6"],
|
|
1765
|
+
u: ["\xFC", "\xFB"],
|
|
1766
|
+
c: ["\xE7"],
|
|
1767
|
+
g: ["\u011F"],
|
|
1768
|
+
s: ["\u015F"]
|
|
1769
|
+
};
|
|
1770
|
+
static generateTurkishVariants(word2) {
|
|
1771
|
+
const lower = word2.trim().toLocaleLowerCase("tr-TR");
|
|
1772
|
+
const variants = [];
|
|
1773
|
+
for (let i = 0; i < lower.length; i++) {
|
|
1774
|
+
for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
|
|
1775
|
+
variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return variants;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
|
|
1782
|
+
* word, scraped from the site's own data API — undocumented, and Kubbealtı
|
|
1783
|
+
* Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
|
|
1784
|
+
* openly-published data, so use this in line with their terms. `anlam` is
|
|
1785
|
+
* raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
|
|
1786
|
+
* plain text. Falls back to `generateTurkishVariants()` if the exact query
|
|
1787
|
+
* comes up empty (see its doc comment). Returns `null` on any fetch/parse
|
|
1788
|
+
* failure, `[]` if no variant matches either.
|
|
1789
|
+
*/
|
|
1790
|
+
static async getKubbealti(word2) {
|
|
1791
|
+
if (!word2 || word2.trim() === "")
|
|
1792
|
+
return null;
|
|
1793
|
+
const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word2.trim())}/`);
|
|
1794
|
+
if (!data || !Array.isArray(data.content))
|
|
1795
|
+
return null;
|
|
1796
|
+
if (data.content.length > 0) {
|
|
1797
|
+
return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
|
|
1798
|
+
}
|
|
1799
|
+
for (const variant of this.generateTurkishVariants(word2)) {
|
|
1800
|
+
const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
|
|
1801
|
+
if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
|
|
1802
|
+
return variantData.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
|
|
1950
1803
|
}
|
|
1951
1804
|
}
|
|
1805
|
+
return [];
|
|
1952
1806
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1807
|
+
/**
|
|
1808
|
+
* Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
|
|
1809
|
+
* plain text via `htmlToPlainText()`.
|
|
1810
|
+
*/
|
|
1811
|
+
static async getKubbealtiMeanings(word2) {
|
|
1812
|
+
const entries = await this.getKubbealti(word2);
|
|
1813
|
+
if (!entries)
|
|
1814
|
+
return null;
|
|
1815
|
+
return entries.map((e) => htmlToPlainText(e.anlam));
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Autocomplete suggestions from Kubbealtı Lugatı's own typeahead endpoint
|
|
1819
|
+
* (separate from `getSuggestions()`, which uses TDK's data).
|
|
1820
|
+
*/
|
|
1821
|
+
static async getKubbealtiSuggestions(prefix) {
|
|
1822
|
+
if (!prefix || prefix.trim() === "")
|
|
1823
|
+
return [];
|
|
1824
|
+
const data = await this.fetchKubbealtiJson(`/rest/word-search/${encodeURIComponent(prefix.trim())}`);
|
|
1825
|
+
if (!Array.isArray(data))
|
|
1826
|
+
return [];
|
|
1827
|
+
return data.map((item) => item.display).filter(Boolean);
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Returns the etymology paragraph for a word from Nişanyan Sözlük, scraped
|
|
1831
|
+
* from that page's server-rendered `<meta name="description">` tag (the
|
|
1832
|
+
* page already puts the full etymology text there for SEO, so no need to
|
|
1833
|
+
* parse the site's internal SvelteKit data format). Returns `null` if the
|
|
1834
|
+
* word isn't found (the page falls back to a generic site tagline in that
|
|
1835
|
+
* case) or the request fails.
|
|
1836
|
+
*/
|
|
1837
|
+
static async getNisanyan(word2) {
|
|
1838
|
+
if (!word2 || word2.trim() === "")
|
|
1839
|
+
return null;
|
|
1840
|
+
try {
|
|
1841
|
+
const response = await fetch(
|
|
1842
|
+
`https://www.nisanyansozluk.com/kelime/${encodeURIComponent(word2.trim().toLocaleLowerCase("tr-TR"))}`,
|
|
1843
|
+
{ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } }
|
|
1844
|
+
);
|
|
1845
|
+
if (!response.ok)
|
|
1846
|
+
return null;
|
|
1847
|
+
const html = await response.text();
|
|
1848
|
+
const match = html.match(/<meta name="description" content="([^"]*)"/);
|
|
1849
|
+
if (!match)
|
|
1850
|
+
return null;
|
|
1851
|
+
const description = htmlToPlainText(match[1]);
|
|
1852
|
+
if (description === "\xC7a\u011Fda\u015F T\xFCrk\xE7enin Etimolojisi")
|
|
1853
|
+
return null;
|
|
1854
|
+
return description;
|
|
1855
|
+
} catch {
|
|
1856
|
+
return null;
|
|
1984
1857
|
}
|
|
1985
1858
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
1859
|
+
static async fetchWiktionaryEntry(title) {
|
|
1860
|
+
try {
|
|
1861
|
+
const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
|
|
1862
|
+
title
|
|
1863
|
+
)}&format=json&explaintext=1&formatversion=2`;
|
|
1864
|
+
const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
|
|
1865
|
+
if (!response.ok)
|
|
1866
|
+
return null;
|
|
1867
|
+
const data = await response.json();
|
|
1868
|
+
const page = data?.query?.pages?.[0];
|
|
1869
|
+
if (!page || page.missing || !page.extract)
|
|
1870
|
+
return null;
|
|
1871
|
+
const raw = page.extract;
|
|
1872
|
+
const sections = {};
|
|
1873
|
+
const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
|
|
1874
|
+
for (let i = 1; i < parts.length; i += 3) {
|
|
1875
|
+
const title2 = parts[i + 1]?.trim();
|
|
1876
|
+
const content = parts[i + 2]?.trim();
|
|
1877
|
+
if (title2)
|
|
1878
|
+
sections[title2] = content ?? "";
|
|
1879
|
+
}
|
|
1880
|
+
return { raw, sections };
|
|
1881
|
+
} catch {
|
|
1882
|
+
return null;
|
|
2010
1883
|
}
|
|
2011
1884
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1885
|
+
/**
|
|
1886
|
+
* Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
|
|
1887
|
+
* via MediaWiki's official Action API (`action=query&prop=extracts`) — no
|
|
1888
|
+
* scraping involved, this is a stable, documented public API. `sections`
|
|
1889
|
+
* splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
|
|
1890
|
+
* markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
|
|
1891
|
+
* unsplit text. This wiki has title capitalization turned off
|
|
1892
|
+
* ($wgCapitalLinks=false — common for Wiktionaries, since case is
|
|
1893
|
+
* meaningful for a dictionary: "Türkiye" the country vs. a lowercase
|
|
1894
|
+
* common word), so an exact-case miss retries with the first letter
|
|
1895
|
+
* uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
|
|
1896
|
+
* "Istanbul") before giving up. Returns `null` if neither is found or the
|
|
1897
|
+
* request fails.
|
|
1898
|
+
*/
|
|
1899
|
+
static async getWiktionary(word2) {
|
|
1900
|
+
if (!word2 || word2.trim() === "")
|
|
1901
|
+
return null;
|
|
1902
|
+
const trimmed = word2.trim();
|
|
1903
|
+
const direct = await this.fetchWiktionaryEntry(trimmed);
|
|
1904
|
+
if (direct)
|
|
1905
|
+
return direct;
|
|
1906
|
+
const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
|
|
1907
|
+
if (capitalized === trimmed)
|
|
1908
|
+
return null;
|
|
1909
|
+
return this.fetchWiktionaryEntry(capitalized);
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Convenience filter over `getWiktionary()`: returns just one section's
|
|
1913
|
+
* text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
|
|
1914
|
+
* case-insensitively. Returns `null` if the word or the section isn't found.
|
|
1915
|
+
*/
|
|
1916
|
+
static async getWiktionarySection(word2, sectionName) {
|
|
1917
|
+
const entry = await this.getWiktionary(word2);
|
|
1918
|
+
if (!entry)
|
|
1919
|
+
return null;
|
|
1920
|
+
const key = Object.keys(entry.sections).find(
|
|
1921
|
+
(k) => k.toLocaleLowerCase("tr-TR") === sectionName.trim().toLocaleLowerCase("tr-TR")
|
|
1922
|
+
);
|
|
1923
|
+
return key ? entry.sections[key] : null;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Returns compound words that contain this word.
|
|
1927
|
+
*/
|
|
1928
|
+
static async getCompoundWords(word2) {
|
|
1929
|
+
const results = await this.getWord(word2);
|
|
1930
|
+
if (results.length === 0)
|
|
1931
|
+
return [];
|
|
1932
|
+
const compound = [];
|
|
1933
|
+
for (const result of results) {
|
|
1934
|
+
if (result.birlesikler) {
|
|
1935
|
+
const words = result.birlesikler.split(",").map((w) => w.trim());
|
|
1936
|
+
compound.push(...words);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return [...new Set(compound)];
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Returns the part of speech (isim, sıfat, zarf vb.).
|
|
1943
|
+
* TDK's `ozelliklerListe` mixes grammatical categories (`tur: "3"`, e.g.
|
|
1944
|
+
* sıfat/zarf/isim) with usage-register tags (`tur: "4"`, e.g. mecaz/argo)
|
|
1945
|
+
* in the same list — only `tur === "3"` entries are actual parts of speech.
|
|
1946
|
+
*/
|
|
1947
|
+
static async getPartOfSpeech(word2) {
|
|
1948
|
+
const results = await this.getWord(word2);
|
|
1949
|
+
const pos = /* @__PURE__ */ new Set();
|
|
1950
|
+
for (const result of results) {
|
|
1951
|
+
if (result.anlamlarListe) {
|
|
1952
|
+
for (const anlam of result.anlamlarListe) {
|
|
1953
|
+
if (anlam.ozelliklerListe) {
|
|
1954
|
+
for (const ozellik of anlam.ozelliklerListe) {
|
|
1955
|
+
if (ozellik.tur === "3")
|
|
1956
|
+
pos.add(ozellik.tam_adi);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
2055
1959
|
}
|
|
2056
1960
|
}
|
|
2057
1961
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
if (currentSyllable) {
|
|
2063
|
-
if (result.length > 0) {
|
|
2064
|
-
result[0] = currentSyllable + result[0];
|
|
2065
|
-
} else {
|
|
2066
|
-
result.push(currentSyllable);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
return result;
|
|
2070
|
-
}
|
|
2071
|
-
/**
|
|
2072
|
-
* Checks if a word follows Turkish Major Vowel Harmony (Büyük Ünlü Uyumu).
|
|
2073
|
-
* Normalizes case via the Turkish locale first: a plain case-insensitive
|
|
2074
|
-
* regex would fold ASCII "I" to "i", misreading the back vowel "I"
|
|
2075
|
-
* (dotless) as the front vowel "i" (dotted).
|
|
2076
|
-
*/
|
|
2077
|
-
static checkVowelHarmony(word2) {
|
|
2078
|
-
const lower = word2.toLocaleLowerCase("tr-TR");
|
|
2079
|
-
const backVowels = /[aıou]/;
|
|
2080
|
-
const frontVowels = /[eiöü]/;
|
|
2081
|
-
const hasBack = backVowels.test(lower);
|
|
2082
|
-
const hasFront = frontVowels.test(lower);
|
|
2083
|
-
return !(hasBack && hasFront);
|
|
2084
|
-
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
|
|
2087
|
-
* Rules:
|
|
2088
|
-
* 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
|
|
2089
|
-
* 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
|
|
2090
|
-
* Single-syllable words and words with <=1 vowel are considered compliant by convention.
|
|
2091
|
-
*/
|
|
2092
|
-
static checkLabialHarmony(word2) {
|
|
2093
|
-
const lower = word2.toLocaleLowerCase("tr-TR");
|
|
2094
|
-
const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
|
|
2095
|
-
if (vowels.length <= 1)
|
|
2096
|
-
return true;
|
|
2097
|
-
for (let i = 0; i < vowels.length - 1; i++) {
|
|
2098
|
-
const v1 = vowels[i];
|
|
2099
|
-
const v2 = vowels[i + 1];
|
|
2100
|
-
if ("ae\u0131i".includes(v1)) {
|
|
2101
|
-
if (!"ae\u0131i".includes(v2))
|
|
2102
|
-
return false;
|
|
2103
|
-
} else if ("o\xF6u\xFC".includes(v1)) {
|
|
2104
|
-
if (!"aeu\xFC".includes(v2))
|
|
2105
|
-
return false;
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
return true;
|
|
2109
|
-
}
|
|
2110
|
-
/**
|
|
2111
|
-
* Searches TDK headwords using a wildcard / pattern string.
|
|
2112
|
-
* Wildcards:
|
|
2113
|
-
* '_' or '?' matches any single character
|
|
2114
|
-
* '*' matches zero or more characters
|
|
2115
|
-
* Example: "k_l_m" matches "kalem", "kelam", "kilim".
|
|
2116
|
-
* Runs in-memory against TDK's 81k headword list.
|
|
2117
|
-
*/
|
|
2118
|
-
static async patternSearch(pattern, options) {
|
|
2119
|
-
if (!pattern || pattern.trim() === "")
|
|
2120
|
-
return [];
|
|
2121
|
-
await this.ensureAutocompleteLoaded();
|
|
2122
|
-
const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
|
|
2123
|
-
const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
|
|
2124
|
-
const regex = new RegExp(`^${escaped}$`, "u");
|
|
2125
|
-
const max = options?.maxResults ?? 50;
|
|
2126
|
-
const matches = [];
|
|
2127
|
-
for (const headword of this.autocompleteCache) {
|
|
2128
|
-
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2129
|
-
if (regex.test(lower)) {
|
|
2130
|
-
matches.push(headword);
|
|
2131
|
-
if (matches.length >= max)
|
|
2132
|
-
break;
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
return matches;
|
|
2136
|
-
}
|
|
2137
|
-
/**
|
|
2138
|
-
* Finds headwords in TDK that can be formed from the given letters (anagrams).
|
|
2139
|
-
* If exact-length anagrams exist, they are returned.
|
|
2140
|
-
* If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
|
|
2141
|
-
* minimum 3 letters) are returned, sorted by length descending.
|
|
2142
|
-
*/
|
|
2143
|
-
static async findAnagrams(letters, options) {
|
|
2144
|
-
if (!letters || letters.trim() === "")
|
|
2145
|
-
return [];
|
|
2146
|
-
await this.ensureAutocompleteLoaded();
|
|
2147
|
-
const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
|
|
2148
|
-
if (clean.length === 0)
|
|
2149
|
-
return [];
|
|
2150
|
-
const forceExact = options?.exactLength === true;
|
|
2151
|
-
const max = options?.maxResults ?? 50;
|
|
2152
|
-
const getFrequency = (str) => {
|
|
2153
|
-
const freq = {};
|
|
2154
|
-
for (const ch of str) {
|
|
2155
|
-
freq[ch] = (freq[ch] || 0) + 1;
|
|
2156
|
-
}
|
|
2157
|
-
return freq;
|
|
2158
|
-
};
|
|
2159
|
-
const targetFreq = getFrequency(clean);
|
|
2160
|
-
const exactMatches = [];
|
|
2161
|
-
const subMatches = [];
|
|
2162
|
-
for (const headword of this.autocompleteCache) {
|
|
2163
|
-
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2164
|
-
if (lower.includes(" ") || lower.includes("-"))
|
|
2165
|
-
continue;
|
|
2166
|
-
if (lower.length > clean.length || lower.length < 3)
|
|
2167
|
-
continue;
|
|
2168
|
-
const wordFreq = getFrequency(lower);
|
|
2169
|
-
let isValid = true;
|
|
2170
|
-
for (const [ch, count] of Object.entries(wordFreq)) {
|
|
2171
|
-
if (!targetFreq[ch] || targetFreq[ch] < count) {
|
|
2172
|
-
isValid = false;
|
|
2173
|
-
break;
|
|
1962
|
+
if (pos.size === 0 && results.length > 0) {
|
|
1963
|
+
pos.add("isim");
|
|
2174
1964
|
}
|
|
1965
|
+
return Array.from(pos);
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Compares two words side by side: meaning count, etymological origin,
|
|
1969
|
+
* syllables and vowel-harmony compliance.
|
|
1970
|
+
*/
|
|
1971
|
+
static async compareWords(a, b) {
|
|
1972
|
+
const [meaningsA, meaningsB, originA, originB] = await Promise.all([
|
|
1973
|
+
this.getMeanings(a),
|
|
1974
|
+
this.getMeanings(b),
|
|
1975
|
+
this.getOrigin(a),
|
|
1976
|
+
this.getOrigin(b)
|
|
1977
|
+
]);
|
|
1978
|
+
return {
|
|
1979
|
+
a: {
|
|
1980
|
+
word: a,
|
|
1981
|
+
meaningCount: meaningsA.length,
|
|
1982
|
+
origin: originA,
|
|
1983
|
+
syllables: this.syllabicate(a),
|
|
1984
|
+
harmony: this.checkVowelHarmony(a),
|
|
1985
|
+
labialHarmony: this.checkLabialHarmony(a)
|
|
1986
|
+
},
|
|
1987
|
+
b: {
|
|
1988
|
+
word: b,
|
|
1989
|
+
meaningCount: meaningsB.length,
|
|
1990
|
+
origin: originB,
|
|
1991
|
+
syllables: this.syllabicate(b),
|
|
1992
|
+
harmony: this.checkVowelHarmony(b),
|
|
1993
|
+
labialHarmony: this.checkLabialHarmony(b)
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
2175
1996
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
1997
|
+
static STOPWORDS = /* @__PURE__ */ new Set([
|
|
1998
|
+
"ve",
|
|
1999
|
+
"veya",
|
|
2000
|
+
"ile",
|
|
2001
|
+
"ama",
|
|
2002
|
+
"fakat",
|
|
2003
|
+
"ancak",
|
|
2004
|
+
"de",
|
|
2005
|
+
"da",
|
|
2006
|
+
"ki",
|
|
2007
|
+
"bu",
|
|
2008
|
+
"\u015Fu",
|
|
2009
|
+
"o",
|
|
2010
|
+
"bir",
|
|
2011
|
+
"\xE7ok",
|
|
2012
|
+
"az",
|
|
2013
|
+
"gibi",
|
|
2014
|
+
"i\xE7in",
|
|
2015
|
+
"mi",
|
|
2016
|
+
"m\u0131",
|
|
2017
|
+
"mu",
|
|
2018
|
+
"m\xFC",
|
|
2019
|
+
"ne",
|
|
2020
|
+
"her",
|
|
2021
|
+
"hi\xE7",
|
|
2022
|
+
"ben",
|
|
2023
|
+
"sen",
|
|
2024
|
+
"biz",
|
|
2025
|
+
"siz",
|
|
2026
|
+
"onlar",
|
|
2027
|
+
"de\u011Fil",
|
|
2028
|
+
"bile",
|
|
2029
|
+
"diye"
|
|
2030
|
+
]);
|
|
2031
|
+
static firstMeaning(results) {
|
|
2032
|
+
for (const result of results) {
|
|
2033
|
+
for (const anlam of result.anlamlarListe ?? []) {
|
|
2034
|
+
if (anlam.anlam)
|
|
2035
|
+
return anlam.anlam;
|
|
2036
|
+
}
|
|
2181
2037
|
}
|
|
2038
|
+
return null;
|
|
2182
2039
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
{
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2040
|
+
/**
|
|
2041
|
+
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
2042
|
+
* returning each word's first meaning and etymological origin if found.
|
|
2043
|
+
* Looks each word up individually (throttled), so scales with text length.
|
|
2044
|
+
* TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
|
|
2045
|
+
* it does no morphological analysis, and neither does this method: a
|
|
2046
|
+
* suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
|
|
2047
|
+
* suffix) will come back `found: false` even though the root is a real
|
|
2048
|
+
* headword. This is an inherent limitation of the data source, not a bug.
|
|
2049
|
+
*/
|
|
2050
|
+
static async analyzeText(text) {
|
|
2051
|
+
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
2052
|
+
const unique = [...new Set(words)];
|
|
2053
|
+
const analyses = [];
|
|
2054
|
+
for (const word2 of unique) {
|
|
2055
|
+
let results = await this.getWord(word2);
|
|
2056
|
+
let found = results.length > 0;
|
|
2057
|
+
let root;
|
|
2058
|
+
let isInflected;
|
|
2059
|
+
if (!found) {
|
|
2060
|
+
const resolvedRoot = await this.findRoot(word2);
|
|
2061
|
+
if (resolvedRoot) {
|
|
2062
|
+
results = await this.getWord(resolvedRoot);
|
|
2063
|
+
if (results.length > 0) {
|
|
2064
|
+
found = true;
|
|
2065
|
+
root = resolvedRoot;
|
|
2066
|
+
isInflected = true;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
analyses.push({
|
|
2071
|
+
word: word2,
|
|
2072
|
+
found,
|
|
2073
|
+
meaning: found ? this.firstMeaning(results) : null,
|
|
2074
|
+
origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null,
|
|
2075
|
+
root,
|
|
2076
|
+
isInflected
|
|
2077
|
+
});
|
|
2078
|
+
await this.delay(200);
|
|
2079
|
+
}
|
|
2080
|
+
return analyses;
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Fetches multiple words concurrently with a small delay to avoid rate limiting.
|
|
2084
|
+
*/
|
|
2085
|
+
static async getWordsBatch(words) {
|
|
2086
|
+
const results = [];
|
|
2087
|
+
for (const word2 of words) {
|
|
2088
|
+
try {
|
|
2089
|
+
const res = await this.getWord(word2);
|
|
2090
|
+
results.push(res);
|
|
2091
|
+
} catch {
|
|
2092
|
+
results.push([]);
|
|
2093
|
+
}
|
|
2094
|
+
await this.delay(200);
|
|
2095
|
+
}
|
|
2096
|
+
return results;
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Syllabicates a Turkish word based on general grammar rules.
|
|
2100
|
+
* Handles syllable separation for vowels, single consonants, double consonants,
|
|
2101
|
+
* and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
|
|
2102
|
+
*/
|
|
2103
|
+
static syllabicate(word2) {
|
|
2104
|
+
const vowels = /[aeıioöuüAEIİOÖUÜ]/;
|
|
2105
|
+
const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
|
|
2106
|
+
const result = [];
|
|
2107
|
+
let currentSyllable = "";
|
|
2108
|
+
for (let i = word2.length - 1; i >= 0; i--) {
|
|
2109
|
+
currentSyllable = word2[i] + currentSyllable;
|
|
2110
|
+
if (vowels.test(word2[i])) {
|
|
2111
|
+
if (i - 1 >= 0 && !vowels.test(word2[i - 1])) {
|
|
2112
|
+
if (i - 2 >= 0 && vowels.test(word2[i - 2])) {
|
|
2113
|
+
currentSyllable = word2[i - 1] + currentSyllable;
|
|
2114
|
+
i--;
|
|
2115
|
+
} else if (i - 2 >= 0 && !vowels.test(word2[i - 2])) {
|
|
2116
|
+
if (i - 3 >= 0 && !vowels.test(word2[i - 3]) && ONSET_CLUSTERS.has((word2[i - 2] + word2[i - 1]).toLowerCase())) {
|
|
2117
|
+
currentSyllable = word2[i - 2] + word2[i - 1] + currentSyllable;
|
|
2118
|
+
i -= 2;
|
|
2119
|
+
} else {
|
|
2120
|
+
currentSyllable = word2[i - 1] + currentSyllable;
|
|
2121
|
+
i--;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
result.unshift(currentSyllable);
|
|
2126
|
+
currentSyllable = "";
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
if (currentSyllable) {
|
|
2130
|
+
if (result.length > 0) {
|
|
2131
|
+
result[0] = currentSyllable + result[0];
|
|
2132
|
+
} else {
|
|
2133
|
+
result.push(currentSyllable);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return result;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Checks if a word follows Turkish Major Vowel Harmony (Büyük Ünlü Uyumu).
|
|
2140
|
+
* Normalizes case via the Turkish locale first: a plain case-insensitive
|
|
2141
|
+
* regex would fold ASCII "I" to "i", misreading the back vowel "I"
|
|
2142
|
+
* (dotless) as the front vowel "i" (dotted).
|
|
2143
|
+
*/
|
|
2144
|
+
static checkVowelHarmony(word2) {
|
|
2145
|
+
const lower = word2.toLocaleLowerCase("tr-TR");
|
|
2146
|
+
const backVowels = /[aıou]/;
|
|
2147
|
+
const frontVowels = /[eiöü]/;
|
|
2148
|
+
const hasBack = backVowels.test(lower);
|
|
2149
|
+
const hasFront = frontVowels.test(lower);
|
|
2150
|
+
return !(hasBack && hasFront);
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
|
|
2154
|
+
* Rules:
|
|
2155
|
+
* 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
|
|
2156
|
+
* 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
|
|
2157
|
+
* Single-syllable words and words with <=1 vowel are considered compliant by convention.
|
|
2158
|
+
*/
|
|
2159
|
+
static checkLabialHarmony(word2) {
|
|
2160
|
+
const lower = word2.toLocaleLowerCase("tr-TR");
|
|
2161
|
+
const vowels = lower.split("").filter((ch) => "ae\u0131io\xF6u\xFC".includes(ch));
|
|
2162
|
+
if (vowels.length <= 1)
|
|
2163
|
+
return true;
|
|
2164
|
+
for (let i = 0; i < vowels.length - 1; i++) {
|
|
2165
|
+
const v1 = vowels[i];
|
|
2166
|
+
const v2 = vowels[i + 1];
|
|
2167
|
+
if ("ae\u0131i".includes(v1)) {
|
|
2168
|
+
if (!"ae\u0131i".includes(v2))
|
|
2169
|
+
return false;
|
|
2170
|
+
} else if ("o\xF6u\xFC".includes(v1)) {
|
|
2171
|
+
if (!"aeu\xFC".includes(v2))
|
|
2172
|
+
return false;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return true;
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Searches TDK headwords using a wildcard / pattern string.
|
|
2179
|
+
* Wildcards:
|
|
2180
|
+
* '_' or '?' matches any single character
|
|
2181
|
+
* '*' matches zero or more characters
|
|
2182
|
+
* Example: "k_l_m" matches "kalem", "kelam", "kilim".
|
|
2183
|
+
* Runs in-memory against TDK's 81k headword list.
|
|
2184
|
+
*/
|
|
2185
|
+
static async patternSearch(pattern, options) {
|
|
2186
|
+
if (!pattern || pattern.trim() === "")
|
|
2187
|
+
return [];
|
|
2188
|
+
await this.ensureAutocompleteLoaded();
|
|
2189
|
+
const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
|
|
2190
|
+
const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/[_?]/g, "[\\p{L}]").replace(/\*/g, "[\\p{L}]*");
|
|
2191
|
+
const regex = new RegExp(`^${escaped}$`, "u");
|
|
2192
|
+
const max = options?.maxResults ?? 50;
|
|
2193
|
+
const matches = [];
|
|
2194
|
+
for (const headword of this.autocompleteCache) {
|
|
2195
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2196
|
+
if (regex.test(lower)) {
|
|
2197
|
+
matches.push(headword);
|
|
2198
|
+
if (matches.length >= max)
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return matches;
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Finds headwords in TDK that can be formed from the given letters (anagrams).
|
|
2206
|
+
* If exact-length anagrams exist, they are returned.
|
|
2207
|
+
* If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
|
|
2208
|
+
* minimum 3 letters) are returned, sorted by length descending.
|
|
2209
|
+
*/
|
|
2210
|
+
static async findAnagrams(letters, options) {
|
|
2211
|
+
if (!letters || letters.trim() === "")
|
|
2212
|
+
return [];
|
|
2213
|
+
await this.ensureAutocompleteLoaded();
|
|
2214
|
+
const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
|
|
2215
|
+
if (clean.length === 0)
|
|
2216
|
+
return [];
|
|
2217
|
+
const forceExact = options?.exactLength === true;
|
|
2218
|
+
const max = options?.maxResults ?? 50;
|
|
2219
|
+
const getFrequency = (str) => {
|
|
2220
|
+
const freq = {};
|
|
2221
|
+
for (const ch of str) {
|
|
2222
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
2223
|
+
}
|
|
2224
|
+
return freq;
|
|
2225
|
+
};
|
|
2226
|
+
const targetFreq = getFrequency(clean);
|
|
2227
|
+
const exactMatches = [];
|
|
2228
|
+
const subMatches = [];
|
|
2229
|
+
for (const headword of this.autocompleteCache) {
|
|
2230
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2231
|
+
if (lower.includes(" ") || lower.includes("-"))
|
|
2232
|
+
continue;
|
|
2233
|
+
if (lower.length > clean.length || lower.length < 3)
|
|
2234
|
+
continue;
|
|
2235
|
+
const wordFreq = getFrequency(lower);
|
|
2236
|
+
let isValid = true;
|
|
2237
|
+
for (const [ch, count] of Object.entries(wordFreq)) {
|
|
2238
|
+
if (!targetFreq[ch] || targetFreq[ch] < count) {
|
|
2239
|
+
isValid = false;
|
|
2240
|
+
break;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
if (isValid && lower !== clean) {
|
|
2244
|
+
if (lower.length === clean.length) {
|
|
2245
|
+
exactMatches.push(headword);
|
|
2246
|
+
} else {
|
|
2247
|
+
subMatches.push(headword);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
if (exactMatches.length > 0 || forceExact) {
|
|
2252
|
+
return exactMatches.slice(0, max);
|
|
2253
|
+
}
|
|
2254
|
+
subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
|
|
2255
|
+
return subMatches.slice(0, max);
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
|
|
2259
|
+
* @param word The target word
|
|
2260
|
+
* @param options.minLetters Minimum number of ending characters that must match (default: 3)
|
|
2261
|
+
* @param options.maxResults Maximum number of rhyme results to return (default: 50)
|
|
2262
|
+
*/
|
|
2263
|
+
static async findRhymes(word2, options) {
|
|
2264
|
+
if (!word2 || word2.trim() === "")
|
|
2265
|
+
return [];
|
|
2266
|
+
await this.ensureAutocompleteLoaded();
|
|
2267
|
+
const clean = word2.trim().toLocaleLowerCase("tr-TR");
|
|
2268
|
+
const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
|
|
2269
|
+
const max = options?.maxResults ?? 50;
|
|
2270
|
+
const suffix = clean.slice(-minLetters);
|
|
2271
|
+
const results = [];
|
|
2272
|
+
for (const headword of this.autocompleteCache) {
|
|
2273
|
+
const lower = headword.toLocaleLowerCase("tr-TR");
|
|
2274
|
+
if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
|
|
2275
|
+
results.push(headword);
|
|
2276
|
+
if (results.length >= max)
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return results;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
|
|
2284
|
+
* Detects:
|
|
2285
|
+
* 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
|
|
2286
|
+
* 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
|
|
2287
|
+
* 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
|
|
2288
|
+
* 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
|
|
2289
|
+
*/
|
|
2290
|
+
static async proofread(text) {
|
|
2291
|
+
if (!text || text.trim() === "") {
|
|
2292
|
+
return { text: text || "", issues: [], isCorrect: true };
|
|
2293
|
+
}
|
|
2294
|
+
await this.ensureAutocompleteLoaded();
|
|
2295
|
+
const issues = [];
|
|
2296
|
+
const SOMBAHCEMI = /* @__PURE__ */ new Set([
|
|
2297
|
+
"sanki",
|
|
2298
|
+
"oysaki",
|
|
2299
|
+
"mademki",
|
|
2300
|
+
"belki",
|
|
2301
|
+
"halbuki",
|
|
2302
|
+
"\xE7\xFCnk\xFC",
|
|
2303
|
+
"me\u011Ferki",
|
|
2304
|
+
"illaki"
|
|
2305
|
+
]);
|
|
2306
|
+
const PHRASE_MISTAKES = [
|
|
2307
|
+
{
|
|
2308
|
+
regex: /\bhiç\s+bir\b/gi,
|
|
2309
|
+
suggestion: "hi\xE7bir",
|
|
2310
|
+
message: "'hi\xE7bir' belgisiz s\u0131fat\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2311
|
+
type: "spelling"
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
regex: /\bbir\s+çok\b/gi,
|
|
2315
|
+
suggestion: "bir\xE7ok",
|
|
2316
|
+
message: "'bir\xE7ok' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2317
|
+
type: "spelling"
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
regex: /\bbir\s+kaç\b/gi,
|
|
2321
|
+
suggestion: "birka\xE7",
|
|
2322
|
+
message: "'birka\xE7' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2323
|
+
type: "spelling"
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
regex: /\bbir\s+az\b/gi,
|
|
2327
|
+
suggestion: "biraz",
|
|
2328
|
+
message: "'biraz' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2329
|
+
type: "spelling"
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
regex: /\bher\s+hangi\b/gi,
|
|
2333
|
+
suggestion: "herhangi",
|
|
2334
|
+
message: "'herhangi' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2335
|
+
type: "spelling"
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
regex: /\bgit\s+gide\b/gi,
|
|
2339
|
+
suggestion: "gitgide",
|
|
2340
|
+
message: "'gitgide' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2341
|
+
type: "spelling"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
regex: /\bbirden\s+bire\b/gi,
|
|
2345
|
+
suggestion: "birdenbire",
|
|
2346
|
+
message: "'birdenbire' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2347
|
+
type: "spelling"
|
|
2348
|
+
},
|
|
2349
|
+
{
|
|
2350
|
+
regex: /\brast\s+gele\b/gi,
|
|
2351
|
+
suggestion: "rastgele",
|
|
2352
|
+
message: "'rastgele' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
|
|
2353
|
+
type: "spelling"
|
|
2354
|
+
}
|
|
2355
|
+
];
|
|
2356
|
+
const coveredRanges = [];
|
|
2357
|
+
for (const pm of PHRASE_MISTAKES) {
|
|
2358
|
+
let pmMatch;
|
|
2359
|
+
while ((pmMatch = pm.regex.exec(text)) !== null) {
|
|
2360
|
+
const start = pmMatch.index;
|
|
2361
|
+
const end = start + pmMatch[0].length;
|
|
2362
|
+
coveredRanges.push({ start, end });
|
|
2324
2363
|
issues.push({
|
|
2325
|
-
type:
|
|
2364
|
+
type: pm.type,
|
|
2365
|
+
word: pmMatch[0],
|
|
2366
|
+
startIndex: start,
|
|
2367
|
+
endIndex: end,
|
|
2368
|
+
suggestion: pm.suggestion,
|
|
2369
|
+
message: pm.message
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
const tokenRegex = /[\p{L}0-9'’]+/gu;
|
|
2374
|
+
let match;
|
|
2375
|
+
while ((match = tokenRegex.exec(text)) !== null) {
|
|
2376
|
+
const rawWord = match[0];
|
|
2377
|
+
const startIndex = match.index;
|
|
2378
|
+
const endIndex = startIndex + rawWord.length;
|
|
2379
|
+
const lower = rawWord.toLocaleLowerCase("tr-TR");
|
|
2380
|
+
if (/^\d+$/.test(lower))
|
|
2381
|
+
continue;
|
|
2382
|
+
if (coveredRanges.some((r) => startIndex >= r.start && endIndex <= r.end))
|
|
2383
|
+
continue;
|
|
2384
|
+
let flagged = false;
|
|
2385
|
+
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)?)$/);
|
|
2386
|
+
if (questionMatch) {
|
|
2387
|
+
const base = questionMatch[1];
|
|
2388
|
+
const particle = questionMatch[2];
|
|
2389
|
+
if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
|
|
2390
|
+
if (!await this.isHeadword(lower)) {
|
|
2391
|
+
issues.push({
|
|
2392
|
+
type: "question_particle",
|
|
2393
|
+
word: rawWord,
|
|
2394
|
+
startIndex,
|
|
2395
|
+
endIndex,
|
|
2396
|
+
suggestion: `${base} ${particle}`,
|
|
2397
|
+
message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
2398
|
+
});
|
|
2399
|
+
flagged = true;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
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;
|
|
2404
|
+
if (!flagged && lower.endsWith("ki") && lower.length > 3) {
|
|
2405
|
+
const base = lower.slice(0, -2);
|
|
2406
|
+
if (!SOMBAHCEMI.has(lower)) {
|
|
2407
|
+
if (!await this.isHeadword(lower)) {
|
|
2408
|
+
const root = await this.findRoot(base);
|
|
2409
|
+
const isVerb = (base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base)) && (root ? root.endsWith("mek") || root.endsWith("mak") : true);
|
|
2410
|
+
if (isVerb) {
|
|
2411
|
+
issues.push({
|
|
2412
|
+
type: "conjunction_ki",
|
|
2413
|
+
word: rawWord,
|
|
2414
|
+
startIndex,
|
|
2415
|
+
endIndex,
|
|
2416
|
+
suggestion: `${base} ki`,
|
|
2417
|
+
message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
2418
|
+
});
|
|
2419
|
+
flagged = true;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
|
|
2425
|
+
const base = lower.slice(0, -2);
|
|
2426
|
+
const ending = lower.slice(-2);
|
|
2427
|
+
if (!await this.isHeadword(lower)) {
|
|
2428
|
+
const root = await this.findRoot(base);
|
|
2429
|
+
const isVerb = VERB_CONJUGATION_REGEX.test(base) && (root ? root.endsWith("mek") || root.endsWith("mak") : false);
|
|
2430
|
+
if (isVerb) {
|
|
2431
|
+
const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
|
|
2432
|
+
issues.push({
|
|
2433
|
+
type: "conjunction_da",
|
|
2434
|
+
word: rawWord,
|
|
2435
|
+
startIndex,
|
|
2436
|
+
endIndex,
|
|
2437
|
+
suggestion: `${base} ${correctEnding}`,
|
|
2438
|
+
message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
|
|
2439
|
+
});
|
|
2440
|
+
flagged = true;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
const seyMatch = lower.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
2445
|
+
if (!flagged && seyMatch && !SEY_EXCEPTIONS.has(lower)) {
|
|
2446
|
+
let prefix = seyMatch[1];
|
|
2447
|
+
const suffix = seyMatch[2] || "";
|
|
2448
|
+
if (prefix === "hicbir")
|
|
2449
|
+
prefix = "hi\xE7bir";
|
|
2450
|
+
if (prefix === "cok")
|
|
2451
|
+
prefix = "\xE7ok";
|
|
2452
|
+
issues.push({
|
|
2453
|
+
type: "spelling",
|
|
2326
2454
|
word: rawWord,
|
|
2327
2455
|
startIndex,
|
|
2328
2456
|
endIndex,
|
|
2329
|
-
suggestion: `${
|
|
2330
|
-
message:
|
|
2457
|
+
suggestion: `${prefix} \u015Fey${suffix}`,
|
|
2458
|
+
message: "'\u015Fey' s\xF6zc\xFC\u011F\xFC kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r."
|
|
2331
2459
|
});
|
|
2332
2460
|
flagged = true;
|
|
2333
2461
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
issues.push({
|
|
2345
|
-
type: "conjunction_ki",
|
|
2346
|
-
word: rawWord,
|
|
2347
|
-
startIndex,
|
|
2348
|
-
endIndex,
|
|
2349
|
-
suggestion: `${base} ki`,
|
|
2350
|
-
message: `'ki' ba\u011Flac\u0131 ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
|
|
2351
|
-
});
|
|
2352
|
-
flagged = true;
|
|
2353
|
-
}
|
|
2462
|
+
if (!flagged && lower === "yada") {
|
|
2463
|
+
issues.push({
|
|
2464
|
+
type: "spelling",
|
|
2465
|
+
word: rawWord,
|
|
2466
|
+
startIndex,
|
|
2467
|
+
endIndex,
|
|
2468
|
+
suggestion: "ya da",
|
|
2469
|
+
message: "'ya da' ba\u011Flac\u0131 her zaman ayr\u0131 yaz\u0131l\u0131r."
|
|
2470
|
+
});
|
|
2471
|
+
flagged = true;
|
|
2354
2472
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
|
|
2358
|
-
const base = lower.slice(0, -2);
|
|
2359
|
-
const ending = lower.slice(-2);
|
|
2360
|
-
if (!await this.isHeadword(lower)) {
|
|
2361
|
-
const root = await this.findRoot(base);
|
|
2362
|
-
const isVerb = VERB_CONJUGATION_REGEX.test(base) && (root ? root.endsWith("mek") || root.endsWith("mak") : false);
|
|
2363
|
-
if (isVerb) {
|
|
2364
|
-
const correctEnding = ending.startsWith("t") ? ending === "te" ? "de" : "da" : ending;
|
|
2473
|
+
if (!flagged && (lower === "burda" || lower === "\u015Furda" || lower === "surda" || lower === "orda" || lower === "i\xE7erde" || lower === "icerde" || lower === "d\u0131\u015Farda" || lower === "disarda" || lower === "yukarda")) {
|
|
2474
|
+
const correct = COMMON_MISSPELLINGS[lower] || lower;
|
|
2365
2475
|
issues.push({
|
|
2366
|
-
type: "
|
|
2476
|
+
type: "spelling",
|
|
2367
2477
|
word: rawWord,
|
|
2368
2478
|
startIndex,
|
|
2369
2479
|
endIndex,
|
|
2370
|
-
suggestion:
|
|
2371
|
-
message: `'
|
|
2480
|
+
suggestion: correct,
|
|
2481
|
+
message: `'${rawWord}' s\xF6zc\xFC\u011F\xFCnde \xFCnl\xFC d\xFC\u015Fmesi yap\u0131lmaz.`
|
|
2372
2482
|
});
|
|
2373
2483
|
flagged = true;
|
|
2374
2484
|
}
|
|
2485
|
+
if (!flagged) {
|
|
2486
|
+
const check = await this.checkSpelling(rawWord);
|
|
2487
|
+
if (!check.isCorrect) {
|
|
2488
|
+
issues.push({
|
|
2489
|
+
type: "spelling",
|
|
2490
|
+
word: rawWord,
|
|
2491
|
+
startIndex,
|
|
2492
|
+
endIndex,
|
|
2493
|
+
suggestion: check.suggestion,
|
|
2494
|
+
message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir.` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2375
2498
|
}
|
|
2499
|
+
issues.sort((a, b) => a.startIndex - b.startIndex);
|
|
2500
|
+
return {
|
|
2501
|
+
text,
|
|
2502
|
+
issues,
|
|
2503
|
+
isCorrect: issues.length === 0
|
|
2504
|
+
};
|
|
2376
2505
|
}
|
|
2377
|
-
const seyMatch = lower.match(/^(.+?)(?:şey|sey)([ıiuaeüodekmnl]+)?$/);
|
|
2378
|
-
if (!flagged && seyMatch && !SEY_EXCEPTIONS.has(lower)) {
|
|
2379
|
-
let prefix = seyMatch[1];
|
|
2380
|
-
const suffix = seyMatch[2] || "";
|
|
2381
|
-
if (prefix === "hicbir")
|
|
2382
|
-
prefix = "hi\xE7bir";
|
|
2383
|
-
if (prefix === "cok")
|
|
2384
|
-
prefix = "\xE7ok";
|
|
2385
|
-
issues.push({
|
|
2386
|
-
type: "spelling",
|
|
2387
|
-
word: rawWord,
|
|
2388
|
-
startIndex,
|
|
2389
|
-
endIndex,
|
|
2390
|
-
suggestion: `${prefix} \u015Fey${suffix}`,
|
|
2391
|
-
message: "'\u015Fey' s\xF6zc\xFC\u011F\xFC kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r."
|
|
2392
|
-
});
|
|
2393
|
-
flagged = true;
|
|
2394
|
-
}
|
|
2395
|
-
if (!flagged && lower === "yada") {
|
|
2396
|
-
issues.push({
|
|
2397
|
-
type: "spelling",
|
|
2398
|
-
word: rawWord,
|
|
2399
|
-
startIndex,
|
|
2400
|
-
endIndex,
|
|
2401
|
-
suggestion: "ya da",
|
|
2402
|
-
message: "'ya da' ba\u011Flac\u0131 her zaman ayr\u0131 yaz\u0131l\u0131r."
|
|
2403
|
-
});
|
|
2404
|
-
flagged = true;
|
|
2405
|
-
}
|
|
2406
|
-
if (!flagged && (lower === "burda" || lower === "\u015Furda" || lower === "surda" || lower === "orda" || lower === "i\xE7erde" || lower === "icerde" || lower === "d\u0131\u015Farda" || lower === "disarda" || lower === "yukarda")) {
|
|
2407
|
-
const correct = COMMON_MISSPELLINGS[lower] || lower;
|
|
2408
|
-
issues.push({
|
|
2409
|
-
type: "spelling",
|
|
2410
|
-
word: rawWord,
|
|
2411
|
-
startIndex,
|
|
2412
|
-
endIndex,
|
|
2413
|
-
suggestion: correct,
|
|
2414
|
-
message: `'${rawWord}' s\xF6zc\xFC\u011F\xFCnde \xFCnl\xFC d\xFC\u015Fmesi yap\u0131lmaz.`
|
|
2415
|
-
});
|
|
2416
|
-
flagged = true;
|
|
2417
|
-
}
|
|
2418
|
-
if (!flagged) {
|
|
2419
|
-
const check = await this.checkSpelling(rawWord);
|
|
2420
|
-
if (!check.isCorrect) {
|
|
2421
|
-
issues.push({
|
|
2422
|
-
type: "spelling",
|
|
2423
|
-
word: rawWord,
|
|
2424
|
-
startIndex,
|
|
2425
|
-
endIndex,
|
|
2426
|
-
suggestion: check.suggestion,
|
|
2427
|
-
message: check.suggestion ? `'${rawWord}' yanl\u0131\u015F yaz\u0131lm\u0131\u015F olabilir.` : `'${rawWord}' s\xF6zl\xFCkte bulunamad\u0131.`
|
|
2428
|
-
});
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
issues.sort((a, b) => a.startIndex - b.startIndex);
|
|
2433
|
-
return {
|
|
2434
|
-
text,
|
|
2435
|
-
issues,
|
|
2436
|
-
isCorrect: issues.length === 0
|
|
2437
2506
|
};
|
|
2438
2507
|
}
|
|
2439
|
-
};
|
|
2508
|
+
});
|
|
2509
|
+
|
|
2510
|
+
// src/mcp.ts
|
|
2511
|
+
var mcp_exports = {};
|
|
2512
|
+
__export(mcp_exports, {
|
|
2513
|
+
createMcpServer: () => createMcpServer,
|
|
2514
|
+
runMcpServer: () => runMcpServer
|
|
2515
|
+
});
|
|
2516
|
+
function ok(data) {
|
|
2517
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2518
|
+
}
|
|
2519
|
+
function fail(message) {
|
|
2520
|
+
return {
|
|
2521
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
|
|
2522
|
+
isError: true
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
function guard(fn) {
|
|
2526
|
+
return async (args2) => {
|
|
2527
|
+
try {
|
|
2528
|
+
return await fn(args2);
|
|
2529
|
+
} catch (error) {
|
|
2530
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
2531
|
+
}
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
function createMcpServer() {
|
|
2535
|
+
const server = new import_mcp.McpServer({
|
|
2536
|
+
name: "TDK API Server",
|
|
2537
|
+
version: VERSION
|
|
2538
|
+
});
|
|
2539
|
+
server.tool(
|
|
2540
|
+
"tdk_lookup",
|
|
2541
|
+
"Bir kelimenin TDK G\xFCncel T\xFCrk\xE7e S\xF6zl\xFCk'teki ham kayd\u0131n\u0131 (t\xFCm anlamlar, \xF6rnekler, birle\u015Fikler, k\xF6ken, atas\xF6zleri) d\xF6nd\xFCr\xFCr.",
|
|
2542
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime (\xF6rn: 'kalem').") },
|
|
2543
|
+
guard(async ({ word: word2 }) => {
|
|
2544
|
+
const results = await TDK.getWord(word2);
|
|
2545
|
+
if (results.length === 0)
|
|
2546
|
+
return fail(`"${word2}" TDK s\xF6zl\xFC\u011F\xFCnde bulunamad\u0131.`);
|
|
2547
|
+
return ok(results);
|
|
2548
|
+
})
|
|
2549
|
+
);
|
|
2550
|
+
server.tool(
|
|
2551
|
+
"tdk_meanings",
|
|
2552
|
+
"Bir kelimenin sadele\u015Ftirilmi\u015F anlam listesini (madde madde tan\u0131mlar) d\xF6nd\xFCr\xFCr.",
|
|
2553
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime.") },
|
|
2554
|
+
guard(async ({ word: word2 }) => ok({ word: word2, meanings: await TDK.getMeanings(word2) }))
|
|
2555
|
+
);
|
|
2556
|
+
server.tool(
|
|
2557
|
+
"tdk_examples",
|
|
2558
|
+
"Bir kelimenin s\xF6zl\xFCkteki \xF6rnek c\xFCmlelerini (varsa yazar\u0131yla) d\xF6nd\xFCr\xFCr.",
|
|
2559
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime.") },
|
|
2560
|
+
guard(async ({ word: word2 }) => ok({ word: word2, examples: await TDK.getExamples(word2) }))
|
|
2561
|
+
);
|
|
2562
|
+
server.tool(
|
|
2563
|
+
"tdk_proverbs",
|
|
2564
|
+
"Bir kelime ile kurulan atas\xF6z\xFC ve deyimleri listeler.",
|
|
2565
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime (\xF6rn: 'g\xF6z').") },
|
|
2566
|
+
guard(async ({ word: word2 }) => ok({ word: word2, proverbs: await TDK.getProverbs(word2) }))
|
|
2567
|
+
);
|
|
2568
|
+
server.tool(
|
|
2569
|
+
"tdk_compound_words",
|
|
2570
|
+
"Bir kelime ile olu\u015Fturulmu\u015F birle\u015Fik kelimeleri listeler (\xF6rn: 'kalem' -> 'dolma kalem').",
|
|
2571
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime.") },
|
|
2572
|
+
guard(async ({ word: word2 }) => ok({ word: word2, compounds: await TDK.getCompoundWords(word2) }))
|
|
2573
|
+
);
|
|
2574
|
+
server.tool(
|
|
2575
|
+
"tdk_part_of_speech",
|
|
2576
|
+
"Bir kelimenin s\xF6zc\xFCk t\xFCrlerini (isim, s\u0131fat, zarf, fiil vb.) d\xF6nd\xFCr\xFCr.",
|
|
2577
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime.") },
|
|
2578
|
+
guard(async ({ word: word2 }) => ok({ word: word2, partsOfSpeech: await TDK.getPartOfSpeech(word2) }))
|
|
2579
|
+
);
|
|
2580
|
+
server.tool(
|
|
2581
|
+
"tdk_synonyms",
|
|
2582
|
+
"Bir kelimenin e\u015F anlaml\u0131lar\u0131n\u0131 (yak\u0131n anlaml\u0131 kelimeler) d\xF6nd\xFCr\xFCr.",
|
|
2583
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime.") },
|
|
2584
|
+
guard(async ({ word: word2 }) => ok({ word: word2, synonyms: await TDK.getSynonyms(word2) }))
|
|
2585
|
+
);
|
|
2586
|
+
server.tool(
|
|
2587
|
+
"tdk_antonyms",
|
|
2588
|
+
"Bir kelimenin z\u0131t (kar\u015F\u0131t) anlaml\u0131lar\u0131n\u0131 d\xF6nd\xFCr\xFCr.",
|
|
2589
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime.") },
|
|
2590
|
+
guard(async ({ word: word2 }) => ok({ word: word2, antonyms: await TDK.getAntonyms(word2) }))
|
|
2591
|
+
);
|
|
2592
|
+
server.tool(
|
|
2593
|
+
"tdk_origin",
|
|
2594
|
+
"Bir kelimenin TDK'deki k\xF6ken bilgisini ve yabanc\u0131 k\xF6kenli olup olmad\u0131\u011F\u0131n\u0131 d\xF6nd\xFCr\xFCr.",
|
|
2595
|
+
{ word: import_zod.z.string().describe("Aranacak T\xFCrk\xE7e kelime.") },
|
|
2596
|
+
guard(async ({ word: word2 }) => {
|
|
2597
|
+
const [origin, foreign] = await Promise.all([TDK.getOrigin(word2), TDK.isForeignWord(word2)]);
|
|
2598
|
+
return ok({ word: word2, origin, isForeign: foreign });
|
|
2599
|
+
})
|
|
2600
|
+
);
|
|
2601
|
+
server.tool(
|
|
2602
|
+
"tdk_nisanyan",
|
|
2603
|
+
"Ni\u015Fanyan S\xF6zl\xFCk'ten bir kelimenin ayr\u0131nt\u0131l\u0131 etimolojisini (server-rendered meta a\xE7\u0131klamas\u0131) \xE7eker.",
|
|
2604
|
+
{ word: import_zod.z.string().describe("Etimolojisi aranacak kelime.") },
|
|
2605
|
+
guard(async ({ word: word2 }) => {
|
|
2606
|
+
const etymology = await TDK.getNisanyan(word2);
|
|
2607
|
+
return etymology ? ok({ word: word2, etymology }) : fail(`Ni\u015Fanyan S\xF6zl\xFCk'te "${word2}" bulunamad\u0131.`);
|
|
2608
|
+
})
|
|
2609
|
+
);
|
|
2610
|
+
server.tool(
|
|
2611
|
+
"tdk_kubbealti",
|
|
2612
|
+
"Kubbealt\u0131 Lugat\u0131'ndan (ticari s\xF6zl\xFCk) bir kelimenin anlamlar\u0131n\u0131 \xE7eker.",
|
|
2613
|
+
{ word: import_zod.z.string().describe("Aranacak kelime.") },
|
|
2614
|
+
guard(async ({ word: word2 }) => {
|
|
2615
|
+
const meanings = await TDK.getKubbealtiMeanings(word2);
|
|
2616
|
+
if (meanings === null)
|
|
2617
|
+
return fail("Kubbealt\u0131 Lugat\u0131'na ula\u015F\u0131lamad\u0131.");
|
|
2618
|
+
return ok({ word: word2, meanings });
|
|
2619
|
+
})
|
|
2620
|
+
);
|
|
2621
|
+
server.tool(
|
|
2622
|
+
"tdk_wiktionary",
|
|
2623
|
+
"T\xFCrk\xE7e Wiktionary'den (tr.wiktionary.org, resmi MediaWiki API) bir maddenin b\xF6l\xFCmlere ayr\u0131lm\u0131\u015F i\xE7eri\u011Fini \xE7eker.",
|
|
2624
|
+
{
|
|
2625
|
+
word: import_zod.z.string().describe("Aranacak madde ba\u015Fl\u0131\u011F\u0131."),
|
|
2626
|
+
section: import_zod.z.string().optional().describe("Sadece belirli bir b\xF6l\xFCm istenirse ba\u015Fl\u0131k ad\u0131 (\xF6rn: 'K\xF6ken', '\xC7eviriler').")
|
|
2627
|
+
},
|
|
2628
|
+
guard(async ({ word: word2, section }) => {
|
|
2629
|
+
if (section) {
|
|
2630
|
+
const text = await TDK.getWiktionarySection(word2, section);
|
|
2631
|
+
return text ? ok({ word: word2, section, text }) : fail(`Wiktionary'de "${word2}" i\xE7in "${section}" b\xF6l\xFCm\xFC bulunamad\u0131.`);
|
|
2632
|
+
}
|
|
2633
|
+
const entry = await TDK.getWiktionary(word2);
|
|
2634
|
+
return entry ? ok(entry) : fail(`Wiktionary'de "${word2}" bulunamad\u0131.`);
|
|
2635
|
+
})
|
|
2636
|
+
);
|
|
2637
|
+
server.tool(
|
|
2638
|
+
"tdk_spell_check",
|
|
2639
|
+
"Bir kelimenin do\u011Fru yaz\u0131l\u0131p yaz\u0131lmad\u0131\u011F\u0131n\u0131 denetler; yanl\u0131\u015Fsa klavye/diakritik fark\u0131ndal\u0131kl\u0131 en yak\u0131n madde \xF6nerisi verir, \xE7ekimli bi\xE7imse k\xF6k\xFCn\xFC d\xF6nd\xFCr\xFCr.",
|
|
2640
|
+
{ word: import_zod.z.string().describe("Yaz\u0131m\u0131 denetlenecek kelime (\xF6rn: 'yanl\u0131z', 'arabs').") },
|
|
2641
|
+
guard(async ({ word: word2 }) => ok(await TDK.checkSpelling(word2)))
|
|
2642
|
+
);
|
|
2643
|
+
server.tool(
|
|
2644
|
+
"tdk_proofread",
|
|
2645
|
+
"Bir T\xFCrk\xE7e metni imla, ayr\u0131/biti\u015Fik yaz\u0131m ve 'da/de', 'ki', 'mi' ba\u011Fla\xE7/ek hatalar\u0131 a\xE7\u0131s\u0131ndan denetler.",
|
|
2646
|
+
{ text: import_zod.z.string().describe("Denetlenecek T\xFCrk\xE7e metin.") },
|
|
2647
|
+
guard(async ({ text }) => ok(await TDK.proofread(text)))
|
|
2648
|
+
);
|
|
2649
|
+
server.tool(
|
|
2650
|
+
"tdk_stem",
|
|
2651
|
+
"Bir kelimenin morfolojik k\xF6k\xFCn\xFC (ek s\u0131y\u0131rma / stemming) bulur ve \xE7ekimli olup olmad\u0131\u011F\u0131n\u0131 belirtir (\xF6rn: 'kitab\u0131m\u0131z\u0131n' -> 'kitap').",
|
|
2652
|
+
{ word: import_zod.z.string().describe("K\xF6k\xFC aranacak kelime.") },
|
|
2653
|
+
guard(async ({ word: word2 }) => {
|
|
2654
|
+
const result = await TDK.stem(word2);
|
|
2655
|
+
return result ? ok(result) : fail(`"${word2}" i\xE7in k\xF6k tespit edilemedi.`);
|
|
2656
|
+
})
|
|
2657
|
+
);
|
|
2658
|
+
server.tool(
|
|
2659
|
+
"tdk_analyze_text",
|
|
2660
|
+
"Bir metindeki her kelime i\xE7in k\xF6k, anlam ve k\xF6ken bilgisini toplu olarak \xE7\u0131kar\u0131r.",
|
|
2661
|
+
{ text: import_zod.z.string().describe("Analiz edilecek T\xFCrk\xE7e metin.") },
|
|
2662
|
+
guard(async ({ text }) => ok(await TDK.analyzeText(text)))
|
|
2663
|
+
);
|
|
2664
|
+
server.tool(
|
|
2665
|
+
"tdk_syllables",
|
|
2666
|
+
"Bir kelimeyi T\xFCrk\xE7e hece kurallar\u0131na g\xF6re hecelere ay\u0131r\u0131r (tamamen yerel, a\u011F iste\u011Fi yok).",
|
|
2667
|
+
{ word: import_zod.z.string().describe("Hecelenecek kelime.") },
|
|
2668
|
+
guard(async ({ word: word2 }) => ok({ word: word2, syllables: TDK.syllabicate(word2) }))
|
|
2669
|
+
);
|
|
2670
|
+
server.tool(
|
|
2671
|
+
"tdk_vowel_harmony",
|
|
2672
|
+
"Bir kelimenin b\xFCy\xFCk \xFCnl\xFC uyumuna ve k\xFC\xE7\xFCk \xFCnl\xFC uyumuna (d\xFCzl\xFCk-yuvarlakl\u0131k) uyup uymad\u0131\u011F\u0131n\u0131 kontrol eder.",
|
|
2673
|
+
{ word: import_zod.z.string().describe("Kontrol edilecek kelime.") },
|
|
2674
|
+
guard(
|
|
2675
|
+
async ({ word: word2 }) => ok({
|
|
2676
|
+
word: word2,
|
|
2677
|
+
vowelHarmony: TDK.checkVowelHarmony(word2),
|
|
2678
|
+
labialHarmony: TDK.checkLabialHarmony(word2)
|
|
2679
|
+
})
|
|
2680
|
+
)
|
|
2681
|
+
);
|
|
2682
|
+
server.tool(
|
|
2683
|
+
"tdk_autocomplete",
|
|
2684
|
+
"Bir \xF6nek ile ba\u015Flayan s\xF6zl\xFCk maddelerini (autocomplete) d\xF6nd\xFCr\xFCr.",
|
|
2685
|
+
{ prefix: import_zod.z.string().describe("Aranacak \xF6nek (\xF6rn: 'kalem').") },
|
|
2686
|
+
guard(async ({ prefix }) => ok({ prefix, suggestions: await TDK.getSuggestions(prefix) }))
|
|
2687
|
+
);
|
|
2688
|
+
server.tool(
|
|
2689
|
+
"tdk_pattern_search",
|
|
2690
|
+
"Bulmaca deseni ile e\u015Fle\u015Fen s\xF6zl\xFCk maddelerini bulur: '_' veya '?' tek harf, '*' s\u0131f\u0131r ya da daha fazla harf yerine ge\xE7er (\xF6rn: 'k_l_m').",
|
|
2691
|
+
{
|
|
2692
|
+
pattern: import_zod.z.string().describe("Desen. '_'/'?' = tek harf, '*' = s\u0131f\u0131r ya da daha fazla harf."),
|
|
2693
|
+
max_results: import_zod.z.number().int().min(1).max(500).default(50).describe("En fazla sonu\xE7 say\u0131s\u0131.")
|
|
2694
|
+
},
|
|
2695
|
+
guard(async ({ pattern, max_results }) => {
|
|
2696
|
+
const matches = await TDK.patternSearch(pattern, { maxResults: max_results });
|
|
2697
|
+
return ok({ pattern, count: matches.length, matches });
|
|
2698
|
+
})
|
|
2699
|
+
);
|
|
2700
|
+
server.tool(
|
|
2701
|
+
"tdk_anagram",
|
|
2702
|
+
"Verilen harflerle kurulabilecek T\xFCrk\xE7e s\xF6zl\xFCk maddelerini bulur. Birebir anagram varsa onlar, yoksa harflerin bir alt k\xFCmesiyle kurulan kelimeler d\xF6ner.",
|
|
2703
|
+
{
|
|
2704
|
+
letters: import_zod.z.string().describe("Kullan\u0131lacak harfler (\xF6rn: 'kalem')."),
|
|
2705
|
+
exact_length: import_zod.z.boolean().default(false).describe("Sadece harflerin tamam\u0131n\u0131 kullanan birebir anagramlar\u0131 d\xF6nd\xFCr."),
|
|
2706
|
+
max_results: import_zod.z.number().int().min(1).max(500).default(50).describe("En fazla sonu\xE7 say\u0131s\u0131.")
|
|
2707
|
+
},
|
|
2708
|
+
guard(async ({ letters, exact_length, max_results }) => {
|
|
2709
|
+
const words = await TDK.findAnagrams(letters, { exactLength: exact_length, maxResults: max_results });
|
|
2710
|
+
return ok({ letters, count: words.length, words });
|
|
2711
|
+
})
|
|
2712
|
+
);
|
|
2713
|
+
server.tool(
|
|
2714
|
+
"tdk_rhymes",
|
|
2715
|
+
"Bir kelime ile kafiyeli (son harfleri uyu\u015Fan) s\xF6zl\xFCk maddelerini bulur.",
|
|
2716
|
+
{
|
|
2717
|
+
word: import_zod.z.string().describe("Kafiyesi aranacak kelime."),
|
|
2718
|
+
min_letters: import_zod.z.number().int().min(1).default(3).describe("Uyu\u015Fmas\u0131 gereken en az son harf say\u0131s\u0131."),
|
|
2719
|
+
max_results: import_zod.z.number().int().min(1).max(500).default(50).describe("En fazla sonu\xE7 say\u0131s\u0131.")
|
|
2720
|
+
},
|
|
2721
|
+
guard(async ({ word: word2, min_letters, max_results }) => {
|
|
2722
|
+
const rhymes = await TDK.findRhymes(word2, { minLetters: min_letters, maxResults: max_results });
|
|
2723
|
+
return ok({ word: word2, count: rhymes.length, rhymes });
|
|
2724
|
+
})
|
|
2725
|
+
);
|
|
2726
|
+
server.tool(
|
|
2727
|
+
"tdk_compare",
|
|
2728
|
+
"\u0130ki kelimeyi anlam say\u0131s\u0131, k\xF6ken, hece b\xF6l\xFCn\xFC\u015F\xFC ve \xFCnl\xFC uyumu a\xE7\u0131s\u0131ndan kar\u015F\u0131la\u015Ft\u0131r\u0131r.",
|
|
2729
|
+
{
|
|
2730
|
+
a: import_zod.z.string().describe("Birinci kelime."),
|
|
2731
|
+
b: import_zod.z.string().describe("\u0130kinci kelime.")
|
|
2732
|
+
},
|
|
2733
|
+
guard(async ({ a, b }) => ok(await TDK.compareWords(a, b)))
|
|
2734
|
+
);
|
|
2735
|
+
server.tool(
|
|
2736
|
+
"tdk_audio_url",
|
|
2737
|
+
"Bir kelimenin TDK seslendirme (.wav) URL'sini d\xF6nd\xFCr\xFCr (bulunamazsa null).",
|
|
2738
|
+
{ word: import_zod.z.string().describe("Seslendirmesi aranacak kelime.") },
|
|
2739
|
+
guard(async ({ word: word2 }) => ok({ word: word2, audioUrl: await TDK.getAudioUrl(word2) }))
|
|
2740
|
+
);
|
|
2741
|
+
server.tool(
|
|
2742
|
+
"tdk_word_of_the_day",
|
|
2743
|
+
"TDK'nin 'g\xFCn\xFCn kelimesi'ni anlamlar\u0131yla d\xF6nd\xFCr\xFCr.",
|
|
2744
|
+
{},
|
|
2745
|
+
guard(async () => {
|
|
2746
|
+
const wotd = await TDK.getWordOfTheDay();
|
|
2747
|
+
return wotd ? ok(wotd) : fail("G\xFCn\xFCn kelimesi al\u0131namad\u0131.");
|
|
2748
|
+
})
|
|
2749
|
+
);
|
|
2750
|
+
server.tool(
|
|
2751
|
+
"tdk_random_word",
|
|
2752
|
+
"TDK i\xE7eri\u011Finden rastgele bir kelime ya da atas\xF6z\xFC d\xF6nd\xFCr\xFCr.",
|
|
2753
|
+
{},
|
|
2754
|
+
guard(async () => {
|
|
2755
|
+
const pick = await TDK.getRandomWord();
|
|
2756
|
+
return pick ? ok(pick) : fail("Rastgele i\xE7erik al\u0131namad\u0131.");
|
|
2757
|
+
})
|
|
2758
|
+
);
|
|
2759
|
+
server.tool(
|
|
2760
|
+
"tdk_rules",
|
|
2761
|
+
"TDK Yaz\u0131m K\u0131lavuzu kurallar\u0131n\u0131 listeler; 'name' verilirse o kural\u0131n tam metnini d\xF6nd\xFCr\xFCr.",
|
|
2762
|
+
{
|
|
2763
|
+
name: import_zod.z.string().optional().describe("\u0130stenirse tek bir kural\u0131n ad\u0131 (\xF6rn: 'Ba\u011Fla\xE7 Olan da, de'nin Yaz\u0131l\u0131\u015F\u0131').")
|
|
2764
|
+
},
|
|
2765
|
+
guard(async ({ name }) => {
|
|
2766
|
+
if (name) {
|
|
2767
|
+
const rule = await TDK.getRule(name);
|
|
2768
|
+
return rule ? ok({ name, rule }) : fail(`"${name}" kural\u0131 bulunamad\u0131.`);
|
|
2769
|
+
}
|
|
2770
|
+
const rules = await TDK.getKurallar();
|
|
2771
|
+
return ok({ count: rules.length, rules });
|
|
2772
|
+
})
|
|
2773
|
+
);
|
|
2774
|
+
return server;
|
|
2775
|
+
}
|
|
2776
|
+
async function runMcpServer() {
|
|
2777
|
+
const server = createMcpServer();
|
|
2778
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
2779
|
+
await server.connect(transport);
|
|
2780
|
+
}
|
|
2781
|
+
var import_mcp, import_stdio, import_zod, VERSION;
|
|
2782
|
+
var init_mcp = __esm({
|
|
2783
|
+
"src/mcp.ts"() {
|
|
2784
|
+
"use strict";
|
|
2785
|
+
init_cjs_shims();
|
|
2786
|
+
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
2787
|
+
import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
2788
|
+
import_zod = require("zod");
|
|
2789
|
+
init_tdk();
|
|
2790
|
+
VERSION = "1.7.0";
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2440
2793
|
|
|
2441
2794
|
// src/cli.ts
|
|
2795
|
+
init_cjs_shims();
|
|
2796
|
+
init_tdk();
|
|
2442
2797
|
var rawArgs = process.argv.slice(2);
|
|
2443
2798
|
var jsonMode = rawArgs.includes("--json");
|
|
2444
2799
|
var args = rawArgs.filter((a) => a !== "--json");
|
|
@@ -2483,7 +2838,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
2483
2838
|
"repl",
|
|
2484
2839
|
"kubbealti",
|
|
2485
2840
|
"nisanyan",
|
|
2486
|
-
"viki"
|
|
2841
|
+
"viki",
|
|
2842
|
+
"mcp"
|
|
2487
2843
|
]);
|
|
2488
2844
|
var command = args[0];
|
|
2489
2845
|
var word = args.slice(1).join(" ");
|
|
@@ -2570,6 +2926,12 @@ async function startRepl() {
|
|
|
2570
2926
|
});
|
|
2571
2927
|
}
|
|
2572
2928
|
async function run() {
|
|
2929
|
+
const binaryName = (process.argv[1] || "").toLowerCase();
|
|
2930
|
+
if (command === "mcp" || rawArgs.includes("--mcp") || binaryName.includes("tdk-mcp")) {
|
|
2931
|
+
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
2932
|
+
await runMcpServer2();
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2573
2935
|
if (!command) {
|
|
2574
2936
|
if (process.stdin.isTTY) {
|
|
2575
2937
|
await startRepl();
|
|
@@ -2577,19 +2939,19 @@ async function run() {
|
|
|
2577
2939
|
}
|
|
2578
2940
|
console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
|
|
2579
2941
|
console.log(
|
|
2580
|
-
"Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki"
|
|
2942
|
+
"Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki, mcp"
|
|
2581
2943
|
);
|
|
2582
2944
|
console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
|
|
2583
2945
|
process.exit(1);
|
|
2584
2946
|
}
|
|
2585
2947
|
if (command === "--version" || command === "-v") {
|
|
2586
|
-
console.log("tdk-api-wrapper v1.
|
|
2948
|
+
console.log("tdk-api-wrapper v1.7.0");
|
|
2587
2949
|
process.exit(0);
|
|
2588
2950
|
}
|
|
2589
2951
|
if (command === "--help" || command === "-h") {
|
|
2590
2952
|
console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
|
|
2591
2953
|
console.log(
|
|
2592
|
-
"Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki"
|
|
2954
|
+
"Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki, mcp"
|
|
2593
2955
|
);
|
|
2594
2956
|
console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
|
|
2595
2957
|
process.exit(0);
|