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