tdk-api-wrapper 1.2.1 → 1.3.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.d.ts CHANGED
@@ -126,6 +126,14 @@ interface WordAnalysis {
126
126
  meaning: string | null;
127
127
  origin: string | null;
128
128
  }
129
+ interface KubbealtiEntry {
130
+ kelime: string;
131
+ anlam: string;
132
+ }
133
+ interface WiktionaryEntry {
134
+ raw: string;
135
+ sections: Record<string, string>;
136
+ }
129
137
  type TDKResponse = WordInfo[] | {
130
138
  error: string;
131
139
  };
@@ -136,6 +144,20 @@ type TDKResponse = WordInfo[] | {
136
144
  declare class TDK {
137
145
  private static readonly BASE_URL;
138
146
  private static readonly AUDIO_API_HOST;
147
+ private static readonly KUBBEALTI_HOST;
148
+ /**
149
+ * `eski.lugatim.com` (Kubbealtı Lugatı's data API) sends only its leaf
150
+ * certificate during the TLS handshake, omitting the intermediates a
151
+ * correctly configured server would include — a server-side misconfiguration,
152
+ * not something we should paper over by disabling verification. These are
153
+ * the two certificates the server *should* be sending (fetched from the
154
+ * leaf's own Authority Information Access URLs), supplied here so Node can
155
+ * still build a full, properly verified chain up to a root it already
156
+ * trusts (ISRG Root X1). If Let's Encrypt rotates this intermediate, this
157
+ * stops working and every Kubbealtı call fails closed to `null` — same
158
+ * fail-closed contract as the rest of this file's fragile integrations.
159
+ */
160
+ private static readonly KUBBEALTI_EXTRA_CA;
139
161
  private static isCacheEnabled;
140
162
  private static wordCache;
141
163
  private static dailyContentCache;
@@ -275,13 +297,17 @@ declare class TDK {
275
297
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
276
298
  * hands back a single randomly-rotated rule per request (out of a pool of
277
299
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
278
- * draw would rarely match a given name — this re-draws (bounded, with a
279
- * short delay) until it finds a match or gives up. Every attempt bypasses
280
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
281
- * 25 attempts would just re-read the same cached `/icerik` response and
282
- * could never find a rule outside whatever the first draw happened to be.
283
- * Returns `null` if no match turns up within the attempt budget or the
284
- * matched page can't be parsed.
300
+ * draw would rarely match a given name — this re-draws until it finds a
301
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
302
+ * request is independent and stateless) rather than one-at-a-time with a
303
+ * delay: same total sample size (25) and hit probability as a sequential
304
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
305
+ * a miss resolves in roughly one round-trip time instead of several
306
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
307
+ * `enableCache(true)` is on, every attempt would just re-read the same
308
+ * cached `/icerik` response and could never find a rule outside whatever
309
+ * the first draw happened to be. Returns `null` if no match turns up
310
+ * within the attempt budget or the matched page can't be parsed.
285
311
  */
286
312
  static getRule(name: string): Promise<string | null>;
287
313
  /**
@@ -292,6 +318,58 @@ declare class TDK {
292
318
  */
293
319
  private static fetchRuleText;
294
320
  private static htmlToPlainText;
321
+ /**
322
+ * GETs a JSON path from Kubbealtı Lugatı's data API (`eski.lugatim.com`),
323
+ * supplying `KUBBEALTI_EXTRA_CA` to work around that host's incomplete
324
+ * certificate chain (see the constant's doc comment). Fails closed to
325
+ * `null` on any error — network, TLS, HTTP, or JSON parse.
326
+ */
327
+ private static fetchKubbealtiJson;
328
+ /**
329
+ * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
330
+ * word, scraped from the site's own data API — undocumented, and Kubbealtı
331
+ * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
332
+ * openly-published data, so use this in line with their terms. `anlam` is
333
+ * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
334
+ * plain text. Returns `null` on any fetch/parse failure, `[]` if the word
335
+ * isn't found.
336
+ */
337
+ static getKubbealti(word: string): Promise<KubbealtiEntry[] | null>;
338
+ /**
339
+ * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
340
+ * plain text via `htmlToPlainText()`.
341
+ */
342
+ static getKubbealtiMeanings(word: string): Promise<string[] | null>;
343
+ /**
344
+ * Autocomplete suggestions from Kubbealtı Lugatı's own typeahead endpoint
345
+ * (separate from `getSuggestions()`, which uses TDK's data).
346
+ */
347
+ static getKubbealtiSuggestions(prefix: string): Promise<string[]>;
348
+ /**
349
+ * Returns the etymology paragraph for a word from Nişanyan Sözlük, scraped
350
+ * from that page's server-rendered `<meta name="description">` tag (the
351
+ * page already puts the full etymology text there for SEO, so no need to
352
+ * parse the site's internal SvelteKit data format). Returns `null` if the
353
+ * word isn't found (the page falls back to a generic site tagline in that
354
+ * case) or the request fails.
355
+ */
356
+ static getNisanyan(word: string): Promise<string | null>;
357
+ /**
358
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
359
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
360
+ * scraping involved, this is a stable, documented public API. `sections`
361
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
362
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
363
+ * unsplit text. Returns `null` if the page doesn't exist or the request
364
+ * fails.
365
+ */
366
+ static getWiktionary(word: string): Promise<WiktionaryEntry | null>;
367
+ /**
368
+ * Convenience filter over `getWiktionary()`: returns just one section's
369
+ * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
370
+ * case-insensitively. Returns `null` if the word or the section isn't found.
371
+ */
372
+ static getWiktionarySection(word: string, sectionName: string): Promise<string | null>;
295
373
  /**
296
374
  * Returns compound words that contain this word.
297
375
  */
@@ -314,12 +392,21 @@ declare class TDK {
314
392
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
315
393
  * returning each word's first meaning and etymological origin if found.
316
394
  * Looks each word up individually (throttled), so scales with text length.
395
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
396
+ * it does no morphological analysis, and neither does this method: a
397
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
398
+ * suffix) will come back `found: false` even though the root is a real
399
+ * headword. This is an inherent limitation of the data source, not a bug.
317
400
  */
318
401
  static analyzeText(text: string): Promise<WordAnalysis[]>;
319
402
  /**
320
- * Classic edit-distance between two strings.
403
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
404
+ * like classic Levenshtein but also counts an adjacent-character
405
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
406
+ * two substitutions — a very common class of typo that plain Levenshtein
407
+ * otherwise misses.
321
408
  */
322
- private static levenshtein;
409
+ private static damerauLevenshtein;
323
410
  /**
324
411
  * Fetches multiple words concurrently with a small delay to avoid rate limiting.
325
412
  */
@@ -362,4 +449,4 @@ declare class TDKNetworkError extends TDKError {
362
449
  });
363
450
  }
364
451
 
365
- export { type Author, type DailyContent, type DailyPick, type Example, type Feature, type Meaning, type Proverb, type SpellCheckResult, TDK, TDKError, TDKNetworkError, type TDKResponse, type TDKRule, TDKValidationError, type WordAnalysis, type WordComparison, type WordComparisonSide, type WordInfo, type WordOfTheDay };
452
+ export { type Author, type DailyContent, type DailyPick, type Example, type Feature, type KubbealtiEntry, type Meaning, type Proverb, type SpellCheckResult, TDK, TDKError, TDKNetworkError, type TDKResponse, type TDKRule, TDKValidationError, type WiktionaryEntry, type WordAnalysis, type WordComparison, type WordComparisonSide, type WordInfo, type WordOfTheDay };
package/dist/index.js CHANGED
@@ -69,9 +69,87 @@ var fs = __toESM(require("fs"));
69
69
  var path = __toESM(require("path"));
70
70
  var os = __toESM(require("os"));
71
71
  var https = __toESM(require("https"));
72
+ var tls = __toESM(require("tls"));
72
73
  var TDK = class {
73
74
  static BASE_URL = "https://sozluk.gov.tr";
74
75
  static AUDIO_API_HOST = "api.sozluk.gov.tr";
76
+ static KUBBEALTI_HOST = "eski.lugatim.com";
77
+ /**
78
+ * `eski.lugatim.com` (Kubbealtı Lugatı's data API) sends only its leaf
79
+ * certificate during the TLS handshake, omitting the intermediates a
80
+ * correctly configured server would include — a server-side misconfiguration,
81
+ * not something we should paper over by disabling verification. These are
82
+ * the two certificates the server *should* be sending (fetched from the
83
+ * leaf's own Authority Information Access URLs), supplied here so Node can
84
+ * still build a full, properly verified chain up to a root it already
85
+ * trusts (ISRG Root X1). If Let's Encrypt rotates this intermediate, this
86
+ * stops working and every Kubbealtı call fails closed to `null` — same
87
+ * fail-closed contract as the rest of this file's fragile integrations.
88
+ */
89
+ static KUBBEALTI_EXTRA_CA = [
90
+ `-----BEGIN CERTIFICATE-----
91
+ MIIE2jCCAsKgAwIBAgIQTr0klH4k05SALYSlL9WzGTANBgkqhkiG9w0BAQsFADAu
92
+ MQswCQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZUjAe
93
+ Fw0yNTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYw
94
+ FAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZUjIwggEiMA0GCSqGSIb3
95
+ DQEBAQUAA4IBDwAwggEKAoIBAQDZ0LxwBppqh84luqMerV/eeL/fXQ7mLQQv1Lnp
96
+ WKZbyvGpx6wh6AfnslAnF6ewTkcHA+gSOoBvm3Dfm06AuGiF+KRut4fAcowqnAQQ
97
+ CW98+QPP/eOv/wug7Iyk4NkOxf2I6g2f55T6nJoOTLFcukeRq80JGQEYan+dPFr9
98
+ OGUgQK2hGKgNkW87pappsOAuUJcroYhRt5uUis4qaZireiseu32gzDJNBAiKtsvd
99
+ 6HX4v25bpkRNcS/B/Gtc9kVbUpD+2PLPxdei3Tim55k4tfAEXwD2qyiPTxrTNq6l
100
+ N+AMr5g2c1dNqkOTwjxeV6L5lpP1rGiYvLnRaPlOqyZRPW+5AgMBAAGjge4wgesw
101
+ DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBIGA1UdEwEB/wQI
102
+ MAYBAf8CAQAwHQYDVR0OBBYEFEAVLSZ57TIgnt+ach3WMh+BDIEMMB8GA1UdIwQY
103
+ MBaAFN7nW2DQIm1AKH0/DQH+pLVStFGUMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEF
104
+ BQcwAoYWaHR0cDovL3lyLmkubGVuY3Iub3JnLzATBgNVHSAEDDAKMAgGBmeBDAEC
105
+ ATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veXIuYy5sZW5jci5vcmcvMA0GCSqG
106
+ SIb3DQEBCwUAA4ICAQB0ZUQWZ9/Yn9COEpo+JfecMnB0h0vwDm/M66IqXqw3LoaL
107
+ mx9lZvRTeDIS67PUeI3yCA2W6PKRD0/FE/G57lOmS+Xy5AaaL00ICGOqjNcCaMWW
108
+ 8o8nevHOd4i4lqgtznE/28QwlcdJyF8yBiWHpnyjhEpmNWJURgOCOg2xpwRMBCsj
109
+ MScqYPtOhBeuYQvSwAEeTML2Ukh6uGuX4E14q65Ja8cdjF5bAldnP1eE4FBaAwsZ
110
+ G2fOqqrKV03Y85Nw2btedP1AtliQuJZs/Jo/gXxXdc7LrH3McgnpnbTiAncX7yES
111
+ hP6kzQejllqMCIt52HOjxDGWafS7Xw+DKwqmH+Eqy8dcbOuag/1AYlQoKNVK3F5q
112
+ Hh6tEDiMqQcLIibGKteE6iHo4A/bIScbzrhXUYuism42ZYzmc48FMVIH3qy4L84E
113
+ TdAH2gtxw0PAhvRVXp8HP7wfngpzsN/8xOTpeRSbM4+Qbc56G6+Bifmv6sk1ieQb
114
+ NA3wJdl4DDUuQSV8hBgx6zoI1ZSGORprDFux7c6rhc77QZMSRrEgomBeklervEve
115
+ 86ylWmZ3WWHV6RLMi8xNvjd71r4EPIGgY7BZU/VPBkq+uA7Gb6mbJnFgV43uh3xy
116
+ LRFgxIAphIukwTGSMZZR+AI+Qnp0BYTWovHXozOf3H8r6hozEoT02JHn0AeTfA==
117
+ -----END CERTIFICATE-----`,
118
+ `-----BEGIN CERTIFICATE-----
119
+ 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
151
+ -----END CERTIFICATE-----`
152
+ ];
75
153
  // Cache Mechanism
76
154
  static isCacheEnabled = false;
77
155
  static wordCache = /* @__PURE__ */ new Map();
@@ -438,11 +516,16 @@ var TDK = class {
438
516
  for (const candidate of this.autocompleteCache) {
439
517
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
440
518
  continue;
441
- const distance = this.levenshtein(cleanWord, candidate);
442
- if (distance > 0 && (!best || distance < best.distance)) {
443
- best = { candidate, distance };
444
- if (distance === 1)
445
- break;
519
+ if (Math.abs(candidate.length - cleanWord.length) > 2)
520
+ continue;
521
+ const distance = this.damerauLevenshtein(cleanWord, candidate);
522
+ if (distance === 0)
523
+ continue;
524
+ const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
525
+ const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
526
+ const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
527
+ if (better) {
528
+ best = { candidate, distance, firstMismatch, lengthMismatch };
446
529
  }
447
530
  }
448
531
  if (best && best.distance <= 2) {
@@ -521,24 +604,33 @@ var TDK = class {
521
604
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
522
605
  * hands back a single randomly-rotated rule per request (out of a pool of
523
606
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
524
- * draw would rarely match a given name — this re-draws (bounded, with a
525
- * short delay) until it finds a match or gives up. Every attempt bypasses
526
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
527
- * 25 attempts would just re-read the same cached `/icerik` response and
528
- * could never find a rule outside whatever the first draw happened to be.
529
- * Returns `null` if no match turns up within the attempt budget or the
530
- * matched page can't be parsed.
607
+ * draw would rarely match a given name — this re-draws until it finds a
608
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
609
+ * request is independent and stateless) rather than one-at-a-time with a
610
+ * delay: same total sample size (25) and hit probability as a sequential
611
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
612
+ * a miss resolves in roughly one round-trip time instead of several
613
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
614
+ * `enableCache(true)` is on, every attempt would just re-read the same
615
+ * cached `/icerik` response and could never find a rule outside whatever
616
+ * the first draw happened to be. Returns `null` if no match turns up
617
+ * within the attempt budget or the matched page can't be parsed.
531
618
  */
532
619
  static async getRule(name) {
533
620
  if (!name || name.trim() === "")
534
621
  return null;
535
622
  const target = name.trim().toLocaleLowerCase("tr-TR");
536
- for (let attempt = 0; attempt < 25; attempt++) {
537
- const rules = await this.getKurallar(true);
538
- const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
539
- if (match)
540
- return this.fetchRuleText(match.url);
541
- await this.delay(100);
623
+ const BATCH_SIZE = 5;
624
+ const ROUNDS = 5;
625
+ for (let round = 0; round < ROUNDS; round++) {
626
+ const batches = await Promise.all(
627
+ Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
628
+ );
629
+ for (const rules of batches) {
630
+ const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
631
+ if (match)
632
+ return this.fetchRuleText(match.url);
633
+ }
542
634
  }
543
635
  return null;
544
636
  }
@@ -567,7 +659,166 @@ var TDK = class {
567
659
  }
568
660
  }
569
661
  static htmlToPlainText(html) {
570
- 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();
662
+ 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();
663
+ }
664
+ /**
665
+ * GETs a JSON path from Kubbealtı Lugatı's data API (`eski.lugatim.com`),
666
+ * supplying `KUBBEALTI_EXTRA_CA` to work around that host's incomplete
667
+ * certificate chain (see the constant's doc comment). Fails closed to
668
+ * `null` on any error — network, TLS, HTTP, or JSON parse.
669
+ */
670
+ static fetchKubbealtiJson(path2) {
671
+ return new Promise((resolve) => {
672
+ const req = https.request(
673
+ {
674
+ hostname: this.KUBBEALTI_HOST,
675
+ path: path2,
676
+ method: "GET",
677
+ ca: [...tls.rootCertificates, ...this.KUBBEALTI_EXTRA_CA],
678
+ headers: {
679
+ "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"
680
+ }
681
+ },
682
+ (res) => {
683
+ if (res.statusCode !== 200) {
684
+ res.resume();
685
+ resolve(null);
686
+ return;
687
+ }
688
+ let body = "";
689
+ res.on("data", (chunk) => body += chunk);
690
+ res.on("end", () => {
691
+ try {
692
+ resolve(JSON.parse(body));
693
+ } catch {
694
+ resolve(null);
695
+ }
696
+ });
697
+ }
698
+ );
699
+ req.on("error", () => resolve(null));
700
+ req.end();
701
+ });
702
+ }
703
+ /**
704
+ * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
705
+ * word, scraped from the site's own data API — undocumented, and Kubbealtı
706
+ * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
707
+ * openly-published data, so use this in line with their terms. `anlam` is
708
+ * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
709
+ * plain text. Returns `null` on any fetch/parse failure, `[]` if the word
710
+ * isn't found.
711
+ */
712
+ static async getKubbealti(word) {
713
+ if (!word || word.trim() === "")
714
+ return null;
715
+ const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word.trim())}/`);
716
+ if (!data || !Array.isArray(data.content))
717
+ return null;
718
+ return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
719
+ }
720
+ /**
721
+ * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
722
+ * plain text via `htmlToPlainText()`.
723
+ */
724
+ static async getKubbealtiMeanings(word) {
725
+ const entries = await this.getKubbealti(word);
726
+ if (!entries)
727
+ return null;
728
+ return entries.map((e) => this.htmlToPlainText(e.anlam));
729
+ }
730
+ /**
731
+ * Autocomplete suggestions from Kubbealtı Lugatı's own typeahead endpoint
732
+ * (separate from `getSuggestions()`, which uses TDK's data).
733
+ */
734
+ static async getKubbealtiSuggestions(prefix) {
735
+ if (!prefix || prefix.trim() === "")
736
+ return [];
737
+ const data = await this.fetchKubbealtiJson(`/rest/word-search/${encodeURIComponent(prefix.trim())}`);
738
+ if (!Array.isArray(data))
739
+ return [];
740
+ return data.map((item) => item.display).filter(Boolean);
741
+ }
742
+ /**
743
+ * Returns the etymology paragraph for a word from Nişanyan Sözlük, scraped
744
+ * from that page's server-rendered `<meta name="description">` tag (the
745
+ * page already puts the full etymology text there for SEO, so no need to
746
+ * parse the site's internal SvelteKit data format). Returns `null` if the
747
+ * word isn't found (the page falls back to a generic site tagline in that
748
+ * case) or the request fails.
749
+ */
750
+ static async getNisanyan(word) {
751
+ if (!word || word.trim() === "")
752
+ return null;
753
+ try {
754
+ const response = await fetch(
755
+ `https://www.nisanyansozluk.com/kelime/${encodeURIComponent(word.trim().toLocaleLowerCase("tr-TR"))}`,
756
+ { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } }
757
+ );
758
+ if (!response.ok)
759
+ return null;
760
+ const html = await response.text();
761
+ const match = html.match(/<meta name="description" content="([^"]*)"/);
762
+ if (!match)
763
+ return null;
764
+ const description = this.htmlToPlainText(match[1]);
765
+ if (description === "\xC7a\u011Fda\u015F T\xFCrk\xE7enin Etimolojisi")
766
+ return null;
767
+ return description;
768
+ } catch {
769
+ return null;
770
+ }
771
+ }
772
+ /**
773
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
774
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
775
+ * scraping involved, this is a stable, documented public API. `sections`
776
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
777
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
778
+ * unsplit text. Returns `null` if the page doesn't exist or the request
779
+ * fails.
780
+ */
781
+ static async getWiktionary(word) {
782
+ if (!word || word.trim() === "")
783
+ return null;
784
+ try {
785
+ const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
786
+ word.trim()
787
+ )}&format=json&explaintext=1&formatversion=2`;
788
+ const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
789
+ if (!response.ok)
790
+ return null;
791
+ const data = await response.json();
792
+ const page = data?.query?.pages?.[0];
793
+ if (!page || page.missing || !page.extract)
794
+ return null;
795
+ const raw = page.extract;
796
+ const sections = {};
797
+ const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
798
+ for (let i = 1; i < parts.length; i += 3) {
799
+ const title = parts[i + 1]?.trim();
800
+ const content = parts[i + 2]?.trim();
801
+ if (title)
802
+ sections[title] = content ?? "";
803
+ }
804
+ return { raw, sections };
805
+ } catch {
806
+ return null;
807
+ }
808
+ }
809
+ /**
810
+ * Convenience filter over `getWiktionary()`: returns just one section's
811
+ * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
812
+ * case-insensitively. Returns `null` if the word or the section isn't found.
813
+ */
814
+ static async getWiktionarySection(word, sectionName) {
815
+ const entry = await this.getWiktionary(word);
816
+ if (!entry)
817
+ return null;
818
+ const key = Object.keys(entry.sections).find(
819
+ (k) => k.toLocaleLowerCase("tr-TR") === sectionName.trim().toLocaleLowerCase("tr-TR")
820
+ );
821
+ return key ? entry.sections[key] : null;
571
822
  }
572
823
  /**
573
824
  * Returns compound words that contain this word.
@@ -686,6 +937,11 @@ var TDK = class {
686
937
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
687
938
  * returning each word's first meaning and etymological origin if found.
688
939
  * Looks each word up individually (throttled), so scales with text length.
940
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
941
+ * it does no morphological analysis, and neither does this method: a
942
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
943
+ * suffix) will come back `found: false` even though the root is a real
944
+ * headword. This is an inherent limitation of the data source, not a bug.
689
945
  */
690
946
  static async analyzeText(text) {
691
947
  const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
@@ -705,9 +961,13 @@ var TDK = class {
705
961
  return analyses;
706
962
  }
707
963
  /**
708
- * Classic edit-distance between two strings.
964
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
965
+ * like classic Levenshtein but also counts an adjacent-character
966
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
967
+ * two substitutions — a very common class of typo that plain Levenshtein
968
+ * otherwise misses.
709
969
  */
710
- static levenshtein(a, b) {
970
+ static damerauLevenshtein(a, b) {
711
971
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
712
972
  for (let i = 0; i <= a.length; i++)
713
973
  dp[i][0] = i;
@@ -717,6 +977,9 @@ var TDK = class {
717
977
  for (let j = 1; j <= b.length; j++) {
718
978
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
719
979
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
980
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
981
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
982
+ }
720
983
  }
721
984
  }
722
985
  return dp[a.length][b.length];
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  TDKError,
4
4
  TDKNetworkError,
5
5
  TDKValidationError
6
- } from "./chunk-6BTOGV2M.mjs";
6
+ } from "./chunk-SLNXKZKR.mjs";
7
7
  export {
8
8
  TDK,
9
9
  TDKError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdk-api-wrapper",
3
- "version": "1.2.1",
3
+ "version": "1.3.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",
package/src/cli.ts CHANGED
@@ -22,6 +22,9 @@ const KNOWN_COMMANDS = new Set([
22
22
  "karsilastir",
23
23
  "analiz",
24
24
  "oneri",
25
+ "kubbealti",
26
+ "nisanyan",
27
+ "viki",
25
28
  ]);
26
29
 
27
30
  let command = args[0];
@@ -52,7 +55,7 @@ async function run() {
52
55
  if (!command || command === "--help" || command === "-h") {
53
56
  console.log("Kullanım: tdk [komut] <kelime> [--json]");
54
57
  console.log(
55
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
58
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, kubbealti, nisanyan, viki"
56
59
  );
57
60
  console.log("Not: Komut belirtilmezse doğrudan kelime anlamı aranır (örn: tdk selam)");
58
61
  process.exit(command ? 0 : 1);
@@ -259,6 +262,43 @@ async function run() {
259
262
  break;
260
263
  }
261
264
 
265
+ case "kubbealti": {
266
+ if (!word) throw new Error("Kelime belirtmelisiniz.");
267
+ const meanings = await TDK.getKubbealtiMeanings(word);
268
+ printResult(meanings, () => {
269
+ if (!meanings) {
270
+ console.log("Kubbealtı Lugatı'na ulaşılamadı.");
271
+ } else if (meanings.length === 0) {
272
+ console.log("Sonuç bulunamadı.");
273
+ } else {
274
+ meanings.forEach((m, i) => console.log(`${i + 1}. ${m}`));
275
+ }
276
+ });
277
+ break;
278
+ }
279
+
280
+ case "nisanyan": {
281
+ if (!word) throw new Error("Kelime belirtmelisiniz.");
282
+ const origin = await TDK.getNisanyan(word);
283
+ printResult(origin, () => console.log(origin ?? "Sonuç bulunamadı."));
284
+ break;
285
+ }
286
+
287
+ case "viki": {
288
+ if (!word) throw new Error("Kelime belirtmelisiniz.");
289
+ const entry = await TDK.getWiktionary(word);
290
+ printResult(entry, () => {
291
+ if (!entry) {
292
+ console.log("Sonuç bulunamadı.");
293
+ } else {
294
+ for (const [title, content] of Object.entries(entry.sections)) {
295
+ if (content) console.log(`-- ${title} --\n${content}\n`);
296
+ }
297
+ }
298
+ });
299
+ break;
300
+ }
301
+
262
302
  default:
263
303
  printError("Bilinmeyen komut.");
264
304
  }