tdk-api-wrapper 1.0.1 → 1.2.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.
@@ -0,0 +1,741 @@
1
+ // src/errors.ts
2
+ var TDKError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "TDKError";
6
+ Object.setPrototypeOf(this, new.target.prototype);
7
+ }
8
+ };
9
+ var TDKValidationError = class extends TDKError {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "TDKValidationError";
13
+ Object.setPrototypeOf(this, new.target.prototype);
14
+ }
15
+ };
16
+ var TDKNetworkError = class extends TDKError {
17
+ status;
18
+ cause;
19
+ constructor(message, options) {
20
+ super(message);
21
+ this.name = "TDKNetworkError";
22
+ this.status = options?.status;
23
+ this.cause = options?.cause;
24
+ Object.setPrototypeOf(this, new.target.prototype);
25
+ }
26
+ };
27
+
28
+ // src/tdk.ts
29
+ import * as fs from "fs";
30
+ import * as path from "path";
31
+ import * as os from "os";
32
+ import * as https from "https";
33
+ var TDK = class {
34
+ static BASE_URL = "https://sozluk.gov.tr";
35
+ static AUDIO_API_HOST = "api.sozluk.gov.tr";
36
+ // Cache Mechanism
37
+ static isCacheEnabled = false;
38
+ static wordCache = /* @__PURE__ */ new Map();
39
+ static dailyContentCache = null;
40
+ static autocompleteCache = [];
41
+ /**
42
+ * Enables or disables in-memory caching for API requests.
43
+ */
44
+ static enableCache(status = true) {
45
+ this.isCacheEnabled = status;
46
+ if (!status) {
47
+ this.clearCache();
48
+ }
49
+ }
50
+ /**
51
+ * Clears the internal cache.
52
+ */
53
+ static clearCache() {
54
+ this.wordCache.clear();
55
+ this.dailyContentCache = null;
56
+ this.autocompleteCache = [];
57
+ }
58
+ static delay(ms) {
59
+ return new Promise((resolve) => setTimeout(resolve, ms));
60
+ }
61
+ /**
62
+ * Fetches detailed information for a given word from the TDK Dictionary.
63
+ */
64
+ static async getWord(word) {
65
+ if (!word || word.trim() === "") {
66
+ throw new TDKValidationError("Word parameter cannot be empty.");
67
+ }
68
+ const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
69
+ if (this.isCacheEnabled && this.wordCache.has(cleanWord)) {
70
+ return this.wordCache.get(cleanWord);
71
+ }
72
+ const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
73
+ let response;
74
+ try {
75
+ response = await fetch(url, {
76
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
77
+ });
78
+ } catch (error) {
79
+ throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
80
+ }
81
+ if (!response.ok) {
82
+ throw new TDKNetworkError(`Failed to fetch word from TDK: HTTP ${response.status}.`, {
83
+ status: response.status
84
+ });
85
+ }
86
+ let data;
87
+ try {
88
+ data = await response.json();
89
+ } catch (error) {
90
+ throw new TDKNetworkError("Failed to fetch word from TDK: invalid JSON response.", { cause: error });
91
+ }
92
+ if (!Array.isArray(data) && data && "error" in data) {
93
+ if (this.isCacheEnabled)
94
+ this.wordCache.set(cleanWord, []);
95
+ return [];
96
+ }
97
+ const results = data;
98
+ if (this.isCacheEnabled) {
99
+ this.wordCache.set(cleanWord, results);
100
+ }
101
+ return results;
102
+ }
103
+ /**
104
+ * Helper method to get only the meanings (definitions) of a word as a string array.
105
+ */
106
+ static async getMeanings(word) {
107
+ const results = await this.getWord(word);
108
+ if (results.length === 0)
109
+ return [];
110
+ const meanings = [];
111
+ for (const result of results) {
112
+ if (result.anlamlarListe) {
113
+ for (const anlam of result.anlamlarListe) {
114
+ if (anlam.anlam)
115
+ meanings.push(anlam.anlam);
116
+ }
117
+ }
118
+ }
119
+ return meanings;
120
+ }
121
+ /**
122
+ * `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
123
+ * routes no longer serve JSON — they fall through to the SPA's `index.html`.
124
+ * The full ~81k-word headword list the site's own autocomplete UI uses is
125
+ * instead bundled directly into its main JS asset as a
126
+ * `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
127
+ * page to find that asset's current hashed filename, downloads it (a few
128
+ * MB, only once per process), and extracts the literal out of it. Fragile
129
+ * scraping of an implementation detail — if TDK's build stops embedding
130
+ * this, this fails closed to `[]` rather than throwing.
131
+ */
132
+ static async fetchAutocompleteData() {
133
+ try {
134
+ const homeResponse = await fetch(`${this.BASE_URL}/`, {
135
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
136
+ });
137
+ if (!homeResponse.ok)
138
+ return [];
139
+ const html = await homeResponse.text();
140
+ const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
141
+ if (!scriptMatch)
142
+ return [];
143
+ const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
144
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
145
+ });
146
+ if (!bundleResponse.ok)
147
+ return [];
148
+ const bundleJs = await bundleResponse.text();
149
+ const startMarker = 'JSON.parse(`[{"madde":';
150
+ const startIdx = bundleJs.indexOf(startMarker);
151
+ if (startIdx === -1)
152
+ return [];
153
+ const jsonStart = startIdx + "JSON.parse(".length + 1;
154
+ const jsonEnd = bundleJs.indexOf("`)", jsonStart);
155
+ if (jsonEnd === -1)
156
+ return [];
157
+ const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
158
+ return data.map((item) => item.madde).filter(Boolean);
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+ /**
164
+ * Returns autocomplete suggestions for a given prefix, searched over TDK's
165
+ * full headword list (see `fetchAutocompleteData`). The list is fetched
166
+ * and cached once per process regardless of `enableCache()` — the same
167
+ * caching behavior as before — and only cleared by `clearCache()`.
168
+ */
169
+ static async getSuggestions(prefix) {
170
+ if (!prefix || prefix.trim() === "")
171
+ return [];
172
+ if (this.autocompleteCache.length === 0) {
173
+ this.autocompleteCache = await this.fetchAutocompleteData();
174
+ }
175
+ const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
176
+ return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
177
+ }
178
+ /**
179
+ * Returns a list of proverbs and idioms containing the word.
180
+ */
181
+ static async getProverbs(word) {
182
+ const results = await this.getWord(word);
183
+ if (results.length === 0)
184
+ return [];
185
+ const proverbs = [];
186
+ for (const result of results) {
187
+ if (result.atasozu) {
188
+ for (const atasoz of result.atasozu) {
189
+ if (atasoz.madde)
190
+ proverbs.push(atasoz.madde);
191
+ }
192
+ }
193
+ }
194
+ return proverbs;
195
+ }
196
+ /**
197
+ * Returns the etymological origin of the word, or "Türkçe" if TDK doesn't
198
+ * record a foreign origin for it. Returns `null` only when the word itself
199
+ * isn't found in the dictionary at all.
200
+ */
201
+ static async getOrigin(word) {
202
+ const results = await this.getWord(word);
203
+ if (results.length === 0)
204
+ return null;
205
+ return results[0].lisan || "T\xFCrk\xE7e";
206
+ }
207
+ /**
208
+ * Returns whether the word has a recorded foreign etymological origin.
209
+ * Returns `null` (instead of a boolean) when the word isn't found at all.
210
+ */
211
+ static async isForeignWord(word) {
212
+ const origin = await this.getOrigin(word);
213
+ if (origin === null)
214
+ return null;
215
+ return origin !== "T\xFCrk\xE7e";
216
+ }
217
+ /**
218
+ * Groups a list of words by their etymological origin. Words not found in
219
+ * the dictionary are grouped under "Bilinmiyor". Throttled like getWordsBatch.
220
+ */
221
+ static async groupByOrigin(words) {
222
+ const groups = {};
223
+ for (const word of words) {
224
+ const origin = await this.getOrigin(word) ?? "Bilinmiyor";
225
+ if (!groups[origin])
226
+ groups[origin] = [];
227
+ groups[origin].push(word);
228
+ await this.delay(200);
229
+ }
230
+ return groups;
231
+ }
232
+ /**
233
+ * Returns literature examples containing the word.
234
+ */
235
+ static async getExamples(word) {
236
+ const results = await this.getWord(word);
237
+ const examples = [];
238
+ for (const result of results) {
239
+ if (result.anlamlarListe) {
240
+ for (const anlam of result.anlamlarListe) {
241
+ if (anlam.orneklerListe) {
242
+ for (const ornek of anlam.orneklerListe) {
243
+ const author = ornek.yazar && ornek.yazar.length > 0 ? ornek.yazar[0].tam_adi : null;
244
+ examples.push({ sentence: ornek.ornek, author });
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ return examples;
251
+ }
252
+ /**
253
+ * Calls the `api.sozluk.gov.tr/gts-yeni` endpoint the official web UI uses
254
+ * internally (richer than the public `/gts`: includes `seskod`,
255
+ * `anlamEsAnlam`/`anlamKarsitAnlam`, etc). That endpoint 403s unless the
256
+ * request looks like it came from a browser tab on sozluk.gov.tr: it needs
257
+ * an `Origin`/`Referer` pair matching that site AND a browser-like
258
+ * `User-Agent` (our usual `TDK-API-Nodejs-Wrapper/…` UA gets rejected).
259
+ * `fetch` (undici) also strips a manually-set `Origin` header as a
260
+ * forbidden header name, so this uses `node:https` directly instead.
261
+ * This is inherently fragile scraping of an undocumented endpoint — if
262
+ * TDK tightens this check further, this should fail closed to `null`
263
+ * rather than throw.
264
+ */
265
+ static fetchGtsYeni(word) {
266
+ return new Promise((resolve) => {
267
+ const req = https.request(
268
+ {
269
+ hostname: this.AUDIO_API_HOST,
270
+ path: `/gts-yeni?ara=${encodeURIComponent(word)}`,
271
+ method: "GET",
272
+ headers: {
273
+ "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",
274
+ Origin: this.BASE_URL,
275
+ Referer: `${this.BASE_URL}/`
276
+ }
277
+ },
278
+ (res) => {
279
+ let body = "";
280
+ res.on("data", (chunk) => body += chunk);
281
+ res.on("end", () => {
282
+ try {
283
+ const data = JSON.parse(body);
284
+ resolve(Array.isArray(data) ? data : null);
285
+ } catch {
286
+ resolve(null);
287
+ }
288
+ });
289
+ }
290
+ );
291
+ req.on("error", () => resolve(null));
292
+ req.end();
293
+ });
294
+ }
295
+ static async fetchSeskod(word) {
296
+ const data = await this.fetchGtsYeni(word);
297
+ const seskod = data?.[0]?.seskod;
298
+ return seskod ? String(seskod) : null;
299
+ }
300
+ /**
301
+ * Returns synonyms ("eş anlamlı kelimeler") recorded for the word, pooled
302
+ * across all of its meanings. Uses the same undocumented `gts-yeni`
303
+ * endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
304
+ */
305
+ static async getSynonyms(word) {
306
+ if (!word || word.trim() === "")
307
+ return [];
308
+ const data = await this.fetchGtsYeni(word.trim().toLocaleLowerCase("tr-TR"));
309
+ if (!data)
310
+ return [];
311
+ const synonyms = [];
312
+ for (const entry of data) {
313
+ for (const anlam of entry.anlamlarListe ?? []) {
314
+ for (const es of anlam.anlamEsAnlam ?? []) {
315
+ if (es.deger)
316
+ synonyms.push(es.deger);
317
+ }
318
+ }
319
+ }
320
+ return [...new Set(synonyms)];
321
+ }
322
+ /**
323
+ * Returns antonyms ("zıt anlamlı kelimeler") recorded for the word, pooled
324
+ * across all of its meanings. Uses the same undocumented `gts-yeni`
325
+ * endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
326
+ */
327
+ static async getAntonyms(word) {
328
+ if (!word || word.trim() === "")
329
+ return [];
330
+ const data = await this.fetchGtsYeni(word.trim().toLocaleLowerCase("tr-TR"));
331
+ if (!data)
332
+ return [];
333
+ const antonyms = [];
334
+ for (const entry of data) {
335
+ for (const anlam of entry.anlamlarListe ?? []) {
336
+ for (const ka of anlam.anlamKarsitAnlam ?? []) {
337
+ if (ka.deger)
338
+ antonyms.push(ka.deger);
339
+ }
340
+ }
341
+ }
342
+ return [...new Set(antonyms)];
343
+ }
344
+ /**
345
+ * Returns the direct URL of the audio pronunciation, if TDK has one recorded for this word.
346
+ */
347
+ static async getAudioUrl(word) {
348
+ if (!word || word.trim() === "") {
349
+ throw new TDKValidationError("Word parameter cannot be empty.");
350
+ }
351
+ const seskod = await this.fetchSeskod(word.trim().toLocaleLowerCase("tr-TR"));
352
+ if (!seskod)
353
+ return null;
354
+ return `https://${this.AUDIO_API_HOST}/ses/${encodeURIComponent(seskod)}.wav`;
355
+ }
356
+ /**
357
+ * Downloads the audio pronunciation to the specified path.
358
+ */
359
+ static async downloadAudio(word, destPath) {
360
+ const url = await this.getAudioUrl(word);
361
+ if (!url)
362
+ return null;
363
+ const finalPath = destPath || path.join(os.tmpdir(), `${word}.wav`);
364
+ try {
365
+ const res = await fetch(url);
366
+ if (!res.ok)
367
+ return null;
368
+ const buffer = await res.arrayBuffer();
369
+ fs.writeFileSync(finalPath, Buffer.from(buffer));
370
+ return finalPath;
371
+ } catch {
372
+ return null;
373
+ }
374
+ }
375
+ /**
376
+ * Checks spelling and returns suggestions if wrong.
377
+ */
378
+ static async checkSpelling(word) {
379
+ const results = await this.getWord(word);
380
+ if (results.length > 0) {
381
+ return { isCorrect: true, word };
382
+ }
383
+ const daily = await this.getDailyContent();
384
+ if (daily) {
385
+ const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
386
+ if (syydMatch) {
387
+ return { isCorrect: false, word, suggestion: syydMatch.dogrukelime };
388
+ }
389
+ const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
390
+ if (mixMatch) {
391
+ return { isCorrect: false, word, suggestion: mixMatch.dogru };
392
+ }
393
+ const candidates = [
394
+ ...daily.syyd.map((s) => s.dogrukelime),
395
+ ...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
396
+ ...daily.kelime.map((k) => k.madde)
397
+ ];
398
+ let best = null;
399
+ for (const candidate of candidates) {
400
+ const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
401
+ if (distance > 0 && (!best || distance < best.distance)) {
402
+ best = { candidate, distance };
403
+ }
404
+ }
405
+ if (best && best.distance <= 2) {
406
+ return { isCorrect: false, word, suggestion: best.candidate };
407
+ }
408
+ }
409
+ return { isCorrect: false, word };
410
+ }
411
+ /**
412
+ * Fetches daily content (word of the day, proverbs, rules, etc).
413
+ */
414
+ static async getDailyContent() {
415
+ if (this.isCacheEnabled && this.dailyContentCache)
416
+ return this.dailyContentCache;
417
+ try {
418
+ const response = await fetch(`${this.BASE_URL}/icerik`, {
419
+ headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
420
+ });
421
+ if (response.ok) {
422
+ const data = await response.json();
423
+ if (this.isCacheEnabled)
424
+ this.dailyContentCache = data;
425
+ return data;
426
+ }
427
+ } catch {
428
+ return null;
429
+ }
430
+ return null;
431
+ }
432
+ /**
433
+ * Returns today's word of the day along with all of its listed meanings.
434
+ */
435
+ static async getWordOfTheDay() {
436
+ const daily = await this.getDailyContent();
437
+ if (!daily || daily.kelime.length === 0)
438
+ return null;
439
+ const word = daily.kelime[0].madde;
440
+ const meanings = daily.kelime.filter((k) => k.madde === word).map((k) => k.anlam);
441
+ return { word, meanings };
442
+ }
443
+ /**
444
+ * Picks a random entry (word or proverb) from today's daily content.
445
+ * Note: this samples from today's `getDailyContent()` picks, not the full dictionary.
446
+ */
447
+ static async getRandomWord() {
448
+ const daily = await this.getDailyContent();
449
+ if (!daily)
450
+ return null;
451
+ const pool = [
452
+ ...daily.kelime.map((k) => ({ type: "kelime", madde: k.madde, anlam: k.anlam })),
453
+ ...daily.atasoz.map((a) => ({ type: "atasoz", madde: a.madde, anlam: a.anlam }))
454
+ ];
455
+ if (pool.length === 0)
456
+ return null;
457
+ return pool[Math.floor(Math.random() * pool.length)];
458
+ }
459
+ /**
460
+ * Returns the spelling-rule page(s) ("yazım kuralları") linked from TDK's
461
+ * `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
462
+ * Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
463
+ * appears to hand back a single randomly-rotated rule per request, so two
464
+ * calls a second apart can return entirely different rules.
465
+ */
466
+ static async getKurallar() {
467
+ const daily = await this.getDailyContent();
468
+ return daily?.kural ?? [];
469
+ }
470
+ /**
471
+ * Fetches the full plain-text content of a named spelling rule (matched
472
+ * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
473
+ * hands back a single randomly-rotated rule per request (out of a pool of
474
+ * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
475
+ * draw would rarely match a given name — this re-draws (bounded, with a
476
+ * short delay) until it finds a match or gives up. Returns `null` if no
477
+ * match turns up within the attempt budget or the matched page can't be
478
+ * parsed.
479
+ */
480
+ static async getRule(name) {
481
+ if (!name || name.trim() === "")
482
+ return null;
483
+ const target = name.trim().toLocaleLowerCase("tr-TR");
484
+ for (let attempt = 0; attempt < 25; attempt++) {
485
+ const rules = await this.getKurallar();
486
+ const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
487
+ if (match)
488
+ return this.fetchRuleText(match.url);
489
+ await this.delay(100);
490
+ }
491
+ return null;
492
+ }
493
+ /**
494
+ * `tdk.gov.tr` rule pages are WordPress/Avada-themed. The actual article
495
+ * text lives in `<div ... itemprop="text">...</div>` right before a
496
+ * `<footer class="entry...">` (share buttons, author box, structured-data
497
+ * spans) — cutting there avoids that trailing cruft.
498
+ */
499
+ static async fetchRuleText(url) {
500
+ try {
501
+ const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
502
+ if (!response.ok)
503
+ return null;
504
+ const html = await response.text();
505
+ const marker = html.indexOf('itemprop="text"');
506
+ if (marker === -1)
507
+ return null;
508
+ const contentStart = html.indexOf(">", marker) + 1;
509
+ const contentEnd = html.indexOf("<footer", contentStart);
510
+ if (contentEnd === -1)
511
+ return null;
512
+ return this.htmlToPlainText(html.slice(contentStart, contentEnd));
513
+ } catch {
514
+ return null;
515
+ }
516
+ }
517
+ static htmlToPlainText(html) {
518
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&quot;/gi, '"').replace(/&#39;|&rsquo;/gi, "'").replace(/[ \t]+/g, " ").replace(/[ \t]*\n[ \t]*/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
519
+ }
520
+ /**
521
+ * Returns compound words that contain this word.
522
+ */
523
+ static async getCompoundWords(word) {
524
+ const results = await this.getWord(word);
525
+ if (results.length === 0)
526
+ return [];
527
+ const compound = [];
528
+ for (const result of results) {
529
+ if (result.birlesikler) {
530
+ const words = result.birlesikler.split(",").map((w) => w.trim());
531
+ compound.push(...words);
532
+ }
533
+ }
534
+ return [...new Set(compound)];
535
+ }
536
+ /**
537
+ * Returns the part of speech (isim, sıfat, zarf vb.).
538
+ * TDK's `ozelliklerListe` mixes grammatical categories (`tur: "3"`, e.g.
539
+ * sıfat/zarf/isim) with usage-register tags (`tur: "4"`, e.g. mecaz/argo)
540
+ * in the same list — only `tur === "3"` entries are actual parts of speech.
541
+ */
542
+ static async getPartOfSpeech(word) {
543
+ const results = await this.getWord(word);
544
+ const pos = /* @__PURE__ */ new Set();
545
+ for (const result of results) {
546
+ if (result.anlamlarListe) {
547
+ for (const anlam of result.anlamlarListe) {
548
+ if (anlam.ozelliklerListe) {
549
+ for (const ozellik of anlam.ozelliklerListe) {
550
+ if (ozellik.tur === "3")
551
+ pos.add(ozellik.tam_adi);
552
+ }
553
+ }
554
+ }
555
+ }
556
+ }
557
+ if (pos.size === 0 && results.length > 0) {
558
+ pos.add("isim");
559
+ }
560
+ return Array.from(pos);
561
+ }
562
+ /**
563
+ * Compares two words side by side: meaning count, etymological origin,
564
+ * syllables and vowel-harmony compliance.
565
+ */
566
+ static async compareWords(a, b) {
567
+ const [meaningsA, meaningsB, originA, originB] = await Promise.all([
568
+ this.getMeanings(a),
569
+ this.getMeanings(b),
570
+ this.getOrigin(a),
571
+ this.getOrigin(b)
572
+ ]);
573
+ return {
574
+ a: {
575
+ word: a,
576
+ meaningCount: meaningsA.length,
577
+ origin: originA,
578
+ syllables: this.syllabicate(a),
579
+ harmony: this.checkVowelHarmony(a)
580
+ },
581
+ b: {
582
+ word: b,
583
+ meaningCount: meaningsB.length,
584
+ origin: originB,
585
+ syllables: this.syllabicate(b),
586
+ harmony: this.checkVowelHarmony(b)
587
+ }
588
+ };
589
+ }
590
+ static STOPWORDS = /* @__PURE__ */ new Set([
591
+ "ve",
592
+ "veya",
593
+ "ile",
594
+ "ama",
595
+ "fakat",
596
+ "ancak",
597
+ "de",
598
+ "da",
599
+ "ki",
600
+ "bu",
601
+ "\u015Fu",
602
+ "o",
603
+ "bir",
604
+ "\xE7ok",
605
+ "az",
606
+ "gibi",
607
+ "i\xE7in",
608
+ "mi",
609
+ "m\u0131",
610
+ "mu",
611
+ "m\xFC",
612
+ "ne",
613
+ "her",
614
+ "hi\xE7",
615
+ "ben",
616
+ "sen",
617
+ "biz",
618
+ "siz",
619
+ "onlar",
620
+ "de\u011Fil",
621
+ "bile",
622
+ "diye"
623
+ ]);
624
+ static firstMeaning(results) {
625
+ for (const result of results) {
626
+ for (const anlam of result.anlamlarListe ?? []) {
627
+ if (anlam.anlam)
628
+ return anlam.anlam;
629
+ }
630
+ }
631
+ return null;
632
+ }
633
+ /**
634
+ * Analyzes every distinct word in a text (Turkish stopwords filtered out),
635
+ * returning each word's first meaning and etymological origin if found.
636
+ * Looks each word up individually (throttled), so scales with text length.
637
+ */
638
+ static async analyzeText(text) {
639
+ const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
640
+ const unique = [...new Set(words)];
641
+ const analyses = [];
642
+ for (const word of unique) {
643
+ const results = await this.getWord(word);
644
+ const found = results.length > 0;
645
+ analyses.push({
646
+ word,
647
+ found,
648
+ meaning: found ? this.firstMeaning(results) : null,
649
+ origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null
650
+ });
651
+ await this.delay(200);
652
+ }
653
+ return analyses;
654
+ }
655
+ /**
656
+ * Classic edit-distance between two strings.
657
+ */
658
+ static levenshtein(a, b) {
659
+ const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
660
+ for (let i = 0; i <= a.length; i++)
661
+ dp[i][0] = i;
662
+ for (let j = 0; j <= b.length; j++)
663
+ dp[0][j] = j;
664
+ for (let i = 1; i <= a.length; i++) {
665
+ for (let j = 1; j <= b.length; j++) {
666
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
667
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
668
+ }
669
+ }
670
+ return dp[a.length][b.length];
671
+ }
672
+ /**
673
+ * Fetches multiple words concurrently with a small delay to avoid rate limiting.
674
+ */
675
+ static async getWordsBatch(words) {
676
+ const results = [];
677
+ for (const word of words) {
678
+ try {
679
+ const res = await this.getWord(word);
680
+ results.push(res);
681
+ } catch {
682
+ results.push([]);
683
+ }
684
+ await this.delay(200);
685
+ }
686
+ return results;
687
+ }
688
+ /**
689
+ * Syllabicates a Turkish word based on general grammar rules.
690
+ */
691
+ static syllabicate(word) {
692
+ const vowels = /[aeıioöuüAEIİOÖUÜ]/;
693
+ const result = [];
694
+ let currentSyllable = "";
695
+ for (let i = word.length - 1; i >= 0; i--) {
696
+ currentSyllable = word[i] + currentSyllable;
697
+ if (vowels.test(word[i])) {
698
+ if (i - 1 >= 0 && !vowels.test(word[i - 1])) {
699
+ if (i - 2 >= 0 && vowels.test(word[i - 2])) {
700
+ currentSyllable = word[i - 1] + currentSyllable;
701
+ i--;
702
+ } else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
703
+ currentSyllable = word[i - 1] + currentSyllable;
704
+ i--;
705
+ }
706
+ }
707
+ result.unshift(currentSyllable);
708
+ currentSyllable = "";
709
+ }
710
+ }
711
+ if (currentSyllable) {
712
+ if (result.length > 0) {
713
+ result[0] = currentSyllable + result[0];
714
+ } else {
715
+ result.push(currentSyllable);
716
+ }
717
+ }
718
+ return result;
719
+ }
720
+ /**
721
+ * Checks if a word follows Turkish Major Vowel Harmony (Büyük Ünlü Uyumu).
722
+ * Normalizes case via the Turkish locale first: a plain case-insensitive
723
+ * regex would fold ASCII "I" to "i", misreading the back vowel "I"
724
+ * (dotless) as the front vowel "i" (dotted).
725
+ */
726
+ static checkVowelHarmony(word) {
727
+ const lower = word.toLocaleLowerCase("tr-TR");
728
+ const backVowels = /[aıou]/;
729
+ const frontVowels = /[eiöü]/;
730
+ const hasBack = backVowels.test(lower);
731
+ const hasFront = frontVowels.test(lower);
732
+ return !(hasBack && hasFront);
733
+ }
734
+ };
735
+
736
+ export {
737
+ TDKError,
738
+ TDKValidationError,
739
+ TDKNetworkError,
740
+ TDK
741
+ };