tdk-api-wrapper 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  TDK,
3
+ TDKClient,
3
4
  TDKError,
4
5
  TDKNetworkError,
5
6
  TDKValidationError,
@@ -8,11 +9,14 @@ import {
8
9
  getStemCandidates,
9
10
  isVowel,
10
11
  restoreConsonantSoftening,
12
+ restoreGemination,
11
13
  restoreInfinitive,
12
- restoreVowelDrop
13
- } from "./chunk-5TYJDVHK.mjs";
14
+ restoreVowelDrop,
15
+ restoreVowelNarrowing
16
+ } from "./chunk-NEC5MIQM.mjs";
14
17
  export {
15
18
  TDK,
19
+ TDKClient,
16
20
  TDKError,
17
21
  TDKNetworkError,
18
22
  TDKValidationError,
@@ -21,6 +25,8 @@ export {
21
25
  getStemCandidates,
22
26
  isVowel,
23
27
  restoreConsonantSoftening,
28
+ restoreGemination,
24
29
  restoreInfinitive,
25
- restoreVowelDrop
30
+ restoreVowelDrop,
31
+ restoreVowelNarrowing
26
32
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tdk-api-wrapper",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "TDK (Türk Dil Kurumu) unofficial live data API wrapper for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --clean --shims",
20
- "test": "node test/morphology.test.js"
20
+ "test": "node test/morphology.test.js && node test/grammar.test.js && node test/proofread.test.js && node test/tools.test.js"
21
21
  },
22
22
  "keywords": [
23
23
  "tdk",
package/src/cli.ts CHANGED
@@ -4,6 +4,17 @@ import { TDK } from "./tdk";
4
4
  const rawArgs = process.argv.slice(2);
5
5
  const jsonMode = rawArgs.includes("--json");
6
6
  const args = rawArgs.filter((a) => a !== "--json");
7
+
8
+ const isColor = !jsonMode && Boolean(process.stdout.isTTY);
9
+ const c = {
10
+ bold: (s: string) => (isColor ? `\x1b[1m${s}\x1b[0m` : s),
11
+ dim: (s: string) => (isColor ? `\x1b[2m${s}\x1b[0m` : s),
12
+ green: (s: string) => (isColor ? `\x1b[32m${s}\x1b[0m` : s),
13
+ yellow: (s: string) => (isColor ? `\x1b[33m${s}\x1b[0m` : s),
14
+ cyan: (s: string) => (isColor ? `\x1b[36m${s}\x1b[0m` : s),
15
+ red: (s: string) => (isColor ? `\x1b[31m${s}\x1b[0m` : s),
16
+ };
17
+
7
18
  const KNOWN_COMMANDS = new Set([
8
19
  "ara",
9
20
  "anlam",
@@ -11,6 +22,7 @@ const KNOWN_COMMANDS = new Set([
11
22
  "ornek",
12
23
  "hece",
13
24
  "uyum",
25
+ "kucukuyum",
14
26
  "yazim",
15
27
  "kok",
16
28
  "stem",
@@ -25,6 +37,14 @@ const KNOWN_COMMANDS = new Set([
25
37
  "karsilastir",
26
38
  "analiz",
27
39
  "oneri",
40
+ "bulmaca",
41
+ "pattern",
42
+ "anagram",
43
+ "kafiye",
44
+ "rhyme",
45
+ "denetle",
46
+ "proofread",
47
+ "repl",
28
48
  "kubbealti",
29
49
  "nisanyan",
30
50
  "viki",
@@ -33,7 +53,7 @@ const KNOWN_COMMANDS = new Set([
33
53
  let command = args[0];
34
54
  let word = args.slice(1).join(" ");
35
55
 
36
- if (command && !KNOWN_COMMANDS.has(command) && command !== "--help" && command !== "-h") {
56
+ if (command && !KNOWN_COMMANDS.has(command) && command !== "--help" && command !== "-h" && command !== "--version" && command !== "-v") {
37
57
  word = args.join(" ");
38
58
  command = "anlam";
39
59
  }
@@ -50,18 +70,101 @@ function printError(message: string) {
50
70
  if (jsonMode) {
51
71
  console.log(JSON.stringify({ error: message }));
52
72
  } else {
53
- console.log(`Hata: ${message}`);
73
+ console.log(c.red(`Hata: ${message}`));
54
74
  }
55
75
  }
56
76
 
77
+ async function startRepl() {
78
+ const readline = await import("node:readline");
79
+ const rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout,
82
+ prompt: c.cyan("tdk> "),
83
+ });
84
+
85
+ console.log(c.bold("TDK İnteraktif Sözlük Kabuğu (Çıkmak için 'exit' veya Ctrl+C)"));
86
+ console.log(c.dim("Komutlar: ara <kelime>, hece <kelime>, bulmaca <desen>, denetle <metin> veya doğrudan kelime"));
87
+ rl.prompt();
88
+
89
+ rl.on("line", async (line) => {
90
+ const trimmed = line.trim();
91
+ if (!trimmed) {
92
+ rl.prompt();
93
+ return;
94
+ }
95
+ if (trimmed === "exit" || trimmed === "quit" || trimmed === ".exit") {
96
+ rl.close();
97
+ return;
98
+ }
99
+
100
+ const parts = trimmed.split(/\s+/);
101
+ let subCmd = parts[0].toLowerCase();
102
+ let subArg = parts.slice(1).join(" ");
103
+ if (!KNOWN_COMMANDS.has(subCmd)) {
104
+ subArg = trimmed;
105
+ subCmd = "anlam";
106
+ }
107
+
108
+ try {
109
+ if (subCmd === "ara" || subCmd === "anlam") {
110
+ const meanings = await TDK.getMeanings(subArg);
111
+ if (meanings.length === 0) console.log(c.dim("Sonuç bulunamadı."));
112
+ else meanings.forEach((m, i) => console.log(`${i + 1}. ${c.green(m)}`));
113
+ } else if (subCmd === "koken") {
114
+ const origin = await TDK.getOrigin(subArg);
115
+ console.log(`Köken: ${c.cyan(origin || "Bilinmiyor")}`);
116
+ } else if (subCmd === "hece") {
117
+ const s = TDK.syllabicate(subArg);
118
+ console.log(`Heceler: ${c.yellow(s.join("-"))}`);
119
+ } else if (subCmd === "uyum") {
120
+ const h = TDK.checkVowelHarmony(subArg);
121
+ console.log(`Büyük Ünlü Uyumu: ${h ? c.green("Uyar") : c.red("Uymaz")}`);
122
+ } else if (subCmd === "kucukuyum") {
123
+ const h = TDK.checkLabialHarmony(subArg);
124
+ console.log(`Küçük Ünlü Uyumu: ${h ? c.green("Uyar") : c.red("Uymaz")}`);
125
+ } else if (subCmd === "bulmaca" || subCmd === "pattern") {
126
+ const matches = await TDK.patternSearch(subArg);
127
+ console.log(matches.slice(0, 15).join(", "));
128
+ } else if (subCmd === "denetle" || subCmd === "proofread") {
129
+ const res = await TDK.proofread(subArg);
130
+ if (res.isCorrect) console.log(c.green("✓ Sorun bulunamadı."));
131
+ else res.issues.forEach((iss) => console.log(`- ${c.yellow(iss.word)}: ${iss.message}${iss.suggestion ? " -> " + c.green(iss.suggestion) : ""}`));
132
+ } else {
133
+ console.log(c.dim("Örnek komutlar: 'ara kalem', 'hece elektrik', 'bulmaca k_l_m', 'denetle Bugün evdeyim'"));
134
+ }
135
+ } catch (e: any) {
136
+ console.log(c.red(`Hata: ${e?.message || e}`));
137
+ }
138
+ rl.prompt();
139
+ });
140
+ }
141
+
57
142
  async function run() {
58
- if (!command || command === "--help" || command === "-h") {
143
+ if (!command) {
144
+ if (process.stdin.isTTY) {
145
+ await startRepl();
146
+ return;
147
+ }
59
148
  console.log("Kullanım: tdk [komut] <kelime> [--json]");
60
149
  console.log(
61
- "Komutlar: ara, anlam, koken, ornek, hece, uyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, kubbealti, nisanyan, viki"
150
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki"
62
151
  );
63
152
  console.log("Not: Komut belirtilmezse doğrudan kelime anlamı aranır (örn: tdk selam)");
64
- process.exit(command ? 0 : 1);
153
+ process.exit(1);
154
+ }
155
+
156
+ if (command === "--version" || command === "-v") {
157
+ console.log("tdk-api-wrapper v1.5.1");
158
+ process.exit(0);
159
+ }
160
+
161
+ if (command === "--help" || command === "-h") {
162
+ console.log("Kullanım: tdk [komut] <kelime> [--json]");
163
+ console.log(
164
+ "Komutlar: ara, anlam, koken, ornek, hece, uyum, kucukuyum, yazim, kok, deyim, gunun, rastgele, esanlam, karsit, yabanci, kurallar, kural, karsilastir, analiz, oneri, bulmaca, anagram, kafiye, denetle, repl, kubbealti, nisanyan, viki"
165
+ );
166
+ console.log("Not: Komut belirtilmezse doğrudan kelime anlamı aranır (örn: tdk selam)");
167
+ process.exit(0);
65
168
  }
66
169
 
67
170
  TDK.enableCache(false);
@@ -121,6 +224,16 @@ async function run() {
121
224
  break;
122
225
  }
123
226
 
227
+ case "kucukuyum":
228
+ case "labial": {
229
+ if (!word) throw new Error("Kelime belirtmelisiniz.");
230
+ const isHarmony = TDK.checkLabialHarmony(word);
231
+ printResult({ word, labialHarmony: isHarmony }, () =>
232
+ console.log(`Küçük Ünlü Uyumu: ${isHarmony ? "Uyar" : "Uymaz"}`)
233
+ );
234
+ break;
235
+ }
236
+
124
237
  case "yazim": {
125
238
  if (!word) throw new Error("Kelime belirtmelisiniz.");
126
239
  const spellResult = await TDK.checkSpelling(word);
@@ -299,6 +412,79 @@ async function run() {
299
412
  break;
300
413
  }
301
414
 
415
+ case "bulmaca":
416
+ case "pattern": {
417
+ if (!word) throw new Error("Desen belirtmelisiniz (örn: k_l_m).");
418
+ const matches = await TDK.patternSearch(word);
419
+ printResult(matches, () => {
420
+ if (matches.length === 0) {
421
+ console.log("Eşleşen kelime bulunamadı.");
422
+ } else {
423
+ console.log(c.bold(`Bulunan Kelimeler (${matches.length}):`));
424
+ matches.forEach((m, i) => console.log(`${i + 1}. ${c.cyan(m)}`));
425
+ }
426
+ });
427
+ break;
428
+ }
429
+
430
+ case "anagram": {
431
+ if (!word) throw new Error("Harfler belirtmelisiniz.");
432
+ const anagrams = await TDK.findAnagrams(word);
433
+ printResult(anagrams, () => {
434
+ if (anagrams.length === 0) {
435
+ console.log("Anagram veya bu harflerle türetilebilecek kelime bulunamadı.");
436
+ } else {
437
+ const clean = word.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
438
+ const hasExact = anagrams.some((a) => a.length === clean.length);
439
+ const title = hasExact
440
+ ? `Anagramlar (${anagrams.length}):`
441
+ : `Birebir anagram bulunamadı. Bu harflerle türetilen kelimeler (${anagrams.length}):`;
442
+ console.log(c.bold(title));
443
+ anagrams.forEach((a, i) => console.log(`${i + 1}. ${c.green(a)} ${c.dim(`(${a.length} harf)`)}`));
444
+ }
445
+ });
446
+ break;
447
+ }
448
+
449
+ case "kafiye":
450
+ case "rhyme": {
451
+ if (!word) throw new Error("Kelime belirtmelisiniz.");
452
+ const rhymes = await TDK.findRhymes(word);
453
+ printResult(rhymes, () => {
454
+ if (rhymes.length === 0) {
455
+ console.log("Kafiye bulunamadı.");
456
+ } else {
457
+ console.log(c.bold(`Kafiyeli Kelimeler (${rhymes.length}):`));
458
+ rhymes.forEach((r, i) => console.log(`${i + 1}. ${c.yellow(r)}`));
459
+ }
460
+ });
461
+ break;
462
+ }
463
+
464
+ case "denetle":
465
+ case "proofread": {
466
+ if (!word) throw new Error("Metin belirtmelisiniz.");
467
+ const result = await TDK.proofread(word);
468
+ printResult(result, () => {
469
+ if (result.isCorrect) {
470
+ console.log(c.green("✓ Metinde imla veya bağlaç hatası tespit edilmedi."));
471
+ } else {
472
+ console.log(c.bold(c.red(`Metinde ${result.issues.length} olası sorun tespit edildi:`)));
473
+ result.issues.forEach((issue, i) => {
474
+ const label = c.yellow(`[${issue.type}]`);
475
+ const sug = issue.suggestion ? c.green(` -> Öneri: ${issue.suggestion}`) : "";
476
+ console.log(`${i + 1}. ${label} "${c.bold(issue.word)}": ${issue.message}${sug}`);
477
+ });
478
+ }
479
+ });
480
+ break;
481
+ }
482
+
483
+ case "repl": {
484
+ await startRepl();
485
+ break;
486
+ }
487
+
302
488
  case "kubbealti": {
303
489
  if (!word) throw new Error("Kelime belirtmelisiniz.");
304
490
  const meanings = await TDK.getKubbealtiMeanings(word);
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { TDK } from "./tdk";
1
+ export { TDK, TDKClient } from "./tdk";
2
2
  export * from "./types";
3
3
  export * from "./errors";
4
4
  export * from "./morphology";
package/src/morphology.ts CHANGED
@@ -67,7 +67,8 @@ export const TURKISH_SUFFIXES: readonly string[] = [
67
67
  "miş", "mış", "muş", "müş", "dim", "dım", "dum", "düm", "tim", "tım", "tum", "tüm",
68
68
  "din", "dın", "dun", "dün", "tin", "tın", "tun", "tün", "dik", "dık", "duk", "dük",
69
69
  "tik", "tık", "tuk", "tük", "ydi", "ydı", "ydu", "ydü", "yim", "yım", "yum", "yüm",
70
- "sin", "sın", "sun", "sün", "siz", "sız", "suz", "süz", "lik", "lık", "luk", "lük",
70
+ "sin", "sın", "sun", "sün", "sen", "san", "sem", "sam", "sek", "sak",
71
+ "siz", "sız", "suz", "süz", "lik", "lık", "luk", "lük",
71
72
  "ici", "ıcı", "ucu", "ücü", "gen", "gan", "ken", "kan",
72
73
  "len", "lan", "leş", "laş", "mek", "mak", "yor",
73
74
  // 2-letter suffixes
@@ -126,6 +127,61 @@ export function restoreVowelDrop(stem: string): string[] {
126
127
  return [];
127
128
  }
128
129
 
130
+ /**
131
+ * Reverses Turkish consonant gemination (ünsüz türemesi / ikizleşmesi):
132
+ * In words of Arabic/foreign origin, when receiving a vowel-initial suffix, the final consonant doubles:
133
+ * e.g. hak->hakkı, his->hissi, sır->sırrı, af->affı, ret->reddi, tıp->tıbbı, zam->zammı, hat->hattı.
134
+ * Restores the single consonant form and checks consonant softening on the result (e.g. redd -> red -> ret).
135
+ */
136
+ export function restoreGemination(stem: string): string[] {
137
+ if (stem.length < 3) return [];
138
+ const c1 = stem[stem.length - 2];
139
+ const c2 = stem[stem.length - 1];
140
+ if (c1 === c2 && !isVowel(c1)) {
141
+ const single = stem.slice(0, -1);
142
+ const hardened = restoreConsonantSoftening(single);
143
+ return [single, ...hardened];
144
+ }
145
+ return [];
146
+ }
147
+
148
+ /**
149
+ * Reverses Turkish vowel narrowing (ünlü daralması):
150
+ * Verbs ending in wide vowels 'a' or 'e' narrow to 'ı', 'i', 'u', 'ü' before the continuous tense suffix -yor:
151
+ * e.g. başla-yor -> başlıyor, bekle-yor -> bekliyor, özle-yor -> özlüyor, anla-yor -> anlıyor.
152
+ * Also handles irregular monosyllabic verbs: de-yor -> diyor, ye-yor -> yiyor.
153
+ */
154
+ export function restoreVowelNarrowing(stem: string): string[] {
155
+ if (stem.length < 2) return [];
156
+
157
+ // Irregular monosyllabic verbs
158
+ if (stem === "di") return ["de"];
159
+ if (stem === "yi") return ["ye"];
160
+
161
+ const lastChar = stem[stem.length - 1];
162
+ const isLastNarrow = "ıiuü".includes(lastChar);
163
+
164
+ // Case 1: stem ends with narrow vowel (e.g. başlı, bekli, özlü, kutlu)
165
+ if (isLastNarrow) {
166
+ const vowelsInBase = stem.slice(0, -1).split("").filter(isVowel);
167
+ const lastVowel = vowelsInBase.length > 0 ? vowelsInBase[vowelsInBase.length - 1] : lastChar;
168
+ const widened = "aıou".includes(lastVowel) ? "a" : "e";
169
+ return [stem.slice(0, -1) + widened];
170
+ }
171
+
172
+ // Case 2: stem ends with consonant (e.g. başlıyor stripped by -ıyor -> stem: başl)
173
+ if (!isVowel(lastChar)) {
174
+ const vowelsInBase = stem.split("").filter(isVowel);
175
+ if (vowelsInBase.length > 0) {
176
+ const lastVowel = vowelsInBase[vowelsInBase.length - 1];
177
+ const widened = "aıou".includes(lastVowel) ? "a" : "e";
178
+ return [stem + widened];
179
+ }
180
+ }
181
+
182
+ return [];
183
+ }
184
+
129
185
  /**
130
186
  * Restores verb infinitive headword form (-mek / -mak):
131
187
  * Since TDK registers verbs in their infinitive form (e.g. okumak, gelmek, yazmak),
@@ -174,6 +230,15 @@ export function getStemCandidates(
174
230
  }
175
231
  }
176
232
 
233
+ // Bare verb imperative candidates (e.g. "söyle" -> "söylemek", "oku" -> "okumak")
234
+ const bareInfinitives = restoreInfinitive(normalized);
235
+ for (const inf of bareInfinitives) {
236
+ if (!seen.has(inf) && inf !== normalized) {
237
+ seen.add(inf);
238
+ candidatesWithWeight.push({ candidate: inf, baseLength: normalized.length });
239
+ }
240
+ }
241
+
177
242
  let frontier = [normalized];
178
243
 
179
244
  for (let depth = 0; depth < maxDepth; depth++) {
@@ -186,11 +251,55 @@ export function getStemCandidates(
186
251
 
187
252
  const hardened = restoreConsonantSoftening(stem);
188
253
  const vowelDropped = restoreVowelDrop(stem);
189
- // Infinitives only apply to direct stems or consonant-hardened stems (e.g. gid -> git -> gitmek),
190
- // NOT to vowel-dropped nouns (nouns like akıl/omuz/şehir don't take infinitive -mek/-mak).
191
- const verbalBases = [stem, ...hardened];
254
+ const geminated = restoreGemination(stem);
255
+
256
+ // Vowel narrowing (ünlü daralması) in Turkish strictly occurs with continuous tense (-yor)
257
+ // or with the monosyllabic verbs de-/ye- before buffer 'y' (diye, yiyen).
258
+ // Restricting narrowing to these suffixes prevents false-positive stems on other suffixes.
259
+ const isNarrowingSuffix =
260
+ suffix.startsWith("yor") ||
261
+ suffix.includes("iyor") ||
262
+ suffix.includes("ıyor") ||
263
+ suffix.includes("uyor") ||
264
+ suffix.includes("üyor");
265
+
266
+ const isDeYeBuffer = (stem === "di" || stem === "yi") && suffix.startsWith("y");
267
+ const narrowed = isNarrowingSuffix || isDeYeBuffer ? restoreVowelNarrowing(stem) : [];
268
+
269
+ // Suffix indicator for verbs: -yor, -ecek, -miş, -di, etc.
270
+ const isVerbSuffix =
271
+ isNarrowingSuffix ||
272
+ suffix.includes("ecek") ||
273
+ suffix.includes("acak") ||
274
+ suffix.includes("miş") ||
275
+ suffix.includes("mış") ||
276
+ suffix.includes("müş") ||
277
+ suffix.includes("muş") ||
278
+ suffix.includes("mek") ||
279
+ suffix.includes("mak") ||
280
+ suffix.includes("erek") ||
281
+ suffix.includes("arak") ||
282
+ suffix.includes("dik") ||
283
+ suffix.includes("dık") ||
284
+ suffix.includes("duk") ||
285
+ suffix.includes("dük") ||
286
+ suffix.includes("tik") ||
287
+ suffix.includes("tık") ||
288
+ suffix.includes("tuk") ||
289
+ suffix.includes("tük") ||
290
+ suffix.includes("sen") ||
291
+ suffix.includes("san") ||
292
+ suffix.includes("sem") ||
293
+ suffix.includes("sam") ||
294
+ suffix.includes("sek") ||
295
+ suffix.includes("sak");
296
+
297
+ // Infinitives apply to direct stems, hardened stems, and widened stems (e.g. başlı -> başla -> başlamak)
298
+ const verbalBases = [stem, ...hardened, ...narrowed];
192
299
  const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
193
- const variants = [stem, ...hardened, ...vowelDropped, ...infinitives];
300
+
301
+ // Base candidates
302
+ const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
194
303
 
195
304
  for (const variant of variants) {
196
305
  if (!seen.has(variant) && variant !== normalized) {
@@ -199,6 +308,16 @@ export function getStemCandidates(
199
308
  candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
200
309
  }
201
310
  }
311
+
312
+ // Push infinitives with high priority if a verbal suffix matched, preventing noun false-positives
313
+ for (const inf of infinitives) {
314
+ if (!seen.has(inf) && inf !== normalized) {
315
+ seen.add(inf);
316
+ nextFrontier.push(inf);
317
+ const weight = isVerbSuffix ? stem.length + 5 : stem.length;
318
+ candidatesWithWeight.push({ candidate: inf, baseLength: weight });
319
+ }
320
+ }
202
321
  }
203
322
  }
204
323
  }