tdk-api-wrapper 1.6.0 → 1.6.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/package.json +23 -3
- package/.github/workflows/publish.yml +0 -41
- package/src/cli.ts +0 -535
- package/src/errors.ts +0 -38
- package/src/index.ts +0 -4
- package/src/morphology.ts +0 -333
- package/src/tdk.ts +0 -2056
- package/src/types.ts +0 -185
- package/test/grammar.test.js +0 -52
- package/test/morphology.test.js +0 -140
- package/test/proofread.test.js +0 -88
- package/test/tools.test.js +0 -51
- package/tsconfig.json +0 -11
package/src/morphology.ts
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Turkish Morphology Engine & Stem Candidate Generator.
|
|
3
|
-
*
|
|
4
|
-
* Implements heuristic-based progressive suffix stripping (BFS) with:
|
|
5
|
-
* 1. Comprehensive Turkish suffix catalogue (inflectional, derivational, composite)
|
|
6
|
-
* 2. Reverse consonant mutation (ünsüz yumuşaması / sertleşmesi: b->p, c->ç, d->t, ğ->k, g->k)
|
|
7
|
-
* 3. Reverse vowel drop (ünlü düşmesi: akl->akıl, şehr->şehir, omz->omuz)
|
|
8
|
-
* 4. Infinitive restoration (-mek / -mak for verbal stems)
|
|
9
|
-
* 5. Apostrophe stripping for proper nouns (İstanbul'da -> İstanbul)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export const TURKISH_VOWELS = "aeıioöuü";
|
|
13
|
-
|
|
14
|
-
export function isVowel(ch: string): boolean {
|
|
15
|
-
return TURKISH_VOWELS.includes(ch);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Turkish suffixes ordered strictly by descending length so that longer
|
|
20
|
-
* composite suffixes match before their individual subcomponents.
|
|
21
|
-
*/
|
|
22
|
-
export const TURKISH_SUFFIXES: readonly string[] = [
|
|
23
|
-
// 9-letter composite suffixes
|
|
24
|
-
"lerimizden", "larımızdan", "lerinizden", "larınızdan",
|
|
25
|
-
// 8-letter composite suffixes
|
|
26
|
-
"lerinin", "larının", "lerinde", "larında", "lerinden", "larından",
|
|
27
|
-
"leriyle", "larıyla", "lerini", "larını", "lerimize", "larımıza",
|
|
28
|
-
"lerimizle", "larımızla", "lerinizin", "larınızın", "lerinizde", "larınızda",
|
|
29
|
-
"dığından", "diğinden", "duğundan", "düğünden", "tığından", "tiğinden", "tuğundan", "tüğünden",
|
|
30
|
-
// 7-letter composite suffixes
|
|
31
|
-
"ecektir", "acaktır", "eceğim", "acağım", "eceksin", "acaksın",
|
|
32
|
-
"eceğiz", "acağız", "lerimiz", "larımız", "leriniz", "larınız",
|
|
33
|
-
"umuzdan", "ümüzden", "inizden", "ınızdan", "ünüzden",
|
|
34
|
-
"dığında", "diğinde", "duğunda", "düğünde", "tığında", "tiğinde", "tuğunda", "tüğünde",
|
|
35
|
-
"masına", "mesine", "ıyorsunuz", "iyorsunuz", "uyorsunuz", "üyorsunuz", "yorsunuz",
|
|
36
|
-
// 6-letter composite suffixes
|
|
37
|
-
"iyorsa", "iyorduk", "iyordu", "iyormuş", "ıyorsa", "ıyorduk", "ıyordu", "ıyormuş",
|
|
38
|
-
"uyorsa", "uyorduk", "uyordu", "uyormuş", "üyorsa", "üyorduk", "üyordu", "üyormuş",
|
|
39
|
-
"ıyorsun", "iyorsun", "uyorsun", "üyorsun", "ıyorlar", "iyorlar", "uyorlar", "üyorlar",
|
|
40
|
-
"iyoruz", "ıyoruz", "uyoruz", "üyoruz",
|
|
41
|
-
"imizin", "ımızın", "umuzun", "ümüzün", "imizde", "ımızda", "umuzda", "ümüzde",
|
|
42
|
-
"imizden", "ımızdan", "imizle", "ımızla", "umuzla", "ümüzle",
|
|
43
|
-
"lerdir", "lardır", "muştur", "miştir", "muştur", "müştür",
|
|
44
|
-
"lerden", "lardan", "lerine", "larına", "leriyle", "larıyla",
|
|
45
|
-
"seniz", "sanız", "diniz", "dınız", "dunuz", "dünüz", "tiniz", "tınız", "tunuz", "tünüz",
|
|
46
|
-
"siniz", "sınız", "sunuz", "sünüz",
|
|
47
|
-
"dıkça", "dikçe", "dukça", "dükçe", "tıkça", "tikçe", "tukça", "tükçe",
|
|
48
|
-
"ırken", "irken", "urken", "ürken", "arken", "erken",
|
|
49
|
-
// 5-letter suffixes
|
|
50
|
-
"lerde", "larda", "lerle", "larla", "lerin", "ların", "lerim", "larım",
|
|
51
|
-
"dirler", "dırlar", "dürler", "durlar", "tirler", "tırlar", "türler", "turlar",
|
|
52
|
-
"siniz", "sınız", "sunuz", "sünüz", "yorum", "yorsun", "uyoruz", "yorsunuz", "yorlar",
|
|
53
|
-
"eceks", "acaks", "eyim", "ayım",
|
|
54
|
-
"indik", "ındık", "unduk", "ündük", "ildik", "ıldık", "ulduk", "üldük",
|
|
55
|
-
"meden", "madan", "yınız", "yiniz", "yunuz", "yünüz",
|
|
56
|
-
// 4-letter suffixes
|
|
57
|
-
"imiz", "ımız", "umuz", "ümüz", "iniz", "ınız", "unuz", "ünüz",
|
|
58
|
-
"leri", "ları", "idir", "ıdır", "udur", "üdür", "ecek", "acak",
|
|
59
|
-
"erek", "arak", "ince", "ınca", "unca", "ünce", "ken",
|
|
60
|
-
"meli", "malı", "iyor", "ıyor", "uyor", "üyor",
|
|
61
|
-
"mişti", "mıştı", "muştu", "müştü", "seydi", "saydı",
|
|
62
|
-
"ydim", "ydım", "ydum", "ydüm", "tiler", "tılar", "diler", "dılar",
|
|
63
|
-
"ikten", "ıktan", "uktan", "ükten",
|
|
64
|
-
// 3-letter suffixes
|
|
65
|
-
"ler", "lar", "den", "dan", "ten", "tan", "dir", "dır", "dur", "dür",
|
|
66
|
-
"tir", "tır", "tur", "tür", "nin", "nın", "nun", "nün", "yle", "yla",
|
|
67
|
-
"miş", "mış", "muş", "müş", "dim", "dım", "dum", "düm", "tim", "tım", "tum", "tüm",
|
|
68
|
-
"din", "dın", "dun", "dün", "tin", "tın", "tun", "tün", "dik", "dık", "duk", "dük",
|
|
69
|
-
"tik", "tık", "tuk", "tük", "ydi", "ydı", "ydu", "ydü", "yim", "yım", "yum", "yüm",
|
|
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",
|
|
72
|
-
"ici", "ıcı", "ucu", "ücü", "gen", "gan", "ken", "kan",
|
|
73
|
-
"len", "lan", "leş", "laş", "mek", "mak", "yor",
|
|
74
|
-
// 2-letter suffixes
|
|
75
|
-
"de", "da", "te", "ta", "im", "ım", "um", "üm", "in", "ın", "un", "ün",
|
|
76
|
-
"iz", "ız", "uz", "üz",
|
|
77
|
-
"si", "sı", "su", "sü", "ye", "ya", "le", "la", "di", "dı", "du", "dü",
|
|
78
|
-
"ti", "tı", "tu", "tü", "se", "sa", "ce", "ca", "çe", "ça", "me", "ma",
|
|
79
|
-
"ip", "ıp", "up", "üp", "en", "an", "iş", "ış", "uş", "üş",
|
|
80
|
-
"li", "lı", "lu", "lü", "ci", "cı", "cu", "cü", "çi", "çı", "çu", "çü",
|
|
81
|
-
// 1-letter suffixes (vowels / basic case endings)
|
|
82
|
-
"e", "a", "i", "ı", "u", "ü"
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Reverses Turkish consonant softening (ünsüz yumuşaması):
|
|
87
|
-
* When a root ends with p, ç, t, k, it softens to b, c, d, ğ, g before a vowel.
|
|
88
|
-
* This restores the hardened dictionary headword form.
|
|
89
|
-
*/
|
|
90
|
-
export function restoreConsonantSoftening(stem: string): string[] {
|
|
91
|
-
if (stem.length < 2) return [];
|
|
92
|
-
const last = stem.slice(-1);
|
|
93
|
-
const base = stem.slice(0, -1);
|
|
94
|
-
switch (last) {
|
|
95
|
-
case "b": return [base + "p"];
|
|
96
|
-
case "c": return [base + "ç"];
|
|
97
|
-
case "d": return [base + "t"];
|
|
98
|
-
case "ğ": return [base + "k"];
|
|
99
|
-
case "g": return [base + "k"];
|
|
100
|
-
default: return [];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Reverses Turkish vowel drop (ünlü düşmesi):
|
|
106
|
-
* In words like akıl->aklım, şehir->şehre, burun->burnu, omuz->omzum,
|
|
107
|
-
* the narrow vowel in the second syllable drops when receiving a vowel-initial suffix.
|
|
108
|
-
* This restores the harmonic dropped vowel between the final consonant cluster.
|
|
109
|
-
*/
|
|
110
|
-
export function restoreVowelDrop(stem: string): string[] {
|
|
111
|
-
if (stem.length < 3) return [];
|
|
112
|
-
const c1 = stem[stem.length - 2];
|
|
113
|
-
const c2 = stem[stem.length - 1];
|
|
114
|
-
if (!isVowel(c1) && !isVowel(c2)) {
|
|
115
|
-
// Look for the last vowel prior to the cluster
|
|
116
|
-
const vowelsInBase = stem.slice(0, -2).split("").filter(isVowel);
|
|
117
|
-
if (vowelsInBase.length > 0) {
|
|
118
|
-
const lastVowel = vowelsInBase[vowelsInBase.length - 1];
|
|
119
|
-
let inserted = "i";
|
|
120
|
-
if ("aı".includes(lastVowel)) inserted = "ı";
|
|
121
|
-
else if ("ei".includes(lastVowel)) inserted = "i";
|
|
122
|
-
else if ("ou".includes(lastVowel)) inserted = "u";
|
|
123
|
-
else if ("öü".includes(lastVowel)) inserted = "ü";
|
|
124
|
-
return [stem.slice(0, -1) + inserted + c2];
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return [];
|
|
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
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Restores verb infinitive headword form (-mek / -mak):
|
|
187
|
-
* Since TDK registers verbs in their infinitive form (e.g. okumak, gelmek, yazmak),
|
|
188
|
-
* conjugated verb stems (e.g. oku, gel, yaz) need -mak/-mek appended according to vowel harmony.
|
|
189
|
-
*/
|
|
190
|
-
export function restoreInfinitive(stem: string): string[] {
|
|
191
|
-
if (stem.length < 2) return [];
|
|
192
|
-
const vowelsInBase = stem.split("").filter(isVowel);
|
|
193
|
-
if (vowelsInBase.length === 0) return [];
|
|
194
|
-
const lastVowel = vowelsInBase[vowelsInBase.length - 1];
|
|
195
|
-
return "aıou".includes(lastVowel) ? [stem + "mak"] : [stem + "mek"];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Generates candidate roots for a given Turkish word using progressive BFS suffix stripping,
|
|
200
|
-
* consonant mutation restoration, vowel drop restoration, and infinitive restoration.
|
|
201
|
-
*
|
|
202
|
-
* Candidates are sorted so that longer base stems (less aggressive stripping) are checked first,
|
|
203
|
-
* preventing spurious 2-letter roots from overshadowing genuine headwords.
|
|
204
|
-
*
|
|
205
|
-
* @param word The input word to analyze
|
|
206
|
-
* @param minStemLength Minimum allowed length for candidate stems (default: 2)
|
|
207
|
-
* @param maxDepth Maximum levels of progressive suffix stripping (default: 4)
|
|
208
|
-
* @returns Array of unique candidate roots in prioritized order
|
|
209
|
-
*/
|
|
210
|
-
export function getStemCandidates(
|
|
211
|
-
word: string,
|
|
212
|
-
minStemLength: number = 2,
|
|
213
|
-
maxDepth: number = 4
|
|
214
|
-
): string[] {
|
|
215
|
-
if (!word || word.trim().length === 0) return [];
|
|
216
|
-
|
|
217
|
-
const raw = word.trim();
|
|
218
|
-
const normalized = raw.toLocaleLowerCase("tr-TR");
|
|
219
|
-
|
|
220
|
-
const candidatesWithWeight: { candidate: string; baseLength: number }[] = [];
|
|
221
|
-
const seen = new Set<string>();
|
|
222
|
-
|
|
223
|
-
// If proper noun contains apostrophe (e.g. "İstanbul'da", "Ankara'dan"),
|
|
224
|
-
// the part before the apostrophe is an immediate high-priority candidate.
|
|
225
|
-
if (raw.includes("'") || raw.includes("’")) {
|
|
226
|
-
const apostropheStem = normalized.split(/['’]/)[0];
|
|
227
|
-
if (apostropheStem.length >= minStemLength) {
|
|
228
|
-
candidatesWithWeight.push({ candidate: apostropheStem, baseLength: apostropheStem.length + 10 });
|
|
229
|
-
seen.add(apostropheStem);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
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
|
-
|
|
242
|
-
let frontier = [normalized];
|
|
243
|
-
|
|
244
|
-
for (let depth = 0; depth < maxDepth; depth++) {
|
|
245
|
-
const nextFrontier: string[] = [];
|
|
246
|
-
|
|
247
|
-
for (const current of frontier) {
|
|
248
|
-
for (const suffix of TURKISH_SUFFIXES) {
|
|
249
|
-
if (current.length - suffix.length >= minStemLength && current.endsWith(suffix)) {
|
|
250
|
-
const stem = current.slice(0, -suffix.length);
|
|
251
|
-
|
|
252
|
-
const hardened = restoreConsonantSoftening(stem);
|
|
253
|
-
const vowelDropped = restoreVowelDrop(stem);
|
|
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];
|
|
299
|
-
const infinitives = verbalBases.flatMap((v) => restoreInfinitive(v));
|
|
300
|
-
|
|
301
|
-
// Base candidates
|
|
302
|
-
const variants = [stem, ...hardened, ...vowelDropped, ...geminated, ...narrowed];
|
|
303
|
-
|
|
304
|
-
for (const variant of variants) {
|
|
305
|
-
if (!seen.has(variant) && variant !== normalized) {
|
|
306
|
-
seen.add(variant);
|
|
307
|
-
nextFrontier.push(variant);
|
|
308
|
-
candidatesWithWeight.push({ candidate: variant, baseLength: stem.length });
|
|
309
|
-
}
|
|
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
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (nextFrontier.length === 0) break;
|
|
326
|
-
frontier = nextFrontier;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Sort candidates by baseLength descending (longer stem = higher priority)
|
|
330
|
-
candidatesWithWeight.sort((a, b) => b.baseLength - a.baseLength);
|
|
331
|
-
|
|
332
|
-
return [...new Set(candidatesWithWeight.map((c) => c.candidate))];
|
|
333
|
-
}
|