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/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
- // src/errors.ts
27
- var TDKError = class extends Error {
28
- constructor(message) {
29
- super(message);
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
- var TDKValidationError = class extends TDKError {
35
- constructor(message) {
36
- super(message);
37
- this.name = "TDKValidationError";
38
- Object.setPrototypeOf(this, new.target.prototype);
39
- }
40
- };
41
- var TDKNetworkError = class extends TDKError {
42
- status;
43
- cause;
44
- constructor(message, options) {
45
- super(message);
46
- this.name = "TDKNetworkError";
47
- this.status = options?.status;
48
- this.cause = options?.cause;
49
- Object.setPrototypeOf(this, new.target.prototype);
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 TDK = class {
824
- static BASE_URL = "https://sozluk.gov.tr";
825
- static AUDIO_API_HOST = "api.sozluk.gov.tr";
826
- static KUBBEALTI_HOST = "eski.lugatim.com";
827
- /**
828
- * `eski.lugatim.com` (Kubbealtı Lugatı's data API) sends only its leaf
829
- * certificate during the TLS handshake, omitting the intermediates a
830
- * correctly configured server would include — a server-side misconfiguration,
831
- * not something we should paper over by disabling verification. These are
832
- * the two certificates the server *should* be sending (fetched from the
833
- * leaf's own Authority Information Access URLs), supplied here so Node can
834
- * still build a full, properly verified chain up to a root it already
835
- * trusts (ISRG Root X1). If Let's Encrypt rotates this intermediate, this
836
- * stops working and every Kubbealtı call fails closed to `null` — same
837
- * fail-closed contract as the rest of this file's fragile integrations.
838
- */
839
- static KUBBEALTI_EXTRA_CA = [
840
- `-----BEGIN CERTIFICATE-----
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
- `-----BEGIN CERTIFICATE-----
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
- // Configuration
904
- static defaultTimeoutMs = 8e3;
905
- static defaultRetries = 1;
906
- static maxCacheSize = 1e3;
907
- // Cache Mechanism
908
- static isCacheEnabled = false;
909
- static wordCache = /* @__PURE__ */ new Map();
910
- static dailyContentCache = null;
911
- static autocompleteCache = [];
912
- static autocompleteSet = /* @__PURE__ */ new Set();
913
- static stemCache = /* @__PURE__ */ new Map();
914
- /**
915
- * Configures global client options such as network timeout, retries, and cache size.
916
- */
917
- static configure(config) {
918
- if (config.timeoutMs !== void 0)
919
- this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
920
- if (config.retries !== void 0)
921
- this.defaultRetries = Math.max(0, config.retries);
922
- if (config.cache !== void 0)
923
- this.enableCache(config.cache);
924
- if (config.maxCacheSize !== void 0)
925
- this.maxCacheSize = Math.max(10, config.maxCacheSize);
926
- }
927
- /**
928
- * Enables or disables in-memory caching for API requests.
929
- */
930
- static enableCache(status = true) {
931
- this.isCacheEnabled = status;
932
- if (!status) {
933
- this.clearCache();
934
- }
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
- if (attempt < retries) {
974
- await this.delay(200 * (attempt + 1));
975
- continue;
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
- return res;
978
- } catch (err) {
979
- lastError = err;
980
- if (attempt < retries) {
981
- await this.delay(200 * (attempt + 1));
982
- continue;
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
- throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
987
- cause: lastError
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
- return meanings;
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
- return proverbs;
1202
- }
1203
- /**
1204
- * Returns the etymological origin of the word, or "Türkçe" if TDK doesn't
1205
- * record a foreign origin for it. Returns `null` only when the word itself
1206
- * isn't found in the dictionary at all.
1207
- */
1208
- static async getOrigin(word2) {
1209
- const results = await this.getWord(word2);
1210
- if (results.length === 0)
1211
- return null;
1212
- return results[0].lisan || "T\xFCrk\xE7e";
1213
- }
1214
- /**
1215
- * Returns whether the word has a recorded foreign etymological origin.
1216
- * Returns `null` (instead of a boolean) when the word isn't found at all.
1217
- */
1218
- static async isForeignWord(word2) {
1219
- const origin = await this.getOrigin(word2);
1220
- if (origin === null)
1221
- return null;
1222
- return origin !== "T\xFCrk\xE7e";
1223
- }
1224
- /**
1225
- * Groups a list of words by their etymological origin. Words not found in
1226
- * the dictionary are grouped under "Bilinmiyor". Throttled like getWordsBatch.
1227
- */
1228
- static async groupByOrigin(words) {
1229
- const groups = {};
1230
- for (const word2 of words) {
1231
- const origin = await this.getOrigin(word2) ?? "Bilinmiyor";
1232
- if (!groups[origin])
1233
- groups[origin] = [];
1234
- groups[origin].push(word2);
1235
- await this.delay(200);
1236
- }
1237
- return groups;
1238
- }
1239
- /**
1240
- * Returns literature examples containing the word.
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
- return examples;
1258
- }
1259
- /**
1260
- * Calls the `api.sozluk.gov.tr/gts-yeni` endpoint the official web UI uses
1261
- * internally (richer than the public `/gts`: includes `seskod`,
1262
- * `anlamEsAnlam`/`anlamKarsitAnlam`, etc). That endpoint 403s unless the
1263
- * request looks like it came from a browser tab on sozluk.gov.tr: it needs
1264
- * an `Origin`/`Referer` pair matching that site AND a browser-like
1265
- * `User-Agent` (our usual `TDK-API-Nodejs-Wrapper/…` UA gets rejected).
1266
- * `fetch` (undici) also strips a manually-set `Origin` header as a
1267
- * forbidden header name, so this uses `node:https` directly instead.
1268
- * This is inherently fragile scraping of an undocumented endpoint — if
1269
- * TDK tightens this check further, this should fail closed to `null`
1270
- * rather than throw.
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
- req.on("error", () => resolve(null));
1299
- req.end();
1300
- });
1301
- }
1302
- static async fetchSeskod(word2) {
1303
- const data = await this.fetchGtsYeni(word2);
1304
- const seskod = data?.[0]?.seskod;
1305
- return seskod ? String(seskod) : null;
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
- return [...new Set(synonyms)];
1328
- }
1329
- /**
1330
- * Returns antonyms ("zıt anlamlı kelimeler") recorded for the word, pooled
1331
- * across all of its meanings. Uses the same undocumented `gts-yeni`
1332
- * endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
1333
- */
1334
- static async getAntonyms(word2) {
1335
- if (!word2 || word2.trim() === "")
1336
- return [];
1337
- const data = await this.fetchGtsYeni(word2.trim().toLocaleLowerCase("tr-TR"));
1338
- if (!data)
1339
- return [];
1340
- const antonyms = [];
1341
- for (const entry of data) {
1342
- for (const anlam of entry.anlamlarListe ?? []) {
1343
- for (const ka of anlam.anlamKarsitAnlam ?? []) {
1344
- if (ka.deger)
1345
- antonyms.push(ka.deger);
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
- return [...new Set(antonyms)];
1350
- }
1351
- /**
1352
- * Returns the direct URL of the audio pronunciation, if TDK has one recorded for this word.
1353
- */
1354
- static async getAudioUrl(word2) {
1355
- if (!word2 || word2.trim() === "") {
1356
- throw new TDKValidationError("Word parameter cannot be empty.");
1357
- }
1358
- const seskod = await this.fetchSeskod(word2.trim().toLocaleLowerCase("tr-TR"));
1359
- if (!seskod)
1360
- return null;
1361
- return `https://${this.AUDIO_API_HOST}/ses/${encodeURIComponent(seskod)}.wav`;
1362
- }
1363
- /**
1364
- * Downloads the audio pronunciation to the specified path.
1365
- */
1366
- static async downloadAudio(word2, destPath) {
1367
- const url = await this.getAudioUrl(word2);
1368
- if (!url)
1369
- return null;
1370
- const finalPath = destPath || path.join(os.tmpdir(), `${word2}.wav`);
1371
- try {
1372
- const res = await fetch(url);
1373
- if (!res.ok)
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
- const root = await this.findRoot(word2);
1423
- if (root) {
1424
- const isInflected = root !== cleanWord;
1425
- return {
1426
- isCorrect: true,
1427
- word: word2,
1428
- isInflected,
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
- let best = null;
1444
- for (const candidate of this.autocompleteCache) {
1445
- if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
1446
- continue;
1447
- if (Math.abs(candidate.length - cleanWord.length) > 2)
1448
- continue;
1449
- const rawDist = this.damerauLevenshtein(cleanWord, candidate);
1450
- if (rawDist === 0 || rawDist > 2)
1451
- continue;
1452
- const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
1453
- const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
1454
- const score = this.keyboardAwareDistance(cleanWord, candidate) + (firstMismatch > 0 ? 1.2 : 0);
1455
- 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;
1456
- if (better) {
1457
- best = { candidate, score, rawDist, firstMismatch, lengthMismatch };
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
- if (best && best.rawDist <= 2 && (best.firstMismatch === 0 || best.rawDist <= 1)) {
1461
- return { isCorrect: false, word: word2, suggestion: best.candidate };
1462
- }
1463
- return { isCorrect: false, word: word2 };
1464
- }
1465
- /**
1466
- * Fetches daily content (word of the day, proverbs, rules, etc).
1467
- * `bypassCache` skips both reading and writing `dailyContentCache` even
1468
- * when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
1469
- * needs a fresh random `/icerik` draw on every attempt; without it, once
1470
- * caching is enabled the loop would just re-read the same cached response
1471
- * 25 times and could never find a rule outside that first random draw.
1472
- */
1473
- static async getDailyContent(bypassCache = false) {
1474
- if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
1475
- return this.dailyContentCache;
1476
- try {
1477
- const response = await fetch(`${this.BASE_URL}/icerik`, {
1478
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
1479
- });
1480
- if (response.ok) {
1481
- const data = await response.json();
1482
- if (!bypassCache && this.isCacheEnabled)
1483
- this.dailyContentCache = data;
1484
- return data;
1485
- }
1486
- } catch {
1487
- return null;
1488
- }
1489
- return null;
1490
- }
1491
- /**
1492
- * Returns today's word of the day along with all of its listed meanings.
1493
- */
1494
- static async getWordOfTheDay() {
1495
- const daily = await this.getDailyContent();
1496
- if (!daily || daily.kelime.length === 0)
1497
- return null;
1498
- const word2 = daily.kelime[0].madde;
1499
- const meanings = daily.kelime.filter((k) => k.madde === word2).map((k) => k.anlam);
1500
- return { word: word2, meanings };
1501
- }
1502
- /**
1503
- * Picks a random entry (word or proverb) from today's daily content.
1504
- * Note: this samples from today's `getDailyContent()` picks, not the full dictionary.
1505
- */
1506
- static async getRandomWord() {
1507
- const daily = await this.getDailyContent();
1508
- if (!daily)
1509
- return null;
1510
- const pool = [
1511
- ...daily.kelime.map((k) => ({ type: "kelime", madde: k.madde, anlam: k.anlam })),
1512
- ...daily.atasoz.map((a) => ({ type: "atasoz", madde: a.madde, anlam: a.anlam }))
1513
- ];
1514
- if (pool.length === 0)
1515
- return null;
1516
- return pool[Math.floor(Math.random() * pool.length)];
1517
- }
1518
- /**
1519
- * Returns the spelling-rule page(s) ("yazım kuralları") linked from TDK's
1520
- * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
1521
- * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
1522
- * appears to hand back a single randomly-rotated rule per request, so two
1523
- * calls a second apart can return entirely different rules. `bypassCache`
1524
- * (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
1525
- * draw even when `enableCache(true)` is on.
1526
- */
1527
- static async getKurallar(bypassCache = false) {
1528
- const daily = await this.getDailyContent(bypassCache);
1529
- return daily?.kural ?? [];
1530
- }
1531
- /**
1532
- * Fetches the full plain-text content of a named spelling rule (matched
1533
- * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
1534
- * hands back a single randomly-rotated rule per request (out of a pool of
1535
- * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
1536
- * draw would rarely match a given name — this re-draws until it finds a
1537
- * match or gives up. Draws happen in concurrent batches (each `/icerik`
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
- return null;
1565
- }
1566
- /**
1567
- * `tdk.gov.tr` rule pages are WordPress/Avada-themed. The actual article
1568
- * text lives in `<div ... itemprop="text">...</div>` right before a
1569
- * `<footer class="entry...">` (share buttons, author box, structured-data
1570
- * spans) cutting there avoids that trailing cruft.
1571
- */
1572
- static async fetchRuleText(url) {
1573
- try {
1574
- const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
1575
- if (!response.ok)
1576
- return null;
1577
- const html = await response.text();
1578
- const marker = html.indexOf('itemprop="text"');
1579
- if (marker === -1)
1580
- return null;
1581
- const contentStart = html.indexOf(">", marker) + 1;
1582
- const contentEnd = html.indexOf("<footer", contentStart);
1583
- if (contentEnd === -1)
1584
- return null;
1585
- return this.htmlToPlainText(html.slice(contentStart, contentEnd));
1586
- } catch {
1587
- return null;
1588
- }
1589
- }
1590
- static htmlToPlainText(html) {
1591
- return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;|&rsquo;/gi, "'").replace(/&amp;/gi, "&").replace(/[ \t]+/g, " ").replace(/[ \t]*\n[ \t]*/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
1592
- }
1593
- /**
1594
- * GETs a JSON path from Kubbealtı Lugatı's data API (`eski.lugatim.com`),
1595
- * supplying `KUBBEALTI_EXTRA_CA` to work around that host's incomplete
1596
- * certificate chain (see the constant's doc comment). Fails closed to
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
- (res) => {
1612
- if (res.statusCode !== 200) {
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
- let body = "";
1618
- res.on("data", (chunk) => body += chunk);
1619
- res.on("end", () => {
1620
- try {
1621
- resolve(JSON.parse(body));
1622
- } catch {
1623
- resolve(null);
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
- return [...new Set(compound)];
1823
- }
1824
- /**
1825
- * Returns the part of speech (isim, sıfat, zarf vb.).
1826
- * TDK's `ozelliklerListe` mixes grammatical categories (`tur: "3"`, e.g.
1827
- * sıfat/zarf/isim) with usage-register tags (`tur: "4"`, e.g. mecaz/argo)
1828
- * in the same list — only `tur === "3"` entries are actual parts of speech.
1829
- */
1830
- static async getPartOfSpeech(word2) {
1831
- const results = await this.getWord(word2);
1832
- const pos = /* @__PURE__ */ new Set();
1833
- for (const result of results) {
1834
- if (result.anlamlarListe) {
1835
- for (const anlam of result.anlamlarListe) {
1836
- if (anlam.ozelliklerListe) {
1837
- for (const ozellik of anlam.ozelliklerListe) {
1838
- if (ozellik.tur === "3")
1839
- pos.add(ozellik.tam_adi);
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
- if (pos.size === 0 && results.length > 0) {
1846
- pos.add("isim");
1847
- }
1848
- return Array.from(pos);
1849
- }
1850
- /**
1851
- * Compares two words side by side: meaning count, etymological origin,
1852
- * syllables and vowel-harmony compliance.
1853
- */
1854
- static async compareWords(a, b) {
1855
- const [meaningsA, meaningsB, originA, originB] = await Promise.all([
1856
- this.getMeanings(a),
1857
- this.getMeanings(b),
1858
- this.getOrigin(a),
1859
- this.getOrigin(b)
1860
- ]);
1861
- return {
1862
- a: {
1863
- word: a,
1864
- meaningCount: meaningsA.length,
1865
- origin: originA,
1866
- syllables: this.syllabicate(a),
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
- static STOPWORDS = /* @__PURE__ */ new Set([
1881
- "ve",
1882
- "veya",
1883
- "ile",
1884
- "ama",
1885
- "fakat",
1886
- "ancak",
1887
- "de",
1888
- "da",
1889
- "ki",
1890
- "bu",
1891
- "\u015Fu",
1892
- "o",
1893
- "bir",
1894
- "\xE7ok",
1895
- "az",
1896
- "gibi",
1897
- "i\xE7in",
1898
- "mi",
1899
- "m\u0131",
1900
- "mu",
1901
- "m\xFC",
1902
- "ne",
1903
- "her",
1904
- "hi\xE7",
1905
- "ben",
1906
- "sen",
1907
- "biz",
1908
- "siz",
1909
- "onlar",
1910
- "de\u011Fil",
1911
- "bile",
1912
- "diye"
1913
- ]);
1914
- static firstMeaning(results) {
1915
- for (const result of results) {
1916
- for (const anlam of result.anlamlarListe ?? []) {
1917
- if (anlam.anlam)
1918
- return anlam.anlam;
1625
+ static htmlToPlainText(html) {
1626
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;|&rsquo;/gi, "'").replace(/&amp;/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
- return null;
1922
- }
1923
- /**
1924
- * Analyzes every distinct word in a text (Turkish stopwords filtered out),
1925
- * returning each word's first meaning and etymological origin if found.
1926
- * Looks each word up individually (throttled), so scales with text length.
1927
- * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
1928
- * it does no morphological analysis, and neither does this method: a
1929
- * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
1930
- * suffix) will come back `found: false` even though the root is a real
1931
- * headword. This is an inherent limitation of the data source, not a bug.
1932
- */
1933
- static async analyzeText(text) {
1934
- const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
1935
- const unique = [...new Set(words)];
1936
- const analyses = [];
1937
- for (const word2 of unique) {
1938
- let results = await this.getWord(word2);
1939
- let found = results.length > 0;
1940
- let root;
1941
- let isInflected;
1942
- if (!found) {
1943
- const resolvedRoot = await this.findRoot(word2);
1944
- if (resolvedRoot) {
1945
- results = await this.getWord(resolvedRoot);
1946
- if (results.length > 0) {
1947
- found = true;
1948
- root = resolvedRoot;
1949
- isInflected = true;
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
- analyses.push({
1954
- word: word2,
1955
- found,
1956
- meaning: found ? this.firstMeaning(results) : null,
1957
- origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null,
1958
- root,
1959
- isInflected
1960
- });
1961
- await this.delay(200);
1962
- }
1963
- return analyses;
1964
- }
1965
- /**
1966
- * Damerau-Levenshtein edit-distance (optimal string alignment variant):
1967
- * like classic Levenshtein but also counts an adjacent-character
1968
- * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
1969
- * two substitutions — a very common class of typo that plain Levenshtein
1970
- * otherwise misses.
1971
- */
1972
- static damerauLevenshtein(a, b) {
1973
- const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
1974
- for (let i = 0; i <= a.length; i++)
1975
- dp[i][0] = i;
1976
- for (let j = 0; j <= b.length; j++)
1977
- dp[0][j] = j;
1978
- for (let i = 1; i <= a.length; i++) {
1979
- for (let j = 1; j <= b.length; j++) {
1980
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1981
- dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
1982
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
1983
- dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
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
- return dp[a.length][b.length];
1988
- }
1989
- /**
1990
- * Keyboard- and diacritic-aware edit distance: same optimal-string-alignment
1991
- * recurrence as {@link damerauLevenshtein}, but a substitution is charged by
1992
- * {@link keyboardSubCost} (a fraction of an edit when the two letters are
1993
- * adjacent on a Turkish Q keyboard or are ASCII/diacritic siblings) and a
1994
- * transposition costs {@link TRANSPOSITION_COST}. Insertions and deletions
1995
- * still cost a full 1. Used only to *rank* spelling candidates; the plain
1996
- * integer distance still gates whether a suggestion is offered at all.
1997
- */
1998
- static keyboardAwareDistance(a, b) {
1999
- const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
2000
- for (let i = 0; i <= a.length; i++)
2001
- dp[i][0] = i;
2002
- for (let j = 0; j <= b.length; j++)
2003
- dp[0][j] = j;
2004
- for (let i = 1; i <= a.length; i++) {
2005
- for (let j = 1; j <= b.length; j++) {
2006
- const cost = keyboardSubCost(a[i - 1], b[j - 1]);
2007
- dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
2008
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
2009
- dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + TRANSPOSITION_COST);
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
- return dp[a.length][b.length];
2014
- }
2015
- /**
2016
- * Fetches multiple words concurrently with a small delay to avoid rate limiting.
2017
- */
2018
- static async getWordsBatch(words) {
2019
- const results = [];
2020
- for (const word2 of words) {
2021
- try {
2022
- const res = await this.getWord(word2);
2023
- results.push(res);
2024
- } catch {
2025
- results.push([]);
2026
- }
2027
- await this.delay(200);
2028
- }
2029
- return results;
2030
- }
2031
- /**
2032
- * Syllabicates a Turkish word based on general grammar rules.
2033
- * Handles syllable separation for vowels, single consonants, double consonants,
2034
- * and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
2035
- */
2036
- static syllabicate(word2) {
2037
- const vowels = /[aeıioöuüAEIİOÖUÜ]/;
2038
- const ONSET_CLUSTERS = /* @__PURE__ */ new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
2039
- const result = [];
2040
- let currentSyllable = "";
2041
- for (let i = word2.length - 1; i >= 0; i--) {
2042
- currentSyllable = word2[i] + currentSyllable;
2043
- if (vowels.test(word2[i])) {
2044
- if (i - 1 >= 0 && !vowels.test(word2[i - 1])) {
2045
- if (i - 2 >= 0 && vowels.test(word2[i - 2])) {
2046
- currentSyllable = word2[i - 1] + currentSyllable;
2047
- i--;
2048
- } else if (i - 2 >= 0 && !vowels.test(word2[i - 2])) {
2049
- if (i - 3 >= 0 && !vowels.test(word2[i - 3]) && ONSET_CLUSTERS.has((word2[i - 2] + word2[i - 1]).toLowerCase())) {
2050
- currentSyllable = word2[i - 2] + word2[i - 1] + currentSyllable;
2051
- i -= 2;
2052
- } else {
2053
- currentSyllable = word2[i - 1] + currentSyllable;
2054
- i--;
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
- result.unshift(currentSyllable);
2059
- currentSyllable = "";
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
- if (isValid && lower !== clean) {
2177
- if (lower.length === clean.length) {
2178
- exactMatches.push(headword);
2179
- } else {
2180
- subMatches.push(headword);
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
- if (exactMatches.length > 0 || forceExact) {
2185
- return exactMatches.slice(0, max);
2186
- }
2187
- subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
2188
- return subMatches.slice(0, max);
2189
- }
2190
- /**
2191
- * Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
2192
- * @param word The target word
2193
- * @param options.minLetters Minimum number of ending characters that must match (default: 3)
2194
- * @param options.maxResults Maximum number of rhyme results to return (default: 50)
2195
- */
2196
- static async findRhymes(word2, options) {
2197
- if (!word2 || word2.trim() === "")
2198
- return [];
2199
- await this.ensureAutocompleteLoaded();
2200
- const clean = word2.trim().toLocaleLowerCase("tr-TR");
2201
- const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
2202
- const max = options?.maxResults ?? 50;
2203
- const suffix = clean.slice(-minLetters);
2204
- const results = [];
2205
- for (const headword of this.autocompleteCache) {
2206
- const lower = headword.toLocaleLowerCase("tr-TR");
2207
- if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
2208
- results.push(headword);
2209
- if (results.length >= max)
2210
- break;
2211
- }
2212
- }
2213
- return results;
2214
- }
2215
- /**
2216
- * Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
2217
- * Detects:
2218
- * 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
2219
- * 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
2220
- * 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
2221
- * 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
2222
- */
2223
- static async proofread(text) {
2224
- if (!text || text.trim() === "") {
2225
- return { text: text || "", issues: [], isCorrect: true };
2226
- }
2227
- await this.ensureAutocompleteLoaded();
2228
- const issues = [];
2229
- const SOMBAHCEMI = /* @__PURE__ */ new Set([
2230
- "sanki",
2231
- "oysaki",
2232
- "mademki",
2233
- "belki",
2234
- "halbuki",
2235
- "\xE7\xFCnk\xFC",
2236
- "me\u011Ferki",
2237
- "illaki"
2238
- ]);
2239
- const PHRASE_MISTAKES = [
2240
- {
2241
- regex: /\bhiç\s+bir\b/gi,
2242
- suggestion: "hi\xE7bir",
2243
- message: "'hi\xE7bir' belgisiz s\u0131fat\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2244
- type: "spelling"
2245
- },
2246
- {
2247
- regex: /\bbir\s+çok\b/gi,
2248
- suggestion: "bir\xE7ok",
2249
- message: "'bir\xE7ok' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2250
- type: "spelling"
2251
- },
2252
- {
2253
- regex: /\bbir\s+kaç\b/gi,
2254
- suggestion: "birka\xE7",
2255
- message: "'birka\xE7' belgisiz s\u0131fat\u0131/zamiri biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2256
- type: "spelling"
2257
- },
2258
- {
2259
- regex: /\bbir\s+az\b/gi,
2260
- suggestion: "biraz",
2261
- message: "'biraz' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2262
- type: "spelling"
2263
- },
2264
- {
2265
- regex: /\bher\s+hangi\b/gi,
2266
- suggestion: "herhangi",
2267
- message: "'herhangi' s\xF6zc\xFC\u011F\xFC biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2268
- type: "spelling"
2269
- },
2270
- {
2271
- regex: /\bgit\s+gide\b/gi,
2272
- suggestion: "gitgide",
2273
- message: "'gitgide' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2274
- type: "spelling"
2275
- },
2276
- {
2277
- regex: /\bbirden\s+bire\b/gi,
2278
- suggestion: "birdenbire",
2279
- message: "'birdenbire' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2280
- type: "spelling"
2281
- },
2282
- {
2283
- regex: /\brast\s+gele\b/gi,
2284
- suggestion: "rastgele",
2285
- message: "'rastgele' zarf\u0131 biti\u015Fik yaz\u0131lmal\u0131d\u0131r.",
2286
- type: "spelling"
2287
- }
2288
- ];
2289
- const coveredRanges = [];
2290
- for (const pm of PHRASE_MISTAKES) {
2291
- let pmMatch;
2292
- while ((pmMatch = pm.regex.exec(text)) !== null) {
2293
- const start = pmMatch.index;
2294
- const end = start + pmMatch[0].length;
2295
- coveredRanges.push({ start, end });
2296
- issues.push({
2297
- type: pm.type,
2298
- word: pmMatch[0],
2299
- startIndex: start,
2300
- endIndex: end,
2301
- suggestion: pm.suggestion,
2302
- message: pm.message
2303
- });
2304
- }
2305
- }
2306
- const tokenRegex = /[\p{L}0-9'’]+/gu;
2307
- let match;
2308
- while ((match = tokenRegex.exec(text)) !== null) {
2309
- const rawWord = match[0];
2310
- const startIndex = match.index;
2311
- const endIndex = startIndex + rawWord.length;
2312
- const lower = rawWord.toLocaleLowerCase("tr-TR");
2313
- if (/^\d+$/.test(lower))
2314
- continue;
2315
- if (coveredRanges.some((r) => startIndex >= r.start && endIndex <= r.end))
2316
- continue;
2317
- let flagged = false;
2318
- 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)?)$/);
2319
- if (questionMatch) {
2320
- const base = questionMatch[1];
2321
- const particle = questionMatch[2];
2322
- if (base.length >= 2 && (await this.isHeadword(base) || await this.findRoot(base) !== null)) {
2323
- if (!await this.isHeadword(lower)) {
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: "question_particle",
2421
+ type: "spelling",
2326
2422
  word: rawWord,
2327
2423
  startIndex,
2328
2424
  endIndex,
2329
- suggestion: `${base} ${particle}`,
2330
- message: `'${particle}' soru eki kendinden \xF6nceki kelimeden ayr\u0131 yaz\u0131lmal\u0131d\u0131r.`
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
- 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;
2337
- if (!flagged && lower.endsWith("ki") && lower.length > 3) {
2338
- const base = lower.slice(0, -2);
2339
- if (!SOMBAHCEMI.has(lower)) {
2340
- if (!await this.isHeadword(lower)) {
2341
- const root = await this.findRoot(base);
2342
- const isVerb = (base === "demek" || base === "kald\u0131" || base === "yeter" || base === "bilmem" || VERB_CONJUGATION_REGEX.test(base)) && (root ? root.endsWith("mek") || root.endsWith("mak") : true);
2343
- if (isVerb) {
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: "conjunction_da",
2444
+ type: "spelling",
2367
2445
  word: rawWord,
2368
2446
  startIndex,
2369
2447
  endIndex,
2370
- suggestion: `${base} ${correctEnding}`,
2371
- message: `'da/de' ba\u011Flac\u0131 fiillerden sonra her zaman ayr\u0131 yaz\u0131l\u0131r (ba\u011Fla\xE7 olan da/de sertle\u015Fmez).`
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.5.1");
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);