1+ #!/usr/bin/env node
2+
3+ const fs = require ( 'fs' ) ;
4+ const path = require ( 'path' ) ;
5+
6+ // Dynamic path resolution to work from any directory
7+ const scriptDir = __dirname ;
8+ const gitHubDiffParserPath = path . join ( scriptDir , 'github-diff-parser' ) ;
9+ const validateGitHubPath = path . join ( scriptDir , 'validate-content-style-github' ) ;
10+ const ruleParserPath = path . join ( scriptDir , 'rule-parser' ) ;
11+ const styleRulesPath = path . join ( scriptDir , 'style-rules.yml' ) ;
12+
13+ const GitHubURLDiffParser = require ( gitHubDiffParserPath ) ;
14+ const GitHubDocumentationValidator = require ( validateGitHubPath ) ;
15+
16+ /**
17+ * Local test script for validating documentation with a diff file
18+ * Usage: node test-local-diff.js path/to/diff-file.patch [PR_NUMBER]
19+ */
20+ class LocalDiffTester {
21+ constructor ( diffFilePath , prNumber = null ) {
22+ this . diffFilePath = diffFilePath ;
23+ this . prNumber = prNumber ;
24+ this . diffParser = new GitHubURLDiffParser ( { verbose : true } ) ;
25+ }
26+
27+ async testValidation ( ) {
28+ try {
29+ console . log ( '🎯 Local Diff Validation Test' ) ;
30+ console . log ( '============================' ) ;
31+ console . log ( `📄 Diff file: ${ this . diffFilePath } ` ) ;
32+ if ( this . prNumber ) {
33+ console . log ( `🔍 PR number: #${ this . prNumber } ` ) ;
34+ }
35+ console . log ( '' ) ;
36+
37+ // Read the diff file
38+ if ( ! fs . existsSync ( this . diffFilePath ) ) {
39+ throw new Error ( `Diff file not found: ${ this . diffFilePath } ` ) ;
40+ }
41+
42+ const diffContent = fs . readFileSync ( this . diffFilePath , 'utf8' ) ;
43+ console . log ( `✅ Diff file loaded (${ diffContent . length } characters)` ) ;
44+
45+ // Parse the diff
46+ const parsedDiff = this . diffParser . parsePRDiff ( diffContent ) ;
47+ console . log ( `📊 Found ${ parsedDiff . totalFiles } markdown files in diff` ) ;
48+
49+ if ( parsedDiff . totalFiles === 0 ) {
50+ console . log ( '📝 No markdown files found in diff' ) ;
51+ return ;
52+ }
53+
54+ // Display files found
55+ console . log ( '\n📋 Files to validate:' ) ;
56+ Object . keys ( parsedDiff . files ) . forEach ( filePath => {
57+ const info = parsedDiff . files [ filePath ] ;
58+ if ( info . isNewFile ) {
59+ console . log ( ` 📄 ${ filePath } (new file)` ) ;
60+ } else if ( info . isDeletedFile ) {
61+ console . log ( ` 🗑️ ${ filePath } (deleted)` ) ;
62+ } else {
63+ console . log ( ` 📝 ${ filePath } (${ info . addedLines . size } added, ${ info . deletedLines . size } deleted lines)` ) ;
64+ }
65+ } ) ;
66+
67+ // Create a mock validator to test our logic
68+ const validator = new MockGitHubValidator ( {
69+ prNumber : this . prNumber ,
70+ verbose : true
71+ } ) ;
72+
73+ // Process each file
74+ let totalIssues = 0 ;
75+ for ( const [ filePath , diffInfo ] of Object . entries ( parsedDiff . files ) ) {
76+ console . log ( `\n🔍 Validating: ${ filePath } ` ) ;
77+
78+ const issues = await validator . validateFileWithDiff ( filePath , diffInfo ) ;
79+ totalIssues += issues . length ;
80+
81+ if ( issues . length === 0 ) {
82+ console . log ( ` ✅ No issues found` ) ;
83+ } else {
84+ console . log ( ` ⚠️ ${ issues . length } issues found:` ) ;
85+ issues . forEach ( issue => {
86+ console . log ( ` - Line ${ issue . line } : ${ issue . message } ` ) ;
87+ } ) ;
88+ }
89+ }
90+
91+ // Generate mock GitHub comment
92+ console . log ( '\n📝 Mock GitHub Comment:' ) ;
93+ console . log ( '========================' ) ;
94+ const mockComment = this . generateMockGitHubComment ( parsedDiff , totalIssues ) ;
95+ console . log ( mockComment ) ;
96+
97+ // Save results to file
98+ const results = {
99+ timestamp : new Date ( ) . toISOString ( ) ,
100+ prNumber : this . prNumber ,
101+ mode : 'local-diff-test' ,
102+ summary : {
103+ filesProcessed : parsedDiff . totalFiles ,
104+ totalIssues : totalIssues
105+ } ,
106+ diffFile : this . diffFilePath
107+ } ;
108+
109+ fs . writeFileSync ( 'local-test-results.json' , JSON . stringify ( results , null , 2 ) ) ;
110+ console . log ( '\n💾 Results saved to: local-test-results.json' ) ;
111+
112+ } catch ( error ) {
113+ console . error ( '❌ Test failed:' , error . message ) ;
114+ process . exit ( 1 ) ;
115+ }
116+ }
117+
118+ generateMockGitHubComment ( parsedDiff , totalIssues ) {
119+ let comment = `## 🎯 Strapi Documentation Style Review (LOCAL TEST)\n\n` ;
120+ comment += `*Automated analysis using Strapi's 12 Rules of Technical Writing*\n\n` ;
121+ comment += `**🚀 Test Mode:** Local diff file analysis\n` ;
122+ comment += `**📊 Source:** ${ this . diffFilePath } \n` ;
123+ if ( this . prNumber ) {
124+ comment += `**🎯 PR:** #${ this . prNumber } \n` ;
125+ }
126+ comment += '\n' ;
127+
128+ comment += '### 📊 Analysis Results\n' ;
129+ comment += `- **Files analyzed:** ${ parsedDiff . totalFiles } \n` ;
130+ comment += `- **Total issues:** ${ totalIssues } \n\n` ;
131+
132+ if ( totalIssues === 0 ) {
133+ comment += '🎉 **Perfect!** Your documentation changes follow all 12 technical writing rules.\n\n' ;
134+ } else {
135+ comment += `⚠️ Found ${ totalIssues } issues that should be addressed.\n\n` ;
136+ }
137+
138+ comment += '**📚 Resources:**\n' ;
139+ comment += '- [Strapi\'s 12 Rules of Technical Writing](https://strapi.notion.site/12-Rules-of-Technical-Writing-c75e080e6b19432287b3dd61c2c9fa04)\n' ;
140+ comment += '- [Documentation Style Guide](https://github.com/strapi/documentation/blob/main/STYLE_GUIDE.pdf)\n\n' ;
141+ comment += '*🧪 This is a local test simulation of what would be posted on GitHub.*\n' ;
142+
143+ return comment ;
144+ }
145+ }
146+
147+ // Mock validator for local testing
148+ class MockGitHubValidator {
149+ constructor ( options ) {
150+ this . options = options ;
151+ // Import the real rule parser with correct path
152+ const Strapi12RulesParser = require ( ruleParserPath ) ;
153+ this . ruleParser = new Strapi12RulesParser ( styleRulesPath ) ;
154+ this . diffParser = new GitHubURLDiffParser ( { verbose : options . verbose } ) ;
155+ }
156+
157+ async validateFileWithDiff ( filePath , diffInfo ) {
158+ try {
159+ if ( diffInfo . isDeletedFile ) {
160+ return [ ] ;
161+ }
162+
163+ if ( ! fs . existsSync ( filePath ) ) {
164+ console . log ( ` ⚠️ File not found locally: ${ filePath } ` ) ;
165+ return [ {
166+ file : filePath ,
167+ line : 1 ,
168+ message : `File not found locally (this is normal for local testing)` ,
169+ severity : 'warning'
170+ } ] ;
171+ }
172+
173+ const originalContent = fs . readFileSync ( filePath , 'utf8' ) ;
174+
175+ // Generate filtered content
176+ const { content : filteredContent , lineMapping, changedLines } =
177+ this . diffParser . generateFilteredContent ( originalContent , diffInfo ) ;
178+
179+ // Apply all rules
180+ const allIssues = this . applyAllRules ( filteredContent , filePath , { lineMapping, changedLines, diffInfo } ) ;
181+
182+ // Filter for changed lines only
183+ return this . filterIssuesForChangedLines ( allIssues , changedLines , lineMapping , diffInfo ) ;
184+
185+ } catch ( error ) {
186+ return [ {
187+ file : filePath ,
188+ line : 1 ,
189+ message : `Validation error: ${ error . message } ` ,
190+ severity : 'error'
191+ } ] ;
192+ }
193+ }
194+
195+ applyAllRules ( content , filePath , diffContext ) {
196+ const allIssues = [ ] ;
197+ const allRules = this . ruleParser . getAllRules ( ) ;
198+
199+ allRules . forEach ( rule => {
200+ try {
201+ const issues = rule . validator ( content , filePath ) ;
202+ issues . forEach ( issue => {
203+ issue . ruleId = rule . id ;
204+ // Map line numbers if we have diff context
205+ if ( diffContext && diffContext . lineMapping ) {
206+ const originalLine = diffContext . lineMapping [ issue . line ] ;
207+ if ( originalLine ) {
208+ issue . line = originalLine ;
209+ }
210+ }
211+ } ) ;
212+ allIssues . push ( ...issues ) ;
213+ } catch ( error ) {
214+ // Ignore rule errors for simplicity
215+ }
216+ } ) ;
217+
218+ return allIssues ;
219+ }
220+
221+ filterIssuesForChangedLines ( issues , changedLines , lineMapping , diffInfo ) {
222+ if ( ! changedLines || diffInfo . isNewFile ) {
223+ return issues ;
224+ }
225+
226+ const actuallyChangedLines = new Set ( [
227+ ...changedLines . added ,
228+ ...changedLines . modified
229+ ] ) ;
230+
231+ return issues . filter ( issue => {
232+ if ( actuallyChangedLines . has ( issue . line ) ) {
233+ return true ;
234+ }
235+
236+ // Simple proximity check for contextual rules
237+ for ( const changedLine of actuallyChangedLines ) {
238+ if ( Math . abs ( issue . line - changedLine ) <= 2 ) {
239+ return true ;
240+ }
241+ }
242+
243+ return false ;
244+ } ) ;
245+ }
246+ }
247+
248+ // Command line interface
249+ async function main ( ) {
250+ const args = process . argv . slice ( 2 ) ;
251+
252+ if ( args . length === 0 ) {
253+ console . log ( 'Usage: node test-local-diff.js <diff-file> [pr-number]' ) ;
254+ console . log ( '' ) ;
255+ console . log ( 'Examples:' ) ;
256+ console . log ( ' node test-local-diff.js pr-2439.diff' ) ;
257+ console . log ( ' node test-local-diff.js pr-2439.diff 2439' ) ;
258+ console . log ( '' ) ;
259+ console . log ( 'To get a diff file:' ) ;
260+ console . log ( ' curl https://patch-diff.githubusercontent.com/raw/strapi/documentation/pull/2439.diff > pr-2439.diff' ) ;
261+ process . exit ( 1 ) ;
262+ }
263+
264+ const diffFile = args [ 0 ] ;
265+ const prNumber = args [ 1 ] ? parseInt ( args [ 1 ] ) : null ;
266+
267+ const tester = new LocalDiffTester ( diffFile , prNumber ) ;
268+ await tester . testValidation ( ) ;
269+ }
270+
271+ if ( require . main === module ) {
272+ main ( ) . catch ( console . error ) ;
273+ }
274+
275+ module . exports = LocalDiffTester ;
0 commit comments