tdk-api-wrapper 1.4.0 → 1.5.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/src/tdk.ts CHANGED
@@ -10,6 +10,12 @@ import type {
10
10
  TDKRule,
11
11
  KubbealtiEntry,
12
12
  WiktionaryEntry,
13
+ ProofreadIssue,
14
+ ProofreadResult,
15
+ PatternSearchOptions,
16
+ AnagramOptions,
17
+ RhymeOptions,
18
+ TDKConfig,
13
19
  } from "./types";
14
20
  import { TDKValidationError, TDKNetworkError } from "./errors";
15
21
  import { getStemCandidates } from "./morphology";
@@ -104,6 +110,11 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
104
110
  -----END CERTIFICATE-----`,
105
111
  ];
106
112
 
113
+ // Configuration
114
+ private static defaultTimeoutMs = 8000;
115
+ private static defaultRetries = 1;
116
+ private static maxCacheSize = 1000;
117
+
107
118
  // Cache Mechanism
108
119
  private static isCacheEnabled = false;
109
120
  private static wordCache = new Map<string, WordInfo[]>();
@@ -112,6 +123,16 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
112
123
  private static autocompleteSet: Set<string> = new Set<string>();
113
124
  private static stemCache = new Map<string, string | null>();
114
125
 
126
+ /**
127
+ * Configures global client options such as network timeout, retries, and cache size.
128
+ */
129
+ public static configure(config: TDKConfig): void {
130
+ if (config.timeoutMs !== undefined) this.defaultTimeoutMs = Math.max(100, config.timeoutMs);
131
+ if (config.retries !== undefined) this.defaultRetries = Math.max(0, config.retries);
132
+ if (config.cache !== undefined) this.enableCache(config.cache);
133
+ if (config.maxCacheSize !== undefined) this.maxCacheSize = Math.max(10, config.maxCacheSize);
134
+ }
135
+
115
136
  /**
116
137
  * Enables or disables in-memory caching for API requests.
117
138
  */
@@ -133,10 +154,58 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
133
154
  this.stemCache.clear();
134
155
  }
135
156
 
157
+ private static setBoundedCache<K, V>(map: Map<K, V>, key: K, value: V): void {
158
+ if (map.size >= this.maxCacheSize) {
159
+ const firstKey = map.keys().next().value;
160
+ if (firstKey !== undefined) map.delete(firstKey);
161
+ }
162
+ map.set(key, value);
163
+ }
164
+
136
165
  private static delay(ms: number) {
137
166
  return new Promise((resolve) => setTimeout(resolve, ms));
138
167
  }
139
168
 
169
+ /**
170
+ * Internal helper that performs HTTP fetch with timeout and automatic retry on network/5xx errors.
171
+ */
172
+ private static async fetchWithRetry(
173
+ url: string,
174
+ options: RequestInit = {},
175
+ retries: number = this.defaultRetries,
176
+ timeoutMs: number = this.defaultTimeoutMs
177
+ ): Promise<Response> {
178
+ let lastError: unknown;
179
+ for (let attempt = 0; attempt <= retries; attempt++) {
180
+ try {
181
+ const signal = AbortSignal.timeout(timeoutMs);
182
+ const headers = {
183
+ "User-Agent": "TDK-API-Nodejs-Wrapper/1.0",
184
+ ...((options.headers as Record<string, string>) || {}),
185
+ };
186
+ const res = await fetch(url, { ...options, headers, signal });
187
+ if (res.ok || (res.status >= 400 && res.status < 500)) {
188
+ return res;
189
+ }
190
+ // If 5xx server error, retry
191
+ if (attempt < retries) {
192
+ await this.delay(200 * (attempt + 1));
193
+ continue;
194
+ }
195
+ return res;
196
+ } catch (err) {
197
+ lastError = err;
198
+ if (attempt < retries) {
199
+ await this.delay(200 * (attempt + 1));
200
+ continue;
201
+ }
202
+ }
203
+ }
204
+ throw new TDKNetworkError(`Request to ${url} failed after ${retries + 1} attempts.`, {
205
+ cause: lastError,
206
+ });
207
+ }
208
+
140
209
  /**
141
210
  * Fetches detailed information for a given word from the TDK Dictionary.
142
211
  */
@@ -155,9 +224,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
155
224
 
156
225
  let response: Response;
157
226
  try {
158
- response = await fetch(url, {
159
- headers: { "User-Agent": "TDK-API-Nodejs-Wrapper/1.0" },
160
- });
227
+ response = await this.fetchWithRetry(url);
161
228
  } catch (error) {
162
229
  throw new TDKNetworkError("Failed to fetch word from TDK: request failed.", { cause: error });
163
230
  }
@@ -176,13 +243,13 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
176
243
  }
177
244
 
178
245
  if (!Array.isArray(data) && data && "error" in (data as Record<string, unknown>)) {
179
- if (this.isCacheEnabled) this.wordCache.set(cleanWord, []);
246
+ if (this.isCacheEnabled) this.setBoundedCache(this.wordCache, cleanWord, []);
180
247
  return [];
181
248
  }
182
249
 
183
250
  const results = data as WordInfo[];
184
251
  if (this.isCacheEnabled) {
185
- this.wordCache.set(cleanWord, results);
252
+ this.setBoundedCache(this.wordCache, cleanWord, results);
186
253
  }
187
254
  return results;
188
255
  }
@@ -320,7 +387,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
320
387
 
321
388
  // 1. If the word itself is an exact headword, it is its own root
322
389
  if (await this.isHeadword(clean)) {
323
- this.stemCache.set(clean, clean);
390
+ this.setBoundedCache(this.stemCache, clean, clean);
324
391
  return clean;
325
392
  }
326
393
 
@@ -328,12 +395,12 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
328
395
  const candidates = getStemCandidates(clean);
329
396
  for (const candidate of candidates) {
330
397
  if (await this.isHeadword(candidate)) {
331
- this.stemCache.set(clean, candidate);
398
+ this.setBoundedCache(this.stemCache, clean, candidate);
332
399
  return candidate;
333
400
  }
334
401
  }
335
402
 
336
- this.stemCache.set(clean, null);
403
+ this.setBoundedCache(this.stemCache, clean, null);
337
404
  return null;
338
405
  }
339
406
 
@@ -1063,6 +1130,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1063
1130
  origin: originA,
1064
1131
  syllables: this.syllabicate(a),
1065
1132
  harmony: this.checkVowelHarmony(a),
1133
+ labialHarmony: this.checkLabialHarmony(a),
1066
1134
  },
1067
1135
  b: {
1068
1136
  word: b,
@@ -1070,6 +1138,7 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1070
1138
  origin: originB,
1071
1139
  syllables: this.syllabicate(b),
1072
1140
  harmony: this.checkVowelHarmony(b),
1141
+ labialHarmony: this.checkLabialHarmony(b),
1073
1142
  },
1074
1143
  };
1075
1144
  }
@@ -1181,13 +1250,15 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1181
1250
 
1182
1251
  /**
1183
1252
  * Syllabicates a Turkish word based on general grammar rules.
1253
+ * Handles syllable separation for vowels, single consonants, double consonants,
1254
+ * and western loanword three-consonant clusters (e.g. e-lek-trik, kon-trol, or-kes-tra).
1184
1255
  */
1185
1256
  public static syllabicate(word: string): string[] {
1186
1257
  const vowels = /[aeıioöuüAEIİOÖUÜ]/;
1258
+ const ONSET_CLUSTERS = new Set(["tr", "pr", "kr", "gr", "br", "fr", "dr", "pl", "kl", "fl", "bl", "gl"]);
1187
1259
  const result: string[] = [];
1188
1260
  let currentSyllable = "";
1189
1261
 
1190
- // Better basic syllabification:
1191
1262
  // Go from right to left.
1192
1263
  for (let i = word.length - 1; i >= 0; i--) {
1193
1264
  currentSyllable = word[i] + currentSyllable;
@@ -1200,9 +1271,14 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1200
1271
  currentSyllable = word[i - 1] + currentSyllable;
1201
1272
  i--; // skip the consonant
1202
1273
  } else if (i - 2 >= 0 && !vowels.test(word[i - 2])) {
1203
- // two consonants before this vowel. The one right before belongs to this syllable
1204
- currentSyllable = word[i - 1] + currentSyllable;
1205
- i--;
1274
+ // Two consonants before this vowel. Check if three consonants exist and end in an onset cluster
1275
+ if (i - 3 >= 0 && !vowels.test(word[i - 3]) && ONSET_CLUSTERS.has((word[i - 2] + word[i - 1]).toLowerCase())) {
1276
+ currentSyllable = word[i - 2] + word[i - 1] + currentSyllable;
1277
+ i -= 2;
1278
+ } else {
1279
+ currentSyllable = word[i - 1] + currentSyllable;
1280
+ i--;
1281
+ }
1206
1282
  }
1207
1283
  }
1208
1284
  result.unshift(currentSyllable);
@@ -1236,4 +1312,344 @@ yDFx8r7i9vIJU5HS3moZLkYWAOilMaV9N56A9Bgb6dNcHkvg3NoaYA==
1236
1312
  // If it has both front and back vowels, it breaks harmony.
1237
1313
  return !(hasBack && hasFront);
1238
1314
  }
1315
+
1316
+ /**
1317
+ * Checks if a word follows Turkish Minor Vowel Harmony (Küçük Ünlü Uyumu / Labial Harmony).
1318
+ * Rules:
1319
+ * 1. After an unrounded vowel (a, e, ı, i), only unrounded vowels (a, e, ı, i) can follow.
1320
+ * 2. After a rounded vowel (o, ö, u, ü), either an unrounded wide (a, e) or rounded narrow (u, ü) vowel can follow.
1321
+ * Single-syllable words and words with <=1 vowel are considered compliant by convention.
1322
+ */
1323
+ public static checkLabialHarmony(word: string): boolean {
1324
+ const lower = word.toLocaleLowerCase("tr-TR");
1325
+ const vowels = lower.split("").filter((ch) => "aeıioöuü".includes(ch));
1326
+ if (vowels.length <= 1) return true;
1327
+
1328
+ for (let i = 0; i < vowels.length - 1; i++) {
1329
+ const v1 = vowels[i];
1330
+ const v2 = vowels[i + 1];
1331
+
1332
+ if ("aeıi".includes(v1)) {
1333
+ if (!"aeıi".includes(v2)) return false;
1334
+ } else if ("oöuü".includes(v1)) {
1335
+ if (!"aeuü".includes(v2)) return false;
1336
+ }
1337
+ }
1338
+ return true;
1339
+ }
1340
+
1341
+ /**
1342
+ * Searches TDK headwords using a wildcard / pattern string.
1343
+ * Wildcards:
1344
+ * '_' or '?' matches any single character
1345
+ * '*' matches zero or more characters
1346
+ * Example: "k_l_m" matches "kalem", "kelam", "kilim".
1347
+ * Runs in-memory against TDK's 81k headword list.
1348
+ */
1349
+ public static async patternSearch(pattern: string, options?: PatternSearchOptions): Promise<string[]> {
1350
+ if (!pattern || pattern.trim() === "") return [];
1351
+ await this.ensureAutocompleteLoaded();
1352
+
1353
+ const cleanPattern = pattern.trim().toLocaleLowerCase("tr-TR");
1354
+ const escaped = cleanPattern
1355
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
1356
+ .replace(/[_?]/g, "[\\p{L}]")
1357
+ .replace(/\*/g, "[\\p{L}]*");
1358
+ const regex = new RegExp(`^${escaped}$`, "u");
1359
+
1360
+ const max = options?.maxResults ?? 50;
1361
+ const matches: string[] = [];
1362
+
1363
+ for (const headword of this.autocompleteCache) {
1364
+ const lower = headword.toLocaleLowerCase("tr-TR");
1365
+ if (regex.test(lower)) {
1366
+ matches.push(headword);
1367
+ if (matches.length >= max) break;
1368
+ }
1369
+ }
1370
+ return matches;
1371
+ }
1372
+
1373
+ /**
1374
+ * Finds headwords in TDK that can be formed from the given letters (anagrams).
1375
+ * If exact-length anagrams exist, they are returned.
1376
+ * If none exist (or exactLength is false), valid sub-anagrams (words using a subset of the letters,
1377
+ * minimum 3 letters) are returned, sorted by length descending.
1378
+ */
1379
+ public static async findAnagrams(letters: string, options?: AnagramOptions): Promise<string[]> {
1380
+ if (!letters || letters.trim() === "") return [];
1381
+ await this.ensureAutocompleteLoaded();
1382
+
1383
+ const clean = letters.trim().toLocaleLowerCase("tr-TR").replace(/[^a-zçğıöşüâîû]/gi, "");
1384
+ if (clean.length === 0) return [];
1385
+
1386
+ const forceExact = options?.exactLength === true;
1387
+ const max = options?.maxResults ?? 50;
1388
+
1389
+ const getFrequency = (str: string): Record<string, number> => {
1390
+ const freq: Record<string, number> = {};
1391
+ for (const ch of str) {
1392
+ freq[ch] = (freq[ch] || 0) + 1;
1393
+ }
1394
+ return freq;
1395
+ };
1396
+
1397
+ const targetFreq = getFrequency(clean);
1398
+ const exactMatches: string[] = [];
1399
+ const subMatches: string[] = [];
1400
+
1401
+ for (const headword of this.autocompleteCache) {
1402
+ const lower = headword.toLocaleLowerCase("tr-TR");
1403
+ if (lower.includes(" ") || lower.includes("-")) continue;
1404
+ if (lower.length > clean.length || lower.length < 3) continue;
1405
+
1406
+ const wordFreq = getFrequency(lower);
1407
+ let isValid = true;
1408
+ for (const [ch, count] of Object.entries(wordFreq)) {
1409
+ if (!targetFreq[ch] || targetFreq[ch] < count) {
1410
+ isValid = false;
1411
+ break;
1412
+ }
1413
+ }
1414
+
1415
+ if (isValid && lower !== clean) {
1416
+ if (lower.length === clean.length) {
1417
+ exactMatches.push(headword);
1418
+ } else {
1419
+ subMatches.push(headword);
1420
+ }
1421
+ }
1422
+ }
1423
+
1424
+ if (exactMatches.length > 0 || forceExact) {
1425
+ return exactMatches.slice(0, max);
1426
+ }
1427
+
1428
+ subMatches.sort((a, b) => b.length - a.length || a.localeCompare(b, "tr-TR"));
1429
+ return subMatches.slice(0, max);
1430
+ }
1431
+
1432
+ /**
1433
+ * Finds words in TDK that rhyme with the given word (sharing the same ending suffix/letters).
1434
+ * @param word The target word
1435
+ * @param options.minLetters Minimum number of ending characters that must match (default: 3)
1436
+ * @param options.maxResults Maximum number of rhyme results to return (default: 50)
1437
+ */
1438
+ public static async findRhymes(word: string, options?: RhymeOptions): Promise<string[]> {
1439
+ if (!word || word.trim() === "") return [];
1440
+ await this.ensureAutocompleteLoaded();
1441
+
1442
+ const clean = word.trim().toLocaleLowerCase("tr-TR");
1443
+ const minLetters = Math.min(options?.minLetters ?? 3, clean.length);
1444
+ const max = options?.maxResults ?? 50;
1445
+
1446
+ const suffix = clean.slice(-minLetters);
1447
+ const results: string[] = [];
1448
+
1449
+ for (const headword of this.autocompleteCache) {
1450
+ const lower = headword.toLocaleLowerCase("tr-TR");
1451
+ if (lower !== clean && lower.endsWith(suffix) && !lower.includes(" ")) {
1452
+ results.push(headword);
1453
+ if (results.length >= max) break;
1454
+ }
1455
+ }
1456
+
1457
+ return results;
1458
+ }
1459
+
1460
+ /**
1461
+ * Performs comprehensive spelling, grammar, and syntax proofreading on a Turkish text.
1462
+ * Detects:
1463
+ * 1. Conjunction 'da/de' erroneously joined to verbs or words (e.g. "gitsende" -> "gitsen de")
1464
+ * 2. Conjunction 'ki' erroneously joined to verbs (e.g. "gördümki" -> "gördüm ki"), respecting SOMBAHÇEMİ exceptions
1465
+ * 3. Question particle 'mi/mı/mu/mü' erroneously joined to words (e.g. "geldimi" -> "geldi mi")
1466
+ * 4. Misspelled words with dictionary suggestions (via edit-distance & morphology)
1467
+ */
1468
+ public static async proofread(text: string): Promise<ProofreadResult> {
1469
+ if (!text || text.trim() === "") {
1470
+ return { text: text || "", issues: [], isCorrect: true };
1471
+ }
1472
+
1473
+ await this.ensureAutocompleteLoaded();
1474
+ const issues: ProofreadIssue[] = [];
1475
+
1476
+ const SOMBAHCEMI = new Set([
1477
+ "sanki", "oysaki", "mademki", "belki", "halbuki", "çünkü", "meğerki", "illaki"
1478
+ ]);
1479
+
1480
+ const tokenRegex = /[\p{L}0-9'’]+/gu;
1481
+ let match: RegExpExecArray | null;
1482
+
1483
+ while ((match = tokenRegex.exec(text)) !== null) {
1484
+ const rawWord = match[0];
1485
+ const startIndex = match.index;
1486
+ const endIndex = startIndex + rawWord.length;
1487
+ const lower = rawWord.toLocaleLowerCase("tr-TR");
1488
+
1489
+ if (/^\d+$/.test(lower)) continue;
1490
+
1491
+ let flagged = false;
1492
+
1493
+ // 1. Check Question Particle (mı, mi, mu, mü) erroneously attached
1494
+ const questionMatch = lower.match(/^(.+?)(m[ıiuü](?:sin|sın|sun|sün|siniz|sınız|sunuz|sünüz|yiz|yız|yuz|yüz|m|k)?)$/);
1495
+ if (questionMatch) {
1496
+ const base = questionMatch[1];
1497
+ const particle = questionMatch[2];
1498
+ if (base.length >= 2 && (await this.isHeadword(base) || (await this.findRoot(base)) !== null)) {
1499
+ if (!(await this.isHeadword(lower))) {
1500
+ issues.push({
1501
+ type: "question_particle",
1502
+ word: rawWord,
1503
+ startIndex,
1504
+ endIndex,
1505
+ suggestion: `${base} ${particle}`,
1506
+ message: `'${particle}' soru eki kendinden önceki kelimeden ayrı yazılmalıdır.`,
1507
+ });
1508
+ flagged = true;
1509
+ }
1510
+ }
1511
+ }
1512
+
1513
+ const VERB_CONJUGATION_REGEX =
1514
+ /(?:d[ıiuü][kmmn]?|t[ıiuü][kmmn]?|d[ıiuü]n[ıiuü]z?|t[ıiuü]n[ıiuü]z?|m[ıiuü]ş(?:[szn][ıiuü]z?|lar)?|yor(?:um|sun|uz|lar)?|ecek(?:sin|iz|ler)?|acak(?:sın|ız|lar)?|s[ae][mnk]|s[ae]n[ıiz]?|meli|malı|me[mz]|ma[mz])$/i;
1515
+
1516
+ // 2. Check Conjunction 'ki' erroneously attached to verbs
1517
+ if (!flagged && lower.endsWith("ki") && lower.length > 3) {
1518
+ const base = lower.slice(0, -2);
1519
+ if (!SOMBAHCEMI.has(lower)) {
1520
+ if (!(await this.isHeadword(lower))) {
1521
+ const root = await this.findRoot(base);
1522
+ const isVerb =
1523
+ (root && (root.endsWith("mek") || root.endsWith("mak"))) ||
1524
+ base === "demek" ||
1525
+ base === "kaldı" ||
1526
+ base === "yeter" ||
1527
+ base === "bilmem" ||
1528
+ VERB_CONJUGATION_REGEX.test(base);
1529
+
1530
+ if (isVerb) {
1531
+ issues.push({
1532
+ type: "conjunction_ki",
1533
+ word: rawWord,
1534
+ startIndex,
1535
+ endIndex,
1536
+ suggestion: `${base} ki`,
1537
+ message: `'ki' bağlacı ayrı yazılmalıdır.`,
1538
+ });
1539
+ flagged = true;
1540
+ }
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ // 3. Check Conjunction 'da/de/ta/te' erroneously attached to verbs
1546
+ if (!flagged && (lower.endsWith("de") || lower.endsWith("da") || lower.endsWith("te") || lower.endsWith("ta")) && lower.length > 3) {
1547
+ const base = lower.slice(0, -2);
1548
+ const ending = lower.slice(-2);
1549
+ if (!(await this.isHeadword(lower))) {
1550
+ const root = await this.findRoot(base);
1551
+ const isVerb =
1552
+ (root && (root.endsWith("mek") || root.endsWith("mak"))) ||
1553
+ VERB_CONJUGATION_REGEX.test(base);
1554
+
1555
+ if (isVerb) {
1556
+ const correctEnding = ending.startsWith("t") ? (ending === "te" ? "de" : "da") : ending;
1557
+ issues.push({
1558
+ type: "conjunction_da",
1559
+ word: rawWord,
1560
+ startIndex,
1561
+ endIndex,
1562
+ suggestion: `${base} ${correctEnding}`,
1563
+ message: `'da/de' bağlacı fiillerden sonra her zaman ayrı yazılır (bağlaç olan da/de sertleşmez).`,
1564
+ });
1565
+ flagged = true;
1566
+ }
1567
+ }
1568
+ }
1569
+
1570
+ // 4. General Spell Check
1571
+ if (!flagged) {
1572
+ const check = await this.checkSpelling(rawWord);
1573
+ if (!check.isCorrect) {
1574
+ issues.push({
1575
+ type: "spelling",
1576
+ word: rawWord,
1577
+ startIndex,
1578
+ endIndex,
1579
+ suggestion: check.suggestion,
1580
+ message: check.suggestion
1581
+ ? `'${rawWord}' yanlış yazılmış olabilir. Öneri: '${check.suggestion}'`
1582
+ : `'${rawWord}' sözlükte bulunamadı.`,
1583
+ });
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+ return {
1589
+ text,
1590
+ issues,
1591
+ isCorrect: issues.length === 0,
1592
+ };
1593
+ }
1239
1594
  }
1595
+
1596
+ /**
1597
+ * Configurable instance-based client for TDK API.
1598
+ * Useful for multi-tenant applications or backend services requiring isolated configurations.
1599
+ */
1600
+ export class TDKClient {
1601
+ constructor(config?: TDKConfig) {
1602
+ if (config) {
1603
+ TDK.configure(config);
1604
+ }
1605
+ }
1606
+
1607
+ public getWord(word: string): Promise<WordInfo[]> {
1608
+ return TDK.getWord(word);
1609
+ }
1610
+
1611
+ public getMeanings(word: string): Promise<string[]> {
1612
+ return TDK.getMeanings(word);
1613
+ }
1614
+
1615
+ public checkSpelling(word: string): Promise<SpellCheckResult> {
1616
+ return TDK.checkSpelling(word);
1617
+ }
1618
+
1619
+ public findRoot(word: string): Promise<string | null> {
1620
+ return TDK.findRoot(word);
1621
+ }
1622
+
1623
+ public stem(word: string): Promise<StemResult | null> {
1624
+ return TDK.stem(word);
1625
+ }
1626
+
1627
+ public proofread(text: string): Promise<ProofreadResult> {
1628
+ return TDK.proofread(text);
1629
+ }
1630
+
1631
+ public patternSearch(pattern: string, options?: PatternSearchOptions): Promise<string[]> {
1632
+ return TDK.patternSearch(pattern, options);
1633
+ }
1634
+
1635
+ public findAnagrams(letters: string, options?: AnagramOptions): Promise<string[]> {
1636
+ return TDK.findAnagrams(letters, options);
1637
+ }
1638
+
1639
+ public findRhymes(word: string, options?: RhymeOptions): Promise<string[]> {
1640
+ return TDK.findRhymes(word, options);
1641
+ }
1642
+
1643
+ public syllabicate(word: string): string[] {
1644
+ return TDK.syllabicate(word);
1645
+ }
1646
+
1647
+ public checkVowelHarmony(word: string): boolean {
1648
+ return TDK.checkVowelHarmony(word);
1649
+ }
1650
+
1651
+ public checkLabialHarmony(word: string): boolean {
1652
+ return TDK.checkLabialHarmony(word);
1653
+ }
1654
+ }
1655
+
package/src/types.ts CHANGED
@@ -118,6 +118,7 @@ export interface WordComparisonSide {
118
118
  origin: string | null;
119
119
  syllables: string[];
120
120
  harmony: boolean;
121
+ labialHarmony?: boolean;
121
122
  }
122
123
 
123
124
  export interface WordComparison {
@@ -134,6 +135,42 @@ export interface WordAnalysis {
134
135
  isInflected?: boolean;
135
136
  }
136
137
 
138
+ export interface ProofreadIssue {
139
+ type: "spelling" | "conjunction_da" | "conjunction_ki" | "question_particle";
140
+ word: string;
141
+ startIndex: number;
142
+ endIndex: number;
143
+ suggestion?: string;
144
+ message: string;
145
+ }
146
+
147
+ export interface ProofreadResult {
148
+ text: string;
149
+ issues: ProofreadIssue[];
150
+ isCorrect: boolean;
151
+ }
152
+
153
+ export interface PatternSearchOptions {
154
+ maxResults?: number;
155
+ }
156
+
157
+ export interface AnagramOptions {
158
+ exactLength?: boolean;
159
+ maxResults?: number;
160
+ }
161
+
162
+ export interface RhymeOptions {
163
+ minLetters?: number;
164
+ maxResults?: number;
165
+ }
166
+
167
+ export interface TDKConfig {
168
+ timeoutMs?: number;
169
+ retries?: number;
170
+ cache?: boolean;
171
+ maxCacheSize?: number;
172
+ }
173
+
137
174
  export interface KubbealtiEntry {
138
175
  kelime: string;
139
176
  anlam: string;
@@ -145,3 +182,4 @@ export interface WiktionaryEntry {
145
182
  }
146
183
 
147
184
  export type TDKResponse = WordInfo[] | { error: string };
185
+
@@ -0,0 +1,52 @@
1
+ const assert = require("node:assert");
2
+ const { TDK } = require("../dist/index.js");
3
+
4
+ async function runTests() {
5
+ console.log("=== Running Grammar & Phonology Unit Tests ===");
6
+
7
+ // 1. Büyük Ünlü Uyumu (Major Vowel Harmony)
8
+ console.log("1. Testing checkVowelHarmony (Büyük Ünlü Uyumu)...");
9
+ assert.strictEqual(TDK.checkVowelHarmony("adım"), true);
10
+ assert.strictEqual(TDK.checkVowelHarmony("kapı"), true);
11
+ assert.strictEqual(TDK.checkVowelHarmony("gözlük"), true);
12
+ assert.strictEqual(TDK.checkVowelHarmony("otobüs"), false);
13
+ assert.strictEqual(TDK.checkVowelHarmony("kalem"), false);
14
+ assert.strictEqual(TDK.checkVowelHarmony("kitap"), false);
15
+ assert.strictEqual(TDK.checkVowelHarmony("ev"), true);
16
+ console.log(" ✓ checkVowelHarmony passed.");
17
+
18
+ // 2. Küçük Ünlü Uyumu (Minor Vowel / Labial Harmony)
19
+ console.log("2. Testing checkLabialHarmony (Küçük Ünlü Uyumu)...");
20
+ assert.strictEqual(TDK.checkLabialHarmony("elma"), true);
21
+ assert.strictEqual(TDK.checkLabialHarmony("kalem"), true);
22
+ assert.strictEqual(TDK.checkLabialHarmony("odun"), true);
23
+ assert.strictEqual(TDK.checkLabialHarmony("kömür"), true);
24
+ assert.strictEqual(TDK.checkLabialHarmony("çocuk"), true);
25
+ assert.strictEqual(TDK.checkLabialHarmony("armut"), false);
26
+ assert.strictEqual(TDK.checkLabialHarmony("yağmur"), false);
27
+ assert.strictEqual(TDK.checkLabialHarmony("tavuk"), false);
28
+ assert.strictEqual(TDK.checkLabialHarmony("doktor"), false);
29
+ assert.strictEqual(TDK.checkLabialHarmony("horoz"), false);
30
+ assert.strictEqual(TDK.checkLabialHarmony("müzik"), false);
31
+ assert.strictEqual(TDK.checkLabialHarmony("ev"), true);
32
+ console.log(" ✓ checkLabialHarmony passed.");
33
+
34
+ // 3. Heceleme (Syllabification)
35
+ console.log("3. Testing syllabicate...");
36
+ assert.deepStrictEqual(TDK.syllabicate("kalem"), ["ka", "lem"]);
37
+ assert.deepStrictEqual(TDK.syllabicate("ilkokul"), ["il", "ko", "kul"]);
38
+ assert.deepStrictEqual(TDK.syllabicate("muvaffakiyet"), ["mu", "vaf", "fa", "ki", "yet"]);
39
+ assert.deepStrictEqual(TDK.syllabicate("elektrik"), ["e", "lek", "trik"]);
40
+ assert.deepStrictEqual(TDK.syllabicate("kontrol"), ["kon", "trol"]);
41
+ assert.deepStrictEqual(TDK.syllabicate("orkestra"), ["or", "kes", "tra"]);
42
+ assert.deepStrictEqual(TDK.syllabicate("kral"), ["kral"]);
43
+ assert.deepStrictEqual(TDK.syllabicate("tren"), ["tren"]);
44
+ console.log(" ✓ syllabicate passed.");
45
+
46
+ console.log("\n All grammar & phonology tests passed successfully!");
47
+ }
48
+
49
+ runTests().catch((err) => {
50
+ console.error("Test failed:", err);
51
+ process.exit(1);
52
+ });