use std::collections::HashMap;
use std::fmt;
pub mod builtin;
pub mod parser;
pub mod roles;
pub use builtin::*;
pub use parser::*;
pub use roles::*;
#[derive(Debug, Clone, PartialEq)]
pub struct SourceLocation {
pub file: String,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedDirective {
pub name: String,
pub arguments: Vec<String>,
pub options: HashMap<String, String>,
pub content: String,
pub location: SourceLocation,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedRole {
pub name: String,
pub target: String,
pub display_text: Option<String>,
pub location: SourceLocation,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DirectiveValidationResult {
Valid,
Warning(String),
Error(String),
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RoleValidationResult {
Valid,
Warning(String),
Error(String),
Unknown,
}
pub trait DirectiveValidator: Send + Sync {
fn name(&self) -> &str;
fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult;
fn expected_arguments(&self) -> Vec<String>;
fn valid_options(&self) -> Vec<String>;
fn requires_content(&self) -> bool;
fn allows_content(&self) -> bool;
fn get_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
let mut suggestions = Vec::new();
if directive.content.is_empty() && self.requires_content() {
suggestions.push(format!("The '{}' directive requires content", self.name()));
}
if !directive.content.is_empty() && !self.allows_content() {
suggestions.push(format!(
"The '{}' directive does not allow content",
self.name()
));
}
let valid_options = self.valid_options();
for option in directive.options.keys() {
if !valid_options.contains(&option.to_string()) {
suggestions.push(format!(
"Unknown option '{}' for directive '{}'",
option,
self.name()
));
for valid_option in &valid_options {
if valid_option.contains(option) || option.contains(valid_option) {
suggestions.push(format!("Did you mean '{}'?", valid_option));
break;
}
}
}
}
suggestions
}
}
pub trait RoleValidator: Send + Sync {
fn name(&self) -> &str;
fn validate(&self, role: &ParsedRole) -> RoleValidationResult;
fn requires_target(&self) -> bool;
fn allows_display_text(&self) -> bool;
fn get_suggestions(&self, role: &ParsedRole) -> Vec<String> {
let mut suggestions = Vec::new();
if role.target.is_empty() && self.requires_target() {
suggestions.push(format!("The '{}' role requires a target", self.name()));
}
if role.display_text.is_some() && !self.allows_display_text() {
suggestions.push(format!(
"The '{}' role does not support display text",
self.name()
));
}
suggestions
}
}
#[derive(Default)]
pub struct DirectiveRegistry {
validators: HashMap<String, Box<dyn DirectiveValidator>>,
}
impl DirectiveRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtin_validators() -> Self {
let mut registry = Self::new();
registry.register_builtin_validators();
registry
}
pub fn register_validator(&mut self, validator: Box<dyn DirectiveValidator>) {
let name = validator.name().to_string();
self.validators.insert(name, validator);
}
pub fn register_builtin_validators(&mut self) {
self.register_validator(Box::new(builtin::CodeBlockValidator::new()));
self.register_validator(Box::new(builtin::NoteValidator::new()));
self.register_validator(Box::new(builtin::WarningValidator::new()));
self.register_validator(Box::new(builtin::ImageValidator::new()));
self.register_validator(Box::new(builtin::FigureValidator::new()));
self.register_validator(Box::new(builtin::TocTreeValidator::new()));
self.register_validator(Box::new(builtin::IncludeValidator::new()));
self.register_validator(Box::new(builtin::LiteralIncludeValidator::new()));
self.register_validator(Box::new(builtin::AdmonitionValidator::new()));
self.register_validator(Box::new(builtin::MathValidator::new()));
}
pub fn validate_directive(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
match self.validators.get(&directive.name) {
Some(validator) => validator.validate(directive),
None => DirectiveValidationResult::Unknown,
}
}
pub fn get_directive_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
match self.validators.get(&directive.name) {
Some(validator) => validator.get_suggestions(directive),
None => {
let mut suggestions = vec![format!("Unknown directive '{}'", directive.name)];
for validator_name in self.validators.keys() {
if validator_name.contains(&directive.name)
|| directive.name.contains(validator_name)
{
suggestions.push(format!("Did you mean '{}'?", validator_name));
}
}
suggestions
}
}
}
pub fn get_registered_directives(&self) -> Vec<String> {
self.validators.keys().cloned().collect()
}
pub fn is_directive_registered(&self, name: &str) -> bool {
self.validators.contains_key(name)
}
}
#[derive(Default)]
pub struct RoleRegistry {
validators: HashMap<String, Box<dyn RoleValidator>>,
}
impl RoleRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtin_validators() -> Self {
let mut registry = Self::new();
registry.register_builtin_validators();
registry
}
pub fn register_validator(&mut self, validator: Box<dyn RoleValidator>) {
let name = validator.name().to_string();
self.validators.insert(name, validator);
}
pub fn register_builtin_validators(&mut self) {
self.register_validator(Box::new(roles::DocRoleValidator::new()));
self.register_validator(Box::new(roles::RefRoleValidator::new()));
self.register_validator(Box::new(roles::DownloadRoleValidator::new()));
self.register_validator(Box::new(roles::MathRoleValidator::new()));
self.register_validator(Box::new(roles::AbbreviationRoleValidator::new()));
self.register_validator(Box::new(roles::CommandRoleValidator::new()));
self.register_validator(Box::new(roles::FileRoleValidator::new()));
self.register_validator(Box::new(roles::KbdRoleValidator::new()));
self.register_validator(Box::new(roles::MenuSelectionRoleValidator::new()));
self.register_validator(Box::new(roles::GuiLabelRoleValidator::new()));
}
pub fn validate_role(&self, role: &ParsedRole) -> RoleValidationResult {
match self.validators.get(&role.name) {
Some(validator) => validator.validate(role),
None => RoleValidationResult::Unknown,
}
}
pub fn get_role_suggestions(&self, role: &ParsedRole) -> Vec<String> {
match self.validators.get(&role.name) {
Some(validator) => validator.get_suggestions(role),
None => {
let mut suggestions = vec![format!("Unknown role '{}'", role.name)];
for validator_name in self.validators.keys() {
if validator_name.contains(&role.name) || role.name.contains(validator_name) {
suggestions.push(format!("Did you mean '{}'?", validator_name));
}
}
suggestions
}
}
}
pub fn get_registered_roles(&self) -> Vec<String> {
self.validators.keys().cloned().collect()
}
pub fn is_role_registered(&self, name: &str) -> bool {
self.validators.contains_key(name)
}
}
#[derive(Debug, Default, Clone)]
pub struct ValidationStatistics {
pub total_directives: usize,
pub valid_directives: usize,
pub warning_directives: usize,
pub error_directives: usize,
pub unknown_directives: usize,
pub total_roles: usize,
pub valid_roles: usize,
pub warning_roles: usize,
pub error_roles: usize,
pub unknown_roles: usize,
pub directives_by_type: HashMap<String, usize>,
pub roles_by_type: HashMap<String, usize>,
}
impl ValidationStatistics {
pub fn new() -> Self {
Self::default()
}
pub fn record_directive(
&mut self,
directive: &ParsedDirective,
result: &DirectiveValidationResult,
) {
self.total_directives += 1;
*self
.directives_by_type
.entry(directive.name.clone())
.or_insert(0) += 1;
match result {
DirectiveValidationResult::Valid => self.valid_directives += 1,
DirectiveValidationResult::Warning(_) => self.warning_directives += 1,
DirectiveValidationResult::Error(_) => self.error_directives += 1,
DirectiveValidationResult::Unknown => self.unknown_directives += 1,
}
}
pub fn record_role(&mut self, role: &ParsedRole, result: &RoleValidationResult) {
self.total_roles += 1;
*self.roles_by_type.entry(role.name.clone()).or_insert(0) += 1;
match result {
RoleValidationResult::Valid => self.valid_roles += 1,
RoleValidationResult::Warning(_) => self.warning_roles += 1,
RoleValidationResult::Error(_) => self.error_roles += 1,
RoleValidationResult::Unknown => self.unknown_roles += 1,
}
}
pub fn directive_success_rate(&self) -> f64 {
if self.total_directives == 0 {
return 1.0;
}
self.valid_directives as f64 / self.total_directives as f64
}
pub fn role_success_rate(&self) -> f64 {
if self.total_roles == 0 {
return 1.0;
}
self.valid_roles as f64 / self.total_roles as f64
}
pub fn overall_success_rate(&self) -> f64 {
let total = self.total_directives + self.total_roles;
if total == 0 {
return 1.0;
}
(self.valid_directives + self.valid_roles) as f64 / total as f64
}
}
impl fmt::Display for ValidationStatistics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Directive & Role Validation Statistics")?;
writeln!(f, "=======================================")?;
writeln!(f)?;
writeln!(f, "Directives:")?;
writeln!(f, " Total: {}", self.total_directives)?;
if self.total_directives > 0 {
writeln!(
f,
" Valid: {} ({:.1}%)",
self.valid_directives,
self.valid_directives as f64 / self.total_directives as f64 * 100.0
)?;
writeln!(
f,
" Warnings: {} ({:.1}%)",
self.warning_directives,
self.warning_directives as f64 / self.total_directives as f64 * 100.0
)?;
writeln!(
f,
" Errors: {} ({:.1}%)",
self.error_directives,
self.error_directives as f64 / self.total_directives as f64 * 100.0
)?;
writeln!(
f,
" Unknown: {} ({:.1}%)",
self.unknown_directives,
self.unknown_directives as f64 / self.total_directives as f64 * 100.0
)?;
}
writeln!(f)?;
writeln!(f, "Roles:")?;
writeln!(f, " Total: {}", self.total_roles)?;
if self.total_roles > 0 {
writeln!(
f,
" Valid: {} ({:.1}%)",
self.valid_roles,
self.valid_roles as f64 / self.total_roles as f64 * 100.0
)?;
writeln!(
f,
" Warnings: {} ({:.1}%)",
self.warning_roles,
self.warning_roles as f64 / self.total_roles as f64 * 100.0
)?;
writeln!(
f,
" Errors: {} ({:.1}%)",
self.error_roles,
self.error_roles as f64 / self.total_roles as f64 * 100.0
)?;
writeln!(
f,
" Unknown: {} ({:.1}%)",
self.unknown_roles,
self.unknown_roles as f64 / self.total_roles as f64 * 100.0
)?;
}
writeln!(f)?;
writeln!(
f,
"Overall Success Rate: {:.1}%",
self.overall_success_rate() * 100.0
)?;
Ok(())
}
}
pub struct DirectiveValidationSystem {
directive_registry: DirectiveRegistry,
role_registry: RoleRegistry,
statistics: ValidationStatistics,
}
impl Default for DirectiveValidationSystem {
fn default() -> Self {
Self::new()
}
}
impl DirectiveValidationSystem {
pub fn new() -> Self {
Self {
directive_registry: DirectiveRegistry::with_builtin_validators(),
role_registry: RoleRegistry::with_builtin_validators(),
statistics: ValidationStatistics::new(),
}
}
pub fn directive_registry(&self) -> &DirectiveRegistry {
&self.directive_registry
}
pub fn directive_registry_mut(&mut self) -> &mut DirectiveRegistry {
&mut self.directive_registry
}
pub fn role_registry(&self) -> &RoleRegistry {
&self.role_registry
}
pub fn role_registry_mut(&mut self) -> &mut RoleRegistry {
&mut self.role_registry
}
pub fn validate_directive(&mut self, directive: &ParsedDirective) -> DirectiveValidationResult {
let result = self.directive_registry.validate_directive(directive);
self.statistics.record_directive(directive, &result);
result
}
pub fn validate_role(&mut self, role: &ParsedRole) -> RoleValidationResult {
let result = self.role_registry.validate_role(role);
self.statistics.record_role(role, &result);
result
}
pub fn get_directive_suggestions(&self, directive: &ParsedDirective) -> Vec<String> {
self.directive_registry.get_directive_suggestions(directive)
}
pub fn get_role_suggestions(&self, role: &ParsedRole) -> Vec<String> {
self.role_registry.get_role_suggestions(role)
}
pub fn statistics(&self) -> &ValidationStatistics {
&self.statistics
}
pub fn reset_statistics(&mut self) {
self.statistics = ValidationStatistics::new();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_directive_registry_creation() {
let registry = DirectiveRegistry::new();
assert_eq!(registry.get_registered_directives().len(), 0);
}
#[test]
fn test_directive_registry_with_builtin() {
let registry = DirectiveRegistry::with_builtin_validators();
assert!(!registry.get_registered_directives().is_empty());
assert!(registry.is_directive_registered("code-block"));
assert!(registry.is_directive_registered("note"));
assert!(registry.is_directive_registered("warning"));
}
#[test]
fn test_role_registry_creation() {
let registry = RoleRegistry::new();
assert_eq!(registry.get_registered_roles().len(), 0);
}
#[test]
fn test_role_registry_with_builtin() {
let registry = RoleRegistry::with_builtin_validators();
assert!(!registry.get_registered_roles().is_empty());
assert!(registry.is_role_registered("doc"));
assert!(registry.is_role_registered("ref"));
assert!(registry.is_role_registered("download"));
}
#[test]
fn test_validation_statistics() {
let mut stats = ValidationStatistics::new();
let directive = ParsedDirective {
name: "note".to_string(),
arguments: vec![],
options: HashMap::new(),
content: "Test content".to_string(),
location: SourceLocation {
file: "test.rst".to_string(),
line: 1,
column: 1,
},
};
stats.record_directive(&directive, &DirectiveValidationResult::Valid);
assert_eq!(stats.total_directives, 1);
assert_eq!(stats.valid_directives, 1);
assert_eq!(stats.directive_success_rate(), 1.0);
}
#[test]
fn test_validation_system() {
let mut system = DirectiveValidationSystem::new();
let directive = ParsedDirective {
name: "note".to_string(),
arguments: vec![],
options: HashMap::new(),
content: "Test content".to_string(),
location: SourceLocation {
file: "test.rst".to_string(),
line: 1,
column: 1,
},
};
let result = system.validate_directive(&directive);
assert_eq!(result, DirectiveValidationResult::Valid);
assert_eq!(system.statistics().total_directives, 1);
}
}