1
+ #!/usr/bin/env node
2
+
3
+ const fs = require ( 'fs' ) ;
4
+ const path = require ( 'path' ) ;
5
+ const yaml = require ( 'js-yaml' ) ;
6
+
7
+ /**
8
+ * Simplified Rule Parser for Strapi's 12 Rules of Technical Writing
9
+ * Focused on the most critical validations for quick implementation
10
+ */
11
+ class Strapi12RulesParser {
12
+ constructor ( configPath ) {
13
+ this . config = this . loadConfig ( configPath ) ;
14
+ this . parsedRules = [ ] ;
15
+ this . parseRules ( ) ;
16
+ }
17
+
18
+ loadConfig ( configPath ) {
19
+ try {
20
+ const fileContents = fs . readFileSync ( configPath , 'utf8' ) ;
21
+ return yaml . load ( fileContents ) ;
22
+ } catch ( error ) {
23
+ throw new Error ( `Failed to load configuration: ${ error . message } ` ) ;
24
+ }
25
+ }
26
+
27
+ parseRules ( ) {
28
+ // Parse the most critical rules first
29
+ this . parseCriticalRules ( ) ;
30
+ this . parseContentRules ( ) ;
31
+ this . parseStructureRules ( ) ;
32
+ }
33
+
34
+ parseCriticalRules ( ) {
35
+ const criticalRules = this . config . critical_violations ;
36
+ if ( ! criticalRules ) return ;
37
+
38
+ Object . entries ( criticalRules ) . forEach ( ( [ ruleKey , ruleConfig ] ) => {
39
+ if ( ! ruleConfig . enabled ) return ;
40
+
41
+ const rule = this . createCriticalRule ( ruleKey , ruleConfig ) ;
42
+ if ( rule ) this . parsedRules . push ( rule ) ;
43
+ } ) ;
44
+ }
45
+
46
+ createCriticalRule ( ruleKey , config ) {
47
+ switch ( ruleKey ) {
48
+ case 'procedures_must_be_numbered' :
49
+ return {
50
+ id : ruleKey ,
51
+ category : 'critical' ,
52
+ description : config . rule ,
53
+ severity : 'error' ,
54
+ validator : ( content , filePath ) => {
55
+ const errors = [ ] ;
56
+
57
+ // Detect procedure indicators
58
+ const procedurePatterns = [
59
+ / f o l l o w t h e s e s t e p s / gi,
60
+ / t o d o t h i s / gi,
61
+ / p r o c e d u r e / gi,
62
+ / i n s t r u c t i o n s / gi,
63
+ / h o w t o .* : / gi,
64
+ / s t e p s t o / gi,
65
+ / f i r s t .* t h e n .* n e x t / gi,
66
+ / 1 \. .* 2 \. .* 3 \. / g // Already has numbers - this is good!
67
+ ] ;
68
+
69
+ const hasProceduralContent = procedurePatterns . some ( pattern =>
70
+ pattern . test ( content ) && ! / 1 \. .* 2 \. .* 3 \. / . test ( content )
71
+ ) ;
72
+
73
+ if ( hasProceduralContent ) {
74
+ // Check if content has numbered lists
75
+ const hasNumberedLists = / ^ \d + \. \s + / gm. test ( content ) ;
76
+
77
+ if ( ! hasNumberedLists ) {
78
+ const lineNumber = this . findLineWithPattern ( content , procedurePatterns ) ;
79
+ errors . push ( {
80
+ file : filePath ,
81
+ line : lineNumber ,
82
+ message : 'CRITICAL: Step-by-step instructions must use numbered lists (Rule 7)' ,
83
+ severity : 'error' ,
84
+ rule : 'procedures_must_be_numbered' ,
85
+ suggestion : 'Convert instructions to numbered list format:\n1. First action\n2. Second action\n3. Third action'
86
+ } ) ;
87
+ }
88
+ }
89
+
90
+ return errors ;
91
+ }
92
+ } ;
93
+
94
+ case 'easy_difficult_words' :
95
+ return {
96
+ id : ruleKey ,
97
+ category : 'critical' ,
98
+ description : config . rule ,
99
+ severity : 'error' ,
100
+ validator : ( content , filePath ) => {
101
+ const errors = [ ] ;
102
+ const forbiddenWords = config . words ;
103
+ const lines = content . split ( '\n' ) ;
104
+
105
+ forbiddenWords . forEach ( word => {
106
+ lines . forEach ( ( line , index ) => {
107
+ const regex = new RegExp ( `\\b${ word } \\b` , 'gi' ) ;
108
+ if ( regex . test ( line ) ) {
109
+ errors . push ( {
110
+ file : filePath ,
111
+ line : index + 1 ,
112
+ message : `CRITICAL: Never use "${ word } " - it can discourage readers (Rule 6)` ,
113
+ severity : 'error' ,
114
+ rule : 'easy_difficult_words' ,
115
+ suggestion : 'Remove subjective difficulty assessment and provide clear instructions instead'
116
+ } ) ;
117
+ }
118
+ } ) ;
119
+ } ) ;
120
+
121
+ return errors ;
122
+ }
123
+ } ;
124
+
125
+ case 'jokes_and_casual_tone' :
126
+ return {
127
+ id : ruleKey ,
128
+ category : 'critical' ,
129
+ description : config . rule ,
130
+ severity : 'error' ,
131
+ validator : ( content , filePath ) => {
132
+ const errors = [ ] ;
133
+ const casualPatterns = config . patterns ;
134
+
135
+ casualPatterns . forEach ( pattern => {
136
+ const regex = new RegExp ( pattern , 'gi' ) ;
137
+ let match ;
138
+
139
+ while ( ( match = regex . exec ( content ) ) !== null ) {
140
+ const lineNumber = content . substring ( 0 , match . index ) . split ( '\n' ) . length ;
141
+ errors . push ( {
142
+ file : filePath ,
143
+ line : lineNumber ,
144
+ message : 'CRITICAL: Maintain professional tone - avoid casual language (Rule 3)' ,
145
+ severity : 'error' ,
146
+ rule : 'jokes_and_casual_tone' ,
147
+ suggestion : 'Use professional, neutral language in technical documentation'
148
+ } ) ;
149
+ }
150
+ } ) ;
151
+
152
+ return errors ;
153
+ }
154
+ } ;
155
+
156
+ default :
157
+ return null ;
158
+ }
159
+ }
160
+
161
+ parseContentRules ( ) {
162
+ const contentRules = this . config . content_rules ;
163
+ if ( ! contentRules ) return ;
164
+
165
+ Object . entries ( contentRules ) . forEach ( ( [ ruleKey , ruleConfig ] ) => {
166
+ if ( ! ruleConfig . enabled ) return ;
167
+
168
+ const rule = this . createContentRule ( ruleKey , ruleConfig ) ;
169
+ if ( rule ) this . parsedRules . push ( rule ) ;
170
+ } ) ;
171
+ }
172
+
173
+ createContentRule ( ruleKey , config ) {
174
+ switch ( ruleKey ) {
175
+ case 'minimize_pronouns' :
176
+ return {
177
+ id : ruleKey ,
178
+ category : 'content' ,
179
+ description : config . rule ,
180
+ severity : config . severity ,
181
+ validator : ( content , filePath ) => {
182
+ const errors = [ ] ;
183
+ const pronouns = config . discouraged_pronouns ;
184
+ const lines = content . split ( '\n' ) ;
185
+
186
+ lines . forEach ( ( line , index ) => {
187
+ let pronounCount = 0 ;
188
+ pronouns . forEach ( pronoun => {
189
+ const regex = new RegExp ( `\\b${ pronoun } \\b` , 'gi' ) ;
190
+ const matches = line . match ( regex ) ;
191
+ if ( matches ) pronounCount += matches . length ;
192
+ } ) ;
193
+
194
+ if ( pronounCount > ( config . max_pronouns_per_paragraph || 3 ) ) {
195
+ errors . push ( {
196
+ file : filePath ,
197
+ line : index + 1 ,
198
+ message : `Too many pronouns (${ pronounCount } ) - avoid "you/we" in technical docs (Rule 11)` ,
199
+ severity : config . severity ,
200
+ rule : ruleKey ,
201
+ suggestion : 'Focus on actions and explanations rather than addressing the reader directly'
202
+ } ) ;
203
+ }
204
+ } ) ;
205
+
206
+ return errors ;
207
+ }
208
+ } ;
209
+
210
+ case 'simple_english_vocabulary' :
211
+ return {
212
+ id : ruleKey ,
213
+ category : 'content' ,
214
+ description : config . rule ,
215
+ severity : config . severity ,
216
+ validator : ( content , filePath ) => {
217
+ const errors = [ ] ;
218
+ const complexWords = config . complex_words || [ ] ;
219
+ const replacements = config . replacement_suggestions || { } ;
220
+
221
+ complexWords . forEach ( word => {
222
+ const regex = new RegExp ( `\\b${ word } \\b` , 'gi' ) ;
223
+ let match ;
224
+
225
+ while ( ( match = regex . exec ( content ) ) !== null ) {
226
+ const lineNumber = content . substring ( 0 , match . index ) . split ( '\n' ) . length ;
227
+ const suggestion = replacements [ word ] ?
228
+ `Use "${ replacements [ word ] } " instead of "${ word } "` :
229
+ `Use simpler language instead of "${ word } "` ;
230
+
231
+ errors . push ( {
232
+ file : filePath ,
233
+ line : lineNumber ,
234
+ message : `Complex word detected: "${ word } " - stick to simple English (Rule 4)` ,
235
+ severity : config . severity ,
236
+ rule : ruleKey ,
237
+ suggestion : suggestion
238
+ } ) ;
239
+ }
240
+ } ) ;
241
+
242
+ return errors ;
243
+ }
244
+ } ;
245
+
246
+ default :
247
+ return null ;
248
+ }
249
+ }
250
+
251
+ parseStructureRules ( ) {
252
+ const structureRules = this . config . structure_rules ;
253
+ if ( ! structureRules ) return ;
254
+
255
+ Object . entries ( structureRules ) . forEach ( ( [ ruleKey , ruleConfig ] ) => {
256
+ if ( ! ruleConfig . enabled ) return ;
257
+
258
+ const rule = this . createStructureRule ( ruleKey , ruleConfig ) ;
259
+ if ( rule ) this . parsedRules . push ( rule ) ;
260
+ } ) ;
261
+ }
262
+
263
+ createStructureRule ( ruleKey , config ) {
264
+ switch ( ruleKey ) {
265
+ case 'use_bullet_lists' :
266
+ return {
267
+ id : ruleKey ,
268
+ category : 'structure' ,
269
+ description : config . rule ,
270
+ severity : config . severity ,
271
+ validator : ( content , filePath ) => {
272
+ const errors = [ ] ;
273
+
274
+ // Detect inline enumerations like "features include A, B, C, and D"
275
+ const enumerationPattern = / ( \w + \s + ( i n c l u d e | a r e | c o n s i s t s ? \s + o f ) ) ? \s * ( [ A - Z a - z ] + , \s * [ A - Z a - z ] + , \s * ( a n d \s + ) ? [ A - Z a - z ] + ) / gi;
276
+ let match ;
277
+
278
+ while ( ( match = enumerationPattern . exec ( content ) ) !== null ) {
279
+ const lineNumber = content . substring ( 0 , match . index ) . split ( '\n' ) . length ;
280
+
281
+ // Count items in enumeration
282
+ const items = match [ 3 ] . split ( ',' ) . length ;
283
+
284
+ if ( items >= ( config . max_inline_list_items || 3 ) ) {
285
+ errors . push ( {
286
+ file : filePath ,
287
+ line : lineNumber ,
288
+ message : `Long enumeration detected (${ items } items) - use bullet list instead (Rule 8)` ,
289
+ severity : config . severity ,
290
+ rule : ruleKey ,
291
+ suggestion : 'Convert to bullet list format:\n- Item 1\n- Item 2\n- Item 3'
292
+ } ) ;
293
+ }
294
+ }
295
+
296
+ return errors ;
297
+ }
298
+ } ;
299
+
300
+ default :
301
+ return null ;
302
+ }
303
+ }
304
+
305
+ // Helper method to find line number for patterns
306
+ findLineWithPattern ( content , patterns ) {
307
+ const lines = content . split ( '\n' ) ;
308
+
309
+ for ( let i = 0 ; i < lines . length ; i ++ ) {
310
+ const line = lines [ i ] ;
311
+ for ( const pattern of patterns ) {
312
+ if ( pattern . test ( line ) ) {
313
+ return i + 1 ;
314
+ }
315
+ }
316
+ }
317
+
318
+ return 1 ; // Default to first line if not found
319
+ }
320
+
321
+ // Get all parsed rules
322
+ getAllRules ( ) {
323
+ return this . parsedRules ;
324
+ }
325
+
326
+ // Get rules by category
327
+ getRulesByCategory ( category ) {
328
+ return this . parsedRules . filter ( rule => rule . category === category ) ;
329
+ }
330
+
331
+ // Get critical rules only
332
+ getCriticalRules ( ) {
333
+ return this . getRulesByCategory ( 'critical' ) ;
334
+ }
335
+ }
336
+
337
+ module . exports = Strapi12RulesParser ;
0 commit comments