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