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/cli.js CHANGED
@@ -55,9 +55,87 @@ var fs = __toESM(require("fs"));
55
55
  var path = __toESM(require("path"));
56
56
  var os = __toESM(require("os"));
57
57
  var https = __toESM(require("https"));
58
+ var tls = __toESM(require("tls"));
58
59
  var TDK = class {
59
60
  static BASE_URL = "https://sozluk.gov.tr";
60
61
  static AUDIO_API_HOST = "api.sozluk.gov.tr";
62
+ static KUBBEALTI_HOST = "eski.lugatim.com";
63
+ /**
64
+ * `eski.lugatim.com` (Kubbealtı Lugatı's data API) sends only its leaf
65
+ * certificate during the TLS handshake, omitting the intermediates a
66
+ * correctly configured server would include — a server-side misconfiguration,
67
+ * not something we should paper over by disabling verification. These are
68
+ * the two certificates the server *should* be sending (fetched from the
69
+ * leaf's own Authority Information Access URLs), supplied here so Node can
70
+ * still build a full, properly verified chain up to a root it already
71
+ * trusts (ISRG Root X1). If Let's Encrypt rotates this intermediate, this
72
+ * stops working and every Kubbealtı call fails closed to `null` — same
73
+ * fail-closed contract as the rest of this file's fragile integrations.
74
+ */
75
+ static KUBBEALTI_EXTRA_CA = [
76
+ `-----BEGIN CERTIFICATE-----
77
+ MIIE2jCCAsKgAwIBAgIQTr0klH4k05SALYSlL9WzGTANBgkqhkiG9w0BAQsFADAu
78
+ MQswCQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZUjAe
79
+ Fw0yNTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYw
80
+ FAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZUjIwggEiMA0GCSqGSIb3
81
+ DQEBAQUAA4IBDwAwggEKAoIBAQDZ0LxwBppqh84luqMerV/eeL/fXQ7mLQQv1Lnp
82
+ WKZbyvGpx6wh6AfnslAnF6ewTkcHA+gSOoBvm3Dfm06AuGiF+KRut4fAcowqnAQQ
83
+ CW98+QPP/eOv/wug7Iyk4NkOxf2I6g2f55T6nJoOTLFcukeRq80JGQEYan+dPFr9
84
+ OGUgQK2hGKgNkW87pappsOAuUJcroYhRt5uUis4qaZireiseu32gzDJNBAiKtsvd
85
+ 6HX4v25bpkRNcS/B/Gtc9kVbUpD+2PLPxdei3Tim55k4tfAEXwD2qyiPTxrTNq6l
86
+ N+AMr5g2c1dNqkOTwjxeV6L5lpP1rGiYvLnRaPlOqyZRPW+5AgMBAAGjge4wgesw
87
+ DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBIGA1UdEwEB/wQI
88
+ MAYBAf8CAQAwHQYDVR0OBBYEFEAVLSZ57TIgnt+ach3WMh+BDIEMMB8GA1UdIwQY
89
+ MBaAFN7nW2DQIm1AKH0/DQH+pLVStFGUMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEF
90
+ BQcwAoYWaHR0cDovL3lyLmkubGVuY3Iub3JnLzATBgNVHSAEDDAKMAgGBmeBDAEC
91
+ ATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veXIuYy5sZW5jci5vcmcvMA0GCSqG
92
+ SIb3DQEBCwUAA4ICAQB0ZUQWZ9/Yn9COEpo+JfecMnB0h0vwDm/M66IqXqw3LoaL
93
+ mx9lZvRTeDIS67PUeI3yCA2W6PKRD0/FE/G57lOmS+Xy5AaaL00ICGOqjNcCaMWW
94
+ 8o8nevHOd4i4lqgtznE/28QwlcdJyF8yBiWHpnyjhEpmNWJURgOCOg2xpwRMBCsj
95
+ MScqYPtOhBeuYQvSwAEeTML2Ukh6uGuX4E14q65Ja8cdjF5bAldnP1eE4FBaAwsZ
96
+ G2fOqqrKV03Y85Nw2btedP1AtliQuJZs/Jo/gXxXdc7LrH3McgnpnbTiAncX7yES
97
+ hP6kzQejllqMCIt52HOjxDGWafS7Xw+DKwqmH+Eqy8dcbOuag/1AYlQoKNVK3F5q
98
+ Hh6tEDiMqQcLIibGKteE6iHo4A/bIScbzrhXUYuism42ZYzmc48FMVIH3qy4L84E
99
+ TdAH2gtxw0PAhvRVXp8HP7wfngpzsN/8xOTpeRSbM4+Qbc56G6+Bifmv6sk1ieQb
100
+ NA3wJdl4DDUuQSV8hBgx6zoI1ZSGORprDFux7c6rhc77QZMSRrEgomBeklervEve
101
+ 86ylWmZ3WWHV6RLMi8xNvjd71r4EPIGgY7BZU/VPBkq+uA7Gb6mbJnFgV43uh3xy
102
+ LRFgxIAphIukwTGSMZZR+AI+Qnp0BYTWovHXozOf3H8r6hozEoT02JHn0AeTfA==
103
+ -----END CERTIFICATE-----`,
104
+ `-----BEGIN CERTIFICATE-----
105
+ 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
137
+ -----END CERTIFICATE-----`
138
+ ];
61
139
  // Cache Mechanism
62
140
  static isCacheEnabled = false;
63
141
  static wordCache = /* @__PURE__ */ new Map();
@@ -424,11 +502,16 @@ var TDK = class {
424
502
  for (const candidate of this.autocompleteCache) {
425
503
  if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
426
504
  continue;
427
- const distance = this.levenshtein(cleanWord, candidate);
428
- if (distance > 0 && (!best || distance < best.distance)) {
429
- best = { candidate, distance };
430
- if (distance === 1)
431
- break;
505
+ if (Math.abs(candidate.length - cleanWord.length) > 2)
506
+ continue;
507
+ const distance = this.damerauLevenshtein(cleanWord, candidate);
508
+ if (distance === 0)
509
+ continue;
510
+ const firstMismatch = candidate[0] === cleanWord[0] ? 0 : 1;
511
+ const lengthMismatch = candidate.length === cleanWord.length ? 0 : 1;
512
+ const better = !best || distance < best.distance || distance === best.distance && firstMismatch < best.firstMismatch || distance === best.distance && firstMismatch === best.firstMismatch && lengthMismatch < best.lengthMismatch;
513
+ if (better) {
514
+ best = { candidate, distance, firstMismatch, lengthMismatch };
432
515
  }
433
516
  }
434
517
  if (best && best.distance <= 2) {
@@ -507,24 +590,33 @@ var TDK = class {
507
590
  * case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
508
591
  * hands back a single randomly-rotated rule per request (out of a pool of
509
592
  * roughly twenty) rather than a fixed catalog, a single `getKurallar()`
510
- * draw would rarely match a given name — this re-draws (bounded, with a
511
- * short delay) until it finds a match or gives up. Every attempt bypasses
512
- * `dailyContentCache` without that, once `enableCache(true)` is on, all
513
- * 25 attempts would just re-read the same cached `/icerik` response and
514
- * could never find a rule outside whatever the first draw happened to be.
515
- * Returns `null` if no match turns up within the attempt budget or the
516
- * matched page can't be parsed.
593
+ * draw would rarely match a given name — this re-draws until it finds a
594
+ * match or gives up. Draws happen in concurrent batches (each `/icerik`
595
+ * request is independent and stateless) rather than one-at-a-time with a
596
+ * delay: same total sample size (25) and hit probability as a sequential
597
+ * loop, but bounded to a handful of round-trips instead of 25 of them, so
598
+ * a miss resolves in roughly one round-trip time instead of several
599
+ * seconds. Every draw bypasses `dailyContentCache` — without that, once
600
+ * `enableCache(true)` is on, every attempt would just re-read the same
601
+ * cached `/icerik` response and could never find a rule outside whatever
602
+ * the first draw happened to be. Returns `null` if no match turns up
603
+ * within the attempt budget or the matched page can't be parsed.
517
604
  */
518
605
  static async getRule(name) {
519
606
  if (!name || name.trim() === "")
520
607
  return null;
521
608
  const target = name.trim().toLocaleLowerCase("tr-TR");
522
- for (let attempt = 0; attempt < 25; attempt++) {
523
- const rules = await this.getKurallar(true);
524
- const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
525
- if (match)
526
- return this.fetchRuleText(match.url);
527
- await this.delay(100);
609
+ const BATCH_SIZE = 5;
610
+ const ROUNDS = 5;
611
+ for (let round = 0; round < ROUNDS; round++) {
612
+ const batches = await Promise.all(
613
+ Array.from({ length: BATCH_SIZE }, () => this.getKurallar(true))
614
+ );
615
+ for (const rules of batches) {
616
+ const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
617
+ if (match)
618
+ return this.fetchRuleText(match.url);
619
+ }
528
620
  }
529
621
  return null;
530
622
  }
@@ -553,7 +645,166 @@ var TDK = class {
553
645
  }
554
646
  }
555
647
  static htmlToPlainText(html) {
556
- 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();
648
+ 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();
649
+ }
650
+ /**
651
+ * GETs a JSON path from Kubbealtı Lugatı's data API (`eski.lugatim.com`),
652
+ * supplying `KUBBEALTI_EXTRA_CA` to work around that host's incomplete
653
+ * certificate chain (see the constant's doc comment). Fails closed to
654
+ * `null` on any error — network, TLS, HTTP, or JSON parse.
655
+ */
656
+ static fetchKubbealtiJson(path2) {
657
+ return new Promise((resolve) => {
658
+ const req = https.request(
659
+ {
660
+ hostname: this.KUBBEALTI_HOST,
661
+ path: path2,
662
+ method: "GET",
663
+ ca: [...tls.rootCertificates, ...this.KUBBEALTI_EXTRA_CA],
664
+ headers: {
665
+ "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"
666
+ }
667
+ },
668
+ (res) => {
669
+ if (res.statusCode !== 200) {
670
+ res.resume();
671
+ resolve(null);
672
+ return;
673
+ }
674
+ let body = "";
675
+ res.on("data", (chunk) => body += chunk);
676
+ res.on("end", () => {
677
+ try {
678
+ resolve(JSON.parse(body));
679
+ } catch {
680
+ resolve(null);
681
+ }
682
+ });
683
+ }
684
+ );
685
+ req.on("error", () => resolve(null));
686
+ req.end();
687
+ });
688
+ }
689
+ /**
690
+ * Returns Kubbealtı Lugatı ("Misalli Büyük Türkçe Sözlük") entries for a
691
+ * word, scraped from the site's own data API — undocumented, and Kubbealtı
692
+ * Lugatı is a commercial dictionary product, unlike TDK's or Wiktionary's
693
+ * openly-published data, so use this in line with their terms. `anlam` is
694
+ * 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.
697
+ */
698
+ static async getKubbealti(word2) {
699
+ if (!word2 || word2.trim() === "")
700
+ return null;
701
+ const data = await this.fetchKubbealtiJson(`/rest/s/${encodeURIComponent(word2.trim())}/`);
702
+ if (!data || !Array.isArray(data.content))
703
+ return null;
704
+ return data.content.map((entry) => ({ kelime: entry.kelime, anlam: entry.anlam }));
705
+ }
706
+ /**
707
+ * Same as `getKubbealti()` but with each entry's `anlam` HTML stripped to
708
+ * plain text via `htmlToPlainText()`.
709
+ */
710
+ static async getKubbealtiMeanings(word2) {
711
+ const entries = await this.getKubbealti(word2);
712
+ if (!entries)
713
+ return null;
714
+ return entries.map((e) => this.htmlToPlainText(e.anlam));
715
+ }
716
+ /**
717
+ * Autocomplete suggestions from Kubbealtı Lugatı's own typeahead endpoint
718
+ * (separate from `getSuggestions()`, which uses TDK's data).
719
+ */
720
+ static async getKubbealtiSuggestions(prefix) {
721
+ if (!prefix || prefix.trim() === "")
722
+ return [];
723
+ const data = await this.fetchKubbealtiJson(`/rest/word-search/${encodeURIComponent(prefix.trim())}`);
724
+ if (!Array.isArray(data))
725
+ return [];
726
+ return data.map((item) => item.display).filter(Boolean);
727
+ }
728
+ /**
729
+ * Returns the etymology paragraph for a word from Nişanyan Sözlük, scraped
730
+ * from that page's server-rendered `<meta name="description">` tag (the
731
+ * page already puts the full etymology text there for SEO, so no need to
732
+ * parse the site's internal SvelteKit data format). Returns `null` if the
733
+ * word isn't found (the page falls back to a generic site tagline in that
734
+ * case) or the request fails.
735
+ */
736
+ static async getNisanyan(word2) {
737
+ if (!word2 || word2.trim() === "")
738
+ return null;
739
+ try {
740
+ const response = await fetch(
741
+ `https://www.nisanyansozluk.com/kelime/${encodeURIComponent(word2.trim().toLocaleLowerCase("tr-TR"))}`,
742
+ { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } }
743
+ );
744
+ if (!response.ok)
745
+ return null;
746
+ const html = await response.text();
747
+ const match = html.match(/<meta name="description" content="([^"]*)"/);
748
+ if (!match)
749
+ return null;
750
+ const description = this.htmlToPlainText(match[1]);
751
+ if (description === "\xC7a\u011Fda\u015F T\xFCrk\xE7enin Etimolojisi")
752
+ return null;
753
+ return description;
754
+ } catch {
755
+ return null;
756
+ }
757
+ }
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;
770
+ try {
771
+ const url = `https://tr.wiktionary.org/w/api.php?action=query&prop=extracts&titles=${encodeURIComponent(
772
+ word2.trim()
773
+ )}&format=json&explaintext=1&formatversion=2`;
774
+ const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
775
+ if (!response.ok)
776
+ return null;
777
+ const data = await response.json();
778
+ const page = data?.query?.pages?.[0];
779
+ if (!page || page.missing || !page.extract)
780
+ return null;
781
+ const raw = page.extract;
782
+ const sections = {};
783
+ const parts = raw.split(/\n(={2,4})\s*(.+?)\s*\1\n/);
784
+ for (let i = 1; i < parts.length; i += 3) {
785
+ const title = parts[i + 1]?.trim();
786
+ const content = parts[i + 2]?.trim();
787
+ if (title)
788
+ sections[title] = content ?? "";
789
+ }
790
+ return { raw, sections };
791
+ } catch {
792
+ return null;
793
+ }
794
+ }
795
+ /**
796
+ * Convenience filter over `getWiktionary()`: returns just one section's
797
+ * text (e.g. `getWiktionarySection(word, "Köken")` for etymology), matched
798
+ * case-insensitively. Returns `null` if the word or the section isn't found.
799
+ */
800
+ static async getWiktionarySection(word2, sectionName) {
801
+ const entry = await this.getWiktionary(word2);
802
+ if (!entry)
803
+ return null;
804
+ const key = Object.keys(entry.sections).find(
805
+ (k) => k.toLocaleLowerCase("tr-TR") === sectionName.trim().toLocaleLowerCase("tr-TR")
806
+ );
807
+ return key ? entry.sections[key] : null;
557
808
  }
558
809
  /**
559
810
  * Returns compound words that contain this word.
@@ -672,6 +923,11 @@ var TDK = class {
672
923
  * Analyzes every distinct word in a text (Turkish stopwords filtered out),
673
924
  * returning each word's first meaning and etymological origin if found.
674
925
  * Looks each word up individually (throttled), so scales with text length.
926
+ * TDK only indexes dictionary (dictionary/root) forms, not inflected ones —
927
+ * it does no morphological analysis, and neither does this method: a
928
+ * suffixed word like "evde" or "dildir" (root "ev"/"dil" plus a case/verb
929
+ * suffix) will come back `found: false` even though the root is a real
930
+ * headword. This is an inherent limitation of the data source, not a bug.
675
931
  */
676
932
  static async analyzeText(text) {
677
933
  const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
@@ -691,9 +947,13 @@ var TDK = class {
691
947
  return analyses;
692
948
  }
693
949
  /**
694
- * Classic edit-distance between two strings.
950
+ * Damerau-Levenshtein edit-distance (optimal string alignment variant):
951
+ * like classic Levenshtein but also counts an adjacent-character
952
+ * transposition (e.g. "yanlız" -> "yalnız") as a single edit instead of
953
+ * two substitutions — a very common class of typo that plain Levenshtein
954
+ * otherwise misses.
695
955
  */
696
- static levenshtein(a, b) {
956
+ static damerauLevenshtein(a, b) {
697
957
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
698
958
  for (let i = 0; i <= a.length; i++)
699
959
  dp[i][0] = i;
@@ -703,6 +963,9 @@ var TDK = class {
703
963
  for (let j = 1; j <= b.length; j++) {
704
964
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
705
965
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
966
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
967
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + cost);
968
+ }
706
969
  }
707
970
  }
708
971
  return dp[a.length][b.length];
@@ -792,7 +1055,10 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
792
1055
  "kural",
793
1056
  "karsilastir",
794
1057
  "analiz",
795
- "oneri"
1058
+ "oneri",
1059
+ "kubbealti",
1060
+ "nisanyan",
1061
+ "viki"
796
1062
  ]);
797
1063
  var command = args[0];
798
1064
  var word = args.slice(1).join(" ");
@@ -818,7 +1084,7 @@ async function run() {
818
1084
  if (!command || command === "--help" || command === "-h") {
819
1085
  console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
820
1086
  console.log(
821
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
1087
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, kubbealti, nisanyan, viki"
822
1088
  );
823
1089
  console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
824
1090
  process.exit(command ? 0 : 1);
@@ -1021,6 +1287,46 @@ async function run() {
1021
1287
  });
1022
1288
  break;
1023
1289
  }
1290
+ case "kubbealti": {
1291
+ if (!word)
1292
+ throw new Error("Kelime belirtmelisiniz.");
1293
+ const meanings = await TDK.getKubbealtiMeanings(word);
1294
+ printResult(meanings, () => {
1295
+ if (!meanings) {
1296
+ console.log("Kubbealt\u0131 Lugat\u0131'na ula\u015F\u0131lamad\u0131.");
1297
+ } else if (meanings.length === 0) {
1298
+ console.log("Sonu\xE7 bulunamad\u0131.");
1299
+ } else {
1300
+ meanings.forEach((m, i) => console.log(`${i + 1}. ${m}`));
1301
+ }
1302
+ });
1303
+ break;
1304
+ }
1305
+ case "nisanyan": {
1306
+ if (!word)
1307
+ throw new Error("Kelime belirtmelisiniz.");
1308
+ const origin = await TDK.getNisanyan(word);
1309
+ printResult(origin, () => console.log(origin ?? "Sonu\xE7 bulunamad\u0131."));
1310
+ break;
1311
+ }
1312
+ case "viki": {
1313
+ if (!word)
1314
+ throw new Error("Kelime belirtmelisiniz.");
1315
+ const entry = await TDK.getWiktionary(word);
1316
+ printResult(entry, () => {
1317
+ if (!entry) {
1318
+ console.log("Sonu\xE7 bulunamad\u0131.");
1319
+ } else {
1320
+ for (const [title, content] of Object.entries(entry.sections)) {
1321
+ if (content)
1322
+ console.log(`-- ${title} --
1323
+ ${content}
1324
+ `);
1325
+ }
1326
+ }
1327
+ });
1328
+ break;
1329
+ }
1024
1330
  default:
1025
1331
  printError("Bilinmeyen komut.");
1026
1332
  }
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TDK
4
- } from "./chunk-6BTOGV2M.mjs";
4
+ } from "./chunk-SLNXKZKR.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  var rawArgs = process.argv.slice(2);
@@ -24,7 +24,10 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
24
24
  "kural",
25
25
  "karsilastir",
26
26
  "analiz",
27
- "oneri"
27
+ "oneri",
28
+ "kubbealti",
29
+ "nisanyan",
30
+ "viki"
28
31
  ]);
29
32
  var command = args[0];
30
33
  var word = args.slice(1).join(" ");
@@ -50,7 +53,7 @@ async function run() {
50
53
  if (!command || command === "--help" || command === "-h") {
51
54
  console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
52
55
  console.log(
53
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
56
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, kubbealti, nisanyan, viki"
54
57
  );
55
58
  console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
56
59
  process.exit(command ? 0 : 1);
@@ -253,6 +256,46 @@ async function run() {
253
256
  });
254
257
  break;
255
258
  }
259
+ case "kubbealti": {
260
+ if (!word)
261
+ throw new Error("Kelime belirtmelisiniz.");
262
+ const meanings = await TDK.getKubbealtiMeanings(word);
263
+ printResult(meanings, () => {
264
+ if (!meanings) {
265
+ console.log("Kubbealt\u0131 Lugat\u0131'na ula\u015F\u0131lamad\u0131.");
266
+ } else if (meanings.length === 0) {
267
+ console.log("Sonu\xE7 bulunamad\u0131.");
268
+ } else {
269
+ meanings.forEach((m, i) => console.log(`${i + 1}. ${m}`));
270
+ }
271
+ });
272
+ break;
273
+ }
274
+ case "nisanyan": {
275
+ if (!word)
276
+ throw new Error("Kelime belirtmelisiniz.");
277
+ const origin = await TDK.getNisanyan(word);
278
+ printResult(origin, () => console.log(origin ?? "Sonu\xE7 bulunamad\u0131."));
279
+ break;
280
+ }
281
+ case "viki": {
282
+ if (!word)
283
+ throw new Error("Kelime belirtmelisiniz.");
284
+ const entry = await TDK.getWiktionary(word);
285
+ printResult(entry, () => {
286
+ if (!entry) {
287
+ console.log("Sonu\xE7 bulunamad\u0131.");
288
+ } else {
289
+ for (const [title, content] of Object.entries(entry.sections)) {
290
+ if (content)
291
+ console.log(`-- ${title} --
292
+ ${content}
293
+ `);
294
+ }
295
+ }
296
+ });
297
+ break;
298
+ }
256
299
  default:
257
300
  printError("Bilinmeyen komut.");
258
301
  }
package/dist/index.d.mts 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 };