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/README.md +14 -3
- package/dist/{chunk-6BTOGV2M.mjs → chunk-SLNXKZKR.mjs} +284 -21
- package/dist/cli.js +329 -23
- package/dist/cli.mjs +46 -3
- package/dist/index.d.mts +97 -10
- package/dist/index.d.ts +97 -10
- package/dist/index.js +284 -21
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/cli.ts +41 -1
- package/src/tdk.ts +298 -20
- package/src/types.ts +10 -0
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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(/ /gi, " ").replace(/&
|
|
648
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/ /gi, " ").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'|’/gi, "'").replace(/&/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
|
-
*
|
|
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
|
|
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-
|
|
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
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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 };
|