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
| @@ -0,0 +1,528 @@ | ||
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 | |