1use std::collections::HashMap;
7use std::fmt;
8
9pub mod builtin;
10pub mod parser;
11pub mod roles;
12
13pub use builtin::*;
14pub use parser::*;
15pub use roles::*;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct SourceLocation {
20 pub file: String,
22 pub line: usize,
24 pub column: usize,
26}
27
28#[derive(Debug, Clone, PartialEq)]
30pub struct ParsedDirective {
31 pub name: String,
33 pub arguments: Vec<String>,
35 pub options: HashMap<String, String>,
37 pub content: String,
39 pub location: SourceLocation,
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct ParsedRole {
46 pub name: String,
48 pub target: String,
50 pub display_text: Option<String>,
52 pub location: SourceLocation,
54}
55
56#[derive(Debug, Clone, PartialEq)]
58pub enum DirectiveValidationResult {
59 Valid,
61 Warning(String),
63 Error(String),
65 Unknown,
67}
68
69#[derive(Debug, Clone, PartialEq)]
71pub enum RoleValidationResult {
72 Valid,
74 Warning(String),
76 Error(String),
78 Unknown,
80}
81
82pub trait DirectiveValidator: Send + Sync {
84 fn name(&self) -> &str;
86
87 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult;
89
90 fn expected_arguments(&self) -> Vec<String>;
92
93 fn valid_options(&self) -> Vec<String>;
95
96 fn requires_content(&self) -> bool;
98
99 fn allows_content(&self) -> bool;
101
102 fn get_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
104 let mut suggestions = Vec::new();
105
106 if directive.content.is_empty() && self.requires_content() {
108 suggestions.push(format!("The '{}' directive requires content", self.name()));
109 }
110
111 if !directive.content.is_empty() && !self.allows_content() {
112 suggestions.push(format!(
113 "The '{}' directive does not allow content",
114 self.name()
115 ));
116 }
117
118 let valid_options = self.valid_options();
120 for option in directive.options.keys() {
121 if !valid_options.contains(&option.to_string()) {
122 suggestions.push(format!(
123 "Unknown option '{}' for directive '{}'",
124 option,
125 self.name()
126 ));
127
128 for valid_option in &valid_options {
130 if valid_option.contains(option) || option.contains(valid_option) {
131 suggestions.push(format!("Did you mean '{}'?", valid_option));
132 break;
133 }
134 }
135 }
136 }
137
138 suggestions
139 }
140}
141
142pub trait RoleValidator: Send + Sync {
144 fn name(&self) -> &str;
146
147 fn validate(&self, role: &ParsedRole) -> RoleValidationResult;
149
150 fn requires_target(&self) -> bool;
152
153 fn allows_display_text(&self) -> bool;
155
156 fn get_suggestions(&self, role: &ParsedRole) -> Vec<String> {
158 let mut suggestions = Vec::new();
159
160 if role.target.is_empty() && self.requires_target() {
161 suggestions.push(format!("The '{}' role requires a target", self.name()));
162 }
163
164 if role.display_text.is_some() && !self.allows_display_text() {
165 suggestions.push(format!(
166 "The '{}' role does not support display text",
167 self.name()
168 ));
169 }
170
171 suggestions
172 }
173}
174
175#[derive(Default)]
177pub struct DirectiveRegistry {
178 validators: HashMap<String, Box<dyn DirectiveValidator>>,
179}
180
181impl DirectiveRegistry {
182 pub fn new() -> Self {
184 Self::default()
185 }
186
187 pub fn with_builtin_validators() -> Self {
189 let mut registry = Self::new();
190 registry.register_builtin_validators();
191 registry
192 }
193
194 pub fn register_validator(&mut self, validator: Box<dyn DirectiveValidator>) {
196 let name = validator.name().to_string();
197 self.validators.insert(name, validator);
198 }
199
200 pub fn register_builtin_validators(&mut self) {
202 self.register_validator(Box::new(builtin::CodeBlockValidator::new()));
204 self.register_validator(Box::new(builtin::NoteValidator::new()));
205 self.register_validator(Box::new(builtin::WarningValidator::new()));
206 self.register_validator(Box::new(builtin::ImageValidator::new()));
207 self.register_validator(Box::new(builtin::FigureValidator::new()));
208 self.register_validator(Box::new(builtin::TocTreeValidator::new()));
209 self.register_validator(Box::new(builtin::IncludeValidator::new()));
210 self.register_validator(Box::new(builtin::LiteralIncludeValidator::new()));
211 self.register_validator(Box::new(builtin::AdmonitionValidator::new()));
212 self.register_validator(Box::new(builtin::MathValidator::new()));
213 }
214
215 pub fn validate_directive(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
217 match self.validators.get(&directive.name) {
218 Some(validator) => validator.validate(directive),
219 None => DirectiveValidationResult::Unknown,
220 }
221 }
222
223 pub fn get_directive_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
225 match self.validators.get(&directive.name) {
226 Some(validator) => validator.get_suggestions(directive),
227 None => {
228 let mut suggestions = vec![format!("Unknown directive '{}'", directive.name)];
229
230 for validator_name in self.validators.keys() {
232 if validator_name.contains(&directive.name)
233 || directive.name.contains(validator_name)
234 {
235 suggestions.push(format!("Did you mean '{}'?", validator_name));
236 }
237 }
238
239 suggestions
240 }
241 }
242 }
243
244 pub fn get_registered_directives(&self) -> Vec<String> {
246 self.validators.keys().cloned().collect()
247 }
248
249 pub fn is_directive_registered(&self, name: &str) -> bool {
251 self.validators.contains_key(name)
252 }
253}
254
255#[derive(Default)]
257pub struct RoleRegistry {
258 validators: HashMap<String, Box<dyn RoleValidator>>,
259}
260
261impl RoleRegistry {
262 pub fn new() -> Self {
264 Self::default()
265 }
266
267 pub fn with_builtin_validators() -> Self {
269 let mut registry = Self::new();
270 registry.register_builtin_validators();
271 registry
272 }
273
274 pub fn register_validator(&mut self, validator: Box<dyn RoleValidator>) {
276 let name = validator.name().to_string();
277 self.validators.insert(name, validator);
278 }
279
280 pub fn register_builtin_validators(&mut self) {
282 self.register_validator(Box::new(roles::DocRoleValidator::new()));
284 self.register_validator(Box::new(roles::RefRoleValidator::new()));
285 self.register_validator(Box::new(roles::DownloadRoleValidator::new()));
286 self.register_validator(Box::new(roles::MathRoleValidator::new()));
287 self.register_validator(Box::new(roles::AbbreviationRoleValidator::new()));
288 self.register_validator(Box::new(roles::CommandRoleValidator::new()));
289 self.register_validator(Box::new(roles::FileRoleValidator::new()));
290 self.register_validator(Box::new(roles::KbdRoleValidator::new()));
291 self.register_validator(Box::new(roles::MenuSelectionRoleValidator::new()));
292 self.register_validator(Box::new(roles::GuiLabelRoleValidator::new()));
293 }
294
295 pub fn validate_role(&self, role: &ParsedRole) -> RoleValidationResult {
297 match self.validators.get(&role.name) {
298 Some(validator) => validator.validate(role),
299 None => RoleValidationResult::Unknown,
300 }
301 }
302
303 pub fn get_role_suggestions(&self, role: &ParsedRole) -> Vec<String> {
305 match self.validators.get(&role.name) {
306 Some(validator) => validator.get_suggestions(role),
307 None => {
308 let mut suggestions = vec![format!("Unknown role '{}'", role.name)];
309
310 for validator_name in self.validators.keys() {
312 if validator_name.contains(&role.name) || role.name.contains(validator_name) {
313 suggestions.push(format!("Did you mean '{}'?", validator_name));
314 }
315 }
316
317 suggestions
318 }
319 }
320 }
321
322 pub fn get_registered_roles(&self) -> Vec<String> {
324 self.validators.keys().cloned().collect()
325 }
326
327 pub fn is_role_registered(&self, name: &str) -> bool {
329 self.validators.contains_key(name)
330 }
331}
332
333#[derive(Debug, Default, Clone)]
335pub struct ValidationStatistics {
336 pub total_directives: usize,
338 pub valid_directives: usize,
340 pub warning_directives: usize,
342 pub error_directives: usize,
344 pub unknown_directives: usize,
346
347 pub total_roles: usize,
349 pub valid_roles: usize,
351 pub warning_roles: usize,
353 pub error_roles: usize,
355 pub unknown_roles: usize,
357
358 pub directives_by_type: HashMap<String, usize>,
360 pub roles_by_type: HashMap<String, usize>,
362}
363
364impl ValidationStatistics {
365 pub fn new() -> Self {
367 Self::default()
368 }
369
370 pub fn record_directive(
372 &mut self,
373 directive: &ParsedDirective,
374 result: &DirectiveValidationResult,
375 ) {
376 self.total_directives += 1;
377 *self
378 .directives_by_type
379 .entry(directive.name.clone())
380 .or_insert(0) += 1;
381
382 match result {
383 DirectiveValidationResult::Valid => self.valid_directives += 1,
384 DirectiveValidationResult::Warning(_) => self.warning_directives += 1,
385 DirectiveValidationResult::Error(_) => self.error_directives += 1,
386 DirectiveValidationResult::Unknown => self.unknown_directives += 1,
387 }
388 }
389
390 pub fn record_role(&mut self, role: &ParsedRole, result: &RoleValidationResult) {
392 self.total_roles += 1;
393 *self.roles_by_type.entry(role.name.clone()).or_insert(0) += 1;
394
395 match result {
396 RoleValidationResult::Valid => self.valid_roles += 1,
397 RoleValidationResult::Warning(_) => self.warning_roles += 1,
398 RoleValidationResult::Error(_) => self.error_roles += 1,
399 RoleValidationResult::Unknown => self.unknown_roles += 1,
400 }
401 }
402
403 pub fn directive_success_rate(&self) -> f64 {
405 if self.total_directives == 0 {
406 return 1.0;
407 }
408 self.valid_directives as f64 / self.total_directives as f64
409 }
410
411 pub fn role_success_rate(&self) -> f64 {
413 if self.total_roles == 0 {
414 return 1.0;
415 }
416 self.valid_roles as f64 / self.total_roles as f64
417 }
418
419 pub fn overall_success_rate(&self) -> f64 {
421 let total = self.total_directives + self.total_roles;
422 if total == 0 {
423 return 1.0;
424 }
425 (self.valid_directives + self.valid_roles) as f64 / total as f64
426 }
427}
428
429impl fmt::Display for ValidationStatistics {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 writeln!(f, "Directive & Role Validation Statistics")?;
432 writeln!(f, "=======================================")?;
433 writeln!(f)?;
434
435 writeln!(f, "Directives:")?;
436 writeln!(f, " Total: {}", self.total_directives)?;
437 if self.total_directives > 0 {
438 writeln!(
439 f,
440 " Valid: {} ({:.1}%)",
441 self.valid_directives,
442 self.valid_directives as f64 / self.total_directives as f64 * 100.0
443 )?;
444 writeln!(
445 f,
446 " Warnings: {} ({:.1}%)",
447 self.warning_directives,
448 self.warning_directives as f64 / self.total_directives as f64 * 100.0
449 )?;
450 writeln!(
451 f,
452 " Errors: {} ({:.1}%)",
453 self.error_directives,
454 self.error_directives as f64 / self.total_directives as f64 * 100.0
455 )?;
456 writeln!(
457 f,
458 " Unknown: {} ({:.1}%)",
459 self.unknown_directives,
460 self.unknown_directives as f64 / self.total_directives as f64 * 100.0
461 )?;
462 }
463 writeln!(f)?;
464
465 writeln!(f, "Roles:")?;
466 writeln!(f, " Total: {}", self.total_roles)?;
467 if self.total_roles > 0 {
468 writeln!(
469 f,
470 " Valid: {} ({:.1}%)",
471 self.valid_roles,
472 self.valid_roles as f64 / self.total_roles as f64 * 100.0
473 )?;
474 writeln!(
475 f,
476 " Warnings: {} ({:.1}%)",
477 self.warning_roles,
478 self.warning_roles as f64 / self.total_roles as f64 * 100.0
479 )?;
480 writeln!(
481 f,
482 " Errors: {} ({:.1}%)",
483 self.error_roles,
484 self.error_roles as f64 / self.total_roles as f64 * 100.0
485 )?;
486 writeln!(
487 f,
488 " Unknown: {} ({:.1}%)",
489 self.unknown_roles,
490 self.unknown_roles as f64 / self.total_roles as f64 * 100.0
491 )?;
492 }
493 writeln!(f)?;
494
495 writeln!(
496 f,
497 "Overall Success Rate: {:.1}%",
498 self.overall_success_rate() * 100.0
499 )?;
500
501 Ok(())
502 }
503}
504
505pub struct DirectiveValidationSystem {
507 directive_registry: DirectiveRegistry,
508 role_registry: RoleRegistry,
509 statistics: ValidationStatistics,
510}
511
512impl Default for DirectiveValidationSystem {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518impl DirectiveValidationSystem {
519 pub fn new() -> Self {
521 Self {
522 directive_registry: DirectiveRegistry::with_builtin_validators(),
523 role_registry: RoleRegistry::with_builtin_validators(),
524 statistics: ValidationStatistics::new(),
525 }
526 }
527
528 pub fn directive_registry(&self) -> &DirectiveRegistry {
530 &self.directive_registry
531 }
532
533 pub fn directive_registry_mut(&mut self) -> &mut DirectiveRegistry {
535 &mut self.directive_registry
536 }
537
538 pub fn role_registry(&self) -> &RoleRegistry {
540 &self.role_registry
541 }
542
543 pub fn role_registry_mut(&mut self) -> &mut RoleRegistry {
545 &mut self.role_registry
546 }
547
548 pub fn validate_directive(&mut self, directive: &ParsedDirective) -> DirectiveValidationResult {
550 let result = self.directive_registry.validate_directive(directive);
551 self.statistics.record_directive(directive, &result);
552 result
553 }
554
555 pub fn validate_role(&mut self, role: &ParsedRole) -> RoleValidationResult {
557 let result = self.role_registry.validate_role(role);
558 self.statistics.record_role(role, &result);
559 result
560 }
561
562 pub fn get_directive_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
564 self.directive_registry.get_directive_suggestions(directive)
565 }
566
567 pub fn get_role_suggestions(&self, role: &ParsedRole) -> Vec<String> {
569 self.role_registry.get_role_suggestions(role)
570 }
571
572 pub fn statistics(&self) -> &ValidationStatistics {
574 &self.statistics
575 }
576
577 pub fn reset_statistics(&mut self) {
579 self.statistics = ValidationStatistics::new();
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn test_directive_registry_creation() {
589 let registry = DirectiveRegistry::new();
590 assert_eq!(registry.get_registered_directives().len(), 0);
591 }
592
593 #[test]
594 fn test_directive_registry_with_builtin() {
595 let registry = DirectiveRegistry::with_builtin_validators();
596 assert!(!registry.get_registered_directives().is_empty());
597 assert!(registry.is_directive_registered("code-block"));
598 assert!(registry.is_directive_registered("note"));
599 assert!(registry.is_directive_registered("warning"));
600 }
601
602 #[test]
603 fn test_role_registry_creation() {
604 let registry = RoleRegistry::new();
605 assert_eq!(registry.get_registered_roles().len(), 0);
606 }
607
608 #[test]
609 fn test_role_registry_with_builtin() {
610 let registry = RoleRegistry::with_builtin_validators();
611 assert!(!registry.get_registered_roles().is_empty());
612 assert!(registry.is_role_registered("doc"));
613 assert!(registry.is_role_registered("ref"));
614 assert!(registry.is_role_registered("download"));
615 }
616
617 #[test]
618 fn test_validation_statistics() {
619 let mut stats = ValidationStatistics::new();
620
621 let directive = ParsedDirective {
622 name: "note".to_string(),
623 arguments: vec![],
624 options: HashMap::new(),
625 content: "Test content".to_string(),
626 location: SourceLocation {
627 file: "test.rst".to_string(),
628 line: 1,
629 column: 1,
630 },
631 };
632
633 stats.record_directive(&directive, &DirectiveValidationResult::Valid);
634 assert_eq!(stats.total_directives, 1);
635 assert_eq!(stats.valid_directives, 1);
636 assert_eq!(stats.directive_success_rate(), 1.0);
637 }
638
639 #[test]
640 fn test_validation_system() {
641 let mut system = DirectiveValidationSystem::new();
642
643 let directive = ParsedDirective {
644 name: "note".to_string(),
645 arguments: vec![],
646 options: HashMap::new(),
647 content: "Test content".to_string(),
648 location: SourceLocation {
649 file: "test.rst".to_string(),
650 line: 1,
651 column: 1,
652 },
653 };
654
655 let result = system.validate_directive(&directive);
656 assert_eq!(result, DirectiveValidationResult::Valid);
657 assert_eq!(system.statistics().total_directives, 1);
658 }
659}