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/README.md +43 -14
- package/dist/{chunk-5TYJDVHK.mjs → chunk-7KJHYRJZ.mjs} +426 -15
- package/dist/cli.js +559 -19
- package/dist/cli.mjs +180 -5
- package/dist/index.d.mts +119 -1
- package/dist/index.d.ts +119 -1
- package/dist/index.js +430 -16
- package/dist/index.mjs +9 -3
- package/package.json +2 -2
- package/src/cli.ts +185 -4
- package/src/index.ts +1 -1
- package/src/morphology.ts +115 -5
- package/src/tdk.ts +428 -12
- package/src/types.ts +38 -0
- package/test/grammar.test.js +52 -0
- package/test/morphology.test.js +25 -1
- package/test/proofread.test.js +60 -0
- package/test/tools.test.js +51 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
398
|
+
this.setBoundedCache(this.stemCache, clean, candidate);
|
|
332
399
|
return candidate;
|
|
333
400
|
}
|
|
334
401
|
}
|
|
335
402
|
|
|
336
|
-
this.stemCache
|
|
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
|
-
//
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
+
});
|