@@ -110,6 +110,9 @@ struct FileResult {
110110 #[ serde( skip_serializing_if = "Option::is_none" ) ]
111111 error : Option < String > ,
112112 errors : Vec < ErrorInfo > ,
113+ /// Source content for displaying excerpts (not serialized)
114+ #[ serde( skip) ]
115+ source_content : Option < String > ,
113116}
114117
115118/// Error information for JSON output
@@ -118,6 +121,7 @@ struct ErrorInfo {
118121 path : String ,
119122 message : String ,
120123 code : String ,
124+ severity : String ,
121125 #[ serde( skip_serializing_if = "Option::is_none" ) ]
122126 line : Option < usize > ,
123127 #[ serde( skip_serializing_if = "Option::is_none" ) ]
@@ -305,12 +309,13 @@ fn check_schema(validator: &SchemaValidator, file: &PathBuf) -> FileResult {
305309 valid : false ,
306310 error : Some ( e. to_string ( ) ) ,
307311 errors : vec ! [ ] ,
312+ source_content : None ,
308313 } ;
309314 }
310315 } ;
311316
312317 let result = validator. validate ( & content) ;
313- validation_result_to_file_result ( & file_name, result)
318+ validation_result_to_file_result ( & file_name, result, Some ( content ) )
314319}
315320
316321/// Validate a single instance file
@@ -333,22 +338,25 @@ fn validate_instance(
333338 valid : false ,
334339 error : Some ( e. to_string ( ) ) ,
335340 errors : vec ! [ ] ,
341+ source_content : None ,
336342 } ;
337343 }
338344 } ;
339345
340346 let result = validator. validate ( & content, schema) ;
341- validation_result_to_file_result ( & file_name, result)
347+ validation_result_to_file_result ( & file_name, result, Some ( content ) )
342348}
343349
344350/// Convert ValidationResult to FileResult
345- fn validation_result_to_file_result ( file : & str , result : ValidationResult ) -> FileResult {
351+ fn validation_result_to_file_result ( file : & str , result : ValidationResult , source_content : Option < String > ) -> FileResult {
346352 let errors: Vec < ErrorInfo > = result
347- . errors ( )
353+ . all_errors ( )
354+ . iter ( )
348355 . map ( |e| ErrorInfo {
349356 path : e. path . clone ( ) ,
350357 message : e. message . clone ( ) ,
351358 code : e. code . clone ( ) ,
359+ severity : e. severity . to_string ( ) ,
352360 line : if e. location . is_unknown ( ) {
353361 None
354362 } else {
@@ -367,6 +375,7 @@ fn validation_result_to_file_result(file: &str, result: ValidationResult) -> Fil
367375 valid : result. is_valid ( ) ,
368376 error : None ,
369377 errors,
378+ source_content,
370379 }
371380}
372381
@@ -392,23 +401,45 @@ fn output_results(results: &[FileResult], format: OutputFormat, verbose: bool) {
392401
393402/// Output results as human-readable text
394403fn output_text ( results : & [ FileResult ] , verbose : bool ) {
395- for result in results {
404+ // Pre-compute source lines for all results that have source content
405+ let source_lines: Vec < Option < Vec < & str > > > = results
406+ . iter ( )
407+ . map ( |r| r. source_content . as_ref ( ) . map ( |s| s. lines ( ) . collect ( ) ) )
408+ . collect ( ) ;
409+
410+ for ( idx, result) in results. iter ( ) . enumerate ( ) {
396411 if let Some ( ref error) = result. error {
397412 println ! ( "\u{2717} {}: {}" , result. file, error) ;
398413 } else if result. valid {
399414 println ! ( "\u{2713} {}: valid" , result. file) ;
400415 } else {
401416 println ! ( "\u{2717} {}: invalid" , result. file) ;
417+ let lines = source_lines[ idx] . as_ref ( ) ;
402418 for error in & result. errors {
403419 let path = if error. path . is_empty ( ) { "/" } else { & error. path } ;
404- let loc = if verbose {
405- error. line . map ( |l| {
406- format ! ( " (line {}, col {})" , l, error. column. unwrap_or( 0 ) )
407- } ) . unwrap_or_default ( )
408- } else {
409- String :: new ( )
410- } ;
411- println ! ( " - {}: {}{}" , path, error. message, loc) ;
420+ let severity_icon = if error. severity == "warning" { "\u{26A0} " } else { "\u{2717} " } ;
421+
422+ // Always show line/column when available
423+ let loc = error. line . map ( |l| {
424+ format ! ( " (line {}, col {})" , l, error. column. unwrap_or( 0 ) )
425+ } ) . unwrap_or_default ( ) ;
426+
427+ println ! ( " {} [{}] {}: {}{}" , severity_icon, error. code, path, error. message, loc) ;
428+
429+ // In verbose mode, show source excerpt with caret marker
430+ if verbose {
431+ if let ( Some ( line_num) , Some ( col) , Some ( src_lines) ) = ( error. line , error. column , lines) {
432+ if line_num > 0 && line_num <= src_lines. len ( ) {
433+ let source_line = src_lines[ line_num - 1 ] ;
434+ println ! ( " |" ) ;
435+ println ! ( " {} | {}" , line_num, source_line) ;
436+ // Create caret marker at the column position
437+ let line_num_width = line_num. to_string ( ) . len ( ) ;
438+ let padding = " " . repeat ( line_num_width + col) ;
439+ println ! ( " |{}^" , padding) ;
440+ }
441+ }
442+ }
412443 }
413444 }
414445 }
@@ -428,6 +459,12 @@ fn output_json(results: &[FileResult]) {
428459fn output_tap ( results : & [ FileResult ] , verbose : bool ) {
429460 println ! ( "1..{}" , results. len( ) ) ;
430461
462+ // Pre-compute source lines for all results that have source content
463+ let source_lines: Vec < Option < Vec < & str > > > = results
464+ . iter ( )
465+ . map ( |r| r. source_content . as_ref ( ) . map ( |s| s. lines ( ) . collect ( ) ) )
466+ . collect ( ) ;
467+
431468 for ( i, result) in results. iter ( ) . enumerate ( ) {
432469 let n = i + 1 ;
433470
@@ -438,16 +475,31 @@ fn output_tap(results: &[FileResult], verbose: bool) {
438475 println ! ( "ok {} - {}" , n, result. file) ;
439476 } else {
440477 println ! ( "not ok {} - {}" , n, result. file) ;
478+ let lines = source_lines[ i] . as_ref ( ) ;
441479 for error in & result. errors {
442480 let path = if error. path . is_empty ( ) { "/" } else { & error. path } ;
443- let loc = if verbose {
444- error. line . map ( |l| {
445- format ! ( " (line {}, col {})" , l, error. column. unwrap_or( 0 ) )
446- } ) . unwrap_or_default ( )
447- } else {
448- String :: new ( )
449- } ;
450- println ! ( " # {}: {}{}" , path, error. message, loc) ;
481+ let severity = if error. severity == "warning" { "warning" } else { "error" } ;
482+
483+ // Always show line/column when available
484+ let loc = error. line . map ( |l| {
485+ format ! ( " (line {}, col {})" , l, error. column. unwrap_or( 0 ) )
486+ } ) . unwrap_or_default ( ) ;
487+
488+ println ! ( " # [{}] {} {}: {}{}" , error. code, severity, path, error. message, loc) ;
489+
490+ // In verbose mode, show source excerpt
491+ if verbose {
492+ if let ( Some ( line_num) , Some ( src_lines) ) = ( error. line , lines) {
493+ if line_num > 0 && line_num <= src_lines. len ( ) {
494+ let source_line = src_lines[ line_num - 1 ] ;
495+ println ! ( " # > {}" , source_line) ;
496+ if let Some ( col) = error. column {
497+ let padding = " " . repeat ( col. saturating_sub ( 1 ) ) ;
498+ println ! ( " # > {}^" , padding) ;
499+ }
500+ }
501+ }
502+ }
451503 }
452504 }
453505 }
0 commit comments