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