feat(core): Update opengrammar/backend/src/analyzer-simple.ts (Commit #29)

Swadhin Biswas committed 7 months ago
271690d
1 parent 30ef1f7
commit 271690d
1 file changed
+528 -0
Showing 1 changed file+528-0
▼opengrammar/backend/src/analyzer-simple.ts
+528-0
1
+import type { Issue, CustomRule } from './shared-types.js';
2
+
3
+export class RuleBasedAnalyzer {
4
+ private static dictionary: Set<string> = new Set();
5
+ private static customRules: CustomRule[] = [];
6
+
7
+ static analyze(text: string, options?: { dictionary?: string[]; customRules?: CustomRule[] }): Issue[] {
8
+ const issues: Issue[] = [];
9
+
10
+ if (options?.dictionary) {
11
+ this.dictionary = new Set(options.dictionary.map(w => w.toLowerCase()));
12
+ }
13
+
14
+ if (options?.customRules) {
15
+ this.customRules = options.customRules;
16
+ }
17
+
18
+ // Run all checks
19
+ issues.push(...this.checkRepetition(text));
20
+ issues.push(...this.checkSpacingErrors(text));
21
+ issues.push(...this.checkBasicGrammar(text));
22
+ issues.push(...this.checkCommonMisspellings(text));
23
+ issues.push(...this.checkPassiveVoice(text));
24
+ issues.push(...this.checkItsIt(text));
25
+ issues.push(...this.checkYourYoure(text));
26
+ issues.push(...this.checkTheirThereTheyre(text));
27
+ issues.push(...this.checkApostropheErrors(text));
28
+ issues.push(...this.checkWeakWords(text));
29
+ issues.push(...this.checkRedundantPhrases(text));
30
+ issues.push(...this.checkLongSentences(text));
31
+ issues.push(...this.checkCustomRules(text));
32
+
33
+ return issues;
34
+ }
35
+
36
+ private static checkBasicGrammar(text: string): Issue[] {
37
+ const issues: Issue[] = [];
38
+
39
+ const grammarRules: Array<{pattern: RegExp, suggestion: string, reason: string}> = [
40
+ {
41
+ pattern: /\b(me|him|her|them|us)\s+and\s+(I|he|she|they|we)\b/gi,
42
+ suggestion: '$2 and $1',
43
+ reason: 'Use subject pronouns (I, he, she, they, we) when they are part of the subject.'
44
+ },
45
+ {
46
+ pattern: /\bbuyed\b/gi,
47
+ suggestion: 'bought',
48
+ reason: '"Buyed" is not a word. The past tense of "buy" is "bought".'
49
+ },
50
+ {
51
+ pattern: /\brunned\b/gi,
52
+ suggestion: 'ran',
53
+ reason: '"Runned" is not a word. The past tense of "run" is "ran".'
54
+ },
55
+ {
56
+ pattern: /\bgoed\b/gi,
57
+ suggestion: 'went',
58
+ reason: '"Goed" is not a word. The past tense of "go" is "went".'
59
+ },
60
+ {
61
+ pattern: /\beated\b/gi,
62
+ suggestion: 'ate',
63
+ reason: '"Eated" is not a word. The past tense of "eat" is "ate".'
64
+ },
65
+ {
66
+ pattern: /\bseed\b/gi,
67
+ suggestion: 'saw',
68
+ reason: '"Seed" is not the past tense. The past tense of "see" is "saw".'
69
+ },
70
+ {
71
+ pattern: /\bcomed\b/gi,
72
+ suggestion: 'came',
73
+ reason: '"Comed" is not a word. The past tense of "come" is "came".'
74
+ },
75
+ {
76
+ pattern: /\btaked\b/gi,
77
+ suggestion: 'took',
78
+ reason: '"Taked" is not a word. The past tense of "take" is "took".'
79
+ },
80
+ {
81
+ pattern: /\bbringed\b/gi,
82
+ suggestion: 'brought',
83
+ reason: '"Bringed" is not a word. The past tense of "bring" is "brought".'
84
+ },
85
+ {
86
+ pattern: /\bthinked\b/gi,
87
+ suggestion: 'thought',
88
+ reason: '"Thinked" is not a word. The past tense of "think" is "thought".'
89
+ },
90
+ {
91
+ pattern: /\bknowed\b/gi,
92
+ suggestion: 'knew',
93
+ reason: '"Knowed" is not a word. The past tense of "know" is "knew".'
94
+ },
95
+ {
96
+ pattern: /\bwanna\b/gi,
97
+ suggestion: 'want to',
98
+ reason: '"Wanna" is informal. Use "want to" in formal writing.'
99
+ },
100
+ {
101
+ pattern: /\bgotta\b/gi,
102
+ suggestion: 'have to',
103
+ reason: '"Gotta" is informal. Use "have to" in formal writing.'
104
+ },
105
+ {
106
+ pattern: /\bkinda\b/gi,
107
+ suggestion: 'kind of',
108
+ reason: '"Kinda" is informal. Use "kind of" in formal writing.'
109
+ },
110
+ {
111
+ pattern: /\bcould\s+of\b/gi,
112
+ suggestion: 'could have',
113
+ reason: '"Could of" is incorrect. Use "could have".'
114
+ },
115
+ {
116
+ pattern: /\bwould\s+of\b/gi,
117
+ suggestion: 'would have',
118
+ reason: '"Would of" is incorrect. Use "would have".'
119
+ },
120
+ {
121
+ pattern: /\bshould\s+of\b/gi,
122
+ suggestion: 'should have',
123
+ reason: '"Should of" is incorrect. Use "should have".'
124
+ },
125
+ {
126
+ pattern: /\balot\b/gi,
127
+ suggestion: 'a lot',
128
+ reason: '"Alot" is misspelled. Use "a lot" (two words).'
129
+ },
130
+ {
131
+ pattern: /\bshouldnt\b/gi,
132
+ suggestion: "shouldn't",
133
+ reason: 'Missing apostrophe in "shouldn\'t".'
134
+ },
135
+ {
136
+ pattern: /\bcouldnt\b/gi,
137
+ suggestion: "couldn't",
138
+ reason: 'Missing apostrophe in "couldn\'t".'
139
+ },
140
+ {
141
+ pattern: /\bwouldnt\b/gi,
142
+ suggestion: "wouldn't",
143
+ reason: 'Missing apostrophe in "wouldn\'t".'
144
+ },
145
+ {
146
+ pattern: /\bdidnt\b/gi,
147
+ suggestion: "didn't",
148
+ reason: 'Missing apostrophe in "didn\'t".'
149
+ },
150
+ {
151
+ pattern: /\bdoesnt\b/gi,
152
+ suggestion: "doesn't",
153
+ reason: 'Missing apostrophe in "doesn\'t".'
154
+ },
155
+ {
156
+ pattern: /\bisnt\b/gi,
157
+ suggestion: "isn't",
158
+ reason: 'Missing apostrophe in "isn\'t".'
159
+ },
160
+ {
161
+ pattern: /\barent\b/gi,
162
+ suggestion: "aren't",
163
+ reason: 'Missing apostrophe in "aren\'t".'
164
+ },
165
+ {
166
+ pattern: /\bwasnt\b/gi,
167
+ suggestion: "wasn't",
168
+ reason: 'Missing apostrophe in "wasn\'t".'
169
+ },
170
+ {
171
+ pattern: /\bwerent\b/gi,
172
+ suggestion: "weren't",
173
+ reason: 'Missing apostrophe in "weren\'t".'
174
+ },
175
+ {
176
+ pattern: /\bdont\b/gi,
177
+ suggestion: "don't",
178
+ reason: 'Missing apostrophe in "don\'t".'
179
+ },
180
+ {
181
+ pattern: /\bwrited\b/gi,
182
+ suggestion: 'wrote',
183
+ reason: '"Writed" is not a word. The past tense of "write" is "wrote".'
184
+ },
185
+ {
186
+ pattern: /\bwas\s+(\w+ed)\b/gi,
187
+ suggestion: 'Consider active voice',
188
+ reason: 'Passive voice construction detected.'
189
+ },
190
+ {
191
+ pattern: /\bwere\s+(\w+ed)\b/gi,
192
+ suggestion: 'Consider active voice',
193
+ reason: 'Passive voice construction detected.'
194
+ },
195
+ ];
196
+
197
+ for (const rule of grammarRules) {
198
+ const matches = text.matchAll(rule.pattern);
199
+ for (const match of matches) {
200
+ if (match[0] && match.index !== undefined) {
201
+ issues.push({
202
+ type: 'grammar',
203
+ original: match[0],
204
+ suggestion: match[0].replace(rule.pattern, rule.suggestion),
205
+ reason: rule.reason,
206
+ offset: match.index,
207
+ length: match[0].length,
208
+ });
209
+ }
210
+ }
211
+ }
212
+
213
+ return issues;
214
+ }
215
+
216
+ private static checkCommonMisspellings(text: string): Issue[] {
217
+ const issues: Issue[] = [];
218
+
219
+ const commonMisspellings: Record<string, string> = {
220
+ 'teh': 'the',
221
+ 'taht': 'that',
222
+ 'waht': 'what',
223
+ 'whta': 'what',
224
+ 'hte': 'the',
225
+ 'iwth': 'with',
226
+ 'witht': 'with',
227
+ 'adn': 'and',
228
+ 'nad': 'and',
229
+ 'becuase': 'because',
230
+ 'becasue': 'because',
231
+ 'beacuse': 'because',
232
+ 'becomeing': 'becoming',
233
+ 'begining': 'beginning',
234
+ 'buisness': 'business',
235
+ 'calender': 'calendar',
236
+ 'cant': "can't",
237
+ 'collegue': 'colleague',
238
+ 'comming': 'coming',
239
+ 'completly': 'completely',
240
+ 'definately': 'definitely',
241
+ 'definitly': 'definitely',
242
+ 'dissapear': 'disappear',
243
+ 'enviroment': 'environment',
244
+ 'existance': 'existence',
245
+ 'experiance': 'experience',
246
+ 'familar': 'familiar',
247
+ 'finaly': 'finally',
248
+ 'freind': 'friend',
249
+ 'goverment': 'government',
250
+ 'grammer': 'grammar',
251
+ 'happend': 'happened',
252
+ 'immediatly': 'immediately',
253
+ 'independant': 'independent',
254
+ 'knowlege': 'knowledge',
255
+ 'libary': 'library',
256
+ 'neccessary': 'necessary',
257
+ 'occured': 'occurred',
258
+ 'seperate': 'separate',
259
+ 'similer': 'similar',
260
+ 'sincerly': 'sincerely',
261
+ 'speach': 'speech',
262
+ 'strenght': 'strength',
263
+ 'succesful': 'successful',
264
+ 'tommorrow': 'tomorrow',
265
+ 'truely': 'truly',
266
+ 'unfortunatly': 'unfortunately',
267
+ 'untill': 'until',
268
+ 'usefull': 'useful',
269
+ 'wether': 'whether',
270
+ 'wich': 'which',
271
+ };
272
+
273
+ for (const [wrong, correct] of Object.entries(commonMisspellings)) {
274
+ const regex = new RegExp(`\\b${wrong}\\b`, 'gi');
275
+ const matches = text.matchAll(regex);
276
+ for (const match of matches) {
277
+ if (match[0] && match.index !== undefined) {
278
+ issues.push({
279
+ type: 'spelling',
280
+ original: match[0],
281
+ suggestion: correct,
282
+ reason: `Misspelled word. The correct spelling is "${correct}".`,
283
+ offset: match.index,
284
+ length: match[0].length,
285
+ });
286
+ }
287
+ }
288
+ }
289
+
290
+ return issues;
291
+ }
292
+
293
+ private static checkPassiveVoice(text: string): Issue[] {
294
+ const issues: Issue[] = [];
295
+
296
+ const passivePatterns = [
297
+ /\b(am|are|is|was|were|be|been|being)\s+(\w+ed)\b/gi,
298
+ /\b(am|are|is|was|were|be|been|being)\s+(\w+en)\b/gi,
299
+ ];
300
+
301
+ for (const pattern of passivePatterns) {
302
+ const matches = text.matchAll(pattern);
303
+ for (const match of matches) {
304
+ if (match[0] && match.index !== undefined) {
305
+ issues.push({
306
+ type: 'style',
307
+ original: match[0],
308
+ suggestion: 'Consider using active voice',
309
+ reason: 'Passive voice can make sentences weaker and less direct.',
310
+ offset: match.index,
311
+ length: match[0].length,
312
+ });
313
+ }
314
+ }
315
+ }
316
+ return issues;
317
+ }
318
+
319
+ private static checkRepetition(text: string): Issue[] {
320
+ const issues: Issue[] = [];
321
+ const repetitionRegex = /\b(\w+)\s+\1\b/gi;
322
+ const matches = text.matchAll(repetitionRegex);
323
+ for (const match of matches) {
324
+ if (match[0] && match.index !== undefined && match[1]) {
325
+ issues.push({
326
+ type: 'grammar',
327
+ original: match[0],
328
+ suggestion: match[1],
329
+ reason: 'Repeated word detected.',
330
+ offset: match.index,
331
+ length: match[0].length,
332
+ });
333
+ }
334
+ }
335
+ return issues;
336
+ }
337
+
338
+ private static checkLongSentences(text: string): Issue[] {
339
+ const issues: Issue[] = [];
340
+ const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
341
+ let currentIndex = 0;
342
+
343
+ for (const sentence of sentences) {
344
+ const words = sentence.trim().split(/\s+/).length;
345
+ if (words > 35) {
346
+ issues.push({
347
+ type: 'clarity',
348
+ original: sentence.substring(0, 30) + '...',
349
+ suggestion: 'Consider splitting into shorter sentences',
350
+ reason: `This sentence has ${words} words. Shorter sentences are easier to read.`,
351
+ offset: currentIndex,
352
+ length: sentence.length,
353
+ });
354
+ }
355
+ currentIndex += sentence.length + 1;
356
+ }
357
+ return issues;
358
+ }
359
+
360
+ private static checkSpacingErrors(text: string): Issue[] {
361
+ const issues: Issue[] = [];
362
+
363
+ // Double spaces
364
+ const doubleSpaceRegex = / {2,}/g;
365
+ const doubleMatches = text.matchAll(doubleSpaceRegex);
366
+ for (const match of doubleMatches) {
367
+ if (match[0] && match.index !== undefined) {
368
+ issues.push({
369
+ type: 'grammar',
370
+ original: match[0],
371
+ suggestion: ' ',
372
+ reason: 'Multiple spaces detected. Use single space.',
373
+ offset: match.index,
374
+ length: match[0].length,
375
+ });
376
+ }
377
+ }
378
+
379
+ return issues;
380
+ }
381
+
382
+ private static checkItsIt(text: string): Issue[] {
383
+ const issues: Issue[] = [];
384
+
385
+ // "its" followed by common verb forms should be "it's"
386
+ const itsWrongRegex = /\bits\s+(been|become|seemed|gotten|made|done|said|written|created)\b/gi;
387
+ const itsMatches = text.matchAll(itsWrongRegex);
388
+ for (const match of itsMatches) {
389
+ if (match[0] && match.index !== undefined && match[1]) {
390
+ issues.push({
391
+ type: 'spelling',
392
+ original: match[0],
393
+ suggestion: `it's ${match[1]}`,
394
+ reason: "Use 'it's' (contraction) when you mean 'it is' or 'it has'.",
395
+ offset: match.index,
396
+ length: match[0].length,
397
+ });
398
+ }
399
+ }
400
+ return issues;
401
+ }
402
+
403
+ private static checkTheirThereTheyre(text: string): Issue[] {
404
+ const issues: Issue[] = [];
405
+
406
+ // "their" followed by verb forms should be "they're"
407
+ const theirWrongRegex = /\btheir\s+(going|coming|working|doing|making|taking|getting|having|being)\b/gi;
408
+ const theirMatches = text.matchAll(theirWrongRegex);
409
+ for (const match of theirMatches) {
410
+ if (match[0] && match.index !== undefined && match[1]) {
411
+ issues.push({
412
+ type: 'spelling',
413
+ original: match[0],
414
+ suggestion: `they're ${match[1]}`,
415
+ reason: "Use 'they're' (contraction of 'they are') here.",
416
+ offset: match.index,
417
+ length: match[0].length,
418
+ });
419
+ }
420
+ }
421
+ return issues;
422
+ }
423
+
424
+ private static checkYourYoure(text: string): Issue[] {
425
+ const issues: Issue[] = [];
426
+
427
+ // "your" followed by adjectives should be "you're"
428
+ const yourWrongRegex = /\byour\s+(welcome|right|wrong|amazing|awesome|great|excellent|perfect|beautiful|brilliant|smart|intelligent|ready|finished|done|correct|incorrect|mistaken|confused|lost|found|gone|here|there|early|late|busy|free|important|necessary)\b/gi;
429
+ const yourMatches = text.matchAll(yourWrongRegex);
430
+ for (const match of yourMatches) {
431
+ if (match[0] && match.index !== undefined && match[1]) {
432
+ issues.push({
433
+ type: 'spelling',
434
+ original: match[0],
435
+ suggestion: `you're ${match[1]}`,
436
+ reason: "Use 'you're' (contraction of 'you are') here.",
437
+ offset: match.index,
438
+ length: match[0].length,
439
+ });
440
+ }
441
+ }
442
+ return issues;
443
+ }
444
+
445
+ private static checkApostropheErrors(text: string): Issue[] {
446
+ const issues: Issue[] = [];
447
+ return issues;
448
+ }
449
+
450
+ private static checkWeakWords(text: string): Issue[] {
451
+ const issues: Issue[] = [];
452
+
453
+ const weakWords: Record<string, string> = {
454
+ 'very good': 'excellent',
455
+ 'very bad': 'terrible',
456
+ 'very big': 'enormous',
457
+ 'very small': 'tiny',
458
+ 'very important': 'crucial',
459
+ 'very interesting': 'fascinating',
460
+ 'kind of': 'somewhat',
461
+ 'sort of': 'somewhat',
462
+ 'a lot': 'much',
463
+ 'stuff': 'things',
464
+ };
465
+
466
+ for (const [weak, strong] of Object.entries(weakWords)) {
467
+ const regex = new RegExp(`\\b${weak}\\b`, 'gi');
468
+ const matches = text.matchAll(regex);
469
+ for (const match of matches) {
470
+ if (match[0] && match.index !== undefined) {
471
+ issues.push({
472
+ type: 'style',
473
+ original: match[0],
474
+ suggestion: strong,
475
+ reason: `Consider a stronger word: "${strong}".`,
476
+ offset: match.index,
477
+ length: match[0].length,
478
+ });
479
+ }
480
+ }
481
+ }
482
+ return issues;
483
+ }
484
+
485
+ private static checkRedundantPhrases(text: string): Issue[] {
486
+ const issues: Issue[] = [];
487
+
488
+ const redundantPhrases: Record<string, string> = {
489
+ 'absolutely essential': 'essential',
490
+ 'advance planning': 'planning',
491
+ 'basic fundamentals': 'fundamentals',
492
+ 'close proximity': 'proximity',
493
+ 'completely eliminate': 'eliminate',
494
+ 'end result': 'result',
495
+ 'exact same': 'same',
496
+ 'final outcome': 'outcome',
497
+ 'free gift': 'gift',
498
+ 'future plans': 'plans',
499
+ 'past history': 'history',
500
+ 'personal opinion': 'opinion',
501
+ 'true fact': 'fact',
502
+ 'unexpected surprise': 'surprise',
503
+ };
504
+
505
+ for (const [phrase, replacement] of Object.entries(redundantPhrases)) {
506
+ const regex = new RegExp(`\\b${phrase}\\b`, 'gi');
507
+ const matches = text.matchAll(regex);
508
+ for (const match of matches) {
509
+ if (match[0] && match.index !== undefined) {
510
+ issues.push({
511
+ type: 'clarity',
512
+ original: match[0],
513
+ suggestion: replacement,
514
+ reason: `Redundant phrase. Use "${replacement}" instead.`,
515
+ offset: match.index,
516
+ length: match[0].length,
517
+ });
518
+ }
519
+ }
520
+ }
521
+ return issues;
522
+ }
523
+
524
+ private static checkCustomRules(text: string): Issue[] {
525
+ const issues: Issue[] = [];
526
+ return issues;
527
+ }
528
+}
0
529