tdk-api-wrapper 1.0.1 → 1.1.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 +51 -5
- package/dist/chunk-P3GX7I53.mjs +704 -0
- package/dist/cli.js +620 -73
- package/dist/cli.mjs +207 -36
- package/dist/index.d.mts +157 -4
- package/dist/index.d.ts +157 -4
- package/dist/index.js +422 -40
- package/dist/index.mjs +9 -3
- package/package.json +1 -1
- package/src/cli.ts +213 -36
- package/src/errors.ts +38 -0
- package/src/index.ts +1 -0
- package/src/tdk.ts +399 -48
- package/src/types.ts +36 -0
- package/dist/chunk-MGSXCUAX.mjs +0 -325
package/dist/index.js
CHANGED
|
@@ -30,16 +30,48 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
-
TDK: () => TDK
|
|
33
|
+
TDK: () => TDK,
|
|
34
|
+
TDKError: () => TDKError,
|
|
35
|
+
TDKNetworkError: () => TDKNetworkError,
|
|
36
|
+
TDKValidationError: () => TDKValidationError
|
|
34
37
|
});
|
|
35
38
|
module.exports = __toCommonJS(src_exports);
|
|
36
39
|
|
|
40
|
+
// src/errors.ts
|
|
41
|
+
var TDKError = class extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "TDKError";
|
|
45
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var TDKValidationError = class extends TDKError {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "TDKValidationError";
|
|
52
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var TDKNetworkError = class extends TDKError {
|
|
56
|
+
status;
|
|
57
|
+
cause;
|
|
58
|
+
constructor(message, options) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "TDKNetworkError";
|
|
61
|
+
this.status = options?.status;
|
|
62
|
+
this.cause = options?.cause;
|
|
63
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
37
67
|
// src/tdk.ts
|
|
38
68
|
var fs = __toESM(require("fs"));
|
|
39
69
|
var path = __toESM(require("path"));
|
|
40
70
|
var os = __toESM(require("os"));
|
|
71
|
+
var https = __toESM(require("https"));
|
|
41
72
|
var TDK = class {
|
|
42
73
|
static BASE_URL = "https://sozluk.gov.tr";
|
|
74
|
+
static AUDIO_API_HOST = "api.sozluk.gov.tr";
|
|
43
75
|
// Cache Mechanism
|
|
44
76
|
static isCacheEnabled = false;
|
|
45
77
|
static wordCache = /* @__PURE__ */ new Map();
|
|
@@ -70,35 +102,42 @@ var TDK = class {
|
|
|
70
102
|
*/
|
|
71
103
|
static async getWord(word) {
|
|
72
104
|
if (!word || word.trim() === "") {
|
|
73
|
-
throw new
|
|
105
|
+
throw new TDKValidationError("Word parameter cannot be empty.");
|
|
74
106
|
}
|
|
75
|
-
const cleanWord = word.trim().
|
|
107
|
+
const cleanWord = word.trim().toLocaleLowerCase("tr-TR");
|
|
76
108
|
if (this.isCacheEnabled && this.wordCache.has(cleanWord)) {
|
|
77
109
|
return this.wordCache.get(cleanWord);
|
|
78
110
|
}
|
|
79
111
|
const url = `${this.BASE_URL}/gts?ara=${encodeURIComponent(cleanWord)}`;
|
|
112
|
+
let response;
|
|
80
113
|
try {
|
|
81
|
-
|
|
114
|
+
response = await fetch(url, {
|
|
82
115
|
headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" }
|
|
83
116
|
});
|
|
84
|
-
if (!response.ok)
|
|
85
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
86
|
-
const data = await response.json();
|
|
87
|
-
if (!Array.isArray(data) && data && "error" in data) {
|
|
88
|
-
if (this.isCacheEnabled)
|
|
89
|
-
this.wordCache.set(cleanWord, []);
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
const results = data;
|
|
93
|
-
if (this.isCacheEnabled) {
|
|
94
|
-
this.wordCache.set(cleanWord, results);
|
|
95
|
-
}
|
|
96
|
-
return results;
|
|
97
117
|
} catch (error) {
|
|
98
|
-
|
|
99
|
-
throw new Error(`Failed to fetch word from TDK: ${error.message}`);
|
|
100
|
-
throw new Error("Failed to fetch word from TDK: Unknown error");
|
|
118
|
+
throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
|
|
101
119
|
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new TDKNetworkError(`Failed to fetch word from TDK: HTTP ${response.status}.`, {
|
|
122
|
+
status: response.status
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
let data;
|
|
126
|
+
try {
|
|
127
|
+
data = await response.json();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new TDKNetworkError("Failed to fetch word from TDK: invalid JSON response.", { cause: error });
|
|
130
|
+
}
|
|
131
|
+
if (!Array.isArray(data) && data && "error" in data) {
|
|
132
|
+
if (this.isCacheEnabled)
|
|
133
|
+
this.wordCache.set(cleanWord, []);
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const results = data;
|
|
137
|
+
if (this.isCacheEnabled) {
|
|
138
|
+
this.wordCache.set(cleanWord, results);
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
102
141
|
}
|
|
103
142
|
/**
|
|
104
143
|
* Helper method to get only the meanings (definitions) of a word as a string array.
|
|
@@ -135,8 +174,8 @@ var TDK = class {
|
|
|
135
174
|
return [];
|
|
136
175
|
}
|
|
137
176
|
}
|
|
138
|
-
const cleanPrefix = prefix.
|
|
139
|
-
return this.autocompleteCache.filter((w) => w.
|
|
177
|
+
const cleanPrefix = prefix.toLocaleLowerCase("tr-TR");
|
|
178
|
+
return this.autocompleteCache.filter((w) => w.toLocaleLowerCase("tr-TR").startsWith(cleanPrefix)).slice(0, 10);
|
|
140
179
|
}
|
|
141
180
|
/**
|
|
142
181
|
* Returns a list of proverbs and idioms containing the word.
|
|
@@ -157,14 +196,40 @@ var TDK = class {
|
|
|
157
196
|
return proverbs;
|
|
158
197
|
}
|
|
159
198
|
/**
|
|
160
|
-
* Returns the etymological origin of the word
|
|
199
|
+
* Returns the etymological origin of the word, or "Türkçe" if TDK doesn't
|
|
200
|
+
* record a foreign origin for it. Returns `null` only when the word itself
|
|
201
|
+
* isn't found in the dictionary at all.
|
|
161
202
|
*/
|
|
162
203
|
static async getOrigin(word) {
|
|
163
204
|
const results = await this.getWord(word);
|
|
164
|
-
if (results.length
|
|
165
|
-
return
|
|
205
|
+
if (results.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
return results[0].lisan || "T\xFCrk\xE7e";
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Returns whether the word has a recorded foreign etymological origin.
|
|
211
|
+
* Returns `null` (instead of a boolean) when the word isn't found at all.
|
|
212
|
+
*/
|
|
213
|
+
static async isForeignWord(word) {
|
|
214
|
+
const origin = await this.getOrigin(word);
|
|
215
|
+
if (origin === null)
|
|
216
|
+
return null;
|
|
217
|
+
return origin !== "T\xFCrk\xE7e";
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Groups a list of words by their etymological origin. Words not found in
|
|
221
|
+
* the dictionary are grouped under "Bilinmiyor". Throttled like getWordsBatch.
|
|
222
|
+
*/
|
|
223
|
+
static async groupByOrigin(words) {
|
|
224
|
+
const groups = {};
|
|
225
|
+
for (const word of words) {
|
|
226
|
+
const origin = await this.getOrigin(word) ?? "Bilinmiyor";
|
|
227
|
+
if (!groups[origin])
|
|
228
|
+
groups[origin] = [];
|
|
229
|
+
groups[origin].push(word);
|
|
230
|
+
await this.delay(200);
|
|
166
231
|
}
|
|
167
|
-
return
|
|
232
|
+
return groups;
|
|
168
233
|
}
|
|
169
234
|
/**
|
|
170
235
|
* Returns literature examples containing the word.
|
|
@@ -187,15 +252,108 @@ var TDK = class {
|
|
|
187
252
|
return examples;
|
|
188
253
|
}
|
|
189
254
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
255
|
+
* Calls the `api.sozluk.gov.tr/gts-yeni` endpoint the official web UI uses
|
|
256
|
+
* internally (richer than the public `/gts`: includes `seskod`,
|
|
257
|
+
* `anlamEsAnlam`/`anlamKarsitAnlam`, etc). That endpoint 403s unless the
|
|
258
|
+
* request looks like it came from a browser tab on sozluk.gov.tr: it needs
|
|
259
|
+
* an `Origin`/`Referer` pair matching that site AND a browser-like
|
|
260
|
+
* `User-Agent` (our usual `TDK-API-Nodejs-Wrapper/…` UA gets rejected).
|
|
261
|
+
* `fetch` (undici) also strips a manually-set `Origin` header as a
|
|
262
|
+
* forbidden header name, so this uses `node:https` directly instead.
|
|
263
|
+
* This is inherently fragile scraping of an undocumented endpoint — if
|
|
264
|
+
* TDK tightens this check further, this should fail closed to `null`
|
|
265
|
+
* rather than throw.
|
|
266
|
+
*/
|
|
267
|
+
static fetchGtsYeni(word) {
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
const req = https.request(
|
|
270
|
+
{
|
|
271
|
+
hostname: this.AUDIO_API_HOST,
|
|
272
|
+
path: `/gts-yeni?ara=${encodeURIComponent(word)}`,
|
|
273
|
+
method: "GET",
|
|
274
|
+
headers: {
|
|
275
|
+
"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",
|
|
276
|
+
Origin: this.BASE_URL,
|
|
277
|
+
Referer: `${this.BASE_URL}/`
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
(res) => {
|
|
281
|
+
let body = "";
|
|
282
|
+
res.on("data", (chunk) => body += chunk);
|
|
283
|
+
res.on("end", () => {
|
|
284
|
+
try {
|
|
285
|
+
const data = JSON.parse(body);
|
|
286
|
+
resolve(Array.isArray(data) ? data : null);
|
|
287
|
+
} catch {
|
|
288
|
+
resolve(null);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
req.on("error", () => resolve(null));
|
|
294
|
+
req.end();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
static async fetchSeskod(word) {
|
|
298
|
+
const data = await this.fetchGtsYeni(word);
|
|
299
|
+
const seskod = data?.[0]?.seskod;
|
|
300
|
+
return seskod ? String(seskod) : null;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Returns synonyms ("eş anlamlı kelimeler") recorded for the word, pooled
|
|
304
|
+
* across all of its meanings. Uses the same undocumented `gts-yeni`
|
|
305
|
+
* endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
|
|
306
|
+
*/
|
|
307
|
+
static async getSynonyms(word) {
|
|
308
|
+
if (!word || word.trim() === "")
|
|
309
|
+
return [];
|
|
310
|
+
const data = await this.fetchGtsYeni(word.trim().toLocaleLowerCase("tr-TR"));
|
|
311
|
+
if (!data)
|
|
312
|
+
return [];
|
|
313
|
+
const synonyms = [];
|
|
314
|
+
for (const entry of data) {
|
|
315
|
+
for (const anlam of entry.anlamlarListe ?? []) {
|
|
316
|
+
for (const es of anlam.anlamEsAnlam ?? []) {
|
|
317
|
+
if (es.deger)
|
|
318
|
+
synonyms.push(es.deger);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return [...new Set(synonyms)];
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Returns antonyms ("zıt anlamlı kelimeler") recorded for the word, pooled
|
|
326
|
+
* across all of its meanings. Uses the same undocumented `gts-yeni`
|
|
327
|
+
* endpoint as `getAudioUrl` — returns `[]` if the lookup fails.
|
|
328
|
+
*/
|
|
329
|
+
static async getAntonyms(word) {
|
|
330
|
+
if (!word || word.trim() === "")
|
|
331
|
+
return [];
|
|
332
|
+
const data = await this.fetchGtsYeni(word.trim().toLocaleLowerCase("tr-TR"));
|
|
333
|
+
if (!data)
|
|
334
|
+
return [];
|
|
335
|
+
const antonyms = [];
|
|
336
|
+
for (const entry of data) {
|
|
337
|
+
for (const anlam of entry.anlamlarListe ?? []) {
|
|
338
|
+
for (const ka of anlam.anlamKarsitAnlam ?? []) {
|
|
339
|
+
if (ka.deger)
|
|
340
|
+
antonyms.push(ka.deger);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return [...new Set(antonyms)];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Returns the direct URL of the audio pronunciation, if TDK has one recorded for this word.
|
|
192
348
|
*/
|
|
193
349
|
static async getAudioUrl(word) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return `https://sozluk.gov.tr/ses/${encodeURIComponent(word)}.wav`;
|
|
350
|
+
if (!word || word.trim() === "") {
|
|
351
|
+
throw new TDKValidationError("Word parameter cannot be empty.");
|
|
197
352
|
}
|
|
198
|
-
|
|
353
|
+
const seskod = await this.fetchSeskod(word.trim().toLocaleLowerCase("tr-TR"));
|
|
354
|
+
if (!seskod)
|
|
355
|
+
return null;
|
|
356
|
+
return `https://${this.AUDIO_API_HOST}/ses/${encodeURIComponent(seskod)}.wav`;
|
|
199
357
|
}
|
|
200
358
|
/**
|
|
201
359
|
* Downloads the audio pronunciation to the specified path.
|
|
@@ -226,14 +384,29 @@ var TDK = class {
|
|
|
226
384
|
}
|
|
227
385
|
const daily = await this.getDailyContent();
|
|
228
386
|
if (daily) {
|
|
229
|
-
const syydMatch = daily.syyd.find((s) => s.yanliskelime.
|
|
387
|
+
const syydMatch = daily.syyd.find((s) => s.yanliskelime.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
|
|
230
388
|
if (syydMatch) {
|
|
231
389
|
return { isCorrect: false, word, suggestion: syydMatch.dogrukelime };
|
|
232
390
|
}
|
|
233
|
-
const mixMatch = daily.karistirma.find((s) => s.yanlis.
|
|
391
|
+
const mixMatch = daily.karistirma.find((s) => s.yanlis.toLocaleLowerCase("tr-TR") === word.toLocaleLowerCase("tr-TR"));
|
|
234
392
|
if (mixMatch) {
|
|
235
393
|
return { isCorrect: false, word, suggestion: mixMatch.dogru };
|
|
236
394
|
}
|
|
395
|
+
const candidates = [
|
|
396
|
+
...daily.syyd.map((s) => s.dogrukelime),
|
|
397
|
+
...daily.karistirma.flatMap((s) => [s.yanlis, s.dogru]),
|
|
398
|
+
...daily.kelime.map((k) => k.madde)
|
|
399
|
+
];
|
|
400
|
+
let best = null;
|
|
401
|
+
for (const candidate of candidates) {
|
|
402
|
+
const distance = this.levenshtein(word.toLocaleLowerCase("tr-TR"), candidate.toLocaleLowerCase("tr-TR"));
|
|
403
|
+
if (distance > 0 && (!best || distance < best.distance)) {
|
|
404
|
+
best = { candidate, distance };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (best && best.distance <= 2) {
|
|
408
|
+
return { isCorrect: false, word, suggestion: best.candidate };
|
|
409
|
+
}
|
|
237
410
|
}
|
|
238
411
|
return { isCorrect: false, word };
|
|
239
412
|
}
|
|
@@ -258,6 +431,94 @@ var TDK = class {
|
|
|
258
431
|
}
|
|
259
432
|
return null;
|
|
260
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Returns today's word of the day along with all of its listed meanings.
|
|
436
|
+
*/
|
|
437
|
+
static async getWordOfTheDay() {
|
|
438
|
+
const daily = await this.getDailyContent();
|
|
439
|
+
if (!daily || daily.kelime.length === 0)
|
|
440
|
+
return null;
|
|
441
|
+
const word = daily.kelime[0].madde;
|
|
442
|
+
const meanings = daily.kelime.filter((k) => k.madde === word).map((k) => k.anlam);
|
|
443
|
+
return { word, meanings };
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Picks a random entry (word or proverb) from today's daily content.
|
|
447
|
+
* Note: this samples from today's `getDailyContent()` picks, not the full dictionary.
|
|
448
|
+
*/
|
|
449
|
+
static async getRandomWord() {
|
|
450
|
+
const daily = await this.getDailyContent();
|
|
451
|
+
if (!daily)
|
|
452
|
+
return null;
|
|
453
|
+
const pool = [
|
|
454
|
+
...daily.kelime.map((k) => ({ type: "kelime", madde: k.madde, anlam: k.anlam })),
|
|
455
|
+
...daily.atasoz.map((a) => ({ type: "atasoz", madde: a.madde, anlam: a.anlam }))
|
|
456
|
+
];
|
|
457
|
+
if (pool.length === 0)
|
|
458
|
+
return null;
|
|
459
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Returns the spelling-rule page(s) ("yazım kuralları") linked from TDK's
|
|
463
|
+
* `/icerik` daily-content feed, e.g. `{ adi: "Kısaltmalar", url: "https://..." }`.
|
|
464
|
+
* Note: like `getRandomWord()`, this is NOT a fixed catalog — `/icerik`
|
|
465
|
+
* appears to hand back a single randomly-rotated rule per request, so two
|
|
466
|
+
* calls a second apart can return entirely different rules.
|
|
467
|
+
*/
|
|
468
|
+
static async getKurallar() {
|
|
469
|
+
const daily = await this.getDailyContent();
|
|
470
|
+
return daily?.kural ?? [];
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Fetches the full plain-text content of a named spelling rule (matched
|
|
474
|
+
* case-insensitively, substring match) from `tdk.gov.tr`. Since `/icerik`
|
|
475
|
+
* hands back a single randomly-rotated rule per request (out of a pool of
|
|
476
|
+
* roughly twenty) rather than a fixed catalog, a single `getKurallar()`
|
|
477
|
+
* draw would rarely match a given name — this re-draws (bounded, with a
|
|
478
|
+
* short delay) until it finds a match or gives up. Returns `null` if no
|
|
479
|
+
* match turns up within the attempt budget or the matched page can't be
|
|
480
|
+
* parsed.
|
|
481
|
+
*/
|
|
482
|
+
static async getRule(name) {
|
|
483
|
+
if (!name || name.trim() === "")
|
|
484
|
+
return null;
|
|
485
|
+
const target = name.trim().toLocaleLowerCase("tr-TR");
|
|
486
|
+
for (let attempt = 0; attempt < 25; attempt++) {
|
|
487
|
+
const rules = await this.getKurallar();
|
|
488
|
+
const match = rules.find((r) => r.adi.toLocaleLowerCase("tr-TR").includes(target));
|
|
489
|
+
if (match)
|
|
490
|
+
return this.fetchRuleText(match.url);
|
|
491
|
+
await this.delay(100);
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* `tdk.gov.tr` rule pages are WordPress/Avada-themed. The actual article
|
|
497
|
+
* text lives in `<div ... itemprop="text">...</div>` right before a
|
|
498
|
+
* `<footer class="entry...">` (share buttons, author box, structured-data
|
|
499
|
+
* spans) — cutting there avoids that trailing cruft.
|
|
500
|
+
*/
|
|
501
|
+
static async fetchRuleText(url) {
|
|
502
|
+
try {
|
|
503
|
+
const response = await fetch(url, { headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" } });
|
|
504
|
+
if (!response.ok)
|
|
505
|
+
return null;
|
|
506
|
+
const html = await response.text();
|
|
507
|
+
const marker = html.indexOf('itemprop="text"');
|
|
508
|
+
if (marker === -1)
|
|
509
|
+
return null;
|
|
510
|
+
const contentStart = html.indexOf(">", marker) + 1;
|
|
511
|
+
const contentEnd = html.indexOf("<footer", contentStart);
|
|
512
|
+
if (contentEnd === -1)
|
|
513
|
+
return null;
|
|
514
|
+
return this.htmlToPlainText(html.slice(contentStart, contentEnd));
|
|
515
|
+
} catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
static htmlToPlainText(html) {
|
|
520
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/ /gi, " ").replace(/&/gi, "&").replace(/"/gi, '"').replace(/'|’/gi, "'").replace(/[ \t]+/g, " ").replace(/[ \t]*\n[ \t]*/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
521
|
+
}
|
|
261
522
|
/**
|
|
262
523
|
* Returns compound words that contain this word.
|
|
263
524
|
*/
|
|
@@ -276,6 +537,9 @@ var TDK = class {
|
|
|
276
537
|
}
|
|
277
538
|
/**
|
|
278
539
|
* Returns the part of speech (isim, sıfat, zarf vb.).
|
|
540
|
+
* TDK's `ozelliklerListe` mixes grammatical categories (`tur: "3"`, e.g.
|
|
541
|
+
* sıfat/zarf/isim) with usage-register tags (`tur: "4"`, e.g. mecaz/argo)
|
|
542
|
+
* in the same list — only `tur === "3"` entries are actual parts of speech.
|
|
279
543
|
*/
|
|
280
544
|
static async getPartOfSpeech(word) {
|
|
281
545
|
const results = await this.getWord(word);
|
|
@@ -285,7 +549,8 @@ var TDK = class {
|
|
|
285
549
|
for (const anlam of result.anlamlarListe) {
|
|
286
550
|
if (anlam.ozelliklerListe) {
|
|
287
551
|
for (const ozellik of anlam.ozelliklerListe) {
|
|
288
|
-
|
|
552
|
+
if (ozellik.tur === "3")
|
|
553
|
+
pos.add(ozellik.tam_adi);
|
|
289
554
|
}
|
|
290
555
|
}
|
|
291
556
|
}
|
|
@@ -296,6 +561,116 @@ var TDK = class {
|
|
|
296
561
|
}
|
|
297
562
|
return Array.from(pos);
|
|
298
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Compares two words side by side: meaning count, etymological origin,
|
|
566
|
+
* syllables and vowel-harmony compliance.
|
|
567
|
+
*/
|
|
568
|
+
static async compareWords(a, b) {
|
|
569
|
+
const [meaningsA, meaningsB, originA, originB] = await Promise.all([
|
|
570
|
+
this.getMeanings(a),
|
|
571
|
+
this.getMeanings(b),
|
|
572
|
+
this.getOrigin(a),
|
|
573
|
+
this.getOrigin(b)
|
|
574
|
+
]);
|
|
575
|
+
return {
|
|
576
|
+
a: {
|
|
577
|
+
word: a,
|
|
578
|
+
meaningCount: meaningsA.length,
|
|
579
|
+
origin: originA,
|
|
580
|
+
syllables: this.syllabicate(a),
|
|
581
|
+
harmony: this.checkVowelHarmony(a)
|
|
582
|
+
},
|
|
583
|
+
b: {
|
|
584
|
+
word: b,
|
|
585
|
+
meaningCount: meaningsB.length,
|
|
586
|
+
origin: originB,
|
|
587
|
+
syllables: this.syllabicate(b),
|
|
588
|
+
harmony: this.checkVowelHarmony(b)
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
static STOPWORDS = /* @__PURE__ */ new Set([
|
|
593
|
+
"ve",
|
|
594
|
+
"veya",
|
|
595
|
+
"ile",
|
|
596
|
+
"ama",
|
|
597
|
+
"fakat",
|
|
598
|
+
"ancak",
|
|
599
|
+
"de",
|
|
600
|
+
"da",
|
|
601
|
+
"ki",
|
|
602
|
+
"bu",
|
|
603
|
+
"\u015Fu",
|
|
604
|
+
"o",
|
|
605
|
+
"bir",
|
|
606
|
+
"\xE7ok",
|
|
607
|
+
"az",
|
|
608
|
+
"gibi",
|
|
609
|
+
"i\xE7in",
|
|
610
|
+
"mi",
|
|
611
|
+
"m\u0131",
|
|
612
|
+
"mu",
|
|
613
|
+
"m\xFC",
|
|
614
|
+
"ne",
|
|
615
|
+
"her",
|
|
616
|
+
"hi\xE7",
|
|
617
|
+
"ben",
|
|
618
|
+
"sen",
|
|
619
|
+
"biz",
|
|
620
|
+
"siz",
|
|
621
|
+
"onlar",
|
|
622
|
+
"de\u011Fil",
|
|
623
|
+
"bile",
|
|
624
|
+
"diye"
|
|
625
|
+
]);
|
|
626
|
+
static firstMeaning(results) {
|
|
627
|
+
for (const result of results) {
|
|
628
|
+
for (const anlam of result.anlamlarListe ?? []) {
|
|
629
|
+
if (anlam.anlam)
|
|
630
|
+
return anlam.anlam;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Analyzes every distinct word in a text (Turkish stopwords filtered out),
|
|
637
|
+
* returning each word's first meaning and etymological origin if found.
|
|
638
|
+
* Looks each word up individually (throttled), so scales with text length.
|
|
639
|
+
*/
|
|
640
|
+
static async analyzeText(text) {
|
|
641
|
+
const words = text.toLocaleLowerCase("tr-TR").replace(/[^\p{L}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !this.STOPWORDS.has(w));
|
|
642
|
+
const unique = [...new Set(words)];
|
|
643
|
+
const analyses = [];
|
|
644
|
+
for (const word of unique) {
|
|
645
|
+
const results = await this.getWord(word);
|
|
646
|
+
const found = results.length > 0;
|
|
647
|
+
analyses.push({
|
|
648
|
+
word,
|
|
649
|
+
found,
|
|
650
|
+
meaning: found ? this.firstMeaning(results) : null,
|
|
651
|
+
origin: found ? results[0].lisan || "T\xFCrk\xE7e" : null
|
|
652
|
+
});
|
|
653
|
+
await this.delay(200);
|
|
654
|
+
}
|
|
655
|
+
return analyses;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Classic edit-distance between two strings.
|
|
659
|
+
*/
|
|
660
|
+
static levenshtein(a, b) {
|
|
661
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
662
|
+
for (let i = 0; i <= a.length; i++)
|
|
663
|
+
dp[i][0] = i;
|
|
664
|
+
for (let j = 0; j <= b.length; j++)
|
|
665
|
+
dp[0][j] = j;
|
|
666
|
+
for (let i = 1; i <= a.length; i++) {
|
|
667
|
+
for (let j = 1; j <= b.length; j++) {
|
|
668
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
669
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return dp[a.length][b.length];
|
|
673
|
+
}
|
|
299
674
|
/**
|
|
300
675
|
* Fetches multiple words concurrently with a small delay to avoid rate limiting.
|
|
301
676
|
*/
|
|
@@ -346,16 +721,23 @@ var TDK = class {
|
|
|
346
721
|
}
|
|
347
722
|
/**
|
|
348
723
|
* Checks if a word follows Turkish Major Vowel Harmony (Büyük Ünlü Uyumu).
|
|
724
|
+
* Normalizes case via the Turkish locale first: a plain case-insensitive
|
|
725
|
+
* regex would fold ASCII "I" to "i", misreading the back vowel "I"
|
|
726
|
+
* (dotless) as the front vowel "i" (dotted).
|
|
349
727
|
*/
|
|
350
728
|
static checkVowelHarmony(word) {
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const
|
|
729
|
+
const lower = word.toLocaleLowerCase("tr-TR");
|
|
730
|
+
const backVowels = /[aıou]/;
|
|
731
|
+
const frontVowels = /[eiöü]/;
|
|
732
|
+
const hasBack = backVowels.test(lower);
|
|
733
|
+
const hasFront = frontVowels.test(lower);
|
|
355
734
|
return !(hasBack && hasFront);
|
|
356
735
|
}
|
|
357
736
|
};
|
|
358
737
|
// Annotate the CommonJS export names for ESM import in node:
|
|
359
738
|
0 && (module.exports = {
|
|
360
|
-
TDK
|
|
739
|
+
TDK,
|
|
740
|
+
TDKError,
|
|
741
|
+
TDKNetworkError,
|
|
742
|
+
TDKValidationError
|
|
361
743
|
});
|
package/dist/index.mjs
CHANGED