tdk-api-wrapper 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,9 +97,9 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
97
97
 
98
98
  TDK dışındaki bu üç kaynak da her zaman kullanılabilir/dokümante edilmiş resmî API'ler değildir; her biri **fragile scraping** (kırılgan, dokümante edilmemiş entegrasyon) — kaynak taraflarında bir değişiklik olursa `null`/`[]` dönerler, hataya düşmezler. Verinin telif/kullanım koşulları kaynağa göre farklıdır: Wiktionary içeriği CC BY-SA lisanslıdır (açık); Nişanyan Sözlük ücretsiz, açık bir kişisel/akademik kaynaktır; **Kubbealtı Lugatı ise ticari bir sözlük ürünüdür** — bu kütüphane onu da dokümante edilmemiş bir uç noktadan çekebiliyor olsa da, kullanımınızı Kubbealtı'nın kendi kullanım şartlarına göre değerlendirmeniz önerilir.
99
99
 
100
- - **`TDK.getKubbealti(word)`**: Kubbealtı Lugatı'nın ("Misalli Büyük Türkçe Sözlük") verilerini `{ kelime, anlam }` dizisi olarak döner (`anlam` zengin tipografi içeren ham HTML'dir). `getKubbealtiMeanings(word)` aynı veriyi düz metne çevirir. `getKubbealtiSuggestions(prefix)` Kubbealtı'nın kendi otomatik tamamlama uç noktasını kullanır (TDK'nin `getSuggestions()`'ından bağımsız, ayrı bir veri kaynağı). Not: Kubbealtı'nın veri sunucusu (`eski.lugatim.com`) sertifika zincirini eksik gönderiyor; bu kütüphane eksik ara sertifikaları ekleyerek zinciri düzgün doğruluyor (doğrulamayı kapatmıyor) — Let's Encrypt bu ara sertifikayı döndürürse bu entegrasyon `null` dönmeye başlar.
100
+ - **`TDK.getKubbealti(word)`**: Kubbealtı Lugatı'nın ("Misalli Büyük Türkçe Sözlük") verilerini `{ kelime, anlam }` dizisi olarak döner (`anlam` zengin tipografi içeren ham HTML'dir). `getKubbealtiMeanings(word)` aynı veriyi düz metne çevirir. `getKubbealtiSuggestions(prefix)` Kubbealtı'nın kendi otomatik tamamlama uç noktasını kullanır (TDK'nin `getSuggestions()`'ından bağımsız, ayrı bir veri kaynağı). Kubbealtı başlıkları klasik Türkçe imlayla (ü/ö/ç/ğ/ş, düzeltme işareti) indekslidir; düz ASCII'ye yakın bir sorgu (örn. `ruzgar`) boş dönerse, kütüphane tek harflik Türkçeleştirme varyasyonlarını (`rüzgâr`, `rûzgar` vb.) otomatik dener. Not: Kubbealtı'nın veri sunucusu (`eski.lugatim.com`) sertifika zincirini eksik gönderiyor; bu kütüphane eksik ara sertifikaları ekleyerek zinciri düzgün doğruluyor (doğrulamayı kapatmıyor) — Let's Encrypt bu ara sertifikayı döndürürse bu entegrasyon `null` dönmeye başlar.
101
101
  - **`TDK.getNisanyan(word)`**: Nişanyan Sözlük'ten kelimenin etimoloji paragrafını düz metin olarak döner; kelime bulunamazsa `null`.
102
- - **`TDK.getWiktionary(word)`**: Türkçe Vikisözlük'ten (`tr.wiktionary.org`) resmî MediaWiki API'si (`action=query&prop=extracts`) üzerinden veri çeker — bu üçü arasında scraping olmayan, resmî ve en kararlı olanı. `{ raw, sections }` döner; `sections` metni `== Köken ==`, `=== Söyleniş ===` gibi başlıklara göre bir sözlüğe ayırır. `getWiktionarySection(word, sectionName)` tek bir bölümü (örn. `"Köken"`) büyük/küçük harf duyarsız süzer.
102
+ - **`TDK.getWiktionary(word)`**: Türkçe Vikisözlük'ten (`tr.wiktionary.org`) resmî MediaWiki API'si (`action=query&prop=extracts`) üzerinden veri çeker — bu üçü arasında scraping olmayan, resmî ve en kararlı olanı. `{ raw, sections }` döner; `sections` metni `== Köken ==`, `=== Söyleniş ===` gibi başlıklara göre bir sözlüğe ayırır. `getWiktionarySection(word, sectionName)` tek bir bölümü (örn. `"Köken"`) büyük/küçük harf duyarsız süzer. Bu wiki'de başlık büyütme kapalı (`$wgCapitalLinks=false` — "Türkiye" ile bir küçük harfli kelime ayrı sayfalardır), o yüzden `TDK.getWiktionary("türkiye")` gibi tam eşleşmeyen aramalar otomatik olarak ilk harfi (Türkçe kurallarına göre, örn. `istanbul` → `İstanbul`) büyütülmüş hâliyle tekrar denenir.
103
103
 
104
104
  ## Hata Yönetimi
105
105
 
@@ -661,14 +661,46 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
661
661
  req.end();
662
662
  });
663
663
  }
664
+ /**
665
+ * Kubbealtı indexes headwords with full classical Turkish orthography,
666
+ * including letters that a plain-ASCII-ish query tends to drop — most
667
+ * commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
668
+ * Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
669
+ * entirely (verified: even "ruzgâr" alone still misses — it's the missing
670
+ * ü, not the missing â, that actually breaks the match). This generates
671
+ * single-letter-substitution variants to retry, one substitution per
672
+ * variant (not combinatorial) — covers the overwhelmingly common case of
673
+ * one "de-Turkished" letter without an explosion of API calls for words
674
+ * with several.
675
+ */
676
+ static TURKISH_DEASCII_MAP = {
677
+ a: ["\xE2"],
678
+ i: ["\u0131", "\xEE"],
679
+ o: ["\xF6"],
680
+ u: ["\xFC", "\xFB"],
681
+ c: ["\xE7"],
682
+ g: ["\u011F"],
683
+ s: ["\u015F"]
684
+ };
685
+ static generateTurkishVariants(word) {
686
+ const lower = word.trim().toLocaleLowerCase("tr-TR");
687
+ const variants = [];
688
+ for (let i = 0; i < lower.length; i++) {
689
+ for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
690
+ variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
691
+ }
692
+ }
693
+ return variants;
694
+ }
664
695
  /**
665
696
  * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
666
697
  * word, scraped from the site's own data API — undocumented, and Kubbealtı
667
698
  * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
668
699
  * openly-published data, so use this in line with their terms. `anlam` is
669
700
  * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
670
- * plain text. Returns `null` on any fetch/parse failure, `[]` if the word
671
- * isn't found.
701
+ * plain text. Falls back to `generateTurkishVariants()` if the exact query
702
+ * comes up empty (see its doc comment). Returns `null` on any fetch/parse
703
+ * failure, `[]` if no variant matches either.
672
704
  */
673
705
  static async getKubbealti(word) {
674
706
  if (!word || word.trim() === "")
@@ -676,7 +708,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
676
708
  const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word.trim())}/`);
677
709
  if (!data || !Array.isArray(data.content))
678
710
  return null;
679
- return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
711
+ if (data.content.length > 0) {
712
+ return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
713
+ }
714
+ for (const variant of this.generateTurkishVariants(word)) {
715
+ const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
716
+ if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
717
+ return variantData.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
718
+ }
719
+ }
720
+ return [];
680
721
  }
681
722
  /**
682
723
  * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
@@ -730,21 +771,10 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
730
771
  return null;
731
772
  }
732
773
  }
733
- /**
734
- * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
735
- * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
736
- * scraping involved, this is a stable, documented public API. `sections`
737
- * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
738
- * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
739
- * unsplit text. Returns `null` if the page doesn't exist or the request
740
- * fails.
741
- */
742
- static async getWiktionary(word) {
743
- if (!word || word.trim() === "")
744
- return null;
774
+ static async fetchWiktionaryEntry(title) {
745
775
  try {
746
776
  const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
747
- word.trim()
777
+ title
748
778
  )}&format=json&explaintext=1&formatversion=2`;
749
779
  const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
750
780
  if (!response.ok)
@@ -757,16 +787,42 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
757
787
  const sections = {};
758
788
  const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
759
789
  for (let i = 1; i < parts.length; i += 3) {
760
- const title = parts[i + 1]?.trim();
790
+ const title2 = parts[i + 1]?.trim();
761
791
  const content = parts[i + 2]?.trim();
762
- if (title)
763
- sections[title] = content ?? "";
792
+ if (title2)
793
+ sections[title2] = content ?? "";
764
794
  }
765
795
  return { raw, sections };
766
796
  } catch {
767
797
  return null;
768
798
  }
769
799
  }
800
+ /**
801
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
802
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
803
+ * scraping involved, this is a stable, documented public API. `sections`
804
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
805
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
806
+ * unsplit text. This wiki has title capitalization turned off
807
+ * ($wgCapitalLinks=false — common for Wiktionaries, since case is
808
+ * meaningful for a dictionary: "Türkiye" the country vs. a lowercase
809
+ * common word), so an exact-case miss retries with the first letter
810
+ * uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
811
+ * "Istanbul") before giving up. Returns `null` if neither is found or the
812
+ * request fails.
813
+ */
814
+ static async getWiktionary(word) {
815
+ if (!word || word.trim() === "")
816
+ return null;
817
+ const trimmed = word.trim();
818
+ const direct = await this.fetchWiktionaryEntry(trimmed);
819
+ if (direct)
820
+ return direct;
821
+ const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
822
+ if (capitalized === trimmed)
823
+ return null;
824
+ return this.fetchWiktionaryEntry(capitalized);
825
+ }
770
826
  /**
771
827
  * Convenience filter over `getWiktionary()`: returns just one section's
772
828
  * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
package/dist/cli.js CHANGED
@@ -686,14 +686,46 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
686
686
  req.end();
687
687
  });
688
688
  }
689
+ /**
690
+ * Kubbealtı indexes headwords with full classical Turkish orthography,
691
+ * including letters that a plain-ASCII-ish query tends to drop — most
692
+ * commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
693
+ * Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
694
+ * entirely (verified: even "ruzgâr" alone still misses — it's the missing
695
+ * ü, not the missing â, that actually breaks the match). This generates
696
+ * single-letter-substitution variants to retry, one substitution per
697
+ * variant (not combinatorial) — covers the overwhelmingly common case of
698
+ * one "de-Turkished" letter without an explosion of API calls for words
699
+ * with several.
700
+ */
701
+ static TURKISH_DEASCII_MAP = {
702
+ a: ["\xE2"],
703
+ i: ["\u0131", "\xEE"],
704
+ o: ["\xF6"],
705
+ u: ["\xFC", "\xFB"],
706
+ c: ["\xE7"],
707
+ g: ["\u011F"],
708
+ s: ["\u015F"]
709
+ };
710
+ static generateTurkishVariants(word2) {
711
+ const lower = word2.trim().toLocaleLowerCase("tr-TR");
712
+ const variants = [];
713
+ for (let i = 0; i < lower.length; i++) {
714
+ for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
715
+ variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
716
+ }
717
+ }
718
+ return variants;
719
+ }
689
720
  /**
690
721
  * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
691
722
  * word, scraped from the site's own data API — undocumented, and Kubbealtı
692
723
  * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
693
724
  * openly-published data, so use this in line with their terms. `anlam` is
694
725
  * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
695
- * plain text. Returns `null` on any fetch/parse failure, `[]` if the word
696
- * isn't found.
726
+ * plain text. Falls back to `generateTurkishVariants()` if the exact query
727
+ * comes up empty (see its doc comment). Returns `null` on any fetch/parse
728
+ * failure, `[]` if no variant matches either.
697
729
  */
698
730
  static async getKubbealti(word2) {
699
731
  if (!word2 || word2.trim() === "")
@@ -701,7 +733,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
701
733
  const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word2.trim())}/`);
702
734
  if (!data || !Array.isArray(data.content))
703
735
  return null;
704
- return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
736
+ if (data.content.length > 0) {
737
+ return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
738
+ }
739
+ for (const variant of this.generateTurkishVariants(word2)) {
740
+ const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
741
+ if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
742
+ return variantData.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
743
+ }
744
+ }
745
+ return [];
705
746
  }
706
747
  /**
707
748
  * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
@@ -755,21 +796,10 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
755
796
  return null;
756
797
  }
757
798
  }
758
- /**
759
- * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
760
- * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
761
- * scraping involved, this is a stable, documented public API. `sections`
762
- * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
763
- * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
764
- * unsplit text. Returns `null` if the page doesn't exist or the request
765
- * fails.
766
- */
767
- static async getWiktionary(word2) {
768
- if (!word2 || word2.trim() === "")
769
- return null;
799
+ static async fetchWiktionaryEntry(title) {
770
800
  try {
771
801
  const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
772
- word2.trim()
802
+ title
773
803
  )}&format=json&explaintext=1&formatversion=2`;
774
804
  const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
775
805
  if (!response.ok)
@@ -782,16 +812,42 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
782
812
  const sections = {};
783
813
  const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
784
814
  for (let i = 1; i < parts.length; i += 3) {
785
- const title = parts[i + 1]?.trim();
815
+ const title2 = parts[i + 1]?.trim();
786
816
  const content = parts[i + 2]?.trim();
787
- if (title)
788
- sections[title] = content ?? "";
817
+ if (title2)
818
+ sections[title2] = content ?? "";
789
819
  }
790
820
  return { raw, sections };
791
821
  } catch {
792
822
  return null;
793
823
  }
794
824
  }
825
+ /**
826
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
827
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
828
+ * scraping involved, this is a stable, documented public API. `sections`
829
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
830
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
831
+ * unsplit text. This wiki has title capitalization turned off
832
+ * ($wgCapitalLinks=false — common for Wiktionaries, since case is
833
+ * meaningful for a dictionary: "Türkiye" the country vs. a lowercase
834
+ * common word), so an exact-case miss retries with the first letter
835
+ * uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
836
+ * "Istanbul") before giving up. Returns `null` if neither is found or the
837
+ * request fails.
838
+ */
839
+ static async getWiktionary(word2) {
840
+ if (!word2 || word2.trim() === "")
841
+ return null;
842
+ const trimmed = word2.trim();
843
+ const direct = await this.fetchWiktionaryEntry(trimmed);
844
+ if (direct)
845
+ return direct;
846
+ const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
847
+ if (capitalized === trimmed)
848
+ return null;
849
+ return this.fetchWiktionaryEntry(capitalized);
850
+ }
795
851
  /**
796
852
  * Convenience filter over `getWiktionary()`: returns just one section's
797
853
  * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TDK
4
- } from "./chunk-SLNXKZKR.mjs";
4
+ } from "./chunk-ACMGCL7T.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  var rawArgs = process.argv.slice(2);
package/dist/index.d.mts CHANGED
@@ -325,14 +325,29 @@ declare class TDK {
325
325
  * `null` on any error — network, TLS, HTTP, or JSON parse.
326
326
  */
327
327
  private static fetchKubbealtiJson;
328
+ /**
329
+ * Kubbealtı indexes headwords with full classical Turkish orthography,
330
+ * including letters that a plain-ASCII-ish query tends to drop — most
331
+ * commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
332
+ * Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
333
+ * entirely (verified: even "ruzgâr" alone still misses — it's the missing
334
+ * ü, not the missing â, that actually breaks the match). This generates
335
+ * single-letter-substitution variants to retry, one substitution per
336
+ * variant (not combinatorial) — covers the overwhelmingly common case of
337
+ * one "de-Turkished" letter without an explosion of API calls for words
338
+ * with several.
339
+ */
340
+ private static readonly TURKISH_DEASCII_MAP;
341
+ private static generateTurkishVariants;
328
342
  /**
329
343
  * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
330
344
  * word, scraped from the site's own data API — undocumented, and Kubbealtı
331
345
  * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
332
346
  * openly-published data, so use this in line with their terms. `anlam` is
333
347
  * 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.
348
+ * plain text. Falls back to `generateTurkishVariants()` if the exact query
349
+ * comes up empty (see its doc comment). Returns `null` on any fetch/parse
350
+ * failure, `[]` if no variant matches either.
336
351
  */
337
352
  static getKubbealti(word: string): Promise<KubbealtiEntry[] | null>;
338
353
  /**
@@ -354,14 +369,20 @@ declare class TDK {
354
369
  * case) or the request fails.
355
370
  */
356
371
  static getNisanyan(word: string): Promise<string | null>;
372
+ private static fetchWiktionaryEntry;
357
373
  /**
358
374
  * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
359
375
  * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
360
376
  * scraping involved, this is a stable, documented public API. `sections`
361
377
  * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
362
378
  * 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.
379
+ * unsplit text. This wiki has title capitalization turned off
380
+ * ($wgCapitalLinks=false — common for Wiktionaries, since case is
381
+ * meaningful for a dictionary: "Türkiye" the country vs. a lowercase
382
+ * common word), so an exact-case miss retries with the first letter
383
+ * uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
384
+ * "Istanbul") before giving up. Returns `null` if neither is found or the
385
+ * request fails.
365
386
  */
366
387
  static getWiktionary(word: string): Promise<WiktionaryEntry | null>;
367
388
  /**
package/dist/index.d.ts CHANGED
@@ -325,14 +325,29 @@ declare class TDK {
325
325
  * `null` on any error — network, TLS, HTTP, or JSON parse.
326
326
  */
327
327
  private static fetchKubbealtiJson;
328
+ /**
329
+ * Kubbealtı indexes headwords with full classical Turkish orthography,
330
+ * including letters that a plain-ASCII-ish query tends to drop — most
331
+ * commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
332
+ * Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
333
+ * entirely (verified: even "ruzgâr" alone still misses — it's the missing
334
+ * ü, not the missing â, that actually breaks the match). This generates
335
+ * single-letter-substitution variants to retry, one substitution per
336
+ * variant (not combinatorial) — covers the overwhelmingly common case of
337
+ * one "de-Turkished" letter without an explosion of API calls for words
338
+ * with several.
339
+ */
340
+ private static readonly TURKISH_DEASCII_MAP;
341
+ private static generateTurkishVariants;
328
342
  /**
329
343
  * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
330
344
  * word, scraped from the site's own data API — undocumented, and Kubbealtı
331
345
  * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
332
346
  * openly-published data, so use this in line with their terms. `anlam` is
333
347
  * 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.
348
+ * plain text. Falls back to `generateTurkishVariants()` if the exact query
349
+ * comes up empty (see its doc comment). Returns `null` on any fetch/parse
350
+ * failure, `[]` if no variant matches either.
336
351
  */
337
352
  static getKubbealti(word: string): Promise<KubbealtiEntry[] | null>;
338
353
  /**
@@ -354,14 +369,20 @@ declare class TDK {
354
369
  * case) or the request fails.
355
370
  */
356
371
  static getNisanyan(word: string): Promise<string | null>;
372
+ private static fetchWiktionaryEntry;
357
373
  /**
358
374
  * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
359
375
  * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
360
376
  * scraping involved, this is a stable, documented public API. `sections`
361
377
  * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
362
378
  * 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.
379
+ * unsplit text. This wiki has title capitalization turned off
380
+ * ($wgCapitalLinks=false — common for Wiktionaries, since case is
381
+ * meaningful for a dictionary: "Türkiye" the country vs. a lowercase
382
+ * common word), so an exact-case miss retries with the first letter
383
+ * uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
384
+ * "Istanbul") before giving up. Returns `null` if neither is found or the
385
+ * request fails.
365
386
  */
366
387
  static getWiktionary(word: string): Promise<WiktionaryEntry | null>;
367
388
  /**
package/dist/index.js CHANGED
@@ -700,14 +700,46 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
700
700
  req.end();
701
701
  });
702
702
  }
703
+ /**
704
+ * Kubbealtı indexes headwords with full classical Turkish orthography,
705
+ * including letters that a plain-ASCII-ish query tends to drop — most
706
+ * commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
707
+ * Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
708
+ * entirely (verified: even "ruzgâr" alone still misses — it's the missing
709
+ * ü, not the missing â, that actually breaks the match). This generates
710
+ * single-letter-substitution variants to retry, one substitution per
711
+ * variant (not combinatorial) — covers the overwhelmingly common case of
712
+ * one "de-Turkished" letter without an explosion of API calls for words
713
+ * with several.
714
+ */
715
+ static TURKISH_DEASCII_MAP = {
716
+ a: ["\xE2"],
717
+ i: ["\u0131", "\xEE"],
718
+ o: ["\xF6"],
719
+ u: ["\xFC", "\xFB"],
720
+ c: ["\xE7"],
721
+ g: ["\u011F"],
722
+ s: ["\u015F"]
723
+ };
724
+ static generateTurkishVariants(word) {
725
+ const lower = word.trim().toLocaleLowerCase("tr-TR");
726
+ const variants = [];
727
+ for (let i = 0; i < lower.length; i++) {
728
+ for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
729
+ variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
730
+ }
731
+ }
732
+ return variants;
733
+ }
703
734
  /**
704
735
  * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
705
736
  * word, scraped from the site's own data API — undocumented, and Kubbealtı
706
737
  * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
707
738
  * openly-published data, so use this in line with their terms. `anlam` is
708
739
  * 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.
740
+ * plain text. Falls back to `generateTurkishVariants()` if the exact query
741
+ * comes up empty (see its doc comment). Returns `null` on any fetch/parse
742
+ * failure, `[]` if no variant matches either.
711
743
  */
712
744
  static async getKubbealti(word) {
713
745
  if (!word || word.trim() === "")
@@ -715,7 +747,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
715
747
  const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word.trim())}/`);
716
748
  if (!data || !Array.isArray(data.content))
717
749
  return null;
718
- return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
750
+ if (data.content.length > 0) {
751
+ return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
752
+ }
753
+ for (const variant of this.generateTurkishVariants(word)) {
754
+ const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
755
+ if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
756
+ return variantData.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
757
+ }
758
+ }
759
+ return [];
719
760
  }
720
761
  /**
721
762
  * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
@@ -769,21 +810,10 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
769
810
  return null;
770
811
  }
771
812
  }
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;
813
+ static async fetchWiktionaryEntry(title) {
784
814
  try {
785
815
  const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
786
- word.trim()
816
+ title
787
817
  )}&format=json&explaintext=1&formatversion=2`;
788
818
  const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
789
819
  if (!response.ok)
@@ -796,16 +826,42 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
796
826
  const sections = {};
797
827
  const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
798
828
  for (let i = 1; i < parts.length; i += 3) {
799
- const title = parts[i + 1]?.trim();
829
+ const title2 = parts[i + 1]?.trim();
800
830
  const content = parts[i + 2]?.trim();
801
- if (title)
802
- sections[title] = content ?? "";
831
+ if (title2)
832
+ sections[title2] = content ?? "";
803
833
  }
804
834
  return { raw, sections };
805
835
  } catch {
806
836
  return null;
807
837
  }
808
838
  }
839
+ /**
840
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
841
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
842
+ * scraping involved, this is a stable, documented public API. `sections`
843
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
844
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
845
+ * unsplit text. This wiki has title capitalization turned off
846
+ * ($wgCapitalLinks=false — common for Wiktionaries, since case is
847
+ * meaningful for a dictionary: "Türkiye" the country vs. a lowercase
848
+ * common word), so an exact-case miss retries with the first letter
849
+ * uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
850
+ * "Istanbul") before giving up. Returns `null` if neither is found or the
851
+ * request fails.
852
+ */
853
+ static async getWiktionary(word) {
854
+ if (!word || word.trim() === "")
855
+ return null;
856
+ const trimmed = word.trim();
857
+ const direct = await this.fetchWiktionaryEntry(trimmed);
858
+ if (direct)
859
+ return direct;
860
+ const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
861
+ if (capitalized === trimmed)
862
+ return null;
863
+ return this.fetchWiktionaryEntry(capitalized);
864
+ }
809
865
  /**
810
866
  * Convenience filter over `getWiktionary()`: returns just one section's
811
867
  * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  TDKError,
4
4
  TDKNetworkError,
5
5
  TDKValidationError
6
- } from "./chunk-SLNXKZKR.mjs";
6
+ } from "./chunk-ACMGCL7T.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.3.0",
3
+ "version": "1.3.1",
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/tdk.ts CHANGED
@@ -713,20 +713,65 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
713
713
  });
714
714
  }
715
715
 
716
+ /**
717
+ * Kubbealtı indexes headwords with full classical Turkish orthography,
718
+ * including letters that a plain-ASCII-ish query tends to drop — most
719
+ * commonly ü/ö/ç/ğ/ş, but also the circumflex ("düzeltme işareti") used in
720
+ * Arabic/Persian loanwords like "rüzgâr". A search for "ruzgar" misses
721
+ * entirely (verified: even "ruzgâr" alone still misses — it's the missing
722
+ * ü, not the missing â, that actually breaks the match). This generates
723
+ * single-letter-substitution variants to retry, one substitution per
724
+ * variant (not combinatorial) — covers the overwhelmingly common case of
725
+ * one "de-Turkished" letter without an explosion of API calls for words
726
+ * with several.
727
+ */
728
+ private static readonly TURKISH_DEASCII_MAP: Record<string, string[]> = {
729
+ a: ["â"],
730
+ i: ["ı", "î"],
731
+ o: ["ö"],
732
+ u: ["ü", "û"],
733
+ c: ["ç"],
734
+ g: ["ğ"],
735
+ s: ["ş"],
736
+ };
737
+
738
+ private static generateTurkishVariants(word: string): string[] {
739
+ const lower = word.trim().toLocaleLowerCase("tr-TR");
740
+ const variants: string[] = [];
741
+ for (let i = 0; i < lower.length; i++) {
742
+ for (const replacement of this.TURKISH_DEASCII_MAP[lower[i]] ?? []) {
743
+ variants.push(lower.slice(0, i) + replacement + lower.slice(i + 1));
744
+ }
745
+ }
746
+ return variants;
747
+ }
748
+
716
749
  /**
717
750
  * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
718
751
  * word, scraped from the site's own data API — undocumented, and Kubbealtı
719
752
  * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
720
753
  * openly-published data, so use this in line with their terms. `anlam` is
721
754
  * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
722
- * plain text. Returns `null` on any fetch/parse failure, `[]` if the word
723
- * isn't found.
755
+ * plain text. Falls back to `generateTurkishVariants()` if the exact query
756
+ * comes up empty (see its doc comment). Returns `null` on any fetch/parse
757
+ * failure, `[]` if no variant matches either.
724
758
  */
725
759
  public static async getKubbealti(word: string): Promise<KubbealtiEntry[] | null> {
726
760
  if (!word || word.trim() === "") return null;
761
+
727
762
  const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word.trim())}/`);
728
763
  if (!data || !Array.isArray(data.content)) return null;
729
- return data.content.map((entry: any) => ({ kelime: entry.kelime, anlam: entry.anlam }));
764
+ if (data.content.length > 0) {
765
+ return data.content.map((entry: any) => ({ kelime: entry.kelime, anlam: entry.anlam }));
766
+ }
767
+
768
+ for (const variant of this.generateTurkishVariants(word)) {
769
+ const variantData = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(variant)}/`);
770
+ if (variantData && Array.isArray(variantData.content) && variantData.content.length > 0) {
771
+ return variantData.content.map((entry: any) => ({ kelime: entry.kelime, anlam: entry.anlam }));
772
+ }
773
+ }
774
+ return [];
730
775
  }
731
776
 
732
777
  /**
@@ -777,20 +822,10 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
777
822
  }
778
823
  }
779
824
 
780
- /**
781
- * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
782
- * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
783
- * scraping involved, this is a stable, documented public API. `sections`
784
- * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
785
- * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
786
- * unsplit text. Returns `null` if the page doesn't exist or the request
787
- * fails.
788
- */
789
- public static async getWiktionary(word: string): Promise<WiktionaryEntry | null> {
790
- if (!word || word.trim() === "") return null;
825
+ private static async fetchWiktionaryEntry(title: string): Promise<WiktionaryEntry | null> {
791
826
  try {
792
827
  const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
793
- word.trim()
828
+ title
794
829
  )}&format=json&explaintext=1&formatversion=2`;
795
830
  const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
796
831
  if (!response.ok) return null;
@@ -814,6 +849,32 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
814
849
  }
815
850
  }
816
851
 
852
+ /**
853
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
854
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
855
+ * scraping involved, this is a stable, documented public API. `sections`
856
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
857
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
858
+ * unsplit text. This wiki has title capitalization turned off
859
+ * ($wgCapitalLinks=false — common for Wiktionaries, since case is
860
+ * meaningful for a dictionary: "Türkiye" the country vs. a lowercase
861
+ * common word), so an exact-case miss retries with the first letter
862
+ * uppercased (Turkish-locale-aware, so "istanbul" tries "İstanbul", not
863
+ * "Istanbul") before giving up. Returns `null` if neither is found or the
864
+ * request fails.
865
+ */
866
+ public static async getWiktionary(word: string): Promise<WiktionaryEntry | null> {
867
+ if (!word || word.trim() === "") return null;
868
+ const trimmed = word.trim();
869
+
870
+ const direct = await this.fetchWiktionaryEntry(trimmed);
871
+ if (direct) return direct;
872
+
873
+ const capitalized = trimmed.charAt(0).toLocaleUpperCase("tr-TR") + trimmed.slice(1);
874
+ if (capitalized === trimmed) return null;
875
+ return this.fetchWiktionaryEntry(capitalized);
876
+ }
877
+
817
878
  /**
818
879
  * Convenience filter over `getWiktionary()`: returns just one section's
819
880
  * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched