tdk-api-wrapper 1.2.2 → 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/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;
@@ -296,6 +318,79 @@ declare class TDK {
296
318
  */
297
319
  private static fetchRuleText;
298
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
+ * 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;
342
+ /**
343
+ * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
344
+ * word, scraped from the site's own data API — undocumented, and Kubbealtı
345
+ * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
346
+ * openly-published data, so use this in line with their terms. `anlam` is
347
+ * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
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.
351
+ */
352
+ static getKubbealti(word: string): Promise<KubbealtiEntry[] | null>;
353
+ /**
354
+ * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
355
+ * plain text via `htmlToPlainText()`.
356
+ */
357
+ static getKubbealtiMeanings(word: string): Promise<string[] | null>;
358
+ /**
359
+ * Autocomplete suggestions from Kubbealtı Lugatı's own typeahead endpoint
360
+ * (separate from `getSuggestions()`, which uses TDK's data).
361
+ */
362
+ static getKubbealtiSuggestions(prefix: string): Promise<string[]>;
363
+ /**
364
+ * Returns the etymology paragraph for a word from Nişanyan Sözlük, scraped
365
+ * from that page's server-rendered `<meta name="description">` tag (the
366
+ * page already puts the full etymology text there for SEO, so no need to
367
+ * parse the site's internal SvelteKit data format). Returns `null` if the
368
+ * word isn't found (the page falls back to a generic site tagline in that
369
+ * case) or the request fails.
370
+ */
371
+ static getNisanyan(word: string): Promise<string | null>;
372
+ private static fetchWiktionaryEntry;
373
+ /**
374
+ * Returns the Turkish Wiktionary (`tr.wiktionary.org`) entry for a word,
375
+ * via MediaWiki's official Action API (`action=query&prop=extracts`) — no
376
+ * scraping involved, this is a stable, documented public API. `sections`
377
+ * splits the plain-text extract on its `== Heading ==`/`=== Heading ===`
378
+ * markers (e.g. "Köken", "Söyleniş", "Ad") for convenience; `raw` has the
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.
386
+ */
387
+ static getWiktionary(word: string): Promise<WiktionaryEntry | null>;
388
+ /**
389
+ * Convenience filter over `getWiktionary()`: returns just one section's
390
+ * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
391
+ * case-insensitively. Returns `null` if the word or the section isn't found.
392
+ */
393
+ static getWiktionarySection(word: string, sectionName: string): Promise<string | null>;
299
394
  /**
300
395
  * Returns compound words that contain this word.
301
396
  */
@@ -375,4 +470,4 @@ declare class TDKNetworkError extends TDKError {
375
470
  });
376
471
  }
377
472
 
378
- 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 };
473
+ 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();
@@ -581,7 +659,222 @@ var TDK = class {
581
659
  }
582
660
  }
583
661
  static htmlToPlainText(html) {
584
- 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
+ * 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
+ }
734
+ /**
735
+ * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
736
+ * word, scraped from the site's own data API — undocumented, and Kubbealtı
737
+ * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
738
+ * openly-published data, so use this in line with their terms. `anlam` is
739
+ * raw HTML (rich typography markup); use `getKubbealtiMeanings()` for
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.
743
+ */
744
+ static async getKubbealti(word) {
745
+ if (!word || word.trim() === "")
746
+ return null;
747
+ const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word.trim())}/`);
748
+ if (!data || !Array.isArray(data.content))
749
+ return null;
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 [];
760
+ }
761
+ /**
762
+ * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
763
+ * plain text via `htmlToPlainText()`.
764
+ */
765
+ static async getKubbealtiMeanings(word) {
766
+ const entries = await this.getKubbealti(word);
767
+ if (!entries)
768
+ return null;
769
+ return entries.map((e) => this.htmlToPlainText(e.anlam));
770
+ }
771
+ /**
772
+ * Autocomplete suggestions from Kubbealtı Lugatı's own typeahead endpoint
773
+ * (separate from `getSuggestions()`, which uses TDK's data).
774
+ */
775
+ static async getKubbealtiSuggestions(prefix) {
776
+ if (!prefix || prefix.trim() === "")
777
+ return [];
778
+ const data = await this.fetchKubbealtiJson(`/rest/word-search/${encodeURIComponent(prefix.trim())}`);
779
+ if (!Array.isArray(data))
780
+ return [];
781
+ return data.map((item) => item.display).filter(Boolean);
782
+ }
783
+ /**
784
+ * Returns the etymology paragraph for a word from Nişanyan Sözlük, scraped
785
+ * from that page's server-rendered `<meta name="description">` tag (the
786
+ * page already puts the full etymology text there for SEO, so no need to
787
+ * parse the site's internal SvelteKit data format). Returns `null` if the
788
+ * word isn't found (the page falls back to a generic site tagline in that
789
+ * case) or the request fails.
790
+ */
791
+ static async getNisanyan(word) {
792
+ if (!word || word.trim() === "")
793
+ return null;
794
+ try {
795
+ const response = await fetch(
796
+ `https://www.nisanyansozluk.com/kelime/${encodeURIComponent(word.trim().toLocaleLowerCase("tr-TR"))}`,
797
+ { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } }
798
+ );
799
+ if (!response.ok)
800
+ return null;
801
+ const html = await response.text();
802
+ const match = html.match(/<meta name="description" content="([^"]*)"/);
803
+ if (!match)
804
+ return null;
805
+ const description = this.htmlToPlainText(match[1]);
806
+ if (description === "\xC7a\u011Fda\u015F T\xFCrk\xE7enin Etimolojisi")
807
+ return null;
808
+ return description;
809
+ } catch {
810
+ return null;
811
+ }
812
+ }
813
+ static async fetchWiktionaryEntry(title) {
814
+ try {
815
+ const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
816
+ title
817
+ )}&format=json&explaintext=1&formatversion=2`;
818
+ const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
819
+ if (!response.ok)
820
+ return null;
821
+ const data = await response.json();
822
+ const page = data?.query?.pages?.[0];
823
+ if (!page || page.missing || !page.extract)
824
+ return null;
825
+ const raw = page.extract;
826
+ const sections = {};
827
+ const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
828
+ for (let i = 1; i < parts.length; i += 3) {
829
+ const title2 = parts[i + 1]?.trim();
830
+ const content = parts[i + 2]?.trim();
831
+ if (title2)
832
+ sections[title2] = content ?? "";
833
+ }
834
+ return { raw, sections };
835
+ } catch {
836
+ return null;
837
+ }
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
+ }
865
+ /**
866
+ * Convenience filter over `getWiktionary()`: returns just one section's
867
+ * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
868
+ * case-insensitively. Returns `null` if the word or the section isn't found.
869
+ */
870
+ static async getWiktionarySection(word, sectionName) {
871
+ const entry = await this.getWiktionary(word);
872
+ if (!entry)
873
+ return null;
874
+ const key = Object.keys(entry.sections).find(
875
+ (k) => k.toLocaleLowerCase("tr-TR") === sectionName.trim().toLocaleLowerCase("tr-TR")
876
+ );
877
+ return key ? entry.sections[key] : null;
585
878
  }
586
879
  /**
587
880
  * Returns compound words that contain this word.
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  TDKError,
4
4
  TDKNetworkError,
5
5
  TDKValidationError
6
- } from "./chunk-SNY3KUCF.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.2.2",
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/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
  }