opengrammar-server 2.0.615350
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.npm.md +95 -0
- package/bin/opengrammar-server.js +111 -0
- package/dist/server.js +48639 -0
- package/package.json +80 -0
- package/server-node.ts +159 -0
- package/server.ts +15 -0
- package/src/analyzer.ts +542 -0
- package/src/dictionary.ts +1973 -0
- package/src/index.ts +978 -0
- package/src/nlp/nlp-engine.ts +17 -0
- package/src/nlp/tone-analyzer.ts +269 -0
- package/src/rephraser.ts +146 -0
- package/src/rules/categories/academic-writing.ts +182 -0
- package/src/rules/categories/adjectives-adverbs.ts +152 -0
- package/src/rules/categories/articles.ts +160 -0
- package/src/rules/categories/business-writing.ts +250 -0
- package/src/rules/categories/capitalization.ts +79 -0
- package/src/rules/categories/clarity.ts +117 -0
- package/src/rules/categories/common-errors.ts +601 -0
- package/src/rules/categories/confused-words.ts +219 -0
- package/src/rules/categories/conjunctions.ts +176 -0
- package/src/rules/categories/dangling-modifiers.ts +123 -0
- package/src/rules/categories/formality.ts +274 -0
- package/src/rules/categories/formatting-idioms.ts +323 -0
- package/src/rules/categories/gerund-infinitive.ts +274 -0
- package/src/rules/categories/grammar-advanced.ts +294 -0
- package/src/rules/categories/grammar.ts +286 -0
- package/src/rules/categories/inclusive-language.ts +280 -0
- package/src/rules/categories/nouns-pronouns.ts +233 -0
- package/src/rules/categories/prepositions-extended.ts +217 -0
- package/src/rules/categories/prepositions.ts +159 -0
- package/src/rules/categories/punctuation.ts +347 -0
- package/src/rules/categories/quantity-agreement.ts +200 -0
- package/src/rules/categories/readability.ts +293 -0
- package/src/rules/categories/sentence-structure.ts +100 -0
- package/src/rules/categories/spelling-advanced.ts +164 -0
- package/src/rules/categories/spelling.ts +119 -0
- package/src/rules/categories/style-tone.ts +511 -0
- package/src/rules/categories/style.ts +78 -0
- package/src/rules/categories/subject-verb-agreement.ts +201 -0
- package/src/rules/categories/tone-rules.ts +206 -0
- package/src/rules/categories/verb-tense.ts +582 -0
- package/src/rules/context-filter.ts +446 -0
- package/src/rules/index.ts +96 -0
- package/src/rules/ruleset-part1-cj-pu-sp.json +657 -0
- package/src/rules/ruleset-part1-np-ad-aa-pr.json +831 -0
- package/src/rules/ruleset-part1-ss-vt.json +907 -0
- package/src/rules/ruleset-part2-cw-st-nf.json +318 -0
- package/src/rules/ruleset-part3-aw-bw-il-rd.json +161 -0
- package/src/rules/types.ts +79 -0
- package/src/shared-types.ts +152 -0
- package/src/spellchecker.ts +418 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createRegexRule, type Rule } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ════════════════════════════════════════════════════════
|
|
5
|
+
* Gerund vs Infinitive Rules — GI Module
|
|
6
|
+
* Modal Verb Errors — MV Module
|
|
7
|
+
* ════════════════════════════════════════════════════════
|
|
8
|
+
*/
|
|
9
|
+
export const gerundInfinitiveRules: Rule[] = [
|
|
10
|
+
|
|
11
|
+
// ═══════════════════════════════════════════════
|
|
12
|
+
// MODAL VERBS: Cannot be followed by "to" + base
|
|
13
|
+
// ═══════════════════════════════════════════════
|
|
14
|
+
createRegexRule({
|
|
15
|
+
id: 'MV_must_to',
|
|
16
|
+
category: 'grammar',
|
|
17
|
+
pattern: /\bmust\s+to\s+(\w+)/i,
|
|
18
|
+
suggestion: (m) => `must ${m[1]}`,
|
|
19
|
+
reason: 'Modal verbs (must, can, will, may, should) are followed by the base form without "to".',
|
|
20
|
+
}),
|
|
21
|
+
createRegexRule({
|
|
22
|
+
id: 'MV_can_to',
|
|
23
|
+
category: 'grammar',
|
|
24
|
+
pattern: /\bcan\s+to\s+(\w+)/i,
|
|
25
|
+
suggestion: (m) => `can ${m[1]}`,
|
|
26
|
+
reason: '"Can" is a modal verb and is followed by the base form without "to".',
|
|
27
|
+
}),
|
|
28
|
+
createRegexRule({
|
|
29
|
+
id: 'MV_will_to',
|
|
30
|
+
category: 'grammar',
|
|
31
|
+
pattern: /\bwill\s+to\s+(\w+)/i,
|
|
32
|
+
suggestion: (m) => `will ${m[1]}`,
|
|
33
|
+
reason: '"Will" is a modal verb and is followed by the base form without "to".',
|
|
34
|
+
}),
|
|
35
|
+
createRegexRule({
|
|
36
|
+
id: 'MV_might_to',
|
|
37
|
+
category: 'grammar',
|
|
38
|
+
pattern: /\bmight\s+to\s+(\w+)/i,
|
|
39
|
+
suggestion: (m) => `might ${m[1]}`,
|
|
40
|
+
reason: '"Might" is a modal verb and is followed by the base form without "to".',
|
|
41
|
+
}),
|
|
42
|
+
createRegexRule({
|
|
43
|
+
id: 'MV_may_to',
|
|
44
|
+
category: 'grammar',
|
|
45
|
+
pattern: /\bmay\s+to\s+(\w+)/i,
|
|
46
|
+
suggestion: (m) => `may ${m[1]}`,
|
|
47
|
+
reason: '"May" is a modal verb and is followed by the base form without "to".',
|
|
48
|
+
}),
|
|
49
|
+
createRegexRule({
|
|
50
|
+
id: 'MV_would_to',
|
|
51
|
+
category: 'grammar',
|
|
52
|
+
pattern: /\bwould\s+to\s+(\w+)/i,
|
|
53
|
+
suggestion: (m) => `would ${m[1]}`,
|
|
54
|
+
reason: '"Would" is followed by the base form without "to".',
|
|
55
|
+
}),
|
|
56
|
+
createRegexRule({
|
|
57
|
+
id: 'MV_could_to',
|
|
58
|
+
category: 'grammar',
|
|
59
|
+
pattern: /\bcould\s+to\s+(\w+)/i,
|
|
60
|
+
suggestion: (m) => `could ${m[1]}`,
|
|
61
|
+
reason: '"Could" is a modal verb and is followed by the base form without "to".',
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
// ═══════════════════════════════════════════════
|
|
65
|
+
// BE + ADJECTIVE (not "be + verb")
|
|
66
|
+
// ═══════════════════════════════════════════════
|
|
67
|
+
createRegexRule({
|
|
68
|
+
id: 'GI_be_agree',
|
|
69
|
+
category: 'grammar',
|
|
70
|
+
pattern: /\b(am|is|are|was|were|be|been)\s+agree\b/i,
|
|
71
|
+
suggestion: (m) => `${m[1]} in agreement`,
|
|
72
|
+
reason: '"Agree" is a verb, not an adjective. Say "I agree" or "I am in agreement".',
|
|
73
|
+
}),
|
|
74
|
+
createRegexRule({
|
|
75
|
+
id: 'GI_be_depend',
|
|
76
|
+
category: 'grammar',
|
|
77
|
+
pattern: /\b(am|is|are|was|were|be|been)\s+depend\b/i,
|
|
78
|
+
suggestion: (m) => `${m[1]} dependent`,
|
|
79
|
+
reason: 'Use "dependent on" not "depend on" after the verb "be".',
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
// ═══════════════════════════════════════════════
|
|
83
|
+
// VERBS THAT REQUIRE GERUND (not TO + infinitive)
|
|
84
|
+
// ═══════════════════════════════════════════════
|
|
85
|
+
|
|
86
|
+
// avoid
|
|
87
|
+
createRegexRule({
|
|
88
|
+
id: 'GI_avoid_to',
|
|
89
|
+
category: 'grammar',
|
|
90
|
+
pattern: /\bavoid\s+to\s+(\w+)/i,
|
|
91
|
+
suggestion: (m) => `avoid ${m[1]}ing`,
|
|
92
|
+
reason: '"Avoid" is followed by a gerund (-ing form), not an infinitive.',
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
// enjoy
|
|
96
|
+
createRegexRule({
|
|
97
|
+
id: 'GI_enjoy_to',
|
|
98
|
+
category: 'grammar',
|
|
99
|
+
pattern: /\benjoy\s+to\s+(\w+)/i,
|
|
100
|
+
suggestion: (m) => `enjoy ${m[1]}ing`,
|
|
101
|
+
reason: '"Enjoy" is followed by a gerund (-ing form), not an infinitive.',
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
// suggest
|
|
105
|
+
createRegexRule({
|
|
106
|
+
id: 'GI_suggest_to',
|
|
107
|
+
category: 'grammar',
|
|
108
|
+
pattern: /\bsuggested?\s+to\s+(?!him\b|her\b|them\b|me\b|us\b|you\b|him|her|them|me|us|you)(\w+)/i,
|
|
109
|
+
suggestion: (m) => `suggest${m[0].match(/suggested/) ? 'ed' : ''} ${m[1]}ing`,
|
|
110
|
+
reason: '"Suggest" is followed by a gerund (-ing form), not an infinitive.',
|
|
111
|
+
}),
|
|
112
|
+
|
|
113
|
+
// consider
|
|
114
|
+
createRegexRule({
|
|
115
|
+
id: 'GI_consider_to',
|
|
116
|
+
category: 'grammar',
|
|
117
|
+
pattern: /\bconsidered?\s+to\s+(?!be\b|have\b)(\w+)/i,
|
|
118
|
+
suggestion: (m) => `consider${m[0].match(/considered/) ? 'ed' : ''} ${m[1]}ing`,
|
|
119
|
+
reason: '"Consider" is followed by a gerund (-ing form), not an infinitive.',
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
// keep
|
|
123
|
+
createRegexRule({
|
|
124
|
+
id: 'GI_keep_to',
|
|
125
|
+
category: 'grammar',
|
|
126
|
+
pattern: /\bkept\s+to\s+(\w+(?:ing)?)\b(?!\s+(the|a|an|his|her|their|our|my|your)\b)/i,
|
|
127
|
+
suggestion: (m) => `kept ${m[1].endsWith('ing') ? m[1] : m[1] + 'ing'}`,
|
|
128
|
+
reason: '"Keep" is followed by a gerund (-ing form), not an infinitive.',
|
|
129
|
+
}),
|
|
130
|
+
|
|
131
|
+
// admit
|
|
132
|
+
createRegexRule({
|
|
133
|
+
id: 'GI_admit_to_verb',
|
|
134
|
+
category: 'grammar',
|
|
135
|
+
pattern: /\badmitted?\s+to\s+(\w+(?:e)?)\b(?!\s+(the|a|an|it|him|her|them|us|me|you)\b)/i,
|
|
136
|
+
suggestion: (m) => {
|
|
137
|
+
const verb = m[1];
|
|
138
|
+
const gerund = verb.endsWith('e') ? verb.slice(0, -1) + 'ing' : verb + 'ing';
|
|
139
|
+
return `admit${m[0].match(/admitted/) ? 'ted' : ''} to ${gerund}`;
|
|
140
|
+
},
|
|
141
|
+
reason: '"Admit to" is followed by a gerund (-ing form).',
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
// practice
|
|
145
|
+
createRegexRule({
|
|
146
|
+
id: 'GI_practice_to',
|
|
147
|
+
category: 'grammar',
|
|
148
|
+
pattern: /\bpractice\s+to\s+(\w+)/i,
|
|
149
|
+
suggestion: (m) => `practice ${m[1]}ing`,
|
|
150
|
+
reason: '"Practice" is followed by a gerund (-ing form), not an infinitive.',
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
// deny
|
|
154
|
+
createRegexRule({
|
|
155
|
+
id: 'GI_deny_to',
|
|
156
|
+
category: 'grammar',
|
|
157
|
+
pattern: /\bdenied?\s+to\s+(?!him\b|her\b|them\b|me\b|us\b|you\b)(\w+)/i,
|
|
158
|
+
suggestion: (m) => `deni${m[0].match(/denied/) ? 'ed' : 'es'} ${m[1]}ing`,
|
|
159
|
+
reason: '"Deny" is followed by a gerund (-ing form), not an infinitive.',
|
|
160
|
+
}),
|
|
161
|
+
|
|
162
|
+
// resist
|
|
163
|
+
createRegexRule({
|
|
164
|
+
id: 'GI_resist_to',
|
|
165
|
+
category: 'grammar',
|
|
166
|
+
pattern: /\bresisted?\s+to\s+(\w+)/i,
|
|
167
|
+
suggestion: (m) => `resisted ${m[1]}ing`,
|
|
168
|
+
reason: '"Resist" is followed by a gerund (-ing form), not an infinitive.',
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
// finish
|
|
172
|
+
createRegexRule({
|
|
173
|
+
id: 'GI_finish_to',
|
|
174
|
+
category: 'grammar',
|
|
175
|
+
pattern: /\bfinished?\s+to\s+(\w+)/i,
|
|
176
|
+
suggestion: (m) => `finish${m[0].match(/finished/) ? 'ed' : ''} ${m[1]}ing`,
|
|
177
|
+
reason: '"Finish" is followed by a gerund (-ing form), not an infinitive.',
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
// mind
|
|
181
|
+
createRegexRule({
|
|
182
|
+
id: 'GI_mind_to',
|
|
183
|
+
category: 'grammar',
|
|
184
|
+
pattern: /\bminds?\s+to\s+(\w+)/i,
|
|
185
|
+
suggestion: (m) => `mind ${m[1]}ing`,
|
|
186
|
+
reason: '"Mind" is followed by a gerund (-ing form), not an infinitive.',
|
|
187
|
+
}),
|
|
188
|
+
|
|
189
|
+
// miss
|
|
190
|
+
createRegexRule({
|
|
191
|
+
id: 'GI_miss_to',
|
|
192
|
+
category: 'grammar',
|
|
193
|
+
pattern: /\bmissed?\s+to\s+(\w+)/i,
|
|
194
|
+
suggestion: (m) => `miss${m[0].match(/missed/) ? 'ed' : ''} ${m[1]}ing`,
|
|
195
|
+
reason: '"Miss" is followed by a gerund (-ing form), not an infinitive.',
|
|
196
|
+
}),
|
|
197
|
+
|
|
198
|
+
// quit
|
|
199
|
+
createRegexRule({
|
|
200
|
+
id: 'GI_quit_to',
|
|
201
|
+
category: 'grammar',
|
|
202
|
+
pattern: /\bquit\s+to\s+(\w+)/i,
|
|
203
|
+
suggestion: (m) => `quit ${m[1]}ing`,
|
|
204
|
+
reason: '"Quit" is followed by a gerund (-ing form), not an infinitive.',
|
|
205
|
+
}),
|
|
206
|
+
|
|
207
|
+
// postpone / delay
|
|
208
|
+
createRegexRule({
|
|
209
|
+
id: 'GI_postpone_to',
|
|
210
|
+
category: 'grammar',
|
|
211
|
+
pattern: /\bpostponed?\s+to\s+(\w+)/i,
|
|
212
|
+
suggestion: (m) => `postponed ${m[1]}ing`,
|
|
213
|
+
reason: '"Postpone" is followed by a gerund (-ing form), not an infinitive.',
|
|
214
|
+
}),
|
|
215
|
+
createRegexRule({
|
|
216
|
+
id: 'GI_delay_to',
|
|
217
|
+
category: 'grammar',
|
|
218
|
+
pattern: /\bdelayed?\s+to\s+(\w+)/i,
|
|
219
|
+
suggestion: (m) => `delayed ${m[1]}ing`,
|
|
220
|
+
reason: '"Delay" is followed by a gerund (-ing form), not an infinitive.',
|
|
221
|
+
}),
|
|
222
|
+
|
|
223
|
+
// imagine
|
|
224
|
+
createRegexRule({
|
|
225
|
+
id: 'GI_imagine_to',
|
|
226
|
+
category: 'grammar',
|
|
227
|
+
pattern: /\bimagined?\s+to\s+(\w+)/i,
|
|
228
|
+
suggestion: (m) => `imagined ${m[1]}ing`,
|
|
229
|
+
reason: '"Imagine" is followed by a gerund (-ing form), not an infinitive.',
|
|
230
|
+
}),
|
|
231
|
+
|
|
232
|
+
// risk
|
|
233
|
+
createRegexRule({
|
|
234
|
+
id: 'GI_risk_to',
|
|
235
|
+
category: 'grammar',
|
|
236
|
+
pattern: /\brisked?\s+to\s+(\w+)/i,
|
|
237
|
+
suggestion: (m) => `risked ${m[1]}ing`,
|
|
238
|
+
reason: '"Risk" is followed by a gerund (-ing form), not an infinitive.',
|
|
239
|
+
}),
|
|
240
|
+
|
|
241
|
+
// cannot help
|
|
242
|
+
createRegexRule({
|
|
243
|
+
id: 'GI_cant_help_to',
|
|
244
|
+
category: 'grammar',
|
|
245
|
+
pattern: /\bcan'?t\s+help\s+to\s+(\w+)/i,
|
|
246
|
+
suggestion: (m) => `can't help ${m[1]}ing`,
|
|
247
|
+
reason: '"Cannot help" is followed by a gerund, not an infinitive.',
|
|
248
|
+
}),
|
|
249
|
+
|
|
250
|
+
// ═══════════════════════════════════════════════
|
|
251
|
+
// SUBJUNCTIVE MOOD
|
|
252
|
+
// ═══════════════════════════════════════════════
|
|
253
|
+
createRegexRule({
|
|
254
|
+
id: 'GI_if_i_was',
|
|
255
|
+
category: 'grammar',
|
|
256
|
+
pattern: /\bif\s+I\s+was\b(?!\s+able|\s+going|\s+supposed|\s+trying|\s+planning|\s+hoping|\s+thinking|\s+looking)/i,
|
|
257
|
+
suggestion: 'if I were',
|
|
258
|
+
reason: 'Use the subjunctive "were" (not "was") in hypothetical "if" clauses.',
|
|
259
|
+
}),
|
|
260
|
+
createRegexRule({
|
|
261
|
+
id: 'GI_wish_i_was',
|
|
262
|
+
category: 'grammar',
|
|
263
|
+
pattern: /\bwish\s+(I|he|she|it)\s+was\b/i,
|
|
264
|
+
suggestion: (m) => `wish ${m[1]} were`,
|
|
265
|
+
reason: 'After "wish," use the subjunctive "were" instead of "was".',
|
|
266
|
+
}),
|
|
267
|
+
createRegexRule({
|
|
268
|
+
id: 'GI_as_if_was',
|
|
269
|
+
category: 'grammar',
|
|
270
|
+
pattern: /\bas\s+if\s+(I|he|she|it|he|they)\s+was\b/i,
|
|
271
|
+
suggestion: (m) => `as if ${m[1]} were`,
|
|
272
|
+
reason: 'After "as if," use the subjunctive "were" instead of "was".',
|
|
273
|
+
}),
|
|
274
|
+
];
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { Issue } from '../../shared-types.js';
|
|
2
|
+
import { createRegexRule, type Rule } from '../types.js';
|
|
3
|
+
|
|
4
|
+
export const advancedGrammarRules: Rule[] = [
|
|
5
|
+
// Spacing Errors
|
|
6
|
+
createRegexRule({
|
|
7
|
+
id: 'double-space',
|
|
8
|
+
category: 'grammar',
|
|
9
|
+
pattern: /[^.](\s{2,})/g,
|
|
10
|
+
suggestion: ' ',
|
|
11
|
+
reason: 'Multiple spaces detected. Use single space.',
|
|
12
|
+
}),
|
|
13
|
+
createRegexRule({
|
|
14
|
+
id: 'space-before-punct',
|
|
15
|
+
category: 'grammar',
|
|
16
|
+
pattern: /\s+([.,!?;:])/g,
|
|
17
|
+
suggestion: (match) => match[1] || '',
|
|
18
|
+
reason: 'Remove space before punctuation.',
|
|
19
|
+
}),
|
|
20
|
+
createRegexRule({
|
|
21
|
+
id: 'no-space-after-punct',
|
|
22
|
+
category: 'grammar',
|
|
23
|
+
pattern: /([.,!?;:])([A-Z][a-z])/g,
|
|
24
|
+
suggestion: (match) => `${match[1] || ''} ${match[2] || ''}`,
|
|
25
|
+
reason: 'Add space after punctuation.',
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
// Apostrophe Errors
|
|
29
|
+
createRegexRule({
|
|
30
|
+
id: 'its-possessive',
|
|
31
|
+
category: 'grammar',
|
|
32
|
+
pattern:
|
|
33
|
+
/\bits\s+(name|own|color|size|shape|kind|type|way|purpose|function|role|effect|impact|result|content|source|code|data|file|path|url|id|user|item|object|property|value|element|node|parent|child|sibling)\b/i,
|
|
34
|
+
suggestion: (match) => `it's ${match[1]}`,
|
|
35
|
+
reason: "Use 'it's' (contraction of 'it is') here.",
|
|
36
|
+
}),
|
|
37
|
+
createRegexRule({
|
|
38
|
+
id: 'youre-verb',
|
|
39
|
+
category: 'grammar',
|
|
40
|
+
pattern:
|
|
41
|
+
/\byour\s+(welcome|going|right|wrong|reading|writing|working|looking|sounding|feeling|thinking|doing|making|taking|getting|having|being|becoming|seeming|appearing)\b/i,
|
|
42
|
+
suggestion: (match) => `you're ${match[1]}`,
|
|
43
|
+
reason: "Use 'you're' (contraction of 'you are') here.",
|
|
44
|
+
}),
|
|
45
|
+
createRegexRule({
|
|
46
|
+
id: 'theyre-verb',
|
|
47
|
+
category: 'grammar',
|
|
48
|
+
pattern:
|
|
49
|
+
/\btheir\s+(going|coming|working|doing|making|taking|getting|having|being|becoming|seeming|looking|sounding|feeling|thinking)\b/i,
|
|
50
|
+
suggestion: (match) => `they're ${match[1]}`,
|
|
51
|
+
reason: "Use 'they're' (contraction of 'they are') here.",
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
// That vs Which
|
|
55
|
+
createRegexRule({
|
|
56
|
+
id: 'which-no-comma',
|
|
57
|
+
category: 'grammar',
|
|
58
|
+
pattern: /([^,]\s+)which\s+(is|are|was|were|has|have|had|does|do|did)\b/i,
|
|
59
|
+
suggestion: (match) => `${match[1].trim()}, which ${match[2]}`,
|
|
60
|
+
reason: "Non-restrictive clauses need a comma before 'which'.",
|
|
61
|
+
}),
|
|
62
|
+
|
|
63
|
+
// Less vs Fewer
|
|
64
|
+
createRegexRule({
|
|
65
|
+
id: 'less-fewer',
|
|
66
|
+
category: 'grammar',
|
|
67
|
+
pattern:
|
|
68
|
+
/\bless\s+(items|things|people|words|sentences|paragraphs|pages|books|cars|houses|dogs|cats|students|teachers|errors|problems|questions|answers|ideas|concepts|rules|examples|cases|instances|occasions|times|days|weeks|months|years)\b/i,
|
|
69
|
+
suggestion: (match) => `fewer ${match[1]}`,
|
|
70
|
+
reason: "Use 'fewer' with countable nouns.",
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
// Comma Splices (Requires more manual custom check logic)
|
|
74
|
+
{
|
|
75
|
+
id: 'comma-splice',
|
|
76
|
+
type: 'regex',
|
|
77
|
+
category: 'grammar',
|
|
78
|
+
pattern: /\b([A-Z][^.]*?)\s*,\s+([A-Z][^.]*?[.!?])/g,
|
|
79
|
+
reason: 'Comma splice detected. Use a period, semicolon, or conjunction.',
|
|
80
|
+
suggestion: '',
|
|
81
|
+
check: (text: string): Issue[] => {
|
|
82
|
+
const issues: Issue[] = [];
|
|
83
|
+
const commaSpliceRegex = /\b([A-Z][^.]*?)\s*,\s+([A-Z][^.]*?[.!?])/g;
|
|
84
|
+
let match: RegExpExecArray | null;
|
|
85
|
+
while ((match = commaSpliceRegex.exec(text)) !== null) {
|
|
86
|
+
const clause1 = match[1]?.trim() || '';
|
|
87
|
+
const clause2 = match[2]?.trim() || '';
|
|
88
|
+
if (clause1.split(' ').length > 3 && clause2.split(' ').length > 3) {
|
|
89
|
+
issues.push({
|
|
90
|
+
id: `comma-splice-${match.index}`,
|
|
91
|
+
type: 'grammar',
|
|
92
|
+
original: `${clause1}, ${clause2}`,
|
|
93
|
+
suggestion: `${clause1}. ${clause2}`,
|
|
94
|
+
reason: 'Comma splice detected. Use a period, semicolon, or conjunction.',
|
|
95
|
+
offset: match.index,
|
|
96
|
+
length: match[0].length,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return issues;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Double Negatives
|
|
105
|
+
{
|
|
106
|
+
id: 'double-negative',
|
|
107
|
+
type: 'regex',
|
|
108
|
+
category: 'grammar',
|
|
109
|
+
pattern: /[^.!?]+[.!?]+/g,
|
|
110
|
+
reason: 'Double negative detected.',
|
|
111
|
+
suggestion: 'Remove one negative',
|
|
112
|
+
check: (text: string): Issue[] => {
|
|
113
|
+
const issues: Issue[] = [];
|
|
114
|
+
const negativeWords = [
|
|
115
|
+
"don't",
|
|
116
|
+
"doesn't",
|
|
117
|
+
"didn't",
|
|
118
|
+
"won't",
|
|
119
|
+
"wouldn't",
|
|
120
|
+
"couldn't",
|
|
121
|
+
"shouldn't",
|
|
122
|
+
"can't",
|
|
123
|
+
'cannot',
|
|
124
|
+
'no',
|
|
125
|
+
'not',
|
|
126
|
+
'never',
|
|
127
|
+
'nothing',
|
|
128
|
+
'nobody',
|
|
129
|
+
'nowhere',
|
|
130
|
+
'neither',
|
|
131
|
+
'nor',
|
|
132
|
+
];
|
|
133
|
+
const sentences = text.match(/[^.!?]+[.!?]+/g) || [];
|
|
134
|
+
let currentIndex = 0;
|
|
135
|
+
sentences.forEach((sentence) => {
|
|
136
|
+
const lowerSentence = sentence.toLowerCase();
|
|
137
|
+
const foundNegatives = negativeWords.filter((word) => lowerSentence.includes(word));
|
|
138
|
+
if (foundNegatives.length >= 2) {
|
|
139
|
+
issues.push({
|
|
140
|
+
id: `double-neg-${currentIndex}`,
|
|
141
|
+
type: 'grammar',
|
|
142
|
+
original: sentence.trim(),
|
|
143
|
+
suggestion: 'Remove one negative',
|
|
144
|
+
reason: `Double negative detected: ${foundNegatives.join(', ')}. This may be unintentional.`,
|
|
145
|
+
offset: currentIndex,
|
|
146
|
+
length: sentence.length,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
currentIndex += sentence.length;
|
|
150
|
+
});
|
|
151
|
+
return issues;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// Article Errors
|
|
156
|
+
{
|
|
157
|
+
id: 'article-a-an',
|
|
158
|
+
type: 'regex',
|
|
159
|
+
category: 'grammar',
|
|
160
|
+
pattern: /\b(?:a|an)\s+\w+/gi,
|
|
161
|
+
reason: 'Article mismatch',
|
|
162
|
+
suggestion: '',
|
|
163
|
+
check: (text: string): Issue[] => {
|
|
164
|
+
const issues: Issue[] = [];
|
|
165
|
+
const vowelExceptions = new Set(['uni', 'use', 'usu', 'eur', 'one', 'once']);
|
|
166
|
+
const aBeforeVowelRegex = /\ba\s+([aeiou]\w*)\b/gi;
|
|
167
|
+
let match: RegExpExecArray | null;
|
|
168
|
+
while ((match = aBeforeVowelRegex.exec(text)) !== null) {
|
|
169
|
+
const nextWord = (match[1] || '').toLowerCase();
|
|
170
|
+
const isException = Array.from(vowelExceptions).some((ex) => nextWord.startsWith(ex));
|
|
171
|
+
if (!isException) {
|
|
172
|
+
issues.push({
|
|
173
|
+
id: `article-a-${match.index}`,
|
|
174
|
+
type: 'grammar',
|
|
175
|
+
original: match[0],
|
|
176
|
+
suggestion: `an ${match[1]}`,
|
|
177
|
+
reason: 'Use "an" before words that begin with a vowel sound.',
|
|
178
|
+
offset: match.index,
|
|
179
|
+
length: match[0].length,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const consonantExceptions = new Set(['hour', 'honest', 'honor', 'honour', 'heir', 'herb']);
|
|
185
|
+
const anBeforeConsonantRegex = /\ban\s+([bcdfghjklmnpqrstvwxyz]\w*)\b/gi;
|
|
186
|
+
while ((match = anBeforeConsonantRegex.exec(text)) !== null) {
|
|
187
|
+
const nextWord = (match[1] || '').toLowerCase();
|
|
188
|
+
const isException = Array.from(consonantExceptions).some((ex) => nextWord.startsWith(ex));
|
|
189
|
+
if (!isException) {
|
|
190
|
+
issues.push({
|
|
191
|
+
id: `article-an-${match.index}`,
|
|
192
|
+
type: 'grammar',
|
|
193
|
+
original: match[0],
|
|
194
|
+
suggestion: `a ${match[1]}`,
|
|
195
|
+
reason: 'Use "a" before words that begin with a consonant sound.',
|
|
196
|
+
offset: match.index,
|
|
197
|
+
length: match[0].length,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return issues;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Missing Commas
|
|
206
|
+
{
|
|
207
|
+
id: 'intro-commas',
|
|
208
|
+
type: 'regex',
|
|
209
|
+
category: 'grammar',
|
|
210
|
+
pattern: /(?:^|[.!?]\s+)(however|therefore|furthermore)\s+(?!,)([A-Za-z])/gi,
|
|
211
|
+
reason: 'Missing comma',
|
|
212
|
+
suggestion: '',
|
|
213
|
+
check: (text: string): Issue[] => {
|
|
214
|
+
const issues: Issue[] = [];
|
|
215
|
+
const introWords = [
|
|
216
|
+
'however',
|
|
217
|
+
'therefore',
|
|
218
|
+
'furthermore',
|
|
219
|
+
'moreover',
|
|
220
|
+
'nevertheless',
|
|
221
|
+
'meanwhile',
|
|
222
|
+
'consequently',
|
|
223
|
+
'additionally',
|
|
224
|
+
'similarly',
|
|
225
|
+
'accordingly',
|
|
226
|
+
'unfortunately',
|
|
227
|
+
'fortunately',
|
|
228
|
+
'finally',
|
|
229
|
+
'obviously',
|
|
230
|
+
'clearly',
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
for (const word of introWords) {
|
|
234
|
+
const regex = new RegExp(`(?:^|[.!?]\\s+)${word}\\s+(?!,)([A-Za-z])`, 'gi');
|
|
235
|
+
let match: RegExpExecArray | null;
|
|
236
|
+
while ((match = regex.exec(text)) !== null) {
|
|
237
|
+
const wordStart = text.indexOf(word, match.index);
|
|
238
|
+
if (wordStart >= 0) {
|
|
239
|
+
issues.push({
|
|
240
|
+
id: `intro-comma-${wordStart}`,
|
|
241
|
+
type: 'grammar',
|
|
242
|
+
original: `${word} ${match[1]}`,
|
|
243
|
+
suggestion: `${word}, ${match[1]}`,
|
|
244
|
+
reason: `Add a comma after the introductory word "${word}".`,
|
|
245
|
+
offset: wordStart,
|
|
246
|
+
length: word.length + 2,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return issues;
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// Sentence Fragments
|
|
256
|
+
{
|
|
257
|
+
id: 'sentence-fragments',
|
|
258
|
+
type: 'regex',
|
|
259
|
+
category: 'grammar',
|
|
260
|
+
pattern: /^\s*(because|although|though|even though)/i,
|
|
261
|
+
reason: 'Sentence Fragment',
|
|
262
|
+
suggestion: 'This may be a sentence fragment. Add a main clause.',
|
|
263
|
+
check: (text: string): Issue[] => {
|
|
264
|
+
const issues: Issue[] = [];
|
|
265
|
+
const sentences = text.match(/[^.!?]+[.!?]+/g) || [];
|
|
266
|
+
let currentIndex = 0;
|
|
267
|
+
const subordinators =
|
|
268
|
+
/^\s*(because|although|though|even though|while|whereas|since|unless|until|if|when|whenever|wherever|after|before|as soon as|in order to|so that)\b/i;
|
|
269
|
+
|
|
270
|
+
for (const sentence of sentences) {
|
|
271
|
+
const trimmed = sentence.trim();
|
|
272
|
+
const match = subordinators.exec(trimmed);
|
|
273
|
+
|
|
274
|
+
if (match) {
|
|
275
|
+
const wordCount = trimmed.split(/\s+/).length;
|
|
276
|
+
const hasComma = trimmed.includes(',');
|
|
277
|
+
if (wordCount < 10 && !hasComma) {
|
|
278
|
+
issues.push({
|
|
279
|
+
id: `fragment-${currentIndex}`,
|
|
280
|
+
type: 'grammar',
|
|
281
|
+
original: trimmed,
|
|
282
|
+
suggestion: 'This may be a sentence fragment. Add a main clause.',
|
|
283
|
+
reason: `Sentences starting with "${match[1]}" need a main clause to be complete.`,
|
|
284
|
+
offset: currentIndex,
|
|
285
|
+
length: sentence.length,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
currentIndex += sentence.length;
|
|
290
|
+
}
|
|
291
|
+
return issues;
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
];
|