1+ ///usr/bin/env jbang "$0" "$@" ; exit $?
2+
3+ //DEPS info.picocli:picocli:4.7.5
4+ //DEPS org.commonmark:commonmark:0.21.0
5+
6+ import org .commonmark .node .*;
7+ import org .commonmark .parser .Parser ;
8+ import org .commonmark .renderer .html .HtmlRenderer ;
9+ import picocli .CommandLine ;
10+ import picocli .CommandLine .Command ;
11+ import picocli .CommandLine .Option ;
12+ import picocli .CommandLine .Parameters ;
13+
14+ import java .io .IOException ;
15+ import java .nio .file .*;
16+ import java .util .*;
17+ import java .util .concurrent .Callable ;
18+ import java .util .stream .Stream ;
19+
20+ @ Command (name = "markdown-validator" ,
21+ mixinStandardHelpOptions = true ,
22+ version = "1.0" ,
23+ description = "Validates markdown files from specified directories" )
24+ public class MarkdownValidator implements Callable <Integer > {
25+
26+ @ Option (names = {"-v" , "--verbose" }, description = "Enable verbose output" )
27+ boolean verbose ;
28+
29+ @ Option (names = {"-f" , "--fail-fast" }, description = "Stop on first validation error" )
30+ boolean failFast ;
31+
32+ @ Option (names = {"-d" , "--directories" },
33+ description = "Directories to scan for markdown files (default: .cursor/rules,.cursor/rules/templates)" ,
34+ split = "," )
35+ List <String > targetDirectories = List .of (".cursor/rules" , ".cursor/rules/templates" );
36+
37+ @ Parameters (description = "Root directory to scan (default: current directory)" )
38+ String rootDir = "." ;
39+
40+ private static final List <String > MARKDOWN_EXTENSIONS = List .of (".md" , ".mdc" );
41+
42+ private final Parser parser ;
43+ private final HtmlRenderer renderer ;
44+ private final List <ValidationError > errors = new ArrayList <>();
45+
46+ public MarkdownValidator () {
47+ this .parser = Parser .builder ().build ();
48+ this .renderer = HtmlRenderer .builder ().build ();
49+ }
50+
51+ public static void main (String ... args ) {
52+ int exitCode = new CommandLine (new MarkdownValidator ()).execute (args );
53+ System .exit (exitCode );
54+ }
55+
56+ @ Override
57+ public Integer call () throws Exception {
58+ System .out .println ("🔍 Starting markdown validation..." );
59+
60+ Path root = Paths .get (rootDir );
61+ if (!Files .exists (root )) {
62+ System .err .println ("❌ Root directory does not exist: " + root );
63+ return 1 ;
64+ }
65+
66+ List <Path > markdownFiles = findMarkdownFiles (root );
67+ if (markdownFiles .isEmpty ()) {
68+ System .out .println ("⚠️ No markdown files found in target directories" );
69+ return 0 ;
70+ }
71+
72+ System .out .printf ("📄 Found %d markdown files to validate\n " , markdownFiles .size ());
73+
74+ for (Path file : markdownFiles ) {
75+ validateFile (file );
76+ if (failFast && !errors .isEmpty ()) {
77+ break ;
78+ }
79+ }
80+
81+ printResults ();
82+ return errors .isEmpty () ? 0 : 1 ;
83+ }
84+
85+ private List <Path > findMarkdownFiles (Path root ) throws IOException {
86+ List <Path > files = new ArrayList <>();
87+
88+ for (String targetDir : targetDirectories ) {
89+ Path dir = root .resolve (targetDir );
90+ if (Files .exists (dir ) && Files .isDirectory (dir )) {
91+ try (Stream <Path > paths = Files .walk (dir )) {
92+ paths .filter (Files ::isRegularFile )
93+ .filter (this ::isMarkdownFile )
94+ .forEach (files ::add );
95+ }
96+ } else if (verbose ) {
97+ System .out .printf ("⚠️ Directory not found: %s\n " , dir );
98+ }
99+ }
100+
101+ return files ;
102+ }
103+
104+ private boolean isMarkdownFile (Path file ) {
105+ String fileName = file .getFileName ().toString ().toLowerCase ();
106+ return MARKDOWN_EXTENSIONS .stream ().anyMatch (fileName ::endsWith );
107+ }
108+
109+ private void validateFile (Path file ) {
110+ System .out .printf ("🔍 Validating: %s\n " , file );
111+
112+ try {
113+ String content = Files .readString (file );
114+ validateContent (file , content );
115+ } catch (IOException e ) {
116+ addError (file , 0 , "Failed to read file: " + e .getMessage ());
117+ }
118+ }
119+
120+ private void validateContent (Path file , String content ) {
121+ try {
122+ // Parse markdown content
123+ Node document = parser .parse (content );
124+
125+ // Try to render to HTML to validate structure
126+ String html = renderer .render (document );
127+
128+ // Assert that HTML output is not null
129+ if (html == null ) {
130+ addError (file , 0 , "HTML rendering produced null output" );
131+ return ;
132+ }
133+
134+ if (verbose ) {
135+ System .out .printf ("✅ Successfully parsed: %s (%d characters, HTML: %d characters)\n " ,
136+ file .getFileName (), content .length (), html .length ());
137+ }
138+ } catch (Exception e ) {
139+ addError (file , 0 , "Failed to parse markdown: " + e .getMessage ());
140+ }
141+ }
142+
143+ private void addError (Path file , int lineNumber , String message ) {
144+ errors .add (new ValidationError (file , lineNumber , message ));
145+ if (verbose ) {
146+ System .out .printf ("❌ %s:%d - %s\n " , file , lineNumber , message );
147+ }
148+ }
149+
150+ private void printResults () {
151+ System .out .println ("\n " + "=" .repeat (60 ));
152+
153+ if (errors .isEmpty ()) {
154+ System .out .println ("✅ All markdown files are valid!" );
155+ } else {
156+ System .out .printf ("❌ Found %d validation errors:\n \n " , errors .size ());
157+
158+ Map <Path , List <ValidationError >> errorsByFile = new LinkedHashMap <>();
159+ for (ValidationError error : errors ) {
160+ errorsByFile .computeIfAbsent (error .file , k -> new ArrayList <>()).add (error );
161+ }
162+
163+ for (Map .Entry <Path , List <ValidationError >> entry : errorsByFile .entrySet ()) {
164+ System .out .printf ("📄 %s:\n " , entry .getKey ());
165+ for (ValidationError error : entry .getValue ()) {
166+ if (error .lineNumber > 0 ) {
167+ System .out .printf (" Line %d: %s\n " , error .lineNumber , error .message );
168+ } else {
169+ System .out .printf (" %s\n " , error .message );
170+ }
171+ }
172+ System .out .println ();
173+ }
174+ }
175+
176+ System .out .println ("=" .repeat (60 ));
177+ }
178+
179+ private static class ValidationError {
180+ final Path file ;
181+ final int lineNumber ;
182+ final String message ;
183+
184+ ValidationError (Path file , int lineNumber , String message ) {
185+ this .file = file ;
186+ this .lineNumber = lineNumber ;
187+ this .message = message ;
188+ }
189+ }
190+ }
0 commit comments