tdk-api-wrapper 1.1.0 → 1.2.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 +3 -2
- package/dist/{chunk-P3GX7I53.mjs → chunk-6BTOGV2M.mjs} +87 -37
- package/dist/cli.js +103 -39
- package/dist/cli.mjs +17 -3
- package/dist/index.d.mts +32 -7
- package/dist/index.d.ts +32 -7
- package/dist/index.js +87 -37
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/cli.ts +15 -1
- package/src/tdk.ts +95 -45
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ tdk kurallar
|
|
|
34
34
|
tdk kural kısaltmalar
|
|
35
35
|
tdk karsilastir kalem kağıt
|
|
36
36
|
tdk analiz "Bu güzel kalem masanın üstünde duruyor"
|
|
37
|
+
tdk oneri kale
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
Herhangi bir komuta `--json` bayrağı eklendiğinde çıktı, insan-okunur metin yerine tek satırlık JSON olarak basılır (script/otomasyon kullanımı için):
|
|
@@ -67,7 +68,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
|
|
|
67
68
|
- **`TDK.syllabicate(word)`**: Kelimeyi Türkçe heceleme kurallarına göre doğru hecelerine ayırır (Örn: `['mu', 'vaf', 'fa', 'ki', 'yet']`). API isteği atmaz, çok hızlıdır.
|
|
68
69
|
- **`TDK.checkVowelHarmony(word)`**: Kelimenin büyük ünlü uyumuna uyup uymadığını (boolean) kontrol eder.
|
|
69
70
|
- **`TDK.getPartOfSpeech(word)`**: Kelimenin sözcük türünü (isim, sıfat, zarf vb.) döndürür.
|
|
70
|
-
- **`TDK.checkSpelling(word)`**:
|
|
71
|
+
- **`TDK.checkSpelling(word)`**: Kelimenin doğru yazılıp yazılmadığını kontrol eder. Önce TDK'nin "sık yapılan yanlışlar" listesinde tam eşleşme arar; bulamazsa TDK'nin ~81 bin kelimelik tam madde listesi üzerinde edit-distance (Levenshtein) ile en yakın kelimeyi önerir (örn. `herkez` → `herkes`, `mektub` → `mektup`).
|
|
71
72
|
- **`TDK.getCompoundWords(word)`**: Aranan kelime ile oluşturulmuş birleşik kelimeleri (Örn: dolma kalem) listeler.
|
|
72
73
|
|
|
73
74
|
### 3. Edebi ve Kültürel Analiz
|
|
@@ -81,7 +82,7 @@ Aşağıdaki metotlar `TDK` sınıfı üzerinden statik olarak erişilebilir dur
|
|
|
81
82
|
- **`TDK.analyzeText(text)`**: Bir metindeki (Türkçe bağlaçlar/edatlar hariç) her benzersiz kelimeyi tek tek arayıp ilk anlamını ve kökenini döner.
|
|
82
83
|
|
|
83
84
|
### 4. Yardımcı Metotlar
|
|
84
|
-
- **`TDK.getSuggestions(prefix)`**:
|
|
85
|
+
- **`TDK.getSuggestions(prefix)`**: TDK'nin ~81 bin kelimelik tam madde listesi üzerinden önek bazlı otomatik tamamlama önerileri döner (ilk çağrıda listeyi indirip önbelleğe alır, sonraki çağrılar anlıktır).
|
|
85
86
|
- **`TDK.getAudioUrl(word)`**: TDK'nin bu kelime için gerçekten bir ses kaydı varsa doğrudan indirme URL'sini döner, yoksa `null`. `downloadAudio(word, destPath)` ile cihazınıza indirebilirsiniz.
|
|
86
87
|
- **`TDK.getDailyContent()`**: TDK anasayfasında yer alan "Günün Kelimesi, Atasözü ve Kuralı" içeriklerini çeker.
|
|
87
88
|
- **`TDK.getWordOfTheDay()`**: `getDailyContent()`'in üzerine ince bir katman; günün kelimesini ve tüm anlamlarını `{ word, meanings }` şeklinde döner.
|
|
@@ -119,23 +119,60 @@ var TDK = class {
|
|
|
119
119
|
return meanings;
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
|
-
*
|
|
122
|
+
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
123
|
+
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
124
|
+
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
125
|
+
* instead bundled directly into its main JS asset as a
|
|
126
|
+
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
127
|
+
* page to find that asset's current hashed filename, downloads it (a few
|
|
128
|
+
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
129
|
+
* scraping of an implementation detail — if TDK's build stops embedding
|
|
130
|
+
* this, this fails closed to `[]` rather than throwing.
|
|
131
|
+
*/
|
|
132
|
+
static async fetchAutocompleteData() {
|
|
133
|
+
try {
|
|
134
|
+
const homeResponse = await fetch(`${this.BASE_URL}/`, {
|
|
135
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
136
|
+
});
|
|
137
|
+
if (!homeResponse.ok)
|
|
138
|
+
return [];
|
|
139
|
+
const html = await homeResponse.text();
|
|
140
|
+
const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
|
|
141
|
+
if (!scriptMatch)
|
|
142
|
+
return [];
|
|
143
|
+
const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
|
|
144
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
145
|
+
});
|
|
146
|
+
if (!bundleResponse.ok)
|
|
147
|
+
return [];
|
|
148
|
+
const bundleJs = await bundleResponse.text();
|
|
149
|
+
const startMarker = 'JSON.parse(`[{"madde":';
|
|
150
|
+
const startIdx = bundleJs.indexOf(startMarker);
|
|
151
|
+
if (startIdx === -1)
|
|
152
|
+
return [];
|
|
153
|
+
const jsonStart = startIdx + "JSON.parse(".length + 1;
|
|
154
|
+
const jsonEnd = bundleJs.indexOf("`)", jsonStart);
|
|
155
|
+
if (jsonEnd === -1)
|
|
156
|
+
return [];
|
|
157
|
+
const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
|
|
158
|
+
return data.map((item) => item.madde).filter(Boolean);
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
165
|
+
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
166
|
+
* and cached once per process regardless of `enableCache()` — the same
|
|
167
|
+
* caching behavior as before — and only cleared by `clearCache()`.
|
|
123
168
|
*/
|
|
124
169
|
static async getSuggestions(prefix) {
|
|
170
|
+
if (!prefix || prefix.trim() === "")
|
|
171
|
+
return [];
|
|
125
172
|
if (this.autocompleteCache.length === 0) {
|
|
126
|
-
|
|
127
|
-
const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
|
|
128
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
129
|
-
});
|
|
130
|
-
if (response.ok) {
|
|
131
|
-
const data = await response.json();
|
|
132
|
-
this.autocompleteCache = data.map((item) => item.madde);
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
173
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
137
174
|
}
|
|
138
|
-
const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
|
|
175
|
+
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
139
176
|
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
140
177
|
}
|
|
141
178
|
/**
|
|
@@ -353,29 +390,37 @@ var TDK = class {
|
|
|
353
390
|
if (mixMatch) {
|
|
354
391
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
355
392
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
393
|
+
}
|
|
394
|
+
if (this.autocompleteCache.length === 0) {
|
|
395
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
396
|
+
}
|
|
397
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
398
|
+
let best = null;
|
|
399
|
+
for (const candidate of this.autocompleteCache) {
|
|
400
|
+
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
401
|
+
continue;
|
|
402
|
+
const distance = this.levenshtein(cleanWord, candidate);
|
|
403
|
+
if (distance > 0 && (!best || distance < best.distance)) {
|
|
404
|
+
best = { candidate, distance };
|
|
405
|
+
if (distance === 1)
|
|
406
|
+
break;
|
|
370
407
|
}
|
|
371
408
|
}
|
|
409
|
+
if (best && best.distance <= 2) {
|
|
410
|
+
return { isCorrect: false, word, suggestion: best.candidate };
|
|
411
|
+
}
|
|
372
412
|
return { isCorrect: false, word };
|
|
373
413
|
}
|
|
374
414
|
/**
|
|
375
415
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
416
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
417
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
418
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
419
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
420
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
376
421
|
*/
|
|
377
|
-
static async getDailyContent() {
|
|
378
|
-
if (this.isCacheEnabled && this.dailyContentCache)
|
|
422
|
+
static async getDailyContent(bypassCache = false) {
|
|
423
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
|
|
379
424
|
return this.dailyContentCache;
|
|
380
425
|
try {
|
|
381
426
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
@@ -383,7 +428,7 @@ var TDK = class {
|
|
|
383
428
|
});
|
|
384
429
|
if (response.ok) {
|
|
385
430
|
const data = await response.json();
|
|
386
|
-
if (this.isCacheEnabled)
|
|
431
|
+
if (!bypassCache && this.isCacheEnabled)
|
|
387
432
|
this.dailyContentCache = data;
|
|
388
433
|
return data;
|
|
389
434
|
}
|
|
@@ -424,10 +469,12 @@ var TDK = class {
|
|
|
424
469
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
425
470
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
426
471
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
427
|
-
* calls a second apart can return entirely different rules.
|
|
472
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
473
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
474
|
+
* draw even when `enableCache(true)` is on.
|
|
428
475
|
*/
|
|
429
|
-
static async getKurallar() {
|
|
430
|
-
const daily = await this.getDailyContent();
|
|
476
|
+
static async getKurallar(bypassCache = false) {
|
|
477
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
431
478
|
return daily?.kural ?? [];
|
|
432
479
|
}
|
|
433
480
|
/**
|
|
@@ -436,16 +483,19 @@ var TDK = class {
|
|
|
436
483
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
437
484
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
438
485
|
* draw would rarely match a given name — this re-draws (bounded, with a
|
|
439
|
-
* short delay) until it finds a match or gives up.
|
|
440
|
-
*
|
|
441
|
-
*
|
|
486
|
+
* short delay) until it finds a match or gives up. Every attempt bypasses
|
|
487
|
+
* `dailyContentCache` — without that, once `enableCache(true)` is on, all
|
|
488
|
+
* 25 attempts would just re-read the same cached `/icerik` response and
|
|
489
|
+
* could never find a rule outside whatever the first draw happened to be.
|
|
490
|
+
* Returns `null` if no match turns up within the attempt budget or the
|
|
491
|
+
* matched page can't be parsed.
|
|
442
492
|
*/
|
|
443
493
|
static async getRule(name) {
|
|
444
494
|
if (!name || name.trim() === "")
|
|
445
495
|
return null;
|
|
446
496
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
447
497
|
for (let attempt = 0; attempt < 25; attempt++) {
|
|
448
|
-
const rules = await this.getKurallar();
|
|
498
|
+
const rules = await this.getKurallar(true);
|
|
449
499
|
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
450
500
|
if (match)
|
|
451
501
|
return this.fetchRuleText(match.url);
|
package/dist/cli.js
CHANGED
|
@@ -144,23 +144,60 @@ var TDK = class {
|
|
|
144
144
|
return meanings;
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
147
|
+
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
148
|
+
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
149
|
+
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
150
|
+
* instead bundled directly into its main JS asset as a
|
|
151
|
+
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
152
|
+
* page to find that asset's current hashed filename, downloads it (a few
|
|
153
|
+
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
154
|
+
* scraping of an implementation detail — if TDK's build stops embedding
|
|
155
|
+
* this, this fails closed to `[]` rather than throwing.
|
|
156
|
+
*/
|
|
157
|
+
static async fetchAutocompleteData() {
|
|
158
|
+
try {
|
|
159
|
+
const homeResponse = await fetch(`${this.BASE_URL}/`, {
|
|
160
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
161
|
+
});
|
|
162
|
+
if (!homeResponse.ok)
|
|
163
|
+
return [];
|
|
164
|
+
const html = await homeResponse.text();
|
|
165
|
+
const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
|
|
166
|
+
if (!scriptMatch)
|
|
167
|
+
return [];
|
|
168
|
+
const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
|
|
169
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
170
|
+
});
|
|
171
|
+
if (!bundleResponse.ok)
|
|
172
|
+
return [];
|
|
173
|
+
const bundleJs = await bundleResponse.text();
|
|
174
|
+
const startMarker = 'JSON.parse(`[{"madde":';
|
|
175
|
+
const startIdx = bundleJs.indexOf(startMarker);
|
|
176
|
+
if (startIdx === -1)
|
|
177
|
+
return [];
|
|
178
|
+
const jsonStart = startIdx + "JSON.parse(".length + 1;
|
|
179
|
+
const jsonEnd = bundleJs.indexOf("`)", jsonStart);
|
|
180
|
+
if (jsonEnd === -1)
|
|
181
|
+
return [];
|
|
182
|
+
const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
|
|
183
|
+
return data.map((item) => item.madde).filter(Boolean);
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
190
|
+
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
191
|
+
* and cached once per process regardless of `enableCache()` — the same
|
|
192
|
+
* caching behavior as before — and only cleared by `clearCache()`.
|
|
148
193
|
*/
|
|
149
194
|
static async getSuggestions(prefix) {
|
|
195
|
+
if (!prefix || prefix.trim() === "")
|
|
196
|
+
return [];
|
|
150
197
|
if (this.autocompleteCache.length === 0) {
|
|
151
|
-
|
|
152
|
-
const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
|
|
153
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
154
|
-
});
|
|
155
|
-
if (response.ok) {
|
|
156
|
-
const data = await response.json();
|
|
157
|
-
this.autocompleteCache = data.map((item) => item.madde);
|
|
158
|
-
}
|
|
159
|
-
} catch (e) {
|
|
160
|
-
return [];
|
|
161
|
-
}
|
|
198
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
162
199
|
}
|
|
163
|
-
const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
|
|
200
|
+
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
164
201
|
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
165
202
|
}
|
|
166
203
|
/**
|
|
@@ -378,29 +415,37 @@ var TDK = class {
|
|
|
378
415
|
if (mixMatch) {
|
|
379
416
|
return { isCorrect: false, word: word2, suggestion: mixMatch.dogru };
|
|
380
417
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
418
|
+
}
|
|
419
|
+
if (this.autocompleteCache.length === 0) {
|
|
420
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
421
|
+
}
|
|
422
|
+
const cleanWord = word2.trim().toLocaleLowerCase("tr-TR");
|
|
423
|
+
let best = null;
|
|
424
|
+
for (const candidate of this.autocompleteCache) {
|
|
425
|
+
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
426
|
+
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;
|
|
395
432
|
}
|
|
396
433
|
}
|
|
434
|
+
if (best && best.distance <= 2) {
|
|
435
|
+
return { isCorrect: false, word: word2, suggestion: best.candidate };
|
|
436
|
+
}
|
|
397
437
|
return { isCorrect: false, word: word2 };
|
|
398
438
|
}
|
|
399
439
|
/**
|
|
400
440
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
441
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
442
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
443
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
444
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
445
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
401
446
|
*/
|
|
402
|
-
static async getDailyContent() {
|
|
403
|
-
if (this.isCacheEnabled && this.dailyContentCache)
|
|
447
|
+
static async getDailyContent(bypassCache = false) {
|
|
448
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
|
|
404
449
|
return this.dailyContentCache;
|
|
405
450
|
try {
|
|
406
451
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
@@ -408,7 +453,7 @@ var TDK = class {
|
|
|
408
453
|
});
|
|
409
454
|
if (response.ok) {
|
|
410
455
|
const data = await response.json();
|
|
411
|
-
if (this.isCacheEnabled)
|
|
456
|
+
if (!bypassCache && this.isCacheEnabled)
|
|
412
457
|
this.dailyContentCache = data;
|
|
413
458
|
return data;
|
|
414
459
|
}
|
|
@@ -449,10 +494,12 @@ var TDK = class {
|
|
|
449
494
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
450
495
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
451
496
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
452
|
-
* calls a second apart can return entirely different rules.
|
|
497
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
498
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
499
|
+
* draw even when `enableCache(true)` is on.
|
|
453
500
|
*/
|
|
454
|
-
static async getKurallar() {
|
|
455
|
-
const daily = await this.getDailyContent();
|
|
501
|
+
static async getKurallar(bypassCache = false) {
|
|
502
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
456
503
|
return daily?.kural ?? [];
|
|
457
504
|
}
|
|
458
505
|
/**
|
|
@@ -461,16 +508,19 @@ var TDK = class {
|
|
|
461
508
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
462
509
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
463
510
|
* draw would rarely match a given name — this re-draws (bounded, with a
|
|
464
|
-
* short delay) until it finds a match or gives up.
|
|
465
|
-
*
|
|
466
|
-
*
|
|
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.
|
|
467
517
|
*/
|
|
468
518
|
static async getRule(name) {
|
|
469
519
|
if (!name || name.trim() === "")
|
|
470
520
|
return null;
|
|
471
521
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
472
522
|
for (let attempt = 0; attempt < 25; attempt++) {
|
|
473
|
-
const rules = await this.getKurallar();
|
|
523
|
+
const rules = await this.getKurallar(true);
|
|
474
524
|
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
475
525
|
if (match)
|
|
476
526
|
return this.fetchRuleText(match.url);
|
|
@@ -741,7 +791,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
741
791
|
"kurallar",
|
|
742
792
|
"kural",
|
|
743
793
|
"karsilastir",
|
|
744
|
-
"analiz"
|
|
794
|
+
"analiz",
|
|
795
|
+
"oneri"
|
|
745
796
|
]);
|
|
746
797
|
var command = args[0];
|
|
747
798
|
var word = args.slice(1).join(" ");
|
|
@@ -767,7 +818,7 @@ async function run() {
|
|
|
767
818
|
if (!command || command === "--help" || command === "-h") {
|
|
768
819
|
console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
|
|
769
820
|
console.log(
|
|
770
|
-
"Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz"
|
|
821
|
+
"Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
|
|
771
822
|
);
|
|
772
823
|
console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
|
|
773
824
|
process.exit(command ? 0 : 1);
|
|
@@ -957,6 +1008,19 @@ async function run() {
|
|
|
957
1008
|
});
|
|
958
1009
|
break;
|
|
959
1010
|
}
|
|
1011
|
+
case "oneri": {
|
|
1012
|
+
if (!word)
|
|
1013
|
+
throw new Error("\xD6nek belirtmelisiniz.");
|
|
1014
|
+
const suggestions = await TDK.getSuggestions(word);
|
|
1015
|
+
printResult(suggestions, () => {
|
|
1016
|
+
if (suggestions.length === 0) {
|
|
1017
|
+
console.log("\xD6neri bulunamad\u0131.");
|
|
1018
|
+
} else {
|
|
1019
|
+
suggestions.forEach((s, i) => console.log(`${i + 1}. ${s}`));
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
960
1024
|
default:
|
|
961
1025
|
printError("Bilinmeyen komut.");
|
|
962
1026
|
}
|
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-6BTOGV2M.mjs";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
var rawArgs = process.argv.slice(2);
|
|
@@ -23,7 +23,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
23
23
|
"kurallar",
|
|
24
24
|
"kural",
|
|
25
25
|
"karsilastir",
|
|
26
|
-
"analiz"
|
|
26
|
+
"analiz",
|
|
27
|
+
"oneri"
|
|
27
28
|
]);
|
|
28
29
|
var command = args[0];
|
|
29
30
|
var word = args.slice(1).join(" ");
|
|
@@ -49,7 +50,7 @@ async function run() {
|
|
|
49
50
|
if (!command || command === "--help" || command === "-h") {
|
|
50
51
|
console.log("Kullan\u0131m: tdk [komut] <kelime> [--json]");
|
|
51
52
|
console.log(
|
|
52
|
-
"Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz"
|
|
53
|
+
"Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
|
|
53
54
|
);
|
|
54
55
|
console.log("Not: Komut belirtilmezse do\u011Frudan kelime anlam\u0131 aran\u0131r (\xF6rn: tdk selam)");
|
|
55
56
|
process.exit(command ? 0 : 1);
|
|
@@ -239,6 +240,19 @@ async function run() {
|
|
|
239
240
|
});
|
|
240
241
|
break;
|
|
241
242
|
}
|
|
243
|
+
case "oneri": {
|
|
244
|
+
if (!word)
|
|
245
|
+
throw new Error("\xD6nek belirtmelisiniz.");
|
|
246
|
+
const suggestions = await TDK.getSuggestions(word);
|
|
247
|
+
printResult(suggestions, () => {
|
|
248
|
+
if (suggestions.length === 0) {
|
|
249
|
+
console.log("\xD6neri bulunamad\u0131.");
|
|
250
|
+
} else {
|
|
251
|
+
suggestions.forEach((s, i) => console.log(`${i + 1}. ${s}`));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
242
256
|
default:
|
|
243
257
|
printError("Bilinmeyen komut.");
|
|
244
258
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -158,7 +158,22 @@ declare class TDK {
|
|
|
158
158
|
*/
|
|
159
159
|
static getMeanings(word: string): Promise<string[]>;
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
161
|
+
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
162
|
+
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
163
|
+
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
164
|
+
* instead bundled directly into its main JS asset as a
|
|
165
|
+
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
166
|
+
* page to find that asset's current hashed filename, downloads it (a few
|
|
167
|
+
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
168
|
+
* scraping of an implementation detail — if TDK's build stops embedding
|
|
169
|
+
* this, this fails closed to `[]` rather than throwing.
|
|
170
|
+
*/
|
|
171
|
+
private static fetchAutocompleteData;
|
|
172
|
+
/**
|
|
173
|
+
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
174
|
+
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
175
|
+
* and cached once per process regardless of `enableCache()` — the same
|
|
176
|
+
* caching behavior as before — and only cleared by `clearCache()`.
|
|
162
177
|
*/
|
|
163
178
|
static getSuggestions(prefix: string): Promise<string[]>;
|
|
164
179
|
/**
|
|
@@ -229,8 +244,13 @@ declare class TDK {
|
|
|
229
244
|
static checkSpelling(word: string): Promise<SpellCheckResult>;
|
|
230
245
|
/**
|
|
231
246
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
247
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
248
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
249
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
250
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
251
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
232
252
|
*/
|
|
233
|
-
static getDailyContent(): Promise<DailyContent | null>;
|
|
253
|
+
static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
|
|
234
254
|
/**
|
|
235
255
|
* Returns today's word of the day along with all of its listed meanings.
|
|
236
256
|
*/
|
|
@@ -245,18 +265,23 @@ declare class TDK {
|
|
|
245
265
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
246
266
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
247
267
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
248
|
-
* calls a second apart can return entirely different rules.
|
|
268
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
269
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
270
|
+
* draw even when `enableCache(true)` is on.
|
|
249
271
|
*/
|
|
250
|
-
static getKurallar(): Promise<TDKRule[]>;
|
|
272
|
+
static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
|
|
251
273
|
/**
|
|
252
274
|
* Fetches the full plain-text content of a named spelling rule (matched
|
|
253
275
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
254
276
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
255
277
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
256
278
|
* draw would rarely match a given name — this re-draws (bounded, with a
|
|
257
|
-
* short delay) until it finds a match or gives up.
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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.
|
|
260
285
|
*/
|
|
261
286
|
static getRule(name: string): Promise<string | null>;
|
|
262
287
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -158,7 +158,22 @@ declare class TDK {
|
|
|
158
158
|
*/
|
|
159
159
|
static getMeanings(word: string): Promise<string[]>;
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
161
|
+
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
162
|
+
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
163
|
+
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
164
|
+
* instead bundled directly into its main JS asset as a
|
|
165
|
+
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
166
|
+
* page to find that asset's current hashed filename, downloads it (a few
|
|
167
|
+
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
168
|
+
* scraping of an implementation detail — if TDK's build stops embedding
|
|
169
|
+
* this, this fails closed to `[]` rather than throwing.
|
|
170
|
+
*/
|
|
171
|
+
private static fetchAutocompleteData;
|
|
172
|
+
/**
|
|
173
|
+
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
174
|
+
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
175
|
+
* and cached once per process regardless of `enableCache()` — the same
|
|
176
|
+
* caching behavior as before — and only cleared by `clearCache()`.
|
|
162
177
|
*/
|
|
163
178
|
static getSuggestions(prefix: string): Promise<string[]>;
|
|
164
179
|
/**
|
|
@@ -229,8 +244,13 @@ declare class TDK {
|
|
|
229
244
|
static checkSpelling(word: string): Promise<SpellCheckResult>;
|
|
230
245
|
/**
|
|
231
246
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
247
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
248
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
249
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
250
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
251
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
232
252
|
*/
|
|
233
|
-
static getDailyContent(): Promise<DailyContent | null>;
|
|
253
|
+
static getDailyContent(bypassCache?: boolean): Promise<DailyContent | null>;
|
|
234
254
|
/**
|
|
235
255
|
* Returns today's word of the day along with all of its listed meanings.
|
|
236
256
|
*/
|
|
@@ -245,18 +265,23 @@ declare class TDK {
|
|
|
245
265
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
246
266
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
247
267
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
248
|
-
* calls a second apart can return entirely different rules.
|
|
268
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
269
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
270
|
+
* draw even when `enableCache(true)` is on.
|
|
249
271
|
*/
|
|
250
|
-
static getKurallar(): Promise<TDKRule[]>;
|
|
272
|
+
static getKurallar(bypassCache?: boolean): Promise<TDKRule[]>;
|
|
251
273
|
/**
|
|
252
274
|
* Fetches the full plain-text content of a named spelling rule (matched
|
|
253
275
|
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
254
276
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
255
277
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
256
278
|
* draw would rarely match a given name — this re-draws (bounded, with a
|
|
257
|
-
* short delay) until it finds a match or gives up.
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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.
|
|
260
285
|
*/
|
|
261
286
|
static getRule(name: string): Promise<string | null>;
|
|
262
287
|
/**
|
package/dist/index.js
CHANGED
|
@@ -158,23 +158,60 @@ var TDK = class {
|
|
|
158
158
|
return meanings;
|
|
159
159
|
}
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
161
|
+
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
162
|
+
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
163
|
+
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
164
|
+
* instead bundled directly into its main JS asset as a
|
|
165
|
+
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
166
|
+
* page to find that asset's current hashed filename, downloads it (a few
|
|
167
|
+
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
168
|
+
* scraping of an implementation detail — if TDK's build stops embedding
|
|
169
|
+
* this, this fails closed to `[]` rather than throwing.
|
|
170
|
+
*/
|
|
171
|
+
static async fetchAutocompleteData() {
|
|
172
|
+
try {
|
|
173
|
+
const homeResponse = await fetch(`${this.BASE_URL}/`, {
|
|
174
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
175
|
+
});
|
|
176
|
+
if (!homeResponse.ok)
|
|
177
|
+
return [];
|
|
178
|
+
const html = await homeResponse.text();
|
|
179
|
+
const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
|
|
180
|
+
if (!scriptMatch)
|
|
181
|
+
return [];
|
|
182
|
+
const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
|
|
183
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
184
|
+
});
|
|
185
|
+
if (!bundleResponse.ok)
|
|
186
|
+
return [];
|
|
187
|
+
const bundleJs = await bundleResponse.text();
|
|
188
|
+
const startMarker = 'JSON.parse(`[{"madde":';
|
|
189
|
+
const startIdx = bundleJs.indexOf(startMarker);
|
|
190
|
+
if (startIdx === -1)
|
|
191
|
+
return [];
|
|
192
|
+
const jsonStart = startIdx + "JSON.parse(".length + 1;
|
|
193
|
+
const jsonEnd = bundleJs.indexOf("`)", jsonStart);
|
|
194
|
+
if (jsonEnd === -1)
|
|
195
|
+
return [];
|
|
196
|
+
const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd));
|
|
197
|
+
return data.map((item) => item.madde).filter(Boolean);
|
|
198
|
+
} catch {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
204
|
+
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
205
|
+
* and cached once per process regardless of `enableCache()` — the same
|
|
206
|
+
* caching behavior as before — and only cleared by `clearCache()`.
|
|
162
207
|
*/
|
|
163
208
|
static async getSuggestions(prefix) {
|
|
209
|
+
if (!prefix || prefix.trim() === "")
|
|
210
|
+
return [];
|
|
164
211
|
if (this.autocompleteCache.length === 0) {
|
|
165
|
-
|
|
166
|
-
const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
|
|
167
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
168
|
-
});
|
|
169
|
-
if (response.ok) {
|
|
170
|
-
const data = await response.json();
|
|
171
|
-
this.autocompleteCache = data.map((item) => item.madde);
|
|
172
|
-
}
|
|
173
|
-
} catch (e) {
|
|
174
|
-
return [];
|
|
175
|
-
}
|
|
212
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
176
213
|
}
|
|
177
|
-
const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
|
|
214
|
+
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
178
215
|
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
179
216
|
}
|
|
180
217
|
/**
|
|
@@ -392,29 +429,37 @@ var TDK = class {
|
|
|
392
429
|
if (mixMatch) {
|
|
393
430
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
394
431
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
432
|
+
}
|
|
433
|
+
if (this.autocompleteCache.length === 0) {
|
|
434
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
435
|
+
}
|
|
436
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
437
|
+
let best = null;
|
|
438
|
+
for (const candidate of this.autocompleteCache) {
|
|
439
|
+
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR"))
|
|
440
|
+
continue;
|
|
441
|
+
const distance = this.levenshtein(cleanWord, candidate);
|
|
442
|
+
if (distance > 0 && (!best || distance < best.distance)) {
|
|
443
|
+
best = { candidate, distance };
|
|
444
|
+
if (distance === 1)
|
|
445
|
+
break;
|
|
409
446
|
}
|
|
410
447
|
}
|
|
448
|
+
if (best && best.distance <= 2) {
|
|
449
|
+
return { isCorrect: false, word, suggestion: best.candidate };
|
|
450
|
+
}
|
|
411
451
|
return { isCorrect: false, word };
|
|
412
452
|
}
|
|
413
453
|
/**
|
|
414
454
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
455
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
456
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
457
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
458
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
459
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
415
460
|
*/
|
|
416
|
-
static async getDailyContent() {
|
|
417
|
-
if (this.isCacheEnabled && this.dailyContentCache)
|
|
461
|
+
static async getDailyContent(bypassCache = false) {
|
|
462
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache)
|
|
418
463
|
return this.dailyContentCache;
|
|
419
464
|
try {
|
|
420
465
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
@@ -422,7 +467,7 @@ var TDK = class {
|
|
|
422
467
|
});
|
|
423
468
|
if (response.ok) {
|
|
424
469
|
const data = await response.json();
|
|
425
|
-
if (this.isCacheEnabled)
|
|
470
|
+
if (!bypassCache && this.isCacheEnabled)
|
|
426
471
|
this.dailyContentCache = data;
|
|
427
472
|
return data;
|
|
428
473
|
}
|
|
@@ -463,10 +508,12 @@ var TDK = class {
|
|
|
463
508
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
464
509
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
465
510
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
466
|
-
* calls a second apart can return entirely different rules.
|
|
511
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
512
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
513
|
+
* draw even when `enableCache(true)` is on.
|
|
467
514
|
*/
|
|
468
|
-
static async getKurallar() {
|
|
469
|
-
const daily = await this.getDailyContent();
|
|
515
|
+
static async getKurallar(bypassCache = false) {
|
|
516
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
470
517
|
return daily?.kural ?? [];
|
|
471
518
|
}
|
|
472
519
|
/**
|
|
@@ -475,16 +522,19 @@ var TDK = class {
|
|
|
475
522
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
476
523
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
477
524
|
* draw would rarely match a given name — this re-draws (bounded, with a
|
|
478
|
-
* short delay) until it finds a match or gives up.
|
|
479
|
-
*
|
|
480
|
-
*
|
|
525
|
+
* short delay) until it finds a match or gives up. Every attempt bypasses
|
|
526
|
+
* `dailyContentCache` — without that, once `enableCache(true)` is on, all
|
|
527
|
+
* 25 attempts would just re-read the same cached `/icerik` response and
|
|
528
|
+
* could never find a rule outside whatever the first draw happened to be.
|
|
529
|
+
* Returns `null` if no match turns up within the attempt budget or the
|
|
530
|
+
* matched page can't be parsed.
|
|
481
531
|
*/
|
|
482
532
|
static async getRule(name) {
|
|
483
533
|
if (!name || name.trim() === "")
|
|
484
534
|
return null;
|
|
485
535
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
486
536
|
for (let attempt = 0; attempt < 25; attempt++) {
|
|
487
|
-
const rules = await this.getKurallar();
|
|
537
|
+
const rules = await this.getKurallar(true);
|
|
488
538
|
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
489
539
|
if (match)
|
|
490
540
|
return this.fetchRuleText(match.url);
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -21,6 +21,7 @@ const KNOWN_COMMANDS = new Set([
|
|
|
21
21
|
"kural",
|
|
22
22
|
"karsilastir",
|
|
23
23
|
"analiz",
|
|
24
|
+
"oneri",
|
|
24
25
|
]);
|
|
25
26
|
|
|
26
27
|
let command = args[0];
|
|
@@ -51,7 +52,7 @@ async function run() {
|
|
|
51
52
|
if (!command || command === "--help" || command === "-h") {
|
|
52
53
|
console.log("Kullanım: tdk [komut] <kelime> [--json]");
|
|
53
54
|
console.log(
|
|
54
|
-
"Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz"
|
|
55
|
+
"Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri"
|
|
55
56
|
);
|
|
56
57
|
console.log("Not: Komut belirtilmezse doğrudan kelime anlamı aranır (örn: tdk selam)");
|
|
57
58
|
process.exit(command ? 0 : 1);
|
|
@@ -245,6 +246,19 @@ async function run() {
|
|
|
245
246
|
break;
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
case "oneri": {
|
|
250
|
+
if (!word) throw new Error("Önek belirtmelisiniz.");
|
|
251
|
+
const suggestions = await TDK.getSuggestions(word);
|
|
252
|
+
printResult(suggestions, () => {
|
|
253
|
+
if (suggestions.length === 0) {
|
|
254
|
+
console.log("Öneri bulunamadı.");
|
|
255
|
+
} else {
|
|
256
|
+
suggestions.forEach((s, i) => console.log(`${i + 1}. ${s}`));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
default:
|
|
249
263
|
printError("Bilinmeyen komut.");
|
|
250
264
|
}
|
package/src/tdk.ts
CHANGED
|
@@ -119,24 +119,61 @@ export class TDK {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
*
|
|
122
|
+
* `sozluk.gov.tr`'s dedicated `/autocomplete.json` (and `/data/autocomplete.json`)
|
|
123
|
+
* routes no longer serve JSON — they fall through to the SPA's `index.html`.
|
|
124
|
+
* The full ~81k-word headword list the site's own autocomplete UI uses is
|
|
125
|
+
* instead bundled directly into its main JS asset as a
|
|
126
|
+
* `JSON.parse(\`[{"madde":"..."}]\`)` literal, so this fetches the home
|
|
127
|
+
* page to find that asset's current hashed filename, downloads it (a few
|
|
128
|
+
* MB, only once per process), and extracts the literal out of it. Fragile
|
|
129
|
+
* scraping of an implementation detail — if TDK's build stops embedding
|
|
130
|
+
* this, this fails closed to `[]` rather than throwing.
|
|
131
|
+
*/
|
|
132
|
+
private static async fetchAutocompleteData(): Promise<string[]> {
|
|
133
|
+
try {
|
|
134
|
+
const homeResponse = await fetch(`${this.BASE_URL}/`, {
|
|
135
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
|
|
136
|
+
});
|
|
137
|
+
if (!homeResponse.ok) return [];
|
|
138
|
+
const html = await homeResponse.text();
|
|
139
|
+
|
|
140
|
+
const scriptMatch = html.match(/src="(\/assets\/index-[^"]+\.js)"/);
|
|
141
|
+
if (!scriptMatch) return [];
|
|
142
|
+
|
|
143
|
+
const bundleResponse = await fetch(`${this.BASE_URL}${scriptMatch[1]}`, {
|
|
144
|
+
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
|
|
145
|
+
});
|
|
146
|
+
if (!bundleResponse.ok) return [];
|
|
147
|
+
const bundleJs = await bundleResponse.text();
|
|
148
|
+
|
|
149
|
+
const startMarker = 'JSON.parse(`[{"madde":';
|
|
150
|
+
const startIdx = bundleJs.indexOf(startMarker);
|
|
151
|
+
if (startIdx === -1) return [];
|
|
152
|
+
const jsonStart = startIdx + "JSON.parse(".length + 1;
|
|
153
|
+
const jsonEnd = bundleJs.indexOf("`)", jsonStart);
|
|
154
|
+
if (jsonEnd === -1) return [];
|
|
155
|
+
|
|
156
|
+
const data = JSON.parse(bundleJs.slice(jsonStart, jsonEnd)) as { madde: string }[];
|
|
157
|
+
return data.map((item) => item.madde).filter(Boolean);
|
|
158
|
+
} catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns autocomplete suggestions for a given prefix, searched over TDK's
|
|
165
|
+
* full headword list (see `fetchAutocompleteData`). The list is fetched
|
|
166
|
+
* and cached once per process regardless of `enableCache()` — the same
|
|
167
|
+
* caching behavior as before — and only cleared by `clearCache()`.
|
|
123
168
|
*/
|
|
124
169
|
public static async getSuggestions(prefix: string): Promise<string[]> {
|
|
170
|
+
if (!prefix || prefix.trim() === "") return [];
|
|
171
|
+
|
|
125
172
|
if (this.autocompleteCache.length === 0) {
|
|
126
|
-
|
|
127
|
-
const response = await fetch(`${this.BASE_URL}/autocomplete.json`, {
|
|
128
|
-
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
|
|
129
|
-
});
|
|
130
|
-
if (response.ok) {
|
|
131
|
-
const data = await response.json() as { madde: string }[];
|
|
132
|
-
this.autocompleteCache = data.map(item => item.madde);
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
173
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
137
174
|
}
|
|
138
|
-
|
|
139
|
-
const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
|
|
175
|
+
|
|
176
|
+
const cleanPrefix = prefix.trim().toLocaleLowerCase("tr-TR");
|
|
140
177
|
return this.autocompleteCache
|
|
141
178
|
.filter(w => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix))
|
|
142
179
|
.slice(0, 10);
|
|
@@ -352,8 +389,10 @@ export class TDK {
|
|
|
352
389
|
if (results.length > 0) {
|
|
353
390
|
return { isCorrect: true, word };
|
|
354
391
|
}
|
|
355
|
-
|
|
356
|
-
// 2. If not, check "sıkça yapılan yanlışlar" from DailyContent
|
|
392
|
+
|
|
393
|
+
// 2. If not, check "sıkça yapılan yanlışlar" from DailyContent — an exact
|
|
394
|
+
// match here is TDK explicitly saying "X is often confused with Y", so
|
|
395
|
+
// it's authoritative when it hits (but only 2-3 rotating entries per call).
|
|
357
396
|
const daily = await this.getDailyContent();
|
|
358
397
|
if (daily) {
|
|
359
398
|
const syydMatch = daily.syyd.find(s => s.yanliskelime.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
|
|
@@ -364,43 +403,49 @@ export class TDK {
|
|
|
364
403
|
if (mixMatch) {
|
|
365
404
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
366
405
|
}
|
|
406
|
+
}
|
|
367
407
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
if (best && best.distance <= 2) {
|
|
385
|
-
return { isCorrect: false, word, suggestion: best.candidate };
|
|
408
|
+
// 3. No exact match in TDK's curated lists: fall back to the closest
|
|
409
|
+
// headword (by edit distance) across TDK's full ~81k-word list (the same
|
|
410
|
+
// data `getSuggestions()` uses). Restricted to single-token, lowercase
|
|
411
|
+
// headwords so it doesn't suggest compounds/phrases or proper nouns.
|
|
412
|
+
if (this.autocompleteCache.length === 0) {
|
|
413
|
+
this.autocompleteCache = await this.fetchAutocompleteData();
|
|
414
|
+
}
|
|
415
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
416
|
+
let best: { candidate: string; distance: number } | null = null;
|
|
417
|
+
for (const candidate of this.autocompleteCache) {
|
|
418
|
+
if (candidate.includes(" ") || candidate !== candidate.toLocaleLowerCase("tr-TR")) continue;
|
|
419
|
+
const distance = this.levenshtein(cleanWord, candidate);
|
|
420
|
+
if (distance > 0 && (!best || distance < best.distance)) {
|
|
421
|
+
best = { candidate, distance };
|
|
422
|
+
if (distance === 1) break;
|
|
386
423
|
}
|
|
387
424
|
}
|
|
425
|
+
if (best && best.distance <= 2) {
|
|
426
|
+
return { isCorrect: false, word, suggestion: best.candidate };
|
|
427
|
+
}
|
|
388
428
|
return { isCorrect: false, word };
|
|
389
429
|
}
|
|
390
430
|
|
|
391
431
|
/**
|
|
392
432
|
* Fetches daily content (word of the day, proverbs, rules, etc).
|
|
433
|
+
* `bypassCache` skips both reading and writing `dailyContentCache` even
|
|
434
|
+
* when `enableCache(true)` is on — used by `getRule()`'s retry loop, which
|
|
435
|
+
* needs a fresh random `/icerik` draw on every attempt; without it, once
|
|
436
|
+
* caching is enabled the loop would just re-read the same cached response
|
|
437
|
+
* 25 times and could never find a rule outside that first random draw.
|
|
393
438
|
*/
|
|
394
|
-
public static async getDailyContent(): Promise<DailyContent | null> {
|
|
395
|
-
if (this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
|
|
396
|
-
|
|
439
|
+
public static async getDailyContent(bypassCache = false): Promise<DailyContent | null> {
|
|
440
|
+
if (!bypassCache && this.isCacheEnabled && this.dailyContentCache) return this.dailyContentCache;
|
|
441
|
+
|
|
397
442
|
try {
|
|
398
443
|
const response = await fetch(`${this.BASE_URL}/icerik`, {
|
|
399
444
|
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
|
|
400
445
|
});
|
|
401
446
|
if (response.ok) {
|
|
402
447
|
const data = await response.json() as DailyContent;
|
|
403
|
-
if (this.isCacheEnabled) this.dailyContentCache = data;
|
|
448
|
+
if (!bypassCache && this.isCacheEnabled) this.dailyContentCache = data;
|
|
404
449
|
return data;
|
|
405
450
|
}
|
|
406
451
|
} catch {
|
|
@@ -443,10 +488,12 @@ export class TDK {
|
|
|
443
488
|
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
444
489
|
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
445
490
|
* appears to hand back a single randomly-rotated rule per request, so two
|
|
446
|
-
* calls a second apart can return entirely different rules.
|
|
491
|
+
* calls a second apart can return entirely different rules. `bypassCache`
|
|
492
|
+
* (used internally by `getRule()`'s retry loop) forces a fresh `/icerik`
|
|
493
|
+
* draw even when `enableCache(true)` is on.
|
|
447
494
|
*/
|
|
448
|
-
public static async getKurallar(): Promise<TDKRule[]> {
|
|
449
|
-
const daily = await this.getDailyContent();
|
|
495
|
+
public static async getKurallar(bypassCache = false): Promise<TDKRule[]> {
|
|
496
|
+
const daily = await this.getDailyContent(bypassCache);
|
|
450
497
|
return daily?.kural ?? [];
|
|
451
498
|
}
|
|
452
499
|
|
|
@@ -456,16 +503,19 @@ export class TDK {
|
|
|
456
503
|
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
457
504
|
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
458
505
|
* draw would rarely match a given name — this re-draws (bounded, with a
|
|
459
|
-
* short delay) until it finds a match or gives up.
|
|
460
|
-
*
|
|
461
|
-
*
|
|
506
|
+
* short delay) until it finds a match or gives up. Every attempt bypasses
|
|
507
|
+
* `dailyContentCache` — without that, once `enableCache(true)` is on, all
|
|
508
|
+
* 25 attempts would just re-read the same cached `/icerik` response and
|
|
509
|
+
* could never find a rule outside whatever the first draw happened to be.
|
|
510
|
+
* Returns `null` if no match turns up within the attempt budget or the
|
|
511
|
+
* matched page can't be parsed.
|
|
462
512
|
*/
|
|
463
513
|
public static async getRule(name: string): Promise<string | null> {
|
|
464
514
|
if (!name || name.trim() === "") return null;
|
|
465
515
|
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
466
516
|
|
|
467
517
|
for (let attempt = 0; attempt < 25; attempt++) {
|
|
468
|
-
const rules = await this.getKurallar();
|
|
518
|
+
const rules = await this.getKurallar(true);
|
|
469
519
|
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
470
520
|
if (match) return this.fetchRuleText(match.url);
|
|
471
521
|
await this.delay(100);
|